aboutsummaryrefslogtreecommitdiffhomepage
path: root/paper-server/patches/unapplied/net
diff options
context:
space:
mode:
Diffstat (limited to 'paper-server/patches/unapplied/net')
-rw-r--r--paper-server/patches/unapplied/net/minecraft/ChatFormatting.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/CrashReport.java.patch22
-rw-r--r--paper-server/patches/unapplied/net/minecraft/CrashReportCategory.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/Util.java.patch143
-rw-r--r--paper-server/patches/unapplied/net/minecraft/advancements/AdvancementHolder.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/advancements/AdvancementTree.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/advancements/DisplayInfo.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/advancements/critereon/LocationPredicate.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch52
-rw-r--r--paper-server/patches/unapplied/net/minecraft/commands/CommandSource.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/commands/CommandSourceStack.java.patch114
-rw-r--r--paper-server/patches/unapplied/net/minecraft/commands/Commands.java.patch372
-rw-r--r--paper-server/patches/unapplied/net/minecraft/commands/arguments/EntityArgument.java.patch52
-rw-r--r--paper-server/patches/unapplied/net/minecraft/commands/arguments/MessageArgument.java.patch41
-rw-r--r--paper-server/patches/unapplied/net/minecraft/commands/arguments/blocks/BlockStateParser.java.patch38
-rw-r--r--paper-server/patches/unapplied/net/minecraft/commands/arguments/item/ItemInput.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelector.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch55
-rw-r--r--paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/BlockPos.java.patch186
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/Direction.java.patch46
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/Holder.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/MappedRegistry.java.patch33
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/Rotations.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/Vec3i.java.patch49
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/cauldron/CauldronInteraction.java.patch330
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/component/DataComponentPatch.java.patch70
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/component/DataComponents.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch58
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch126
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch651
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch82
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch58
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch58
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch81
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch54
-rw-r--r--paper-server/patches/unapplied/net/minecraft/core/registries/BuiltInRegistries.java.patch52
-rw-r--r--paper-server/patches/unapplied/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/nbt/ByteArrayTag.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/nbt/CompoundTag.java.patch74
-rw-r--r--paper-server/patches/unapplied/net/minecraft/nbt/IntArrayTag.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/nbt/NbtIo.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/nbt/NbtUtils.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/nbt/TagParser.java.patch69
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/Connection.java.patch330
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/FriendlyByteBuf.java.patch105
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/PacketEncoder.java.patch57
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/VarInt.java.patch43
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/Varint21FrameDecoder.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/ChatDecorator.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/Component.java.patch27
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/ComponentSerialization.java.patch99
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/ComponentUtils.java.patch42
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/MessageSignature.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/MutableComponent.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/OutgoingChatMessage.java.patch44
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/PlayerChatMessage.java.patch61
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/SignedMessageChain.java.patch37
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/TextColor.java.patch37
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/contents/NbtContents.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/contents/SelectorContents.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/chat/contents/TranslatableContents.java.patch45
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/Packet.java.patch22
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/PacketUtils.java.patch27
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/common/custom/DiscardedPayload.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java.patch114
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java.patch38
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java.patch32
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetPlayerTeamPacket.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java.patch25
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundInteractPacket.java.patch17
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/game/VecDeltaCodec.java.patch22
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java.patch17
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java.patch17
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/login/ClientboundLoginDisconnectPacket.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java.patch43
-rw-r--r--paper-server/patches/unapplied/net/minecraft/network/syncher/SynchedEntityData.java.patch53
-rw-r--r--paper-server/patches/unapplied/net/minecraft/resources/RegistryDataLoader.java.patch79
-rw-r--r--paper-server/patches/unapplied/net/minecraft/resources/ResourceLocation.java.patch42
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/Bootstrap.java.patch129
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/Main.java.patch290
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/MinecraftServer.java.patch1501
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/PlayerAdvancements.java.patch69
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/ReloadableServerRegistries.java.patch32
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/ReloadableServerResources.java.patch25
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/ServerAdvancementManager.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/ServerFunctionLibrary.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/ServerFunctionManager.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/ServerScoreboard.java.patch168
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/ServerTickRateManager.java.patch53
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/Services.java.patch40
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/WorldLoader.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/bossevents/CustomBossEvent.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/BanIpCommands.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/BanPlayerCommands.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/DeOpCommands.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/DefaultGameModeCommands.java.patch18
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/DifficultyCommand.java.patch17
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/EffectCommands.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/GameModeCommand.java.patch18
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/GameRuleCommand.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/GiveCommand.java.patch33
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/KickCommand.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/ListPlayersCommand.java.patch18
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/LootCommand.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/OpCommand.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/PlaceCommand.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/ReloadCommand.java.patch28
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/ScheduleCommand.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/SetSpawnCommand.java.patch42
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/SetWorldSpawnCommand.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/SpreadPlayersCommand.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/SummonCommand.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/TeleportCommand.java.patch55
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/TimeCommand.java.patch55
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/WeatherCommand.java.patch34
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/commands/WorldBorderCommand.java.patch65
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedPlayerList.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServer.java.patch475
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServerProperties.java.patch106
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServerSettings.java.patch27
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/dedicated/Settings.java.patch179
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/gui/MinecraftServerGui.java.patch103
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/gui/StatsComponent.java.patch33
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/level/ChunkHolder.java.patch245
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/level/ChunkMap.java.patch433
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/level/DistanceManager.java.patch152
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/level/ServerChunkCache.java.patch255
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/level/ServerEntity.java.patch179
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/level/ServerLevel.java.patch1261
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/level/ServerPlayer.java.patch1891
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/level/ServerPlayerGameMode.java.patch463
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/level/TicketType.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/level/WorldGenRegion.java.patch105
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/network/LegacyQueryHandler.java.patch211
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/network/PlayerChunkSender.java.patch26
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch418
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch89
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/network/ServerConnectionListener.java.patch156
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch2655
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java.patch169
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch424
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/network/ServerStatusPacketListenerImpl.java.patch137
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/packs/PathPackResources.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/packs/VanillaPackResourcesBuilder.java.patch18
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/packs/repository/ServerPacksSource.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/players/BanListEntry.java.patch34
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/players/GameProfileCache.java.patch217
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/players/OldUsersConverter.java.patch98
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/players/PlayerList.java.patch1276
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/players/SleepStatus.java.patch44
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/players/StoredUserList.java.patch96
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/players/UserBanListEntry.java.patch42
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/players/UserWhiteList.java.patch26
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/rcon/RconConsoleSource.java.patch49
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/rcon/thread/QueryThreadGs4.java.patch126
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/rcon/thread/RconClient.java.patch80
-rw-r--r--paper-server/patches/unapplied/net/minecraft/server/rcon/thread/RconThread.java.patch26
-rw-r--r--paper-server/patches/unapplied/net/minecraft/stats/ServerRecipeBook.java.patch38
-rw-r--r--paper-server/patches/unapplied/net/minecraft/stats/ServerStatsCounter.java.patch58
-rw-r--r--paper-server/patches/unapplied/net/minecraft/stats/StatsCounter.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/tags/TagLoader.java.patch66
-rw-r--r--paper-server/patches/unapplied/net/minecraft/util/SimpleBitStorage.java.patch70
-rw-r--r--paper-server/patches/unapplied/net/minecraft/util/SortedArraySet.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/util/SpawnUtil.java.patch60
-rw-r--r--paper-server/patches/unapplied/net/minecraft/util/StringUtil.java.patch28
-rw-r--r--paper-server/patches/unapplied/net/minecraft/util/TickThrottler.java.patch53
-rw-r--r--paper-server/patches/unapplied/net/minecraft/util/ZeroBitStorage.java.patch32
-rw-r--r--paper-server/patches/unapplied/net/minecraft/util/datafix/DataFixers.java.patch82
-rw-r--r--paper-server/patches/unapplied/net/minecraft/util/datafix/fixes/ItemStackMapIdFix.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/util/datafix/fixes/ItemStackTheFlatteningFix.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/util/thread/BlockableEventLoop.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/util/worldupdate/WorldUpgrader.java.patch32
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/BossEvent.java.patch86
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/CompoundContainer.java.patch74
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/Container.java.patch49
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/RandomizableContainer.java.patch94
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/SimpleContainer.java.patch103
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSource.java.patch128
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSources.java.patch62
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/effect/HealOrHarmMobEffect.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/effect/HungerMobEffect.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/effect/InfestedMobEffect.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/effect/MobEffectUtil.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/effect/OozingMobEffect.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/effect/PoisonMobEffect.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/effect/RegenerationMobEffect.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/effect/SaturationMobEffect.java.patch30
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/effect/WeavingMobEffect.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/AgeableMob.java.patch74
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/AreaEffectCloud.java.patch160
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ConversionParams.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ConversionType.java.patch46
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/Display.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/Entity.java.patch1994
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/EntitySelector.java.patch49
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/EntityType.java.patch179
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ExperienceOrb.java.patch267
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/Interaction.java.patch42
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ItemBasedSteering.java.patch17
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/Leashable.java.patch157
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/LightningBolt.java.patch160
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/LivingEntity.java.patch2034
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/Mob.java.patch472
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/NeutralMob.java.patch86
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/OminousItemSpawner.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/PathfinderMob.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/Shearable.java.patch18
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/TamableAnimal.java.patch85
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/AttributeInstance.java.patch27
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/AttributeMap.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/Attributes.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/AcquirePoi.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java.patch37
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/Behavior.java.patch40
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java.patch64
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/GateBehavior.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/GoToWantedItem.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java.patch63
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java.patch73
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/RamTarget.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/ResetProfession.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/ShufflingList.java.patch44
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/SleepInBed.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/StartAttacking.java.patch36
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java.patch46
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/TryLaySpawnOnWaterNearLand.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java.patch42
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/WorkAtComposter.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/warden/Digging.java.patch22
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/warden/SonicBoom.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/EatBlockGoal.java.patch46
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/FloatGoal.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/Goal.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java.patch55
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java.patch22
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SwellGoal.java.patch17
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/TemptGoal.java.patch44
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/TargetGoal.java.patch30
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/gossip/GossipContainer.java.patch46
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/GroundPathNavigation.java.patch46
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch104
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/WallClimberNavigation.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/sensing/Sensor.java.patch34
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch45
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ai/village/VillageSiege.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/ambient/Bat.java.patch55
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/AbstractSchoolingFish.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Animal.java.patch141
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Bee.java.patch205
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Bucketable.java.patch46
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Cat.java.patch96
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Chicken.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Cow.java.patch33
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Dolphin.java.patch87
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Fox.java.patch168
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/IronGolem.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/MushroomCow.java.patch85
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Ocelot.java.patch34
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Panda.java.patch122
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Parrot.java.patch47
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Pig.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Pufferfish.java.patch60
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Rabbit.java.patch40
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Sheep.java.patch90
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/ShoulderRidingEntity.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/SnowGolem.java.patch86
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Squid.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Turtle.java.patch74
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/WaterAnimal.java.patch13
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/Wolf.java.patch126
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/allay/Allay.java.patch102
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/armadillo/Armadillo.java.patch81
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/axolotl/Axolotl.java.patch52
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/camel/Camel.java.patch81
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Frog.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/ShootTongue.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Tadpole.java.patch87
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/goat/Goat.java.patch76
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/AbstractHorse.java.patch203
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/Llama.java.patch51
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/SkeletonHorse.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java.patch49
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/TraderLlama.java.patch30
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/animal/sniffer/Sniffer.java.patch56
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch91
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch331
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonDeathPhase.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonHoldingPatternPhase.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingApproachPhase.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingPhase.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java.patch35
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonTakeoffPhase.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java.patch42
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/boss/wither/WitherBoss.java.patch165
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/decoration/ArmorStand.java.patch513
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/decoration/BlockAttachedEntity.java.patch140
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/decoration/ItemFrame.java.patch150
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java.patch87
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/decoration/Painting.java.patch54
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/item/FallingBlockEntity.java.patch162
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/item/ItemEntity.java.patch389
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/item/PrimedTnt.java.patch116
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/AbstractSkeleton.java.patch74
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Bogged.java.patch72
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/CaveSpider.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Creeper.java.patch132
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Drowned.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/ElderGuardian.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/EnderMan.java.patch157
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Endermite.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Evoker.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Ghast.java.patch25
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Guardian.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Husk.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Illusioner.java.patch38
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Monster.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Phantom.java.patch78
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Pillager.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Ravager.java.patch41
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Shulker.java.patch59
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Silverfish.java.patch51
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Skeleton.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Slime.java.patch239
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/SpellcasterIllager.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Spider.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Strider.java.patch18
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vex.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vindicator.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Witch.java.patch77
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/WitherSkeleton.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/Zombie.java.patch304
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/ZombieVillager.java.patch149
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/ZombifiedPiglin.java.patch67
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/breeze/Breeze.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/creaking/Creaking.java.patch60
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/hoglin/Hoglin.java.patch48
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/hoglin/HoglinBase.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/Piglin.java.patch140
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/PiglinAi.java.patch210
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/warden/AngerManagement.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/monster/warden/Warden.java.patch59
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/npc/AbstractVillager.java.patch106
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/npc/CatSpawner.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/npc/InventoryCarrier.java.patch35
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/npc/Villager.java.patch211
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/npc/VillagerTrades.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/npc/WanderingTrader.java.patch80
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch100
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/player/Inventory.java.patch130
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/player/Player.java.patch735
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/player/ProfilePublicKey.java.patch28
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/AbstractArrow.java.patch320
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch38
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Arrow.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/DragonFireball.java.patch26
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/EvokerFangs.java.patch30
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/EyeOfEnder.java.patch58
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Fireball.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch132
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/FishingHook.java.patch352
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/LargeFireball.java.patch56
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/LlamaSpit.java.patch42
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Projectile.java.patch231
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ShulkerBullet.java.patch100
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/SmallFireball.java.patch63
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Snowball.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/SpectralArrow.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrowableProjectile.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownEgg.java.patch121
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch95
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java.patch36
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownPotion.java.patch322
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownTrident.java.patch86
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/WitherSkull.java.patch55
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java.patch48
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raid.java.patch174
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raider.java.patch83
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raids.java.patch27
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractBoat.java.patch125
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch121
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch196
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch98
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch81
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/MinecartTNT.java.patch53
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java.patch83
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch75
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/VehicleEntity.java.patch64
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/food/FoodData.java.patch101
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/food/FoodProperties.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractContainerMenu.java.patch310
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractCraftingMenu.java.patch44
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractFurnaceMenu.java.patch60
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/AnvilMenu.java.patch166
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/BeaconMenu.java.patch109
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/BrewingStandMenu.java.patch127
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/CartographyTableMenu.java.patch146
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/ChestMenu.java.patch64
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerLevelAccess.java.patch69
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerListener.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerSynchronizer.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/CrafterMenu.java.patch38
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/CraftingContainer.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/CraftingMenu.java.patch74
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/DispenserMenu.java.patch62
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/EnchantmentMenu.java.patch270
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/FurnaceResultSlot.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/GrindstoneMenu.java.patch141
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/HopperMenu.java.patch51
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/HorseInventoryMenu.java.patch53
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/InventoryMenu.java.patch64
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/ItemCombinerMenu.java.patch65
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/LecternMenu.java.patch143
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/LoomMenu.java.patch203
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/MenuType.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantContainer.java.patch70
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantMenu.java.patch92
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantResultSlot.java.patch37
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch36
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/ResultContainer.java.patch67
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch49
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/SmithingMenu.java.patch66
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/StonecutterMenu.java.patch188
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/inventory/TransientCraftingContainer.java.patch93
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/ArmorStandItem.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/AxeItem.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/BlockItem.java.patch109
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/BoatItem.java.patch33
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/BoneMealItem.java.patch41
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/BucketItem.java.patch168
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/CrossbowItem.java.patch48
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/DebugStickItem.java.patch25
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/DyeItem.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/EggItem.java.patch40
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/EndCrystalItem.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/EnderEyeItem.java.patch56
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/EnderpearlItem.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/ExperienceBottleItem.java.patch54
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/FireChargeItem.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/FireworkRocketItem.java.patch51
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/FishingRodItem.java.patch50
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/FlintAndSteelItem.java.patch28
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/HangingEntityItem.java.patch67
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/HoneycombItem.java.patch17
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/ItemCooldowns.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/ItemStack.java.patch630
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/ItemUtils.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/LeadItem.java.patch92
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/LingeringPotionItem.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/MapItem.java.patch22
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/MinecartItem.java.patch17
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/NameTagItem.java.patch18
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/PotionItem.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/ProjectileWeaponItem.java.patch52
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/ServerItemCooldowns.java.patch43
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/ShovelItem.java.patch33
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/SignItem.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/SnowballItem.java.patch37
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/SpawnEggItem.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/SplashPotionItem.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/StandingAndWallBlockItem.java.patch41
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/ThrowablePotionItem.java.patch34
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/TridentItem.java.patch51
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/WindChargeItem.java.patch42
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/WrittenBookItem.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionBrewing.java.patch96
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionContents.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/component/Consumable.java.patch53
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/component/ConsumableListener.java.patch9
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/component/CustomData.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/component/DeathProtection.java.patch22
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/component/LodestoneTracker.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/component/OminousBottleAmplifier.java.patch18
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/component/ResolvableProfile.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/component/SuspiciousStewEffects.java.patch28
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java.patch32
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ConsumeEffect.java.patch30
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/BlastingRecipe.java.patch35
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/CampfireCookingRecipe.java.patch35
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/CustomRecipe.java.patch54
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/Ingredient.java.patch66
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/Recipe.java.patch9
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeHolder.java.patch26
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeManager.java.patch111
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeMap.java.patch72
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/ShapedRecipe.java.patch86
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/ShapelessRecipe.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmeltingRecipe.java.patch35
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch61
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch69
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmokingRecipe.java.patch35
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/StonecutterRecipe.java.patch34
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/crafting/TransmuteRecipe.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/enchantment/ItemEnchantments.java.patch60
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ApplyMobEffect.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ChangeItemDamage.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/Ignite.java.patch36
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ReplaceBlock.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/ReplaceDisk.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java.patch35
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/trading/Merchant.java.patch17
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/item/trading/MerchantOffer.java.patch90
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/BaseCommandBlock.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/BaseSpawner.java.patch167
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/BlockGetter.java.patch90
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/ChunkPos.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/ClipContext.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/EmptyBlockGetter.java.patch22
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/EntityGetter.java.patch76
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/GameRules.java.patch321
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/Level.java.patch710
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/LevelAccessor.java.patch9
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/LevelReader.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/LevelWriter.java.patch13
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/NaturalSpawner.java.patch212
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/PathNavigationRegion.java.patch51
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/ServerExplosion.java.patch342
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/ServerLevelAccessor.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/StructureManager.java.patch38
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/biome/MobSpawnSettings.java.patch44
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/AbstractCandleBlock.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/AbstractCauldronBlock.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/AnvilBlock.java.patch13
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BambooSaplingBlock.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BambooStalkBlock.java.patch89
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BarrelBlock.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BaseFireBlock.java.patch85
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BasePressurePlateBlock.java.patch56
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BaseRailBlock.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BeaconBlock.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BedBlock.java.patch92
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BeehiveBlock.java.patch79
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BellBlock.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BigDripleafBlock.java.patch116
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BlastFurnaceBlock.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/Block.java.patch154
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BrewingStandBlock.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BubbleColumnBlock.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BuddingAmethystBlock.java.patch17
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/BushBlock.java.patch27
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/ButtonBlock.java.patch78
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CactusBlock.java.patch42
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CakeBlock.java.patch47
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CampfireBlock.java.patch25
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CartographyTableBlock.java.patch13
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CauldronBlock.java.patch56
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CaveVines.java.patch42
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CaveVinesBlock.java.patch22
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CeilingHangingSignBlock.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/ChangeOverTimeBlock.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/ChestBlock.java.patch118
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/ChorusFlowerBlock.java.patch73
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/ChorusPlantBlock.java.patch34
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CocoaBlock.java.patch33
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CommandBlock.java.patch37
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/ComparatorBlock.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/ComposterBlock.java.patch184
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/ConcretePowderBlock.java.patch76
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CoralBlock.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CoralFanBlock.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CoralPlantBlock.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CoralWallFanBlock.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CrafterBlock.java.patch89
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CraftingTableBlock.java.patch13
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/CropBlock.java.patch59
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DaylightDetectorBlock.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DecoratedPotBlock.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DetectorRailBlock.java.patch44
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DiodeBlock.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DirtPathBlock.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DispenserBlock.java.patch61
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DoorBlock.java.patch37
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DoubleBlockCombiner.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DoublePlantBlock.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DragonEggBlock.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DropExperienceBlock.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/DropperBlock.java.patch75
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/EndGatewayBlock.java.patch34
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/EndPortalBlock.java.patch91
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/EnderChestBlock.java.patch25
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/EyeblossomBlock.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/FarmBlock.java.patch112
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/FenceGateBlock.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/FireBlock.java.patch205
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/FlowerPotBlock.java.patch40
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/FrogspawnBlock.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/FrostedIceBlock.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/FungusBlock.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/FurnaceBlock.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/GrindstoneBlock.java.patch13
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/GrowingPlantHeadBlock.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/HoneyBlock.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/HopperBlock.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/HugeMushroomBlock.java.patch34
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/IceBlock.java.patch30
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/InfestedBlock.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/LavaCauldronBlock.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/LayeredCauldronBlock.java.patch127
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/LeavesBlock.java.patch25
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/LecternBlock.java.patch69
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/LeverBlock.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/LightBlock.java.patch18
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/LightningRodBlock.java.patch33
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/LiquidBlock.java.patch78
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/LoomBlock.java.patch13
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/MagmaBlock.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/MangrovePropaguleBlock.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/MultifaceSpreader.java.patch43
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/MushroomBlock.java.patch46
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/NetherPortalBlock.java.patch163
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/NetherWartBlock.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/NoteBlock.java.patch78
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/NyliumBlock.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/ObserverBlock.java.patch30
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/PitcherCropBlock.java.patch28
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/PointedDripstoneBlock.java.patch96
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/PowderSnowBlock.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/PoweredRailBlock.java.patch24
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/PressurePlateBlock.java.patch61
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/PumpkinBlock.java.patch36
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/RailState.java.patch52
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/RedStoneOreBlock.java.patch102
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/RedstoneLampBlock.java.patch35
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch88
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/RespawnAnchorBlock.java.patch46
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/RootedDirtBlock.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SaplingBlock.java.patch117
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/ScaffoldingBlock.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SculkBlock.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SculkCatalystBlock.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SculkSensorBlock.java.patch88
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SculkShriekerBlock.java.patch30
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SculkSpreader.java.patch98
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SculkVeinBlock.java.patch60
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch52
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SignBlock.java.patch58
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SmithingTableBlock.java.patch13
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SmokerBlock.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SnifferEggBlock.java.patch53
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SnowLayerBlock.java.patch14
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SpawnerBlock.java.patch26
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SpongeBlock.java.patch114
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SpreadingSnowyDirtBlock.java.patch25
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/StemBlock.java.patch47
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/StonecutterBlock.java.patch13
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SugarCaneBlock.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/SweetBerryBushBlock.java.patch62
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/TargetBlock.java.patch45
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/TntBlock.java.patch102
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/TrapDoorBlock.java.patch51
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/TripWireBlock.java.patch114
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/TripWireHookBlock.java.patch34
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/TurtleEggBlock.java.patch76
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/VineBlock.java.patch79
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/WallHangingSignBlock.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/WaterlilyBlock.java.patch27
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/WebBlock.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/WeightedPressurePlateBlock.java.patch49
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/WitherRoseBlock.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/WitherSkullBlock.java.patch50
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java.patch356
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BannerBlockEntity.java.patch81
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BarrelBlockEntity.java.patch52
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java.patch62
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch259
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch306
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BellBlockEntity.java.patch65
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BlockEntity.java.patch153
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BlockEntityType.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java.patch232
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BrushableBlockEntity.java.patch36
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CampfireBlockEntity.java.patch121
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ChestBlockEntity.java.patch51
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java.patch85
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch26
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch108
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch76
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CrafterBlockEntity.java.patch68
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/DecoratedPotBlockEntity.java.patch62
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/DispenserBlockEntity.java.patch50
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch322
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/JigsawBlockEntity.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java.patch92
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/LecternBlockEntity.java.patch164
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch29
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java.patch49
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java.patch25
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch67
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SignBlockEntity.java.patch269
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SkullBlockEntity.java.patch73
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java.patch63
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java.patch32
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerState.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java.patch83
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/grower/TreeGrower.java.patch126
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch180
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockBehaviour.java.patch193
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockState.java.patch19
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/EnumProperty.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/IntegerProperty.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/block/state/properties/Property.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/border/WorldBorder.java.patch99
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkAccess.java.patch88
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkGenerator.java.patch224
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java.patch175
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/DataLayer.java.patch7
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/EmptyLevelChunk.java.patch15
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/HashMapPalette.java.patch30
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/LevelChunk.java.patch409
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/LevelChunkSection.java.patch51
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/PalettedContainer.java.patch74
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/ProtoChunk.java.patch22
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/UpgradeData.java.patch47
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch84
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/ChunkStorage.java.patch158
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFile.java.patch127
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch67
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFileVersion.java.patch18
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch216
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java.patch57
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch212
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityAccess.java.patch25
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityLookup.java.patch17
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch276
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/gameevent/DynamicGameEventListener.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/gameevent/GameEvent.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java.patch52
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/DensityFunctions.java.patch52
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/FlatLevelSource.java.patch38
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch7
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch79
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch68
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch72
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/SpikeFeature.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/treedecorators/CocoaDecorator.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java.patch32
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructureCheck.java.patch50
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructurePiece.java.patch164
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch59
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java.patch93
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java.patch18
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java.patch68
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java.patch45
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java.patch37
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java.patch55
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/SwampHutPiece.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings.java.patch25
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java.patch182
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/material/FlowingFluid.java.patch157
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/material/FluidState.java.patch23
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/material/LavaFluid.java.patch53
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/material/WaterFluid.java.patch26
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/pathfinder/Path.java.patch10
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch16
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/portal/PortalForcer.java.patch149
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/portal/PortalShape.java.patch226
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/portal/TeleportTransition.java.patch71
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch11
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java.patch31
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/redstone/NeighborUpdater.java.patch50
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch216
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/storage/DimensionDataStorage.java.patch20
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/storage/LevelStorageSource.java.patch107
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/storage/PlayerDataStorage.java.patch143
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/storage/PrimaryLevelData.java.patch203
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/LootDataType.java.patch22
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/LootTable.java.patch85
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java.patch39
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java.patch21
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java.patch12
-rw-r--r--paper-server/patches/unapplied/net/minecraft/world/scores/ScoreboardSaveData.java.patch10
811 files changed, 66671 insertions, 0 deletions
diff --git a/paper-server/patches/unapplied/net/minecraft/ChatFormatting.java.patch b/paper-server/patches/unapplied/net/minecraft/ChatFormatting.java.patch
new file mode 100644
index 0000000000..eb700c93b5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/ChatFormatting.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/ChatFormatting.java
++++ b/net/minecraft/ChatFormatting.java
+@@ -112,6 +112,18 @@
+ return name == null ? null : FORMATTING_BY_NAME.get(cleanName(name));
+ }
+
++ // Paper start - add method to get by hex value
++ @Nullable public static ChatFormatting getByHexValue(int i) {
++ for (ChatFormatting value : values()) {
++ if (value.getColor() != null && value.getColor() == i) {
++ return value;
++ }
++ }
++
++ return null;
++ }
++ // Paper end - add method to get by hex value
++
+ @Nullable
+ public static ChatFormatting getById(int colorIndex) {
+ if (colorIndex < 0) {
diff --git a/paper-server/patches/unapplied/net/minecraft/CrashReport.java.patch b/paper-server/patches/unapplied/net/minecraft/CrashReport.java.patch
new file mode 100644
index 0000000000..0b8863cd41
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/CrashReport.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/CrashReport.java
++++ b/net/minecraft/CrashReport.java
+@@ -34,8 +34,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;
++ this.systemReport.setDetail("CraftBukkit Information", new org.bukkit.craftbukkit.CraftCrashReport()); // CraftBukkit
+ }
+
+ public String getTitle() {
+@@ -251,7 +253,7 @@
+ }
+
+ public static void preload() {
+- MemoryReserve.allocate();
++ // 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/unapplied/net/minecraft/CrashReportCategory.java.patch
new file mode 100644
index 0000000000..23f5297d2e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/CrashReportCategory.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/CrashReportCategory.java
++++ b/net/minecraft/CrashReportCategory.java
+@@ -110,6 +110,7 @@
+ } else {
+ this.stackTrace = new StackTraceElement[stackTraceElements.length - 3 - ignoredCallCount];
+ System.arraycopy(stackTraceElements, 3 + ignoredCallCount, this.stackTrace, 0, this.stackTrace.length);
++ this.stackTrace = io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateStacktrace(this.stackTrace); // Paper
+ return this.stackTrace.length;
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/Util.java.patch b/paper-server/patches/unapplied/net/minecraft/Util.java.patch
new file mode 100644
index 0000000000..378b82b378
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/Util.java.patch
@@ -0,0 +1,143 @@
+--- a/net/minecraft/Util.java
++++ b/net/minecraft/Util.java
+@@ -92,9 +92,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";
+- private static final TracingExecutor BACKGROUND_EXECUTOR = makeExecutor("Main");
++ private static final TracingExecutor BACKGROUND_EXECUTOR = makeExecutor("Main", -1); // Paper - Perf: add priority
+ private static final TracingExecutor IO_POOL = makeIoExecutor("IO-Worker-", false);
++ public static final TracingExecutor DIMENSION_DATA_IO_POOL = makeExtraIoExecutor("Dimension-Data-IO-Worker-"); // Paper - Separate dimension data IO pool
+ private static final TracingExecutor DOWNLOAD_POOL = makeIoExecutor("Download-", true);
++ // Paper start - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
++ public static final ExecutorService PROFILE_EXECUTOR = Executors.newFixedThreadPool(2, new java.util.concurrent.ThreadFactory() {
++
++ private final AtomicInteger count = new AtomicInteger();
++
++ @Override
++ public Thread newThread(Runnable run) {
++ Thread ret = new Thread(run);
++ ret.setName("Profile Lookup Executor #" + this.count.getAndIncrement());
++ ret.setUncaughtExceptionHandler((Thread thread, Throwable throwable) -> {
++ LOGGER.error("Uncaught exception in thread " + thread.getName(), throwable);
++ });
++ return ret;
++ }
++ });
++ // Paper end - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
+ 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 @@
+ }
+
+ public static long getNanos() {
+- return timeSource.getAsLong();
++ return System.nanoTime(); // Paper
+ }
+
+ public static long getEpochMillis() {
+@@ -147,15 +164,16 @@
+ 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;
++ // 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;
+ if (i <= 0) {
+ executorService = 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);
+ return forkJoinWorkerThread;
+ }, Util::onThreadException, true);
+- }
++ }*/
+
+ return new TracingExecutor(executorService);
+ }
+
+ public static int maxAllowedExecutorThreads() {
+- return Mth.clamp(Runtime.getRuntime().availableProcessors() - 1, 1, getMaxThreads());
++ // Paper start - Perf: use simpler thread pool that allows 1 thread and reduce worldgen thread worker count for low core count CPUs
++ final int cpus = Runtime.getRuntime().availableProcessors() / 2;
++ int maxExecutorThreads;
++ if (cpus <= 4) {
++ maxExecutorThreads = cpus <= 2 ? 1 : 2;
++ } else if (cpus <= 8) {
++ // [5, 8]
++ maxExecutorThreads = Math.max(3, cpus - 2);
++ } else {
++ maxExecutorThreads = cpus * 2 / 3;
++ }
++ maxExecutorThreads = Math.min(8, maxExecutorThreads);
++ return Integer.getInteger("Paper.WorkerThreadCount", maxExecutorThreads);
++ // Paper end - Perf: use simpler thread pool that allows 1 thread and reduce worldgen thread worker count for low core count CPUs
+ }
+
+ 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;
++ }));
++ }
++
++ // Paper start - Separate dimension data IO pool
++ private static TracingExecutor makeExtraIoExecutor(String namePrefix) {
++ AtomicInteger atomicInteger = new AtomicInteger(1);
++ return new TracingExecutor(Executors.newFixedThreadPool(4, runnable -> {
++ Thread thread = new Thread(runnable);
++ String string2 = namePrefix + atomicInteger.getAndIncrement();
++ TracyClient.setThreadName(string2, namePrefix.hashCode());
++ thread.setName(string2);
++ thread.setDaemon(false);
+ 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 void openUri(URI uri) {
+- try {
+- Process process = AccessController.doPrivileged(
+- (PrivilegedExceptionAction<Process>)(() -> Runtime.getRuntime().exec(this.getOpenUriArguments(uri)))
+- );
+- process.getInputStream().close();
+- process.getErrorStream().close();
+- process.getOutputStream().close();
+- } catch (IOException | PrivilegedActionException var3) {
+- Util.LOGGER.error("Couldn't open location '{}'", uri, var3);
+- }
++ throw new IllegalStateException("This method is not useful on dedicated servers."); // Paper - Fix warnings on build by removing client-only code
+ }
+
+ public void openFile(File file) {
diff --git a/paper-server/patches/unapplied/net/minecraft/advancements/AdvancementHolder.java.patch b/paper-server/patches/unapplied/net/minecraft/advancements/AdvancementHolder.java.patch
new file mode 100644
index 0000000000..d41639df88
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/advancements/AdvancementHolder.java.patch
@@ -0,0 +1,24 @@
+--- 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
new file mode 100644
index 0000000000..4bec8c46eb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/advancements/AdvancementTree.java.patch
@@ -0,0 +1,20 @@
+--- 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/advancements/DisplayInfo.java.patch b/paper-server/patches/unapplied/net/minecraft/advancements/DisplayInfo.java.patch
new file mode 100644
index 0000000000..a0fdd33f43
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/advancements/DisplayInfo.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/advancements/DisplayInfo.java
++++ b/net/minecraft/advancements/DisplayInfo.java
+@@ -37,6 +37,7 @@
+ private final boolean hidden;
+ private float x;
+ private float y;
++ public final io.papermc.paper.advancement.AdvancementDisplay paper = new io.papermc.paper.advancement.PaperAdvancementDisplay(this); // Paper - Add more advancement API
+
+ public DisplayInfo(
+ ItemStack icon,
diff --git a/paper-server/patches/unapplied/net/minecraft/advancements/critereon/LocationPredicate.java.patch b/paper-server/patches/unapplied/net/minecraft/advancements/critereon/LocationPredicate.java.patch
new file mode 100644
index 0000000000..7f7873fc42
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/advancements/critereon/LocationPredicate.java.patch
@@ -0,0 +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) {
+ 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
+ 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/unapplied/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch
new file mode 100644
index 0000000000..d080616002
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java.patch
@@ -0,0 +1,52 @@
+--- a/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java
++++ b/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java
+@@ -15,41 +15,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
+
+ @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
+ }
+
+ @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
+ if (set != null) {
+ set.remove(conditions);
+ if (set.isEmpty()) {
+- this.players.remove(manager);
++ manager.criterionData.remove(this); // Paper - fix AdvancementDataPlayer leak
+ }
+ }
+ }
+
+ @Override
+ public final void removePlayerListeners(PlayerAdvancements tracker) {
+- this.players.remove(tracker);
++ tracker.criterionData.remove(this); // Paper - fix AdvancementDataPlayer 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
+ 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
+ List<CriterionTrigger.Listener<T>> list = null;
+
+ for (CriterionTrigger.Listener<T> listener : set) {
+ T simpleInstance = listener.trigger();
+ if (predicate.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
+ if (list == null) {
+ list = Lists.newArrayList();
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/CommandSource.java.patch b/paper-server/patches/unapplied/net/minecraft/commands/CommandSource.java.patch
new file mode 100644
index 0000000000..4d74948c36
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/commands/CommandSource.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/commands/CommandSource.java
++++ b/net/minecraft/commands/CommandSource.java
+@@ -22,6 +22,13 @@
+ public boolean shouldInformAdmins() {
+ return false;
+ }
++
++ // CraftBukkit start
++ @Override
++ public org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper) {
++ return io.papermc.paper.brigadier.NullCommandSender.INSTANCE; // Paper - expose a no-op CommandSender
++ }
++ // CraftBukkit end
+ };
+
+ void sendSystemMessage(Component message);
+@@ -35,4 +42,6 @@
+ default boolean alwaysAccepts() {
+ return false;
+ }
++
++ org.bukkit.command.CommandSender getBukkitSender(CommandSourceStack wrapper); // CraftBukkit
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/CommandSourceStack.java.patch b/paper-server/patches/unapplied/net/minecraft/commands/CommandSourceStack.java.patch
new file mode 100644
index 0000000000..94ee38720d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/commands/CommandSourceStack.java.patch
@@ -0,0 +1,114 @@
+--- a/net/minecraft/commands/CommandSourceStack.java
++++ b/net/minecraft/commands/CommandSourceStack.java
+@@ -45,9 +45,9 @@
+ import net.minecraft.world.level.dimension.DimensionType;
+ 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 @@
+ 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 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 @@
+
+ @Override
+ public boolean hasPermission(int level) {
++ // CraftBukkit start
++ // Paper start - Thread Safe Vanilla Command permission checking
++ 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
++ }
++ // 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);
++ }
++ // Paper end - Fix permission levels for command blocks
++
++ // CraftBukkit start
++ public boolean hasPermission(int i, String bukkitPermission) {
++ // Paper start - Fix permission levels for command blocks
++ final java.util.function.BooleanSupplier hasBukkitPerm = () -> this.source == CommandSource.NULL /*treat NULL as having all bukkit perms*/ || this.getBukkitSender().hasPermission(bukkitPermission); // lazily check bukkit perms to the benefit of custom permission setups
++ // if the server is null, we must check the vanilla perm level system
++ // if ignoreVanillaPermissions is true, we can skip vanilla perms and just run the bukkit perm check
++ //noinspection ConstantValue
++ if (this.getServer() == null || !this.getServer().server.ignoreVanillaPermissions) { // server & level are null for command function loading
++ final boolean hasPermLevel = this.permissionLevel >= i;
++ if (this.forceRespectPermissionLevel()) { // NULL CommandSource and command blocks (if setting is enabled) should always pass the vanilla perm check
++ return hasPermLevel && hasBukkitPerm.getAsBoolean();
++ } else { // otherwise check vanilla perm first then bukkit perm, matching upstream behavior
++ return hasPermLevel || hasBukkitPerm.getAsBoolean();
++ }
++ }
++ 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);
+ }
+ }
+ }
+
+- 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);
+ }
+
+ }
+
+ public void sendFailure(Component message) {
++ // Paper start - Add UnknownCommandEvent
++ this.sendFailure(message, true);
++ }
++ public void sendFailure(Component message, boolean withStyle) {
++ // Paper end - Add UnknownCommandEvent
+ if (this.source.acceptsFailure() && !this.silent) {
+- 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 @@
+ public boolean isSilent() {
+ return this.silent;
+ }
++
++ // Paper start
++ @Override
++ public CommandSourceStack getHandle() {
++ return this;
++ }
++ // Paper end
++ // CraftBukkit start
++ public org.bukkit.command.CommandSender getBukkitSender() {
++ return this.source.getBukkitSender(this);
++ }
++ // CraftBukkit end
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/Commands.java.patch b/paper-server/patches/unapplied/net/minecraft/commands/Commands.java.patch
new file mode 100644
index 0000000000..1fc19d5217
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/commands/Commands.java.patch
@@ -0,0 +1,372 @@
+--- 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
new file mode 100644
index 0000000000..9c45cfda8f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/commands/arguments/EntityArgument.java.patch
@@ -0,0 +1,52 @@
+--- 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/MessageArgument.java.patch b/paper-server/patches/unapplied/net/minecraft/commands/arguments/MessageArgument.java.patch
new file mode 100644
index 0000000000..ba048d7f68
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/commands/arguments/MessageArgument.java.patch
@@ -0,0 +1,41 @@
+--- a/net/minecraft/commands/arguments/MessageArgument.java
++++ b/net/minecraft/commands/arguments/MessageArgument.java
+@@ -40,6 +40,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(MessageArgument.Message message, CommandContext<CommandSourceStack> context, String name, Consumer<PlayerChatMessage> callback) throws CommandSyntaxException {
++ // Paper end
+ CommandSourceStack commandSourceStack = context.getSource();
+ Component component = message.resolveComponent(commandSourceStack);
+ CommandSigningContext commandSigningContext = commandSourceStack.getSigningContext();
+@@ -54,17 +59,21 @@
+ private static void resolveSignedMessage(Consumer<PlayerChatMessage> callback, CommandSourceStack source, PlayerChatMessage message) {
+ MinecraftServer minecraftServer = source.getServer();
+ CompletableFuture<FilteredText> completableFuture = filterPlainText(source, message);
+- Component component = minecraftServer.getChatDecorator().decorate(source.getPlayer(), message.decoratedContent());
+- source.getChatMessageChainer().append(completableFuture, filtered -> {
+- PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(component).filter(filtered.mask());
++ // Paper start - support asynchronous chat decoration
++ CompletableFuture<Component> componentFuture = minecraftServer.getChatDecorator().decorate(source.getPlayer(), source, message.decoratedContent());
++ source.getChatMessageChainer().append(CompletableFuture.allOf(completableFuture, componentFuture), filtered -> {
++ PlayerChatMessage playerChatMessage2 = message.withUnsignedContent(componentFuture.join()).filter(completableFuture.join().mask());
++ // Paper end - support asynchronous chat decoration
+ callback.accept(playerChatMessage2);
+ });
+ }
+
+ private static void resolveDisguisedMessage(Consumer<PlayerChatMessage> callback, CommandSourceStack source, PlayerChatMessage message) {
+ ChatDecorator chatDecorator = source.getServer().getChatDecorator();
+- Component component = chatDecorator.decorate(source.getPlayer(), message.decoratedContent());
+- callback.accept(message.withUnsignedContent(component));
++ // Paper start - support asynchronous chat decoration
++ CompletableFuture<Component> componentFuture = chatDecorator.decorate(source.getPlayer(), source, message.decoratedContent());
++ source.getChatMessageChainer().append(componentFuture, (result) -> callback.accept(message.withUnsignedContent(result)));
++ // Paper end - support asynchronous chat decoration
+ }
+
+ private static CompletableFuture<FilteredText> filterPlainText(CommandSourceStack source, PlayerChatMessage message) {
diff --git a/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
new file mode 100644
index 0000000000..46961c4f64
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/commands/arguments/blocks/BlockStateParser.java.patch
@@ -0,0 +1,38 @@
+--- 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
new file mode 100644
index 0000000000..a20c412208
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/commands/arguments/item/ItemInput.java.patch
@@ -0,0 +1,10 @@
+--- 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/commands/arguments/selector/EntitySelector.java.patch b/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelector.java.patch
new file mode 100644
index 0000000000..cb23767a27
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelector.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/commands/arguments/selector/EntitySelector.java
++++ b/net/minecraft/commands/arguments/selector/EntitySelector.java
+@@ -93,7 +93,7 @@
+ }
+
+ private void checkPermissions(CommandSourceStack source) throws CommandSyntaxException {
+- if (this.usesSelector && !source.hasPermission(2)) {
++ if (!source.bypassSelectorPermissions && (this.usesSelector && !source.hasPermission(2, "minecraft.command.selector"))) { // CraftBukkit // Paper - add bypass for selector perms
+ throw EntityArgument.ERROR_SELECTORS_NOT_ALLOWED.create();
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch b/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch
new file mode 100644
index 0000000000..088ffce992
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/EntitySelectorParser.java.patch
@@ -0,0 +1,55 @@
+--- a/net/minecraft/commands/arguments/selector/EntitySelectorParser.java
++++ b/net/minecraft/commands/arguments/selector/EntitySelectorParser.java
+@@ -133,7 +133,7 @@
+ boolean flag;
+
+ 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);
+ }
+
+- 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 @@
+ };
+ }
+
+- protected void parseSelector() throws CommandSyntaxException {
+- this.usesSelectors = true;
++ // CraftBukkit start
++ protected void parseSelector(boolean overridePermissions) throws CommandSyntaxException {
++ this.usesSelectors = !overridePermissions;
++ // CraftBukkit end
+ this.suggestions = this::suggestSelector;
+ if (!this.reader.canRead()) {
+ throw EntitySelectorParser.ERROR_MISSING_SELECTOR_TYPE.createWithContext(this.reader);
+@@ -505,6 +507,12 @@
+ }
+
+ public EntitySelector parse() throws CommandSyntaxException {
++ // CraftBukkit start
++ return this.parse(false);
++ }
++
++ public EntitySelector parse(boolean overridePermissions) throws CommandSyntaxException {
++ // CraftBukkit end
+ this.startPosition = this.reader.getCursor();
+ this.suggestions = this::suggestNameOrSelector;
+ if (this.reader.canRead() && this.reader.peek() == '@') {
+@@ -513,7 +521,7 @@
+ }
+
+ this.reader.skip();
+- this.parseSelector();
++ this.parseSelector(overridePermissions); // CraftBukkit
+ } else {
+ this.parseNameOrUUID();
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch b/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch
new file mode 100644
index 0000000000..ed6f4da14b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/commands/arguments/selector/SelectorPattern.java.patch
@@ -0,0 +1,11 @@
+--- 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()));
+ } catch (CommandSyntaxException var2) {
+- return DataResult.error(() -> "Invalid selector component: " + selector + ": " + 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/unapplied/net/minecraft/core/BlockPos.java.patch
new file mode 100644
index 0000000000..7858f6cef1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/BlockPos.java.patch
@@ -0,0 +1,186 @@
+--- a/net/minecraft/core/BlockPos.java
++++ b/net/minecraft/core/BlockPos.java
+@@ -158,67 +158,84 @@
+
+ @Override
+ public BlockPos above() {
+- return this.relative(Direction.UP);
++ return new BlockPos(this.getX(), this.getY() + 1, this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos above(int distance) {
+- return this.relative(Direction.UP, distance);
++ return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY() + distance, this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos below() {
+- return this.relative(Direction.DOWN);
++ return new BlockPos(this.getX(), this.getY() - 1, this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @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
+ }
+
+ @Override
+ public BlockPos north() {
+- return this.relative(Direction.NORTH);
++ return new BlockPos(this.getX(), this.getY(), this.getZ() - 1); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos north(int distance) {
+- return this.relative(Direction.NORTH, distance);
++ return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY(), this.getZ() - distance); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos south() {
+- return this.relative(Direction.SOUTH);
++ return new BlockPos(this.getX(), this.getY(), this.getZ() + 1); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos south(int distance) {
+- return this.relative(Direction.SOUTH, distance);
++ return distance == 0 ? this.immutable() : new BlockPos(this.getX(), this.getY(), this.getZ() + distance); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos west() {
+- return this.relative(Direction.WEST);
++ return new BlockPos(this.getX() - 1, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos west(int distance) {
+- return this.relative(Direction.WEST, distance);
++ return distance == 0 ? this.immutable() : new BlockPos(this.getX() - distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos east() {
+- return this.relative(Direction.EAST);
++ return new BlockPos(this.getX() + 1, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos east(int distance) {
+- return this.relative(Direction.EAST, distance);
++ return distance == 0 ? this.immutable() : new BlockPos(this.getX() + distance, this.getY(), this.getZ()); // Paper - Perf: Optimize BlockPosition
+ }
+
+ @Override
+ public BlockPos relative(Direction direction) {
++ // Paper start - Perf: Optimize BlockPosition
++ switch(direction) {
++ case UP:
++ return new BlockPos(this.getX(), this.getY() + 1, this.getZ());
++ case DOWN:
++ return new BlockPos(this.getX(), this.getY() - 1, this.getZ());
++ case NORTH:
++ return new BlockPos(this.getX(), this.getY(), this.getZ() - 1);
++ case SOUTH:
++ return new BlockPos(this.getX(), this.getY(), this.getZ() + 1);
++ case WEST:
++ return new BlockPos(this.getX() - 1, this.getY(), this.getZ());
++ 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());
++ }
++ // 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 @@
+ }
+
+ public BlockPos.MutableBlockPos set(int x, int y, int z) {
+- this.setX(x);
+- this.setY(y);
+- this.setZ(z);
++ this.x = x; // Paper - Perf: Manually inline methods in BlockPosition
++ this.y = y; // Paper - Perf: Manually inline methods in BlockPosition
++ this.z = z; // Paper - Perf: Manually inline methods in BlockPosition
+ return this;
+ }
+
+@@ -636,19 +655,19 @@
+
+ @Override
+ public BlockPos.MutableBlockPos setX(int i) {
+- super.setX(i);
++ this.x = i; // 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
+ return this;
+ }
+
+ @Override
+ public BlockPos.MutableBlockPos setZ(int i) {
+- super.setZ(i);
++ this.z = i; // 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/unapplied/net/minecraft/core/Direction.java.patch
new file mode 100644
index 0000000000..a0a77c5f10
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/Direction.java.patch
@@ -0,0 +1,46 @@
+--- a/net/minecraft/core/Direction.java
++++ b/net/minecraft/core/Direction.java
+@@ -57,6 +57,12 @@
+ .sorted(Comparator.comparingInt(direction -> direction.data2d))
+ .toArray(Direction[]::new);
+
++ // Paper start - Perf: Inline shift direction fields
++ private final int adjX;
++ private final int adjY;
++ private final int adjZ;
++ // 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);
++ // Paper start - Perf: Inline shift direction fields
++ this.adjX = vector.getX();
++ this.adjY = vector.getY();
++ this.adjZ = vector.getZ();
++ // Paper end - Perf: Inline shift direction fields
+ }
+
+ public static Direction[] orderedByNearest(Entity entity) {
+@@ -247,15 +258,15 @@
+ }
+
+ public int getStepX() {
+- return this.normal.getX();
++ return this.adjX; // Paper - Perf: Inline shift direction fields
+ }
+
+ public int getStepY() {
+- return this.normal.getY();
++ return this.adjY; // Paper - Perf: Inline shift direction fields
+ }
+
+ public int getStepZ() {
+- return this.normal.getZ();
++ return this.adjZ; // Paper - Perf: Inline shift direction fields
+ }
+
+ public Vector3f step() {
diff --git a/paper-server/patches/unapplied/net/minecraft/core/Holder.java.patch b/paper-server/patches/unapplied/net/minecraft/core/Holder.java.patch
new file mode 100644
index 0000000000..634a2b2190
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/Holder.java.patch
@@ -0,0 +1,11 @@
+--- 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/MappedRegistry.java.patch b/paper-server/patches/unapplied/net/minecraft/core/MappedRegistry.java.patch
new file mode 100644
index 0000000000..4d3c8b552e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/MappedRegistry.java.patch
@@ -0,0 +1,33 @@
+--- a/net/minecraft/core/MappedRegistry.java
++++ b/net/minecraft/core/MappedRegistry.java
+@@ -33,11 +33,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);
+- private final Reference2IntMap<T> toId = Util.make(new Reference2IntOpenHashMap<>(), map -> map.defaultReturnValue(-1));
+- private final Map<ResourceLocation, Holder.Reference<T>> byLocation = new HashMap<>();
+- private final Map<ResourceKey<T>, Holder.Reference<T>> byKey = new HashMap<>();
+- private final Map<T, Holder.Reference<T>> byValue = new IdentityHashMap<>();
+- private final Map<ResourceKey<T>, RegistrationInfo> registrationInfos = new IdentityHashMap<>();
++ private final Reference2IntMap<T> toId = Util.make(new Reference2IntOpenHashMap<>(2048), map -> map.defaultReturnValue(-1)); // Paper - Perf: Use bigger expected size to reduce collisions
++ private final Map<ResourceLocation, Holder.Reference<T>> byLocation = new HashMap<>(2048); // Paper - Perf: Use bigger expected size to reduce collisions
++ private final Map<ResourceKey<T>, Holder.Reference<T>> byKey = new HashMap<>(2048); // Paper - Perf: Use bigger expected size to reduce collisions
++ private final Map<T, Holder.Reference<T>> byValue = new IdentityHashMap<>(2048); // Paper - Perf: Use bigger expected size to reduce collisions
++ private final Map<ResourceKey<T>, RegistrationInfo> registrationInfos = new IdentityHashMap<>(2048); // Paper - Perf: Use bigger expected size to reduce collisions
+ 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);
+
+ 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/unapplied/net/minecraft/core/Rotations.java.patch
new file mode 100644
index 0000000000..29c5bfc220
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/Rotations.java.patch
@@ -0,0 +1,21 @@
+--- 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));
+ }
+
++ // Paper start - faster alternative constructor
++ private Rotations(float x, float y, float z, Void dummy_var) {
++ this.x = x;
++ this.y = y;
++ this.z = z;
++ }
++
++ public static Rotations createWithoutValidityChecks(float x, float y, float z) {
++ return new Rotations(x, y, z, null);
++ }
++ // Paper end - faster alternative constructor
++
+ public ListTag save() {
+ ListTag listTag = new ListTag();
+ listTag.add(FloatTag.valueOf(this.x));
diff --git a/paper-server/patches/unapplied/net/minecraft/core/Vec3i.java.patch b/paper-server/patches/unapplied/net/minecraft/core/Vec3i.java.patch
new file mode 100644
index 0000000000..8c19b63ae6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/Vec3i.java.patch
@@ -0,0 +1,49 @@
+--- 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())
+ );
+ public static final Vec3i ZERO = new Vec3i(0, 0, 0);
+- private int x;
+- private int y;
+- private int z;
++ protected int x; // Paper - Perf: Manually inline methods in BlockPosition; protected
++ 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) {
+ return CODEC.validate(
+@@ -35,12 +35,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();
+ }
+
+ @Override
+- public int hashCode() {
++ public final int hashCode() { // Paper - Perf: Final for inline
+ return (this.getY() + this.getZ() * 31) * 31 + this.getX();
+ }
+
+@@ -53,15 +53,15 @@
+ }
+ }
+
+- public int getX() {
++ public final int getX() { // Paper - Perf: Final for inline
+ return this.x;
+ }
+
+- public int getY() {
++ public final int getY() { // Paper - Perf: Final for inline
+ return this.y;
+ }
+
+- public int getZ() {
++ public final int getZ() { // Paper - Perf: Final for inline
+ return this.z;
+ }
+
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
new file mode 100644
index 0000000000..f0db760867
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/cauldron/CauldronInteraction.java.patch
@@ -0,0 +1,330 @@
+--- 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/component/DataComponentPatch.java.patch b/paper-server/patches/unapplied/net/minecraft/core/component/DataComponentPatch.java.patch
new file mode 100644
index 0000000000..53846b237e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/component/DataComponentPatch.java.patch
@@ -0,0 +1,70 @@
+--- a/net/minecraft/core/component/DataComponentPatch.java
++++ b/net/minecraft/core/component/DataComponentPatch.java
+@@ -61,7 +61,7 @@
+ }
+ }
+
+- 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);
++ // Paper start - codec errors of random anonymous classes are useless
++ try {
++ type.streamCodec().encode(buf, (T) value); // CraftBukkit - decompile error
++ } catch (final Exception e) {
++ throw new RuntimeException("Error encoding component " + type, 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();
+
+ Builder() {}
++
++ // CraftBukkit start
++ public void copy(DataComponentPatch orig) {
++ this.map.putAll(orig.map);
++ }
++
++ public void clear(DataComponentType<?> type) {
++ this.map.remove(type);
++ }
++
++ public boolean isSet(DataComponentType<?> type) {
++ return this.map.containsKey(type);
++ }
++
++ public boolean isEmpty() {
++ return this.map.isEmpty();
++ }
+
++ @Override
++ public boolean equals(Object object) {
++ if (this == object) {
++ return true;
++ }
++
++ if (object instanceof DataComponentPatch.Builder patch) {
++ return this.map.equals(patch.map);
++ }
++
++ return false;
++ }
++
++ @Override
++ public int hashCode() {
++ 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;
diff --git a/paper-server/patches/unapplied/net/minecraft/core/component/DataComponents.java.patch b/paper-server/patches/unapplied/net/minecraft/core/component/DataComponents.java.patch
new file mode 100644
index 0000000000..eb470da08e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/component/DataComponents.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/core/component/DataComponents.java
++++ b/net/minecraft/core/component/DataComponents.java
+@@ -180,10 +180,10 @@
+ "map_post_processing", builder -> builder.networkSynchronized(MapPostProcessing.STREAM_CODEC)
+ );
+ public static final DataComponentType<ChargedProjectiles> CHARGED_PROJECTILES = register(
+- "charged_projectiles", builder -> builder.persistent(ChargedProjectiles.CODEC).networkSynchronized(ChargedProjectiles.STREAM_CODEC).cacheEncoding()
++ "charged_projectiles", builder -> builder.persistent(ChargedProjectiles.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.CHARGED_PROJECTILES).cacheEncoding() // Paper - sanitize charged projectiles
+ );
+ public static final DataComponentType<BundleContents> BUNDLE_CONTENTS = register(
+- "bundle_contents", builder -> builder.persistent(BundleContents.CODEC).networkSynchronized(BundleContents.STREAM_CODEC).cacheEncoding()
++ "bundle_contents", builder -> builder.persistent(BundleContents.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.BUNDLE_CONTENTS).cacheEncoding() // Paper - sanitize bundle contents
+ );
+ public static final DataComponentType<PotionContents> POTION_CONTENTS = register(
+ "potion_contents", builder -> builder.persistent(PotionContents.CODEC).networkSynchronized(PotionContents.STREAM_CODEC).cacheEncoding()
+@@ -250,7 +250,7 @@
+ "pot_decorations", builder -> builder.persistent(PotDecorations.CODEC).networkSynchronized(PotDecorations.STREAM_CODEC).cacheEncoding()
+ );
+ public static final DataComponentType<ItemContainerContents> CONTAINER = register(
+- "container", builder -> builder.persistent(ItemContainerContents.CODEC).networkSynchronized(ItemContainerContents.STREAM_CODEC).cacheEncoding()
++ "container", builder -> builder.persistent(ItemContainerContents.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.CONTAINER).cacheEncoding() // Paper - sanitize container contents
+ );
+ public static final DataComponentType<BlockItemStateProperties> BLOCK_STATE = register(
+ "block_state", builder -> builder.persistent(BlockItemStateProperties.CODEC).networkSynchronized(BlockItemStateProperties.STREAM_CODEC).cacheEncoding()
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
new file mode 100644
index 0000000000..a97acf2799
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/dispenser/BoatDispenseItemBehavior.java.patch
@@ -0,0 +1,58 @@
+--- 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
new file mode 100644
index 0000000000..9a3359fc85
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/dispenser/DefaultDispenseItemBehavior.java.patch
@@ -0,0 +1,126 @@
+--- 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
new file mode 100644
index 0000000000..824b11e0d1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/dispenser/DispenseItemBehavior.java.patch
@@ -0,0 +1,651 @@
+--- 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
new file mode 100644
index 0000000000..18eb499fba
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/dispenser/EquipmentDispenseItemBehavior.java.patch
@@ -0,0 +1,82 @@
+--- 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
new file mode 100644
index 0000000000..ea22a6dd42
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/dispenser/MinecartDispenseItemBehavior.java.patch
@@ -0,0 +1,58 @@
+--- 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
new file mode 100644
index 0000000000..2061a38275
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/dispenser/ProjectileDispenseBehavior.java.patch
@@ -0,0 +1,58 @@
+--- 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
new file mode 100644
index 0000000000..bc579690dd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java.patch
@@ -0,0 +1,81 @@
+--- 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
new file mode 100644
index 0000000000..bfa73fef42
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/dispenser/ShulkerBoxDispenseBehavior.java.patch
@@ -0,0 +1,54 @@
+--- 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/core/registries/BuiltInRegistries.java.patch b/paper-server/patches/unapplied/net/minecraft/core/registries/BuiltInRegistries.java.patch
new file mode 100644
index 0000000000..d363add98b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/core/registries/BuiltInRegistries.java.patch
@@ -0,0 +1,52 @@
+--- a/net/minecraft/core/registries/BuiltInRegistries.java
++++ b/net/minecraft/core/registries/BuiltInRegistries.java
+@@ -296,6 +296,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;
++ // Paper start - add built-in registry conversions
++ public static final io.papermc.paper.registry.data.util.Conversions BUILT_IN_CONVERSIONS = new io.papermc.paper.registry.data.util.Conversions(new net.minecraft.resources.RegistryOps.RegistryInfoLookup() {
++ @Override
++ public <T> java.util.Optional<net.minecraft.resources.RegistryOps.RegistryInfo<T>> lookup(final ResourceKey<? extends Registry<? extends T>> registryRef) {
++ final Registry<T> registry = net.minecraft.server.RegistryLayer.STATIC_ACCESS.lookupOrThrow(registryRef);
++ return java.util.Optional.of(
++ new net.minecraft.resources.RegistryOps.RegistryInfo<>(registry, registry, Lifecycle.experimental())
++ );
++ }
++ });
++ // 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
+ ) {
+ 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;
+ }
+
+ public static void bootStrap() {
++ // Paper start
++ bootStrap(() -> {});
++ }
++ public static void bootStrap(Runnable runnable) {
++ // Paper end
++ REGISTRY.freeze(); // Paper - freeze main registry early
+ createContents();
++ runnable.run(); // Paper
+ freeze();
+ validate(REGISTRY);
+ }
+@@ -348,6 +367,7 @@
+
+ for (Registry<?> registry : REGISTRY) {
+ bindBootstrappedTagsToEmpty(registry);
++ io.papermc.paper.registry.PaperRegistryListenerManager.INSTANCE.runFreezeListeners(registry.key(), BUILT_IN_CONVERSIONS); // Paper
+ registry.freeze();
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch b/paper-server/patches/unapplied/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch
new file mode 100644
index 0000000000..02e103aaf6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/data/loot/packs/VanillaChestLoot.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/data/loot/packs/VanillaChestLoot.java
++++ b/net/minecraft/data/loot/packs/VanillaChestLoot.java
+@@ -946,7 +946,6 @@
+ .add(
+ LootItem.lootTableItem(Items.COMPASS)
+ .apply(SetItemCountFunction.setCount(ConstantValue.exactly(1.0F)))
+- .apply(SetItemDamageFunction.setDamage(UniformGenerator.between(0.15F, 0.8F)))
+ .setWeight(1)
+ )
+ .add(LootItem.lootTableItem(Items.BUCKET).apply(SetItemCountFunction.setCount(UniformGenerator.between(1.0F, 2.0F))).setWeight(1))
diff --git a/paper-server/patches/unapplied/net/minecraft/nbt/ByteArrayTag.java.patch b/paper-server/patches/unapplied/net/minecraft/nbt/ByteArrayTag.java.patch
new file mode 100644
index 0000000000..d7c707925b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/nbt/ByteArrayTag.java.patch
@@ -0,0 +1,15 @@
+--- 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/CompoundTag.java.patch b/paper-server/patches/unapplied/net/minecraft/nbt/CompoundTag.java.patch
new file mode 100644
index 0000000000..1cb8cb2d7b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/nbt/CompoundTag.java.patch
@@ -0,0 +1,74 @@
+--- a/net/minecraft/nbt/CompoundTag.java
++++ b/net/minecraft/nbt/CompoundTag.java
+@@ -49,7 +49,7 @@
+
+ private static CompoundTag loadCompound(DataInput input, NbtAccounter tracker) throws IOException {
+ tracker.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 @@
+ }
+
+ public CompoundTag() {
+- this(Maps.newHashMap());
++ this(new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(8, 0.8f)); // Paper - Reduce memory footprint of CompoundTag
+ }
+
+ @Override
+@@ -232,14 +232,34 @@
+ }
+
+ public void putUUID(String key, UUID value) {
++ // Paper start - Support old UUID format
++ if (this.contains(key + "Most", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC) && this.contains(key + "Least", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
++ this.tags.remove(key + "Most");
++ this.tags.remove(key + "Least");
++ }
++ // Paper end - Support old UUID format
+ this.tags.put(key, NbtUtils.createUUID(value));
+ }
+
++
++ /**
++ * You must use {@link #hasUUID(String)} before or else it <b>will</b> throw an NPE.
++ */
+ public UUID getUUID(String key) {
++ // Paper start - Support old UUID format
++ if (!contains(key, 11) && this.contains(key + "Most", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC) && this.contains(key + "Least", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
++ return new UUID(this.getLong(key + "Most"), this.getLong(key + "Least"));
++ }
++ // Paper end - Support old UUID format
+ return NbtUtils.loadUUID(this.get(key));
+ }
+
+ public boolean hasUUID(String key) {
++ // Paper start - Support old UUID format
++ if (this.contains(key + "Most", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC) && this.contains(key + "Least", net.minecraft.nbt.Tag.TAG_ANY_NUMERIC)) {
++ return true;
++ }
++ // Paper end - Support old UUID format
+ Tag tag = this.get(key);
+ return tag != null && tag.getType() == IntArrayTag.TYPE && ((IntArrayTag)tag).getAsIntArray().length == 4;
+ }
+@@ -477,8 +497,16 @@
+
+ @Override
+ public CompoundTag copy() {
+- Map<String, Tag> map = Maps.newHashMap(Maps.transformValues(this.tags, Tag::copy));
+- return new CompoundTag(map);
++ // Paper start - Reduce memory footprint of CompoundTag
++ it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<String, Tag> ret = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(this.tags.size(), 0.8f);
++ java.util.Iterator<java.util.Map.Entry<String, Tag>> iterator = (this.tags instanceof it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap) ? ((it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap)this.tags).object2ObjectEntrySet().fastIterator() : this.tags.entrySet().iterator();
++ while (iterator.hasNext()) {
++ Map.Entry<String, Tag> entry = iterator.next();
++ ret.put(entry.getKey(), entry.getValue().copy());
++ }
++
++ return new CompoundTag(ret);
++ // Paper end - Reduce memory footprint of CompoundTag
+ }
+
+ @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/nbt/IntArrayTag.java.patch b/paper-server/patches/unapplied/net/minecraft/nbt/IntArrayTag.java.patch
new file mode 100644
index 0000000000..97872e3339
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/nbt/IntArrayTag.java.patch
@@ -0,0 +1,15 @@
+--- 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
new file mode 100644
index 0000000000..f6dc9e632c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/nbt/NbtIo.java.patch
@@ -0,0 +1,20 @@
+--- 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/nbt/NbtUtils.java.patch b/paper-server/patches/unapplied/net/minecraft/nbt/NbtUtils.java.patch
new file mode 100644
index 0000000000..a126b25999
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/nbt/NbtUtils.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/nbt/NbtUtils.java
++++ b/net/minecraft/nbt/NbtUtils.java
+@@ -149,8 +149,10 @@
+ if (!nbt.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));
++ // 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();
++ // Paper end - Validate resource location
+ if (optional.isEmpty()) {
+ return Blocks.AIR.defaultBlockState();
+ } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/nbt/TagParser.java.patch b/paper-server/patches/unapplied/net/minecraft/nbt/TagParser.java.patch
new file mode 100644
index 0000000000..dd6b67ed47
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/nbt/TagParser.java.patch
@@ -0,0 +1,69 @@
+--- a/net/minecraft/nbt/TagParser.java
++++ b/net/minecraft/nbt/TagParser.java
+@@ -49,6 +49,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 CompoundTag readStruct() throws CommandSyntaxException {
+ this.expect('{');
++ this.increaseDepth(); // Paper
+ CompoundTag compoundTag = new CompoundTag();
+ this.reader.skipWhitespace();
+
+@@ -182,6 +184,7 @@
+ }
+
+ this.expect('}');
++ this.depth--; // Paper
+ return compoundTag;
+ }
+
+@@ -191,6 +194,7 @@
+ if (!this.reader.canRead()) {
+ throw ERROR_EXPECTED_VALUE.createWithContext(this.reader);
+ } else {
++ this.increaseDepth(); // Paper
+ ListTag listTag = new ListTag();
+ TagType<?> tagType = null;
+
+@@ -216,6 +220,7 @@
+ }
+
+ this.expect(']');
++ this.depth--; // Paper
+ 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 @@
+ this.reader.skipWhitespace();
+ this.reader.expect(c);
+ }
++
++ 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/unapplied/net/minecraft/network/Connection.java.patch
new file mode 100644
index 0000000000..f1e414f6b1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/Connection.java.patch
@@ -0,0 +1,330 @@
+--- 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
+ });
+ private static final ProtocolInfo<ServerHandshakePacketListener> INITIAL_PROTOCOL = HandshakeProtocols.SERVERBOUND;
+ private final PacketFlow receiving;
+@@ -96,6 +96,11 @@
+ private final Queue<Consumer<Connection>> pendingActions = Queues.newConcurrentLinkedQueue();
+ public Channel channel;
+ public SocketAddress address;
++ // Spigot Start
++ public java.util.UUID spoofedUUID;
++ public com.mojang.authlib.properties.Property[] spoofedProfile;
++ public boolean preparing = true;
++ // Spigot End
+ @Nullable
+ private volatile PacketListener disconnectListener;
+ @Nullable
+@@ -114,7 +119,42 @@
+ private volatile DisconnectionDetails delayedDisconnect;
+ @Nullable
+ BandwidthDebugMonitor bandwidthDebugMonitor;
++ public String hostname = ""; // CraftBukkit - add field
++ // Paper start - NetworkClient implementation
++ public int protocolVersion;
++ 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) {
++ return impl.player;
++ } else if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) {
++ org.bukkit.craftbukkit.entity.CraftPlayer player = impl.getCraftPlayer();
++ return player == null ? null : player.getHandle();
++ }
++ return null;
++ }
++ // Paper end - add utility methods
++ // Paper start - packet limiter
++ protected final Object PACKET_LIMIT_LOCK = new Object();
++ protected final @Nullable io.papermc.paper.util.IntervalledCounter allPacketCounts = io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.isEnabled() ? new io.papermc.paper.util.IntervalledCounter(
++ (long)(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.interval() * 1.0e9)
++ ) : null;
++ protected final java.util.Map<Class<? extends net.minecraft.network.protocol.Packet<?>>, io.papermc.paper.util.IntervalledCounter> packetSpecificLimits = new java.util.HashMap<>();
++
++ private boolean stopReadingPackets;
++ private void killForPacketSpam() {
++ this.sendPacket(new ClientboundDisconnectPacket(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.kickMessage)), PacketSendListener.thenRun(() -> {
++ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.kickMessage));
++ }), true);
++ this.setReadOnly();
++ this.stopReadingPackets = true;
++ }
++ // 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();
+ this.address = this.channel.remoteAddress();
++ // Spigot Start
++ this.preparing = false;
++ // Spigot End
+ if (this.delayedDisconnect != null) {
+ this.disconnect(this.delayedDisconnect);
+ }
+@@ -134,6 +177,21 @@
+ }
+
+ public void exceptionCaught(ChannelHandlerContext channelhandlercontext, Throwable throwable) {
++ // Paper start - Handle large packets disconnecting client
++ if (throwable instanceof io.netty.handler.codec.EncoderException && throwable.getCause() instanceof PacketEncoder.PacketTooLargeException packetTooLargeException) {
++ final Packet<?> packet = packetTooLargeException.getPacket();
++ if (packet.packetTooLarge(this)) {
++ ProtocolSwapHandler.handleOutboundTerminalPacket(channelhandlercontext, packet);
++ return;
++ } else if (packet.isSkippable()) {
++ Connection.LOGGER.debug("Skipping packet due to errors", throwable.getCause());
++ ProtocolSwapHandler.handleOutboundTerminalPacket(channelhandlercontext, packet);
++ return;
++ } else {
++ throwable = throwable.getCause();
++ }
++ }
++ // Paper end - Handle large packets disconnecting client
+ if (throwable instanceof SkipPacketException) {
+ Connection.LOGGER.debug("Skipping packet due to errors", throwable.getCause());
+ } else {
+@@ -141,8 +199,10 @@
+
+ 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 (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"));
+ } else {
+ MutableComponent ichatmutablecomponent = Component.translatable("disconnect.genericReason", "Internal Exception: " + String.valueOf(throwable));
+@@ -155,9 +215,11 @@
+ disconnectiondetails = new DisconnectionDetails(ichatmutablecomponent);
+ }
+
++ 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);
+- 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 @@
+
+ }
+ }
++ if (net.minecraft.server.MinecraftServer.getServer().isDebugging()) io.papermc.paper.util.TraceUtil.printStackTrace(throwable); // Spigot // Paper
+ }
+
+ protected void channelRead0(ChannelHandlerContext channelhandlercontext, Packet<?> packet) {
+@@ -185,11 +248,61 @@
+ if (packetlistener == null) {
+ throw new IllegalStateException("Received a packet before the packet listener was initialized");
+ } else {
++ // Paper start - packet limiter
++ if (this.stopReadingPackets) {
++ return;
++ }
++ if (this.allPacketCounts != null ||
++ io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.containsKey(packet.getClass())) {
++ long time = System.nanoTime();
++ synchronized (PACKET_LIMIT_LOCK) {
++ if (this.allPacketCounts != null) {
++ this.allPacketCounts.updateAndAdd(1, time);
++ if (this.allPacketCounts.getRate() >= io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.allPackets.maxPacketRate()) {
++ this.killForPacketSpam();
++ return;
++ }
++ }
++
++ for (Class<?> check = packet.getClass(); check != Object.class; check = check.getSuperclass()) {
++ io.papermc.paper.configuration.GlobalConfiguration.PacketLimiter.PacketLimit packetSpecificLimit =
++ io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.get(check);
++ if (packetSpecificLimit == null || !packetSpecificLimit.isEnabled()) {
++ continue;
++ }
++ io.papermc.paper.util.IntervalledCounter counter = this.packetSpecificLimits.computeIfAbsent((Class)check, (clazz) -> {
++ return new io.papermc.paper.util.IntervalledCounter((long)(packetSpecificLimit.interval() * 1.0e9));
++ });
++ counter.updateAndAdd(1, time);
++ if (counter.getRate() >= packetSpecificLimit.maxPacketRate()) {
++ switch (packetSpecificLimit.action()) {
++ case DROP:
++ return;
++ case KICK:
++ String deobfedPacketName = io.papermc.paper.util.ObfHelper.INSTANCE.deobfClassName(check.getName());
++
++ String playerName;
++ if (this.packetListener instanceof net.minecraft.server.network.ServerCommonPacketListenerImpl impl) {
++ playerName = impl.getOwner().getName();
++ } else {
++ playerName = this.getLoggableAddress(net.minecraft.server.MinecraftServer.getServer().logIPs());
++ }
++
++ Connection.LOGGER.warn("{} kicked for packet spamming: {}", playerName, deobfedPacketName.substring(deobfedPacketName.lastIndexOf(".") + 1));
++ this.killForPacketSpam();
++ return;
++ }
++ }
++ }
++ }
++ }
++ // Paper end - packet limiter
+ if (packetlistener.shouldHandleMessage(packet)) {
+ try {
+ Connection.genericsFtw(packet, packetlistener);
+ } catch (RunningOnDifferentThreadException cancelledpackethandleexception) {
+ ;
++ } 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 @@
+ }
+ }
+
++ 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
++ private static int currTick; // Paper - Buffer joins to world
+ public void tick() {
+ this.flushQueue();
++ // Paper start - Buffer joins to world
++ if (Connection.currTick != net.minecraft.server.MinecraftServer.currentTick) {
++ Connection.currTick = net.minecraft.server.MinecraftServer.currentTick;
++ Connection.joinAttemptsThisTick = 0;
++ }
++ // Paper end - Buffer joins to world
+ PacketListener packetlistener = this.packetListener;
+
+ if (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 end - Buffer joins to world
+ }
+
+ if (!this.isConnected() && !this.disconnectionHandled) {
+@@ -431,7 +558,7 @@
+ }
+
+ if (this.channel != null) {
+- this.channel.flush();
++ if (enableExplicitFlush) this.channel.eventLoop().execute(() -> this.channel.flush()); // Paper - Disable explicit network manager flushing; we don't need to explicit flush here, but allow opt in incase issues are found to a better version
+ }
+
+ if (this.tickCount++ % 20 == 0) {
+@@ -464,12 +591,15 @@
+ }
+
+ public void disconnect(DisconnectionDetails disconnectionInfo) {
++ // Spigot Start
++ this.preparing = false;
++ // Spigot End
+ if (this.channel == null) {
+ this.delayedDisconnect = disconnectionInfo;
+ }
+
+ 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;
+ }
+
+@@ -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 @@
+
+ }
+
++ // Paper start - add proper async disconnect
++ public void enableAutoRead() {
++ if (this.channel != null) {
++ this.channel.config().setAutoRead(true);
++ }
++ }
++ // 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 @@
+ } else {
+ this.channel.pipeline().addAfter("prepender", "compress", new CompressionEncoder(compressionThreshold));
+ }
++ 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 @@
+ 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);
+ }
++ 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();
++ new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(profile.getId(),
++ profile.getName(), ((InetSocketAddress) this.address).getAddress(), false).callEvent();
++ } else if (packetListener instanceof net.minecraft.server.network.ServerLoginPacketListenerImpl loginListener) {
++ /* Player is login stage */
++ switch (loginListener.state) {
++ case VERIFYING:
++ case WAITING_FOR_DUPE_DISCONNECT:
++ case PROTOCOL_SWITCHING:
++ case ACCEPTED:
++ final com.mojang.authlib.GameProfile profile = loginListener.authenticatedProfile; /* Should be non-null at this stage */
++ new com.destroystokyo.paper.event.player.PlayerConnectionCloseEvent(profile.getId(), profile.getName(),
++ ((InetSocketAddress) this.address).getAddress(), false).callEvent();
++ }
++ }
++ // Paper end - Add PlayerConnectionCloseEvent
+
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/FriendlyByteBuf.java.patch b/paper-server/patches/unapplied/net/minecraft/network/FriendlyByteBuf.java.patch
new file mode 100644
index 0000000000..c9d512f8d6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/FriendlyByteBuf.java.patch
@@ -0,0 +1,105 @@
+--- 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/PacketEncoder.java.patch b/paper-server/patches/unapplied/net/minecraft/network/PacketEncoder.java.patch
new file mode 100644
index 0000000000..ce1cafa04d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/PacketEncoder.java.patch
@@ -0,0 +1,57 @@
+--- a/net/minecraft/network/PacketEncoder.java
++++ b/net/minecraft/network/PacketEncoder.java
+@@ -17,10 +17,12 @@
+ this.protocolInfo = state;
+ }
+
++ static final ThreadLocal<java.util.Locale> ADVENTURE_LOCALE = ThreadLocal.withInitial(() -> null); // Paper - adventure; set player's locale
+ protected void encode(ChannelHandlerContext channelHandlerContext, Packet<T> packet, ByteBuf byteBuf) throws Exception {
+ PacketType<? extends Packet<? super T>> packetType = packet.type();
+
+ try {
++ ADVENTURE_LOCALE.set(channelHandlerContext.channel().attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).get()); // Paper - adventure; set player's locale
+ this.protocolInfo.codec().encode(byteBuf, packet);
+ int i = byteBuf.readableBytes();
+ if (LOGGER.isDebugEnabled()) {
+@@ -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);
+ }
+
+ throw var9;
+ } finally {
++ // Paper start - Handle large packets disconnecting client
++ int packetLength = byteBuf.readableBytes();
++ if (packetLength > MAX_PACKET_SIZE || (packetLength > MAX_FINAL_PACKET_SIZE && packet.hasLargePacketFallback())) {
++ throw new PacketTooLargeException(packet, packetLength);
++ }
++ // Paper end - Handle large packets disconnecting client
+ ProtocolSwapHandler.handleOutboundTerminalPacket(channelHandlerContext, packet);
+ }
+ }
++
++ // Paper start
++ // packet size is encoded into 3-byte varint
++ private static final int MAX_FINAL_PACKET_SIZE = (1 << 21) - 1;
++ // Vanilla Max size for the encoder (before compression)
++ private static final int MAX_PACKET_SIZE = 8388608;
++
++ public static class PacketTooLargeException extends RuntimeException {
++ private final Packet<?> packet;
++
++ PacketTooLargeException(Packet<?> packet, int packetLength) {
++ super("PacketTooLarge - " + packet.getClass().getSimpleName() + " is " + packetLength + ". Max is " + MAX_PACKET_SIZE);
++ this.packet = packet;
++ }
++
++ public Packet<?> getPacket() {
++ return this.packet;
++ }
++ }
++ // Paper end
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/VarInt.java.patch b/paper-server/patches/unapplied/net/minecraft/network/VarInt.java.patch
new file mode 100644
index 0000000000..e2a07664c5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/VarInt.java.patch
@@ -0,0 +1,43 @@
+--- a/net/minecraft/network/VarInt.java
++++ b/net/minecraft/network/VarInt.java
+@@ -9,6 +9,18 @@
+ private static final int DATA_BITS_PER_BYTE = 7;
+
+ public static int getByteSize(int i) {
++ // Paper start - Optimize VarInts
++ return VARINT_EXACT_BYTE_LENGTHS[Integer.numberOfLeadingZeros(i)];
++ }
++ private static final int[] VARINT_EXACT_BYTE_LENGTHS = new int[33];
++ static {
++ for (int i = 0; i <= 32; ++i) {
++ VARINT_EXACT_BYTE_LENGTHS[i] = (int) Math.ceil((31d - (i - 1)) / 7d);
++ }
++ VARINT_EXACT_BYTE_LENGTHS[32] = 1; // Special case for the number 0.
++ }
++ public static int getByteSizeOld(int i) {
++ // Paper end - Optimize VarInts
+ for (int j = 1; j < 5; j++) {
+ if ((i & -1 << j * 7) == 0) {
+ return j;
+@@ -39,6 +51,21 @@
+ }
+
+ public static ByteBuf write(ByteBuf buf, int i) {
++ // 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);
++ } else {
++ writeOld(buf, i);
++ }
++ return buf;
++ }
++ public static ByteBuf writeOld(ByteBuf buf, int i) {
++ // Paper end - Optimize VarInts
+ while ((i & -128) != 0) {
+ buf.writeByte(i & 127 | 128);
+ i >>>= 7;
diff --git a/paper-server/patches/unapplied/net/minecraft/network/Varint21FrameDecoder.java.patch b/paper-server/patches/unapplied/net/minecraft/network/Varint21FrameDecoder.java.patch
new file mode 100644
index 0000000000..e9aad632b1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/Varint21FrameDecoder.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/network/Varint21FrameDecoder.java
++++ b/net/minecraft/network/Varint21FrameDecoder.java
+@@ -39,6 +39,12 @@
+ }
+
+ protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) {
++ // Paper start - Perf: Optimize exception handling; if channel is not active just discard the packet
++ if (!channelHandlerContext.channel().isActive()) {
++ byteBuf.skipBytes(byteBuf.readableBytes());
++ return;
++ }
++ // Paper end - Perf: Optimize exception handling
+ byteBuf.markReaderIndex();
+ this.helperBuf.clear();
+ if (!copyVarint(byteBuf, this.helperBuf)) {
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
new file mode 100644
index 0000000000..8fe79b8a75
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/ChatDecorator.java.patch
@@ -0,0 +1,23 @@
+--- 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/Component.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/Component.java.patch
new file mode 100644
index 0000000000..5db0b612a2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/Component.java.patch
@@ -0,0 +1,27 @@
+--- a/net/minecraft/network/chat/Component.java
++++ b/net/minecraft/network/chat/Component.java
+@@ -37,9 +37,23 @@
+ import net.minecraft.resources.ResourceLocation;
+ 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)});
++ }
+
++ @Override
++ default Iterator<Component> iterator() {
++ return this.stream().iterator();
++ }
++ // CraftBukkit end
++
+ Style getStyle();
+
+ ComponentContents getContents();
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/ComponentSerialization.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/ComponentSerialization.java.patch
new file mode 100644
index 0000000000..4136889ef4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/ComponentSerialization.java.patch
@@ -0,0 +1,99 @@
+--- a/net/minecraft/network/chat/ComponentSerialization.java
++++ b/net/minecraft/network/chat/ComponentSerialization.java
+@@ -37,9 +37,31 @@
+
+ public class ComponentSerialization {
+ public static final Codec<Component> CODEC = Codec.recursive("Component", ComponentSerialization::createCodec);
+- public static final StreamCodec<RegistryFriendlyByteBuf, Component> STREAM_CODEC = ByteBufCodecs.fromCodecWithRegistries(CODEC);
++ public static final StreamCodec<RegistryFriendlyByteBuf, Component> STREAM_CODEC = createTranslationAware(() -> net.minecraft.nbt.NbtAccounter.create(net.minecraft.network.FriendlyByteBuf.DEFAULT_NBT_QUOTA)); // Paper - adventure
+ public static final StreamCodec<RegistryFriendlyByteBuf, Optional<Component>> OPTIONAL_STREAM_CODEC = STREAM_CODEC.apply(ByteBufCodecs::optional);
+- public static final StreamCodec<RegistryFriendlyByteBuf, Component> TRUSTED_STREAM_CODEC = ByteBufCodecs.fromCodecWithRegistriesTrusted(CODEC);
++ // Paper start - adventure; use locale from bytebuf for translation
++ public static final ThreadLocal<Boolean> DONT_RENDER_TRANSLATABLES = ThreadLocal.withInitial(() -> false);
++ public static final StreamCodec<RegistryFriendlyByteBuf, Component> TRUSTED_STREAM_CODEC = createTranslationAware(net.minecraft.nbt.NbtAccounter::unlimitedHeap);
++ private static StreamCodec<RegistryFriendlyByteBuf, Component> createTranslationAware(final Supplier<net.minecraft.nbt.NbtAccounter> sizeTracker) {
++ return new StreamCodec<>() {
++ final StreamCodec<ByteBuf, net.minecraft.nbt.Tag> streamCodec = ByteBufCodecs.tagCodec(sizeTracker);
++ @Override
++ public Component decode(RegistryFriendlyByteBuf registryFriendlyByteBuf) {
++ net.minecraft.nbt.Tag tag = this.streamCodec.decode(registryFriendlyByteBuf);
++ RegistryOps<net.minecraft.nbt.Tag> registryOps = registryFriendlyByteBuf.registryAccess().createSerializationContext(net.minecraft.nbt.NbtOps.INSTANCE);
++ return CODEC.parse(registryOps, tag).getOrThrow(error -> new io.netty.handler.codec.DecoderException("Failed to decode: " + error + " " + tag));
++ }
++
++ @Override
++ public void encode(RegistryFriendlyByteBuf registryFriendlyByteBuf, Component object) {
++ RegistryOps<net.minecraft.nbt.Tag> registryOps = registryFriendlyByteBuf.registryAccess().createSerializationContext(net.minecraft.nbt.NbtOps.INSTANCE);
++ net.minecraft.nbt.Tag tag = (DONT_RENDER_TRANSLATABLES.get() ? CODEC : ComponentSerialization.localizedCodec(registryFriendlyByteBuf.adventure$locale))
++ .encodeStart(registryOps, object).getOrThrow(error -> new io.netty.handler.codec.EncoderException("Failed to encode: " + error + " " + object));
++ this.streamCodec.encode(registryFriendlyByteBuf, tag);
++ }
++ };
++ }
++ // Paper end - adventure; use locale from bytebuf for translation
+ public static final StreamCodec<RegistryFriendlyByteBuf, Optional<Component>> TRUSTED_OPTIONAL_STREAM_CODEC = TRUSTED_STREAM_CODEC.apply(
+ ByteBufCodecs::optional
+ );
+@@ -100,7 +122,27 @@
+ return ExtraCodecs.orCompressed(mapCodec3, mapCodec2);
+ }
+
++ // Paper start - adventure; create separate codec for each locale
++ private static final java.util.Map<java.util.Locale, Codec<Component>> LOCALIZED_CODECS = new java.util.concurrent.ConcurrentHashMap<>();
++
++ public static Codec<Component> localizedCodec(final [email protected] Locale locale) {
++ if (locale == null) {
++ return CODEC;
++ }
++ return LOCALIZED_CODECS.computeIfAbsent(locale,
++ loc -> Codec.recursive("Component", selfCodec -> createCodec(selfCodec, loc)));
++ }
++
++
++ // Paper end - adventure; create separate codec for each locale
++
+ private static Codec<Component> createCodec(Codec<Component> selfCodec) {
++ // Paper start - adventure; create separate codec for each locale
++ return createCodec(selfCodec, null);
++ }
++
++ private static Codec<Component> createCodec(Codec<Component> selfCodec, @javax.annotation.Nullable java.util.Locale locale) {
++ // Paper end - adventure; create separate codec for each locale
+ ComponentContents.Type<?>[] types = new ComponentContents.Type[]{
+ PlainTextContents.TYPE, TranslatableContents.TYPE, KeybindContents.TYPE, ScoreContents.TYPE, SelectorContents.TYPE, NbtContents.TYPE
+ };
+@@ -113,6 +155,34 @@
+ )
+ .apply(instance, MutableComponent::new)
+ );
++ // Paper start - adventure; create separate codec for each locale
++ final Codec<Component> origCodec = codec;
++ codec = new Codec<>() {
++ @Override
++ public <T> DataResult<com.mojang.datafixers.util.Pair<Component, T>> decode(final DynamicOps<T> ops, final T input) {
++ return origCodec.decode(ops, input);
++ }
++
++ @Override
++ public <T> DataResult<T> encode(final Component input, final DynamicOps<T> ops, final T prefix) {
++ final net.kyori.adventure.text.Component adventureComponent;
++ if (input instanceof io.papermc.paper.adventure.AdventureComponent adv) {
++ adventureComponent = adv.adventure$component();
++ } else if (locale != null && input.getContents() instanceof TranslatableContents && io.papermc.paper.adventure.PaperAdventure.hasAnyTranslations()) {
++ adventureComponent = io.papermc.paper.adventure.PaperAdventure.asAdventure(input);
++ } else {
++ return origCodec.encode(input, ops, prefix);
++ }
++ return io.papermc.paper.adventure.PaperAdventure.localizedCodec(locale)
++ .encode(adventureComponent, ops, prefix);
++ }
++
++ @Override
++ public String toString() {
++ return origCodec.toString() + "[AdventureComponentAware]";
++ }
++ };
++ // Paper end - adventure; create separate codec for each locale
+ return Codec.either(Codec.either(Codec.STRING, ExtraCodecs.nonEmptyList(selfCodec.listOf())), codec)
+ .xmap(either -> either.map(either2 -> either2.map(Component::literal, ComponentSerialization::createFromList), text -> (Component)text), text -> {
+ String string = text.tryCollapseToString();
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/ComponentUtils.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/ComponentUtils.java.patch
new file mode 100644
index 0000000000..c7b1b54e5a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/ComponentUtils.java.patch
@@ -0,0 +1,42 @@
+--- a/net/minecraft/network/chat/ComponentUtils.java
++++ b/net/minecraft/network/chat/ComponentUtils.java
+@@ -33,14 +33,39 @@
+ }
+ }
+
++ @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();
+ }
+
++ // Paper start - validate separator
++ public static Optional<MutableComponent> updateSeparatorForEntity(@Nullable CommandSourceStack source, Optional<Component> text, @Nullable Entity sender, int depth) throws CommandSyntaxException {
++ 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 final net.minecraft.network.chat.contents.TranslatableContents translatableContents) {
++ for (final Object arg : translatableContents.getArgs()) {
++ if (arg instanceof final Component argumentAsComponent && !isValidSelector(argumentAsComponent)) return false;
++ }
++ }
++
++ return true;
++ }
++ // 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();
+ } else {
++ // Paper start - adventure; pass actual vanilla component
++ if (text instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent) {
++ text = adventureComponent.deepConverted();
++ }
++ // Paper end - adventure; pass actual vanilla component
+ MutableComponent mutableComponent = text.getContents().resolve(source, sender, depth + 1);
+
+ for (Component component : text.getSiblings()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/MessageSignature.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/MessageSignature.java.patch
new file mode 100644
index 0000000000..2b6f690732
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/MessageSignature.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/network/chat/MessageSignature.java
++++ b/net/minecraft/network/chat/MessageSignature.java
+@@ -13,6 +13,7 @@
+ import net.minecraft.util.SignatureValidator;
+
+ public record MessageSignature(byte[] bytes) {
++ public net.kyori.adventure.chat.SignedMessage.Signature adventure() { return () -> this.bytes; } // Paper - adventure; support signed messages
+ public static final Codec<MessageSignature> CODEC = ExtraCodecs.BASE64_STRING.xmap(MessageSignature::new, MessageSignature::bytes);
+ public static final int BYTES = 256;
+
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/MutableComponent.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/MutableComponent.java.patch
new file mode 100644
index 0000000000..c4694faca6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/MutableComponent.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/network/chat/MutableComponent.java
++++ b/net/minecraft/network/chat/MutableComponent.java
+@@ -94,6 +94,11 @@
+
+ @Override
+ public boolean equals(Object object) {
++ // Paper start - make AdventureComponent equivalent
++ if (object instanceof io.papermc.paper.adventure.AdventureComponent adventureComponent) {
++ object = adventureComponent.deepConverted();
++ }
++ // Paper end - make AdventureComponent equivalent
+ return this == object
+ || object instanceof MutableComponent mutableComponent
+ && this.contents.equals(mutableComponent.contents)
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/OutgoingChatMessage.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/OutgoingChatMessage.java.patch
new file mode 100644
index 0000000000..3192e18db3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/OutgoingChatMessage.java.patch
@@ -0,0 +1,44 @@
+--- 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/PlayerChatMessage.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/PlayerChatMessage.java.patch
new file mode 100644
index 0000000000..8722bc8269
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/PlayerChatMessage.java.patch
@@ -0,0 +1,61 @@
+--- a/net/minecraft/network/chat/PlayerChatMessage.java
++++ b/net/minecraft/network/chat/PlayerChatMessage.java
+@@ -17,6 +17,42 @@
+ public record PlayerChatMessage(
+ SignedMessageLink link, @Nullable MessageSignature signature, SignedMessageBody signedBody, @Nullable Component unsignedContent, FilterMask filterMask
+ ) {
++ // Paper start - adventure; support signed messages
++ public final class AdventureView implements net.kyori.adventure.chat.SignedMessage {
++ private AdventureView() {
++ }
++ @Override
++ public @org.jetbrains.annotations.NotNull Instant timestamp() {
++ return PlayerChatMessage.this.timeStamp();
++ }
++ @Override
++ public long salt() {
++ return PlayerChatMessage.this.salt();
++ }
++ @Override
++ public @org.jetbrains.annotations.Nullable Signature signature() {
++ return PlayerChatMessage.this.signature == null ? null : PlayerChatMessage.this.signature.adventure();
++ }
++ @Override
++ public [email protected] Component unsignedContent() {
++ return PlayerChatMessage.this.unsignedContent() == null ? null : io.papermc.paper.adventure.PaperAdventure.asAdventure(PlayerChatMessage.this.unsignedContent());
++ }
++ @Override
++ public @org.jetbrains.annotations.NotNull String message() {
++ return PlayerChatMessage.this.signedContent();
++ }
++ @Override
++ public @org.jetbrains.annotations.NotNull net.kyori.adventure.identity.Identity identity() {
++ return net.kyori.adventure.identity.Identity.identity(PlayerChatMessage.this.sender());
++ }
++ public PlayerChatMessage playerChatMessage() {
++ return PlayerChatMessage.this;
++ }
++ }
++ public AdventureView adventureView() {
++ return new AdventureView();
++ }
++ // Paper end - adventure; support signed messages
+ public static final MapCodec<PlayerChatMessage> MAP_CODEC = RecordCodecBuilder.mapCodec(
+ instance -> instance.group(
+ SignedMessageLink.CODEC.fieldOf("link").forGetter(PlayerChatMessage::link),
+@@ -47,7 +83,14 @@
+ }
+
+ public PlayerChatMessage withUnsignedContent(Component unsignedContent) {
+- Component component = !unsignedContent.equals(Component.literal(this.signedContent())) ? unsignedContent : null;
++ // Paper start - adventure
++ final Component component;
++ if (unsignedContent instanceof io.papermc.paper.adventure.AdventureComponent advComponent) {
++ component = this.signedContent().equals(io.papermc.paper.adventure.AdventureCodecs.tryCollapseToString(advComponent.adventure$component())) ? null : unsignedContent;
++ } else {
++ component = !unsignedContent.equals(Component.literal(this.signedContent())) ? unsignedContent : null;
++ }
++ // Paper end - adventure
+ return new PlayerChatMessage(this.link, this.signature, this.signedBody, component, this.filterMask);
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/SignedMessageChain.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/SignedMessageChain.java.patch
new file mode 100644
index 0000000000..9404843b29
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/SignedMessageChain.java.patch
@@ -0,0 +1,37 @@
+--- a/net/minecraft/network/chat/SignedMessageChain.java
++++ b/net/minecraft/network/chat/SignedMessageChain.java
+@@ -40,14 +40,14 @@
+ if (signature == null) {
+ throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.MISSING_PROFILE_KEY);
+ } else if (playerPublicKey.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 {
+ SignedMessageLink signedMessageLink = SignedMessageChain.this.nextLink;
+ if (signedMessageLink == null) {
+ throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.CHAIN_BROKEN);
+ } else if (body.timeStamp().isBefore(SignedMessageChain.this.lastTimeStamp)) {
+ this.setChainBroken();
+- throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.OUT_OF_ORDER_CHAT);
++ throw new SignedMessageChain.DecodeException(SignedMessageChain.DecodeException.OUT_OF_ORDER_CHAT, org.bukkit.event.player.PlayerKickEvent.Cause.OUT_OF_ORDER_CHAT); // Paper - kick event causes
+ } else {
+ SignedMessageChain.this.lastTimeStamp = body.timeStamp();
+ PlayerChatMessage playerChatMessage = new PlayerChatMessage(signedMessageLink, signature, body, null, FilterMask.PASS_THROUGH);
+@@ -80,9 +80,16 @@
+ 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) {
++ // Paper start
++ public final org.bukkit.event.player.PlayerKickEvent.Cause kickCause;
++ public DecodeException(Component message, org.bukkit.event.player.PlayerKickEvent.Cause event) {
+ super(message);
++ this.kickCause = event;
+ }
++ // Paper end
++ public DecodeException(Component message) {
++ this(message, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); // Paper
++ }
+ }
+
+ @FunctionalInterface
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
new file mode 100644
index 0000000000..014fd3eb10
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/TextColor.java.patch
@@ -0,0 +1,37 @@
+--- 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
new file mode 100644
index 0000000000..f30203349e
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/chat/contents/SelectorContents.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/contents/SelectorContents.java.patch
new file mode 100644
index 0000000000..aff540bbdc
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/contents/SelectorContents.java.patch
@@ -0,0 +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) {
+ 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);
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/chat/contents/TranslatableContents.java.patch b/paper-server/patches/unapplied/net/minecraft/network/chat/contents/TranslatableContents.java.patch
new file mode 100644
index 0000000000..88b91fae6d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/chat/contents/TranslatableContents.java.patch
@@ -0,0 +1,45 @@
+--- a/net/minecraft/network/chat/contents/TranslatableContents.java
++++ b/net/minecraft/network/chat/contents/TranslatableContents.java
+@@ -181,6 +181,15 @@
+
+ @Override
+ public <T> Optional<T> visit(FormattedText.ContentConsumer<T> visitor) {
++ // Paper start - Count visited parts
++ try {
++ return this.visit(new TranslatableContentConsumer<>(visitor));
++ } catch (IllegalArgumentException ignored) {
++ return visitor.accept("...");
++ }
++ }
++ private <T> Optional<T> visit(TranslatableContentConsumer<T> visitor) {
++ // Paper end - Count visited parts
+ this.decompose();
+
+ for (FormattedText formattedText : this.decomposedParts) {
+@@ -191,7 +200,26 @@
+ }
+
+ 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 final FormattedText.ContentConsumer<T> visitor;
++ private int visited;
++
++ private TranslatableContentConsumer(FormattedText.ContentConsumer<T> visitor) {
++ this.visitor = visitor;
++ }
++
++ @Override
++ public Optional<T> accept(final String asString) {
++ if (visited++ > 32) {
++ throw EX;
++ }
++ return this.visitor.accept(asString);
++ }
+ }
++ // Paper end - Count visited parts
+
+ @Override
+ public MutableComponent resolve(@Nullable CommandSourceStack source, @Nullable Entity sender, int depth) throws CommandSyntaxException {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/Packet.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/Packet.java.patch
new file mode 100644
index 0000000000..ba40a5940e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/Packet.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/network/protocol/Packet.java
++++ b/net/minecraft/network/protocol/Packet.java
+@@ -11,6 +11,19 @@
+
+ void handle(T listener);
+
++ // Paper start
++ default boolean hasLargePacketFallback() {
++ return false;
++ }
++
++ /**
++ * override {@link #hasLargePacketFallback()} to return true when overriding in subclasses
++ */
++ default boolean packetTooLarge(net.minecraft.network.Connection manager) {
++ return false;
++ }
++ // Paper end
++
+ default boolean isSkippable() {
+ return false;
+ }
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
new file mode 100644
index 0000000000..67d49cd3f7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/PacketUtils.java.patch
@@ -0,0 +1,27 @@
+--- 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
new file mode 100644
index 0000000000..e17dc6200b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/common/ServerboundCustomPayloadPacket.java.patch
@@ -0,0 +1,20 @@
+--- 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/common/custom/DiscardedPayload.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/common/custom/DiscardedPayload.java.patch
new file mode 100644
index 0000000000..785d4efd47
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/common/custom/DiscardedPayload.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/network/protocol/common/custom/DiscardedPayload.java
++++ b/net/minecraft/network/protocol/common/custom/DiscardedPayload.java
+@@ -4,16 +4,18 @@
+ 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);
+- return new DiscardedPayload(id);
++ // CraftBukkit start
++ return new DiscardedPayload(id, packetdataserializer.readBytes(j));
++ // CraftBukkit end
+ } else {
+ throw new IllegalArgumentException("Payload may not be larger than " + maxBytes + " bytes");
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java.patch
new file mode 100644
index 0000000000..4967c17ea2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundBlockEntityDataPacket.java
+@@ -29,7 +29,7 @@
+
+ public static ClientboundBlockEntityDataPacket create(BlockEntity blockEntity, BiFunction<BlockEntity, RegistryAccess, CompoundTag> nbtGetter) {
+ 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
+ }
+
+ 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/unapplied/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java.patch
new file mode 100644
index 0000000000..1a14cb1295
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java.patch
@@ -0,0 +1,24 @@
+--- 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);
+ }
+
++ // Paper start - Handle large packets disconnecting client
++ @Override
++ public boolean hasLargePacketFallback() {
++ return true;
++ }
++
++ @Override
++ public boolean packetTooLarge(net.minecraft.network.Connection manager) {
++ for (int i = 0 ; i < this.items.size() ; i++) {
++ manager.send(new ClientboundContainerSetSlotPacket(this.containerId, this.stateId, i, this.items.get(i)));
++ }
++ return true;
++ }
++ // Paper end - Handle large packets disconnecting client
++
+ private void write(RegistryFriendlyByteBuf buf) {
+ buf.writeContainerId(this.containerId);
+ buf.writeVarInt(this.stateId);
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java.patch
new file mode 100644
index 0000000000..a1740eccbe
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundInitializeBorderPacket.java
+@@ -30,8 +30,10 @@
+ }
+
+ public ClientboundInitializeBorderPacket(WorldBorder worldBorder) {
+- this.newCenterX = worldBorder.getCenterX();
+- this.newCenterZ = worldBorder.getCenterZ();
++ // CraftBukkit start - multiply out nether border
++ this.newCenterX = worldBorder.getCenterX() * worldBorder.world.dimensionType().coordinateScale();
++ this.newCenterZ = worldBorder.getCenterZ() * worldBorder.world.dimensionType().coordinateScale();
++ // CraftBukkit end
+ this.oldSize = worldBorder.getSize();
+ this.newSize = worldBorder.getLerpTarget();
+ this.lerpTime = worldBorder.getLerpRemainingTime();
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java.patch
new file mode 100644
index 0000000000..26f8a0aa6a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
++++ b/net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData.java
+@@ -52,7 +52,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
+ 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());
+ 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);
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java.patch
new file mode 100644
index 0000000000..edb5b971ee
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java.patch
@@ -0,0 +1,114 @@
+--- a/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundPlayerInfoUpdatePacket.java
+@@ -38,7 +38,18 @@
+ this.actions = EnumSet.of(action);
+ this.entries = List.of(new ClientboundPlayerInfoUpdatePacket.Entry(player));
+ }
++ // Paper start - Add Listing API for Player
++ public ClientboundPlayerInfoUpdatePacket(EnumSet<ClientboundPlayerInfoUpdatePacket.Action> actions, List<ClientboundPlayerInfoUpdatePacket.Entry> entries) {
++ 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);
+ }
+
++ // Paper start - Add Listing API for Player
++ public static ClientboundPlayerInfoUpdatePacket createPlayerInitializing(Collection<ServerPlayer> players, ServerPlayer forPlayer) {
++ final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> enumSet = EnumSet.of(
++ ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER,
++ ClientboundPlayerInfoUpdatePacket.Action.INITIALIZE_CHAT,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER
++ );
++ final List<ClientboundPlayerInfoUpdatePacket.Entry> entries = new java.util.ArrayList<>(players.size());
++ final org.bukkit.craftbukkit.entity.CraftPlayer bukkitEntity = forPlayer.getBukkitEntity();
++ for (final ServerPlayer player : players) {
++ entries.add(new ClientboundPlayerInfoUpdatePacket.Entry(player, bukkitEntity.isListed(player.getBukkitEntity())));
++ }
++ return new ClientboundPlayerInfoUpdatePacket(enumSet, entries);
++ }
++
++ public static ClientboundPlayerInfoUpdatePacket createSinglePlayerInitializing(ServerPlayer player, boolean listed) {
++ final EnumSet<ClientboundPlayerInfoUpdatePacket.Action> enumSet = EnumSet.of(
++ ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER,
++ ClientboundPlayerInfoUpdatePacket.Action.INITIALIZE_CHAT,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_GAME_MODE,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LATENCY,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT,
++ ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LIST_ORDER
++ );
++ final List<ClientboundPlayerInfoUpdatePacket.Entry> entries = List.of(new Entry(player, listed));
++ return new ClientboundPlayerInfoUpdatePacket(enumSet, entries);
++ }
++
++ public static ClientboundPlayerInfoUpdatePacket updateListed(UUID playerInfoId, boolean listed) {
++ EnumSet<ClientboundPlayerInfoUpdatePacket.Action> enumSet = EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED);
++ 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 @@
+ }),
+ INITIALIZE_CHAT(
+ (serialized, buf) -> serialized.chatSession = buf.readNullable(RemoteChatSession.Data::read),
+- (buf, entry) -> buf.writeNullable(entry.chatSession, RemoteChatSession.Data::write)
++ // Paper start - Prevent causing expired keys from impacting new joins
++ (buf, entry) -> {
++ RemoteChatSession.Data chatSession = entry.chatSession;
++ if (chatSession != null && chatSession.profilePublicKey().hasExpired()) {
++ chatSession = null;
++ }
++ buf.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 @@
+ @Nullable RemoteChatSession.Data chatSession
+ ) {
+ Entry(ServerPlayer player) {
++ // Paper start - Add Listing API for Player
++ this(player, true);
++ }
++ Entry(ServerPlayer player, boolean listed) {
+ this(
++ // Paper end - Add Listing API for Player
+ player.getUUID(),
+ player.getGameProfile(),
+- true,
++ listed, // Paper - Add Listing API for Player
+ player.connection.latency(),
+ player.gameMode.getGameModeForPlayer(),
+ player.getTabListDisplayName(),
+@@ -169,6 +233,11 @@
+ Optionull.map(player.getChatSession(), RemoteChatSession::asData)
+ );
+ }
++ // Paper start - Add Listing API for Player
++ Entry(UUID profileId, boolean listed) {
++ this(profileId, null, listed, 0, GameType.DEFAULT_MODE, null, true, 0, null);
++ }
++ // Paper end - Add Listing API for Player
+ }
+
+ static class EntryBuilder {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java.patch
new file mode 100644
index 0000000000..372b4afd93
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket.java.patch
@@ -0,0 +1,38 @@
+--- 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();
+
+ 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
+ }
+
+ }
+
++ // CraftBukkit start - Add constructor
++ public ClientboundSectionBlocksUpdatePacket(SectionPos sectionposition, ShortSet shortset, BlockState[] states) {
++ this.sectionPos = sectionposition;
++ this.positions = shortset.toShortArray();
++ 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;
++ this.positions = blockChanges.keySet().toShortArray();
++ this.states = blockChanges.values().toArray(new BlockState[0]);
++ }
++ // Paper end - Multi Block Change API
++
+ private void write(FriendlyByteBuf buf) {
+ buf.writeLong(this.sectionPos.asLong());
+ buf.writeVarInt(this.positions.length);
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java.patch
new file mode 100644
index 0000000000..139eaa42d9
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java
++++ b/net/minecraft/network/protocol/game/ClientboundSetBorderCenterPacket.java
+@@ -13,8 +13,10 @@
+ private final double newCenterZ;
+
+ public ClientboundSetBorderCenterPacket(WorldBorder worldBorder) {
+- this.newCenterX = worldBorder.getCenterX();
+- this.newCenterZ = worldBorder.getCenterZ();
++ // CraftBukkit start - multiply out nether border
++ this.newCenterX = worldBorder.getCenterX() * (worldBorder.world != null ? worldBorder.world.dimensionType().coordinateScale() : 1.0);
++ this.newCenterZ = worldBorder.getCenterZ() * (worldBorder.world != null ? worldBorder.world.dimensionType().coordinateScale() : 1.0);
++ // CraftBukkit end
+ }
+
+ private ClientboundSetBorderCenterPacket(FriendlyByteBuf buf) {
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
new file mode 100644
index 0000000000..6a8d6c8ce8
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..e13e6cd647
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..1d4da793e7
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/game/ClientboundSystemChatPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java.patch
new file mode 100644
index 0000000000..64187dddbd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ClientboundSystemChatPacket.java.patch
@@ -0,0 +1,25 @@
+--- 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);
+
++ // 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);
++ }
++ // Spigot end
++ // Paper start
++ public ClientboundSystemChatPacket(net.kyori.adventure.text.Component content, boolean overlay) {
++ this(io.papermc.paper.adventure.PaperAdventure.asVanilla(content), overlay);
++ }
++ // Paper end
++
+ @Override
+ public PacketType<ClientboundSystemChatPacket> type() {
+ return GamePacketTypes.CLIENTBOUND_SYSTEM_CHAT;
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java.patch
new file mode 100644
index 0000000000..1771a837ae
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java
++++ b/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java
+@@ -19,7 +19,7 @@
+
+ private ServerboundCommandSuggestionPacket(FriendlyByteBuf buf) {
+ this.id = buf.readVarInt();
+- this.command = buf.readUtf(32500);
++ this.command = buf.readUtf(2048); // Paper
+ }
+
+ private void write(FriendlyByteBuf buf) {
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundInteractPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundInteractPacket.java.patch
new file mode 100644
index 0000000000..a4d170433a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundInteractPacket.java.patch
@@ -0,0 +1,17 @@
+--- a/net/minecraft/network/protocol/game/ServerboundInteractPacket.java
++++ b/net/minecraft/network/protocol/game/ServerboundInteractPacket.java
+@@ -176,4 +176,14 @@
+ buf.writeEnum(this.hand);
+ }
+ }
++
++ // Paper start - PlayerUseUnknownEntityEvent
++ public int getEntityId() {
++ return this.entityId;
++ }
++
++ public boolean isAttack() {
++ return this.action.getType() == ActionType.ATTACK;
++ }
++ // Paper end - PlayerUseUnknownEntityEvent
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java.patch
new file mode 100644
index 0000000000..1e2cc2dc52
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemOnPacket.java.patch
@@ -0,0 +1,23 @@
+--- 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 @@
+ private final BlockHitResult blockHit;
+ private final InteractionHand hand;
+ private final int sequence;
++ public long timestamp; // Spigot
+
+ public ServerboundUseItemOnPacket(InteractionHand hand, BlockHitResult blockHitResult, int sequence) {
+ this.hand = hand;
+@@ -21,6 +23,7 @@
+ }
+
+ private ServerboundUseItemOnPacket(FriendlyByteBuf buf) {
++ this.timestamp = System.currentTimeMillis(); // Spigot
+ this.hand = (InteractionHand) buf.readEnum(InteractionHand.class);
+ this.blockHit = buf.readBlockHitResult();
+ this.sequence = buf.readVarInt();
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java.patch
new file mode 100644
index 0000000000..45ba3ec19d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/ServerboundUseItemPacket.java.patch
@@ -0,0 +1,23 @@
+--- 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 @@
+ 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) {
+ this.hand = hand;
+@@ -22,6 +24,7 @@
+ }
+
+ private ServerboundUseItemPacket(FriendlyByteBuf buf) {
++ this.timestamp = System.currentTimeMillis(); // Spigot
+ this.hand = (InteractionHand) buf.readEnum(InteractionHand.class);
+ this.sequence = buf.readVarInt();
+ this.yRot = buf.readFloat();
diff --git a/paper-server/patches/unapplied/net/minecraft/network/protocol/game/VecDeltaCodec.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/VecDeltaCodec.java.patch
new file mode 100644
index 0000000000..ca1a2126c9
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/game/VecDeltaCodec.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/network/protocol/game/VecDeltaCodec.java
++++ b/net/minecraft/network/protocol/game/VecDeltaCodec.java
+@@ -5,16 +5,16 @@
+
+ public class VecDeltaCodec {
+ private static final double TRUNCATION_STEPS = 4096.0;
+- private Vec3 base = Vec3.ZERO;
++ public Vec3 base = Vec3.ZERO; // Paper
+
+ @VisibleForTesting
+ static long encode(double value) {
+- return Math.round(value * 4096.0);
++ return Math.round(value * 4096.0); // Paper - Fix MC-4; diff on change
+ }
+
+ @VisibleForTesting
+ static double decode(long value) {
+- return (double)value / 4096.0;
++ return value / 4096.0; // Paper - Fix MC-4; diff on change
+ }
+
+ public Vec3 decode(long x, long y, long z) {
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
new file mode 100644
index 0000000000..071ea04212
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/handshake/ClientIntentionPacket.java.patch
@@ -0,0 +1,17 @@
+--- 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/ClientboundCustomQueryPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java.patch
new file mode 100644
index 0000000000..60ed80c610
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/login/ClientboundCustomQueryPacket.java.patch
@@ -0,0 +1,17 @@
+--- 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);
+ }
++
++ // Paper start - MC Utils - default query payloads
++ public static record PlayerInfoChannelPayload(ResourceLocation id, FriendlyByteBuf buffer) implements CustomQueryPayload {
++
++ @Override
++ public void write(final FriendlyByteBuf buf) {
++ buf.writeBytes(this.buffer.copy());
++ }
++ }
++ // Paper end - MC Utils - default query payloads
+ }
diff --git a/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
new file mode 100644
index 0000000000..7ad1ef4a1c
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/network/protocol/login/ServerboundCustomQueryAnswerPacket.java.patch b/paper-server/patches/unapplied/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java.patch
new file mode 100644
index 0000000000..663ca9ede2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java.patch
@@ -0,0 +1,43 @@
+--- a/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
++++ b/net/minecraft/network/protocol/login/ServerboundCustomQueryAnswerPacket.java
+@@ -20,7 +20,17 @@
+ }
+
+ private static CustomQueryAnswerPayload readPayload(int queryId, FriendlyByteBuf buf) {
+- return readUnknownPayload(buf);
++ // Paper start - MC Utils - default query payloads
++ FriendlyByteBuf buffer = buf.readNullable((buf2) -> {
++ int i = buf2.readableBytes();
++ if (i >= 0 && i <= MAX_PAYLOAD_SIZE) {
++ return new FriendlyByteBuf(buf2.readBytes(i));
++ } else {
++ throw new IllegalArgumentException("Payload may not be larger than " + MAX_PAYLOAD_SIZE + " bytes");
++ }
++ });
++ return buffer == null ? null : new net.minecraft.network.protocol.login.ServerboundCustomQueryAnswerPacket.QueryAnswerPayload(buffer);
++ // Paper end - MC Utils - default query payloads
+ }
+
+ private static CustomQueryAnswerPayload readUnknownPayload(FriendlyByteBuf buf) {
+@@ -47,4 +57,21 @@
+ public void handle(ServerLoginPacketListener listener) {
+ listener.handleCustomQueryPacket(this);
+ }
++
++ // Paper start - MC Utils - default query payloads
++ public static final class QueryAnswerPayload implements CustomQueryAnswerPayload {
++
++ public final FriendlyByteBuf buffer;
++
++ public QueryAnswerPayload(final net.minecraft.network.FriendlyByteBuf buffer) {
++ this.buffer = buffer;
++ }
++
++ @Override
++ public void write(final net.minecraft.network.FriendlyByteBuf buf) {
++ buf.writeBytes(this.buffer.copy());
++ }
++ }
++ // Paper end - MC Utils - default query payloads
++
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/network/syncher/SynchedEntityData.java.patch b/paper-server/patches/unapplied/net/minecraft/network/syncher/SynchedEntityData.java.patch
new file mode 100644
index 0000000000..d2519790c5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/network/syncher/SynchedEntityData.java.patch
@@ -0,0 +1,53 @@
+--- a/net/minecraft/network/syncher/SynchedEntityData.java
++++ b/net/minecraft/network/syncher/SynchedEntityData.java
+@@ -50,8 +50,8 @@
+ }
+ }
+
+- 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
+ }
+
+ public <T> T get(EntityDataAccessor<T> data) {
+@@ -74,6 +74,13 @@
+
+ }
+
++ // CraftBukkit start - add method from above
++ public <T> void markDirty(EntityDataAccessor<T> datawatcherobject) {
++ this.getItem(datawatcherobject).setDirty(true);
++ this.isDirty = true;
++ }
++ // CraftBukkit end
++
+ 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
+ }
+ }
+
++ // Paper start
++ // We need to pack all as we cannot rely on "non default values" or "dirty" ones.
++ // Because these values can possibly be desynced on the client.
++ @Nullable
++ public List<SynchedEntityData.DataValue<?>> packAll() {
++ final List<SynchedEntityData.DataValue<?>> list = new ArrayList<>();
++ for (final DataItem<?> dataItem : this.itemsById) {
++ list.add(dataItem.value());
++ }
++
++ return list;
++ }
++ // Paper end
++
+ public static class DataItem<T> {
+
+ final EntityDataAccessor<T> accessor;
diff --git a/paper-server/patches/unapplied/net/minecraft/resources/RegistryDataLoader.java.patch b/paper-server/patches/unapplied/net/minecraft/resources/RegistryDataLoader.java.patch
new file mode 100644
index 0000000000..41773a3732
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/resources/RegistryDataLoader.java.patch
@@ -0,0 +1,79 @@
+--- 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/resources/ResourceLocation.java.patch b/paper-server/patches/unapplied/net/minecraft/resources/ResourceLocation.java.patch
new file mode 100644
index 0000000000..4f2bd280fb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/resources/ResourceLocation.java.patch
@@ -0,0 +1,42 @@
+--- a/net/minecraft/resources/ResourceLocation.java
++++ b/net/minecraft/resources/ResourceLocation.java
+@@ -32,6 +32,7 @@
+ public static final char NAMESPACE_SEPARATOR = ':';
+ public static final String DEFAULT_NAMESPACE = "minecraft";
+ public static final String REALMS_NAMESPACE = "realms";
++ public static final String PAPER_NAMESPACE = "paper"; // Paper
+ private final String namespace;
+ private final String path;
+
+@@ -40,6 +41,13 @@
+
+ assert isValidPath(path);
+
++ // Paper start - Validate ResourceLocation
++ // Check for the max network string length (capped at Short.MAX_VALUE) as well as the max bytes of a StringTag (length written as an unsigned short)
++ final String resourceLocation = namespace + ":" + path;
++ if (resourceLocation.length() > Short.MAX_VALUE || io.netty.buffer.ByteBufUtil.utf8MaxBytes(resourceLocation) > 2 * Short.MAX_VALUE + 1) {
++ throw new ResourceLocationException("Resource location too long: " + resourceLocation);
++ }
++ // Paper end - Validate ResourceLocation
+ this.namespace = namespace;
+ this.path = path;
+ }
+@@ -246,7 +254,7 @@
+
+ private static String assertValidNamespace(String namespace, String path) {
+ if (!isValidNamespace(namespace)) {
+- throw new ResourceLocationException("Non [a-z0-9_.-] character in namespace of location: " + namespace + ":" + path);
++ throw new ResourceLocationException("Non [a-z0-9_.-] character in namespace of location: " + org.apache.commons.lang3.StringUtils.normalizeSpace(namespace) + ":" + org.apache.commons.lang3.StringUtils.normalizeSpace(path)); // Paper - Sanitize ResourceLocation error logging
+ } else {
+ return namespace;
+ }
+@@ -267,7 +275,7 @@
+
+ private static String assertValidPath(String namespace, String path) {
+ if (!isValidPath(path)) {
+- throw new ResourceLocationException("Non [a-z0-9/._-] character in path of location: " + namespace + ":" + path);
++ throw new ResourceLocationException("Non [a-z0-9/._-] character in path of location: " + namespace + ":" + org.apache.commons.lang3.StringUtils.normalizeSpace(path)); // Paper - Sanitize ResourceLocation error logging
+ } else {
+ return path;
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/Bootstrap.java.patch b/paper-server/patches/unapplied/net/minecraft/server/Bootstrap.java.patch
new file mode 100644
index 0000000000..efce551cae
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/Bootstrap.java.patch
@@ -0,0 +1,129 @@
+--- 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
new file mode 100644
index 0000000000..a9ae49e37f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/Main.java.patch
@@ -0,0 +1,290 @@
+--- 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
new file mode 100644
index 0000000000..b2aec783a8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/MinecraftServer.java.patch
@@ -0,0 +1,1501 @@
+--- 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/PlayerAdvancements.java.patch b/paper-server/patches/unapplied/net/minecraft/server/PlayerAdvancements.java.patch
new file mode 100644
index 0000000000..93d25149bd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/PlayerAdvancements.java.patch
@@ -0,0 +1,69 @@
+--- a/net/minecraft/server/PlayerAdvancements.java
++++ b/net/minecraft/server/PlayerAdvancements.java
+@@ -50,7 +50,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 final PlayerList playerList;
+ private final Path playerSavePath;
+ private AdvancementTree tree;
+@@ -63,6 +63,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 void save() {
++ if (org.spigotmc.SpigotConfig.disableAdvancementSaving) return; // Spigot
+ JsonElement 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);
+ } else {
+ this.startProgress(advancementholder, advancementprogress);
+@@ -223,14 +226,31 @@
+ boolean flag1 = advancementprogress.isDone();
+
+ if (advancementprogress.grantProgress(criterionName)) {
++ // Paper start - Add PlayerAdvancementCriterionGrantEvent
++ if (!new com.destroystokyo.paper.event.player.PlayerAdvancementCriterionGrantEvent(this.player.getBukkitEntity(), advancement.toBukkit(), criterionName).callEvent()) {
++ advancementprogress.revokeProgress(criterionName);
++ return false;
++ }
++ // Paper end - Add PlayerAdvancementCriterionGrantEvent
+ this.unregisterListeners(advancement);
+ this.progressChanged.add(advancement);
+ flag = true;
+ if (!flag1 && advancementprogress.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(
++ info.shouldAnnounceChat() ? io.papermc.paper.adventure.PaperAdventure.asAdventure(info.getType().createAnnouncement(advancement, this.player)) : null
++ );
++ }).orElse(null);
++ final org.bukkit.event.player.PlayerAdvancementDoneEvent event = new org.bukkit.event.player.PlayerAdvancementDoneEvent(this.player.getBukkitEntity(), advancement.toBukkit(), message);
++ 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);
++ // 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/unapplied/net/minecraft/server/ReloadableServerRegistries.java.patch b/paper-server/patches/unapplied/net/minecraft/server/ReloadableServerRegistries.java.patch
new file mode 100644
index 0000000000..6c906a073c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/ReloadableServerRegistries.java.patch
@@ -0,0 +1,32 @@
+--- 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/ReloadableServerResources.java.patch b/paper-server/patches/unapplied/net/minecraft/server/ReloadableServerResources.java.patch
new file mode 100644
index 0000000000..59120184c0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/ReloadableServerResources.java.patch
@@ -0,0 +1,25 @@
+--- a/net/minecraft/server/ReloadableServerResources.java
++++ b/net/minecraft/server/ReloadableServerResources.java
+@@ -39,6 +39,7 @@
+ this.postponedTags = pendingTagLoads;
+ this.recipes = new RecipeManager(registries);
+ this.commands = new Commands(environment, 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());
+ }
+@@ -83,6 +84,14 @@
+ ReloadableServerResources reloadableServerResources = new ReloadableServerResources(
+ reloadResult.layers(), reloadResult.lookupWithUpdatedTags(), enabledFeatures, environment, pendingTagLoads, functionPermissionLevel
+ );
++ // Paper start - call commands event for bootstraps
++ //noinspection ConstantValue
++ 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,
++ io.papermc.paper.plugin.bootstrap.BootstrapContext.class,
++ MinecraftServer.getServer() == null ? io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.INITIAL : io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.RELOAD);
++ // Paper end - call commands event
+ return SimpleReloadInstance.create(
+ resourceManager,
+ reloadableServerResources.listeners(),
diff --git a/paper-server/patches/unapplied/net/minecraft/server/ServerAdvancementManager.java.patch b/paper-server/patches/unapplied/net/minecraft/server/ServerAdvancementManager.java.patch
new file mode 100644
index 0000000000..5a5a995288
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/ServerAdvancementManager.java.patch
@@ -0,0 +1,39 @@
+--- 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;
+
++// 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
+ private AdvancementTree tree = new AdvancementTree();
+ private final HolderLookup.Provider registries;
+
+@@ -37,13 +41,19 @@
+ Builder<ResourceLocation, AdvancementHolder> builder = ImmutableMap.builder();
+
+ prepared.forEach((minecraftkey, 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()))) {
++ return;
++ }
++ // Spigot end
+ this.validate(minecraftkey, advancement);
+ builder.put(minecraftkey, new AdvancementHolder(minecraftkey, advancement));
+ });
+- this.advancements = builder.buildOrThrow();
++ this.advancements = new 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
+ Iterator iterator = advancementtree.roots().iterator();
+
+ while (iterator.hasNext()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/ServerFunctionLibrary.java.patch b/paper-server/patches/unapplied/net/minecraft/server/ServerFunctionLibrary.java.patch
new file mode 100644
index 0000000000..a5f6fd5796
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/ServerFunctionLibrary.java.patch
@@ -0,0 +1,11 @@
+--- 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
new file mode 100644
index 0000000000..45ee083997
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/ServerFunctionManager.java.patch
@@ -0,0 +1,11 @@
+--- 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/ServerScoreboard.java.patch b/paper-server/patches/unapplied/net/minecraft/server/ServerScoreboard.java.patch
new file mode 100644
index 0000000000..712e8444b1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/ServerScoreboard.java.patch
@@ -0,0 +1,168 @@
+--- a/net/minecraft/server/ServerScoreboard.java
++++ b/net/minecraft/server/ServerScoreboard.java
+@@ -42,7 +42,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 @@
+ @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.setDirty();
+ }
+
+@@ -65,7 +65,7 @@
+ public void onPlayerScoreRemoved(ScoreHolder scoreHolder, Objective objective) {
+ super.onPlayerScoreRemoved(scoreHolder, objective);
+ if (this.trackedObjectives.contains(objective)) {
+- this.server.getPlayerList().broadcastAll(new ClientboundResetScorePacket(scoreHolder.getScoreboardName(), objective.getName()));
++ this.broadcastAll(new ClientboundResetScorePacket(scoreHolder.getScoreboardName(), objective.getName())); // CraftBukkit
+ }
+
+ this.setDirty();
+@@ -78,7 +78,7 @@
+ super.setDisplayObjective(slot, objective);
+ if (scoreboardobjective1 != objective && scoreboardobjective1 != null) {
+ if (this.getObjectiveDisplaySlotCount(scoreboardobjective1) > 0) {
+- this.server.getPlayerList().broadcastAll(new ClientboundSetDisplayObjectivePacket(slot, objective));
++ this.broadcastAll(new ClientboundSetDisplayObjectivePacket(slot, objective)); // CraftBukkit
+ } else {
+ this.stopTrackingObjective(scoreboardobjective1);
+ }
+@@ -86,7 +86,7 @@
+
+ if (objective != null) {
+ if (this.trackedObjectives.contains(objective)) {
+- this.server.getPlayerList().broadcastAll(new ClientboundSetDisplayObjectivePacket(slot, objective));
++ this.broadcastAll(new ClientboundSetDisplayObjectivePacket(slot, objective)); // CraftBukkit
+ } else {
+ this.startTrackingObjective(objective);
+ }
+@@ -98,7 +98,7 @@
+ @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 @@
+ }
+ }
+
++ // Paper start - Multiple Entries with Scoreboards
++ public boolean addPlayersToTeam(java.util.Collection<String> players, PlayerTeam team) {
++ boolean anyAdded = false;
++ for (String playerName : players) {
++ if (super.addPlayerToTeam(playerName, team)) {
++ anyAdded = true;
++ }
++ }
++
++ if (anyAdded) {
++ this.broadcastAll(ClientboundSetPlayerTeamPacket.createMultiplePlayerPacket(team, players, ClientboundSetPlayerTeamPacket.Action.ADD));
++ this.setDirty();
++ return true;
++ } else {
++ return false;
++ }
++ }
++ // 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();
+ }
+
++ // Paper start - Multiple Entries with Scoreboards
++ public void removePlayersFromTeam(java.util.Collection<String> players, PlayerTeam team) {
++ for (String playerName : players) {
++ super.removePlayerFromTeam(playerName, team);
++ }
++
++ this.broadcastAll(ClientboundSetPlayerTeamPacket.createMultiplePlayerPacket(team, players, ClientboundSetPlayerTeamPacket.Action.REMOVE));
++ this.setDirty();
++ }
++ // Paper end - Multiple Entries with Scoreboards
++
+ @Override
+ public void onObjectiveAdded(Objective objective) {
+ super.onObjectiveAdded(objective);
+@@ -123,7 +153,7 @@
+ public void onObjectiveChanged(Objective objective) {
+ super.onObjectiveChanged(objective);
+ if (this.trackedObjectives.contains(objective)) {
+- this.server.getPlayerList().broadcastAll(new ClientboundSetObjectivePacket(objective, 2));
++ this.broadcastAll(new ClientboundSetObjectivePacket(objective, 2)); // CraftBukkit
+ }
+
+ this.setDirty();
+@@ -142,21 +172,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
+ 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
+ this.setDirty();
+ }
+
+ @Override
+ public void onTeamRemoved(PlayerTeam team) {
+ super.onTeamRemoved(team);
+- this.server.getPlayerList().broadcastAll(ClientboundSetPlayerTeamPacket.createRemovePacket(team));
++ this.broadcastAll(ClientboundSetPlayerTeamPacket.createRemovePacket(team)); // CraftBukkit
+ this.setDirty();
+ }
+
+@@ -207,6 +237,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()) {
+@@ -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);
+ }
+
++ // 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);
++ }
++ }
++ }
++ // CraftBukkit end
++
+ public static enum Method {
+
+ CHANGE, REMOVE;
diff --git a/paper-server/patches/unapplied/net/minecraft/server/ServerTickRateManager.java.patch b/paper-server/patches/unapplied/net/minecraft/server/ServerTickRateManager.java.patch
new file mode 100644
index 0000000000..9720b17dc9
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/ServerTickRateManager.java.patch
@@ -0,0 +1,53 @@
+--- 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/Services.java.patch b/paper-server/patches/unapplied/net/minecraft/server/Services.java.patch
new file mode 100644
index 0000000000..c556957aab
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/Services.java.patch
@@ -0,0 +1,40 @@
+--- a/net/minecraft/server/Services.java
++++ b/net/minecraft/server/Services.java
+@@ -10,16 +10,32 @@
+ import net.minecraft.server.players.GameProfileCache;
+ import net.minecraft.util.SignatureValidator;
+
++
+ public record Services(
+- MinecraftSessionService sessionService, ServicesKeySet servicesKeySet, GameProfileRepository profileRepository, GameProfileCache profileCache
++ MinecraftSessionService sessionService, ServicesKeySet servicesKeySet, GameProfileRepository profileRepository, GameProfileCache profileCache, @javax.annotation.Nullable io.papermc.paper.configuration.PaperConfigurations paperConfigurations // Paper - add paper configuration files
+ ) {
+- private static final String USERID_CACHE_FILE = "usercache.json";
++ // 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
+ MinecraftSessionService minecraftSessionService = authenticationService.createMinecraftSessionService();
+ GameProfileRepository gameProfileRepository = authenticationService.createProfileRepository();
+- GameProfileCache gameProfileCache = new GameProfileCache(gameProfileRepository, new File(rootDirectory, "usercache.json"));
+- return new Services(minecraftSessionService, authenticationService.getServicesKeySet(), gameProfileRepository, gameProfileCache);
++ 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"));
++ return new Services(minecraftSessionService, authenticationService.getServicesKeySet(), gameProfileRepository, gameProfileCache, paperConfigurations);
++ // Paper end - load paper config files from cli options
+ }
+
+ @Nullable
diff --git a/paper-server/patches/unapplied/net/minecraft/server/WorldLoader.java.patch b/paper-server/patches/unapplied/net/minecraft/server/WorldLoader.java.patch
new file mode 100644
index 0000000000..c95f9d743c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/WorldLoader.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/server/WorldLoader.java
++++ b/net/minecraft/server/WorldLoader.java
+@@ -37,7 +37,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);
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
new file mode 100644
index 0000000000..6e80d59765
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/bossevents/CustomBossEvent.java.patch
@@ -0,0 +1,31 @@
+--- 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/BanIpCommands.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/BanIpCommands.java.patch
new file mode 100644
index 0000000000..be620cd58a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/BanIpCommands.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/server/commands/BanIpCommands.java
++++ b/net/minecraft/server/commands/BanIpCommands.java
+@@ -66,7 +66,7 @@
+ }
+
+ for (ServerPlayer serverPlayer : list) {
+- 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();
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
new file mode 100644
index 0000000000..a6e8c45f71
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/DeOpCommands.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/DeOpCommands.java.patch
new file mode 100644
index 0000000000..5009b1bdcb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/DeOpCommands.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/server/commands/DeOpCommands.java
++++ b/net/minecraft/server/commands/DeOpCommands.java
+@@ -35,7 +35,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", gameProfile.getName()), true); // Paper - fixes MC-253721
+ }
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/server/commands/DefaultGameModeCommands.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/DefaultGameModeCommands.java.patch
new file mode 100644
index 0000000000..dfb815a54e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/DefaultGameModeCommands.java.patch
@@ -0,0 +1,18 @@
+--- 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)) {
+- 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());
++ if (event != null && event.isCancelled()) {
++ source.sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), false);
+ }
++ // Paper end - Expand PlayerGameModeChangeEvent
++ i++;
+ }
+ }
+
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
new file mode 100644
index 0000000000..0898d7e23d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/DifficultyCommand.java.patch
@@ -0,0 +1,17 @@
+--- 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
new file mode 100644
index 0000000000..94c98f3597
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/GameModeCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/GameModeCommand.java.patch
new file mode 100644
index 0000000000..5984de9d0f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/GameModeCommand.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/server/commands/GameModeCommand.java
++++ b/net/minecraft/server/commands/GameModeCommand.java
+@@ -60,9 +60,14 @@
+ int i = 0;
+
+ for (ServerPlayer serverPlayer : targets) {
+- if (serverPlayer.setGameMode(gameMode)) {
++ // 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());
++ if (event != null && !event.isCancelled()) {
+ logGamemodeChange(context.getSource(), serverPlayer, gameMode);
+ i++;
++ } else if (event != null && event.cancelMessage() != null) {
++ context.getSource().sendSuccess(() -> io.papermc.paper.adventure.PaperAdventure.asVanilla(event.cancelMessage()), true);
++ // Paper end - Expand PlayerGameModeChangeEvent
+ }
+ }
+
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
new file mode 100644
index 0000000000..66f9df2b3b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/GameRuleCommand.java.patch
@@ -0,0 +1,23 @@
+--- 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
new file mode 100644
index 0000000000..025f1543d5
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/KickCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/KickCommand.java.patch
new file mode 100644
index 0000000000..8138965b1c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/KickCommand.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/server/commands/KickCommand.java
++++ b/net/minecraft/server/commands/KickCommand.java
+@@ -48,7 +48,7 @@
+
+ for (ServerPlayer serverPlayer : targets) {
+ 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
+ source.sendSuccess(() -> Component.translatable("commands.kick.success", serverPlayer.getDisplayName(), reason), true);
+ i++;
+ }
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
new file mode 100644
index 0000000000..93aec260e3
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..c579471341
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/LootCommand.java.patch
@@ -0,0 +1,19 @@
+--- 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/OpCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/OpCommand.java.patch
new file mode 100644
index 0000000000..3ca43d3777
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/OpCommand.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/server/commands/OpCommand.java
++++ b/net/minecraft/server/commands/OpCommand.java
+@@ -46,7 +46,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", gameProfile.getName()), true); // Paper - fixes MC-253721
+ }
+ }
+
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
new file mode 100644
index 0000000000..1c1e4cacb4
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..f0b9cdfcce
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..885d99adba
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..f4a5c32f80
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/SetSpawnCommand.java.patch
@@ -0,0 +1,42 @@
+--- 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/SetWorldSpawnCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/SetWorldSpawnCommand.java.patch
new file mode 100644
index 0000000000..5e3b2e9493
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/SetWorldSpawnCommand.java.patch
@@ -0,0 +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();
+
+- if (worldserver.dimension() != Level.OVERWORLD) {
++ if (false && worldserver.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/unapplied/net/minecraft/server/commands/SpreadPlayersCommand.java.patch b/paper-server/patches/unapplied/net/minecraft/server/commands/SpreadPlayersCommand.java.patch
new file mode 100644
index 0000000000..1827c81c69
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/SpreadPlayersCommand.java.patch
@@ -0,0 +1,20 @@
+--- 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
new file mode 100644
index 0000000000..5a74f92631
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..0126e4d505
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/TeleportCommand.java.patch
@@ -0,0 +1,55 @@
+--- 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
new file mode 100644
index 0000000000..52c7c73aa7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/TimeCommand.java.patch
@@ -0,0 +1,55 @@
+--- 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
new file mode 100644
index 0000000000..2f916fdf48
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/WeatherCommand.java.patch
@@ -0,0 +1,34 @@
+--- 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
new file mode 100644
index 0000000000..d64cdd2c7d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/commands/WorldBorderCommand.java.patch
@@ -0,0 +1,65 @@
+--- 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/DedicatedPlayerList.java.patch b/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedPlayerList.java.patch
new file mode 100644
index 0000000000..17d45a61ee
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedPlayerList.java.patch
@@ -0,0 +1,14 @@
+--- 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());
++ // Paper start - fix converting txt to json file; moved from constructor
++ }
++ @Override
++ public void loadAndSaveFiles() {
++ // Paper end - fix converting txt to json file
+ this.loadUserBanList();
+ this.saveUserBanList();
+ this.loadIpBanList();
diff --git a/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServer.java.patch b/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServer.java.patch
new file mode 100644
index 0000000000..0780484bbb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServer.java.patch
@@ -0,0 +1,475 @@
+--- 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 {
+
+ static final Logger LOGGER = LogUtils.getLogger();
+ private static final int CONVERSION_RETRY_DELAY_MS = 5000;
+ private static final int CONVERSION_RETRIES = 2;
+- private final List<ConsoleInput> consoleInput = Collections.synchronizedList(Lists.newArrayList());
++ private final java.util.Queue<ConsoleInput> serverCommandQueue = new java.util.concurrent.ConcurrentLinkedQueue<>(); // Paper - Perf: use a proper queue
+ @Nullable
+ private QueryThreadGs4 queryThreadGs4;
+- private final RconConsoleSource rconConsoleSource;
++ // private final RemoteControlCommandListener rconConsoleSource; // CraftBukkit - remove field
+ @Nullable
+ private RconThread rconThread;
+ public DedicatedServerSettings settings;
+@@ -81,41 +92,117 @@
+ private DebugSampleSubscriptionTracker debugSampleSubscriptionTracker;
+ 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;
+- 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);
+ }
+
+ @Override
+ public boolean initServer() throws IOException {
+ Thread thread = new Thread("Server console handler") {
+ public void run() {
+- BufferedReader bufferedreader = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
++ // CraftBukkit start
++ 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();
++ } catch (IOException ex) {
++ return;
++ }
++ // CraftBukkit end
++
+ String s;
+
+ try {
+- while (!DedicatedServer.this.isStopped() && DedicatedServer.this.isRunning() && (s = bufferedreader.readLine()) != null) {
+- DedicatedServer.this.handleConsoleInput(s, 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);
++ } else {
++ s = bufferedreader.readLine();
++ }
++
++ // SPIGOT-5220: Throttle if EOF (ctrl^d) or stdin is /dev/null
++ if (s == null) {
++ try {
++ Thread.sleep(50L);
++ } catch (InterruptedException ex) {
++ Thread.currentThread().interrupt();
++ }
++ continue;
++ }
++ if (s.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);
+ }
+
++ */
++ // Paper end
+ }
+ };
+
++ // CraftBukkit start - TODO: handle command-line logging arguments
++ java.util.logging.Logger global = java.util.logging.Logger.getLogger("");
++ global.setUseParentHandlers(false);
++ for (java.util.logging.Handler handler : global.getHandlers()) {
++ global.removeHandler(handler);
++ }
++ global.addHandler(new org.bukkit.craftbukkit.util.ForwardLogHandler());
++
++ // Paper start - Not needed with TerminalConsoleAppender
++ final org.apache.logging.log4j.Logger logger = LogManager.getRootLogger();
++ /*
++ final org.apache.logging.log4j.core.Logger logger = ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger());
++ for (org.apache.logging.log4j.core.Appender appender : logger.getAppenders().values()) {
++ if (appender instanceof org.apache.logging.log4j.core.appender.ConsoleAppender) {
++ logger.removeAppender(appender);
++ }
++ }
++
++ TerminalConsoleWriterThread writerThread = new TerminalConsoleWriterThread(System.out, this.reader);
++ this.reader.setCompletionHandler(new TerminalCompletionHandler(writerThread, this.reader.getCompletionHandler()));
++ writerThread.start();
++ */
++ // Paper end - Not needed with TerminalConsoleAppender
++
++ System.setOut(IoBuilder.forLogger(logger).setLevel(Level.INFO).buildPrintStream());
++ System.setErr(IoBuilder.forLogger(logger).setLevel(Level.WARN).buildPrintStream());
++ // CraftBukkit end
++
+ thread.setDaemon(true);
+ thread.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandler(DedicatedServer.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());
+ 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\"");
+ }
+
++ // 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("****************************");
++ }
++ // 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);
+ }
++ // Spigot start
++ this.setPlayerList(new DedicatedPlayerList(this, this.registries(), this.playerDataStorage));
++ org.spigotmc.SpigotConfig.init((java.io.File) this.options.valueOf("spigot-settings"));
++ org.spigotmc.SpigotConfig.registerCommands();
++ // Spigot end
++ io.papermc.paper.util.ObfHelper.INSTANCE.getClass(); // Paper - load mappings for stacktrace deobf and etc.
++ // Paper start - initialize global and world-defaults configuration
++ this.paperConfigurations.initializeGlobalConfiguration(this.registryAccess());
++ this.paperConfigurations.initializeWorldDefaultsConfiguration(this.registryAccess());
++ // Paper end - initialize global and world-defaults configuration
++ this.server.spark.enableEarlyIfRequested(); // Paper - spark
++ // Paper start - fix converting txt to json file; convert old users earlier after PlayerList creation but before file load/save
++ if (this.convertOldUsers()) {
++ this.getProfileCache().save(false); // Paper
++ }
++ this.getPlayerList().loadAndSaveFiles(); // Must be after convertNames
++ // Paper end - fix converting txt to json file
++ org.spigotmc.WatchdogThread.doStart(org.spigotmc.SpigotConfig.timeoutTime, org.spigotmc.SpigotConfig.restartOnCrash); // Paper - start watchdog thread
++ thread.start(); // Paper - Enhance console tab completions for brigadier commands; start console thread after MinecraftServer.console & PaperConfig are initialized
++ io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command
++ 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);
++ // 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.");
++ 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.");
++ return false;
++ }
++ bindAddress = new io.netty.channel.unix.DomainSocketAddress(this.getLocalIp().substring("unix:".length()));
++ } else {
+ InetAddress inetaddress = null;
+
+ if (!this.getLocalIp().isEmpty()) {
+@@ -143,34 +267,55 @@
+ if (this.getPort() < 0) {
+ this.setPort(dedicatedserverproperties.serverPort);
+ }
++ 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());
+
+ 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
+ return false;
+ }
+
++ // CraftBukkit start
++ // this.setPlayerList(new DedicatedPlayerList(this, this.registries(), this.playerDataStorage)); // Spigot - moved up
++ this.server.loadPlugins();
++ this.server.enablePlugins(org.bukkit.plugin.PluginLoadOrder.STARTUP);
++ // CraftBukkit end
++
++ // Paper start - Add Velocity IP Forwarding Support
++ boolean usingProxy = org.spigotmc.SpigotConfig.bungee || io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled;
++ String proxyFlavor = (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) ? "Velocity" : "BungeeCord";
++ 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.");
++ // 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
++ } 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.");
++ }
++ // Spigot end
+ DedicatedServer.LOGGER.warn("To change this, set \"online-mode\" to \"true\" in the server.properties file.");
+ }
+
+- if (this.convertOldUsers()) {
+- this.getProfileCache().save();
+- }
+
+ if (!OldUsersConverter.serverReadyAfterUserconversion(this)) {
+ return false;
+ } else {
+- 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 @@
+ SkullBlockEntity.setup(this.services, this);
+ GameProfileCache.setUsesAuthentication(this.usesAuthentication());
+ DedicatedServer.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
+ }
+
+ if (dedicatedserverproperties.enableQuery) {
+@@ -197,7 +342,7 @@
+ this.rconThread = RconThread.create(this);
+ }
+
+- if (this.getMaxTickLength() > 0L) {
++ if (false && this.getMaxTickLength() > 0L) { // Spigot - disable
+ Thread thread1 = new Thread(new ServerWatchdog(this));
+
+ thread1.setUncaughtExceptionHandler(new DefaultUncaughtExceptionHandlerWithName(DedicatedServer.LOGGER));
+@@ -215,6 +360,12 @@
+ }
+ }
+
++ // Paper start
++ public java.io.File getPluginsFolder() {
++ return (java.io.File) this.options.valueOf("plugins");
++ }
++ // Paper end
++
+ @Override
+ public boolean isSpawningMonsters() {
+ return this.settings.getProperties().spawnMonsters && super.isSpawningMonsters();
+@@ -227,7 +378,7 @@
+
+ @Override
+ public void forceDifficulty() {
+- this.setDifficulty(this.getProperties().difficulty, true);
++ // this.setDifficulty(this.getProperties().difficulty, true); // Paper - per level difficulty; Don't overwrite level.dat's difficulty, keep current
+ }
+
+ @Override
+@@ -286,13 +437,14 @@
+ }
+
+ if (this.rconThread != null) {
+- this.rconThread.stop();
++ this.rconThread.stopNonBlocking(); // Paper - don't wait for remote connections
+ }
+
+ if (this.queryThreadGs4 != null) {
+- this.queryThreadGs4.stop();
++ // this.remoteStatusListener.stop(); // Paper - don't wait for remote connections
+ }
+
++ System.exit(0); // CraftBukkit
+ }
+
+ @Override
+@@ -302,19 +454,29 @@
+ }
+
+ @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 handleConsoleInputs() {
+- while (!this.consoleInput.isEmpty()) {
+- ConsoleInput servercommand = (ConsoleInput) this.consoleInput.remove(0);
++ // 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);
++ this.server.getPluginManager().callEvent(event);
++ if (event.isCancelled()) continue;
++ servercommand = new ConsoleInput(event.getCommand(), servercommand.source);
++
++ // this.getCommands().performPrefixedCommand(servercommand.source, servercommand.msg); // Called in dispatchServerCommand
++ this.server.dispatchServerCommand(this.console, servercommand);
++ // CraftBukkit end
+ }
+
+ }
+@@ -383,7 +545,7 @@
+
+ @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();
++ // Paper start - Add setting for proxy online mode status
++ return dedicatedserverproperties.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 @@
+
+ @Override
+ public String getPluginNames() {
+- return "";
++ // CraftBukkit start - Whole method
++ StringBuilder result = new StringBuilder();
++ org.bukkit.plugin.Plugin[] plugins = this.server.getPluginManager().getPlugins();
++
++ result.append(this.server.getName());
++ result.append(" on Bukkit ");
++ result.append(this.server.getBukkitVersion());
++
++ if (plugins.length > 0 && this.server.getQueryPlugins()) {
++ result.append(": ");
++
++ for (int i = 0; i < plugins.length; i++) {
++ if (i > 0) {
++ result.append("; ");
++ }
++
++ result.append(plugins[i].getDescription().getName());
++ result.append(" ");
++ result.append(plugins[i].getDescription().getVersion().replaceAll(";", ","));
++ }
++ }
++
++ return result.toString();
++ // CraftBukkit end
+ }
+
+ @Override
+ public String runCommand(String command) {
+- this.rconConsoleSource.prepareForCommand();
++ // 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);
++ CommandSourceStack wrapper = rconConsoleSource.createCommandSourceStack();
++ RemoteServerCommandEvent event = new 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 @@
+ }
+ }
+ }
++
++ // CraftBukkit start
++ public boolean isDebugging() {
++ return this.getProperties().debug;
++ }
++
++ @Override
++ public CommandSender getBukkitSender(CommandSourceStack wrapper) {
++ return this.console;
++ }
++ // CraftBukkit end
+ }
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
new file mode 100644
index 0000000000..2020814765
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServerProperties.java.patch
@@ -0,0 +1,106 @@
+--- 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
new file mode 100644
index 0000000000..0aaaffd34b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/dedicated/DedicatedServerSettings.java.patch
@@ -0,0 +1,27 @@
+--- 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
new file mode 100644
index 0000000000..300eeaa029
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/dedicated/Settings.java.patch
@@ -0,0 +1,179 @@
+--- 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/gui/MinecraftServerGui.java.patch b/paper-server/patches/unapplied/net/minecraft/server/gui/MinecraftServerGui.java.patch
new file mode 100644
index 0000000000..84332717d5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/gui/MinecraftServerGui.java.patch
@@ -0,0 +1,103 @@
+--- 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
++
++ // Paper start - Improve ServerGUI
++ try {
++ jframe.setIconImage(javax.imageio.ImageIO.read(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 @@
+ 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 @@
+
+ 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;
+ }
+
++ // Paper start - Add onboarding message for initial server start
++ private JComponent buildOnboardingPanel() {
++ String onboardingLink = "https://docs.papermc.io/paper/next-steps";
++ JPanel jPanel = new JPanel();
++
++ javax.swing.JLabel jLabel = new javax.swing.JLabel("If you need help setting up your server you can visit:");
++ jLabel.setFont(MinecraftServerGui.MONOSPACED);
++
++ javax.swing.JLabel link = new javax.swing.JLabel("<html><u> " + onboardingLink + "</u></html>");
++ link.setFont(MinecraftServerGui.MONOSPACED);
++ link.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR));
++ link.addMouseListener(new java.awt.event.MouseAdapter() {
++ @Override
++ public void mouseClicked(final java.awt.event.MouseEvent e) {
++ try {
++ java.awt.Desktop.getDesktop().browse(java.net.URI.create(onboardingLink));
++ } catch (java.io.IOException exception) {
++ LOGGER.error("Unable to find a default browser. Please manually visit the website: " + onboardingLink, exception);
++ } catch (UnsupportedOperationException exception) {
++ LOGGER.error("This platform does not support the BROWSE action. Please manually visit the website: " + onboardingLink, exception);
++ } catch (SecurityException exception) {
++ LOGGER.error("This action has been denied by the security manager. Please manually visit the website: " + onboardingLink, exception);
++ }
++ }
++ });
++
++ jPanel.add(jLabel);
++ jPanel.add(link);
++
++ 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/unapplied/net/minecraft/server/gui/StatsComponent.java.patch
new file mode 100644
index 0000000000..3fa5e0162c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/gui/StatsComponent.java.patch
@@ -0,0 +1,33 @@
+--- a/net/minecraft/server/gui/StatsComponent.java
++++ b/net/minecraft/server/gui/StatsComponent.java
+@@ -34,10 +34,19 @@
+
+ private void tick() {
+ long l = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
++ // Paper start - Improve ServerGUI
++ 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] );
++ }
+ 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[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 @@
+ public void close() {
+ this.timer.stop();
+ }
++
++ // Paper start - Improve ServerGUI
++ private static String format(double tps) {
++ return (( tps > 21.0 ) ? "*" : "") + Math.min(Math.round(tps * 100.0) / 100.0, 20.0); // only print * at 21, we commonly peak to 20.02 as the tick sleep is not accurate enough, stop the noise
++ }
++ // Paper end - Improve ServerGUI
+ }
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
new file mode 100644
index 0000000000..4516416d44
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/level/ChunkHolder.java.patch
@@ -0,0 +1,245 @@
+--- 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
new file mode 100644
index 0000000000..9a5c5895b8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/level/ChunkMap.java.patch
@@ -0,0 +1,433 @@
+--- 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
new file mode 100644
index 0000000000..60e1437870
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/level/DistanceManager.java.patch
@@ -0,0 +1,152 @@
+--- 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/ServerChunkCache.java.patch b/paper-server/patches/unapplied/net/minecraft/server/level/ServerChunkCache.java.patch
new file mode 100644
index 0000000000..f7949f17c6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/level/ServerChunkCache.java.patch
@@ -0,0 +1,255 @@
+--- a/net/minecraft/server/level/ServerChunkCache.java
++++ b/net/minecraft/server/level/ServerChunkCache.java
+@@ -74,6 +74,13 @@
+ @Nullable
+ @VisibleForDebug
+ private NaturalSpawner.SpawnState lastSpawnState;
++ // Paper start
++ private final ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<net.minecraft.world.level.chunk.LevelChunk> fullChunks = new ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable<>();
++ public int getFullChunksCount() {
++ return this.fullChunks.size();
++ }
++ long chunkFutureAwaitCounter;
++ // Paper end
+
+ public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier<DimensionDataStorage> persistentStateManagerFactory) {
+ this.level = world;
+@@ -95,6 +102,64 @@
+ this.clearCache();
+ }
+
++ // CraftBukkit start - properly implement isChunkLoaded
++ public boolean isChunkLoaded(int chunkX, int chunkZ) {
++ ChunkHolder chunk = this.chunkMap.getUpdatingChunkIfPresent(ChunkPos.asLong(chunkX, chunkZ));
++ if (chunk == null) {
++ return false;
++ }
++ return chunk.getFullChunkNow() != null;
++ }
++ // CraftBukkit end
++ // Paper start
++ public void addLoadedChunk(LevelChunk chunk) {
++ this.fullChunks.put(chunk.coordinateKey, chunk);
++ }
++
++ public void removeLoadedChunk(LevelChunk chunk) {
++ this.fullChunks.remove(chunk.coordinateKey);
++ }
++
++ @Nullable
++ public ChunkAccess getChunkAtImmediately(int x, int z) {
++ ChunkHolder holder = this.chunkMap.getVisibleChunkIfPresent(ChunkPos.asLong(x, z));
++ if (holder == null) {
++ return null;
++ }
++
++ return holder.getLatestChunk();
++ }
++
++ public <T> void addTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
++ this.distanceManager.addTicket(ticketType, chunkPos, ticketLevel, identifier);
++ }
++
++ public <T> void removeTicketAtLevel(TicketType<T> ticketType, ChunkPos chunkPos, int ticketLevel, T identifier) {
++ this.distanceManager.removeTicket(ticketType, chunkPos, ticketLevel, identifier);
++ }
++
++ // "real" get chunk if loaded
++ // Note: Partially copied from the getChunkAt method below
++ @Nullable
++ public LevelChunk getChunkAtIfCachedImmediately(int x, int z) {
++ long k = ChunkPos.asLong(x, z);
++
++ // Note: Bypass cache since we need to check ticket level, and to make this MT-Safe
++
++ ChunkHolder playerChunk = this.getVisibleChunkIfPresent(k);
++ if (playerChunk == null) {
++ return null;
++ }
++
++ return playerChunk.getFullChunkNowUnchecked();
++ }
++
++ @Nullable
++ public LevelChunk getChunkAtIfLoadedImmediately(int x, int z) {
++ return this.fullChunks.get(ChunkPos.asLong(x, z));
++ }
++ // Paper end
++
+ @Override
+ public ThreadedLevelLightEngine getLightEngine() {
+ 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;
+ }
+ }
+@@ -150,8 +215,9 @@
+
+ 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((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) {
++ // 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());
++ 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 @@
+ }
+
+ 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
+ }
+
+ @Override
+@@ -279,7 +353,7 @@
+ return this.mainThreadProcessor.pollTask();
+ }
+
+- boolean runDistanceManagerUpdates() {
++ public boolean runDistanceManagerUpdates() { // Paper - public
+ boolean flag = this.distanceManager.runAllUpdates(this.chunkMap);
+ boolean flag1 = this.chunkMap.promoteChunkMap();
+
+@@ -309,18 +383,40 @@
+
+ @Override
+ public void close() throws IOException {
+- this.save(true);
++ // CraftBukkit start
++ this.close(true);
++ }
++
++ public void close(boolean save) throws IOException {
++ if (save) {
++ this.save(true);
++ }
++ // CraftBukkit end
+ this.dataStorage.close();
+ this.lightEngine.close();
+ this.chunkMap.close();
+ }
+
++ // CraftBukkit start - modelled on below
++ public void purgeUnload() {
++ ProfilerFiller gameprofilerfiller = Profiler.get();
++
++ gameprofilerfiller.push("purge");
++ this.distanceManager.purgeStaleTickets();
++ this.runDistanceManagerUpdates();
++ gameprofilerfiller.popPush("unload");
++ this.chunkMap.tick(() -> true);
++ gameprofilerfiller.pop();
++ this.clearCache();
++ }
++ // CraftBukkit end
++
+ @Override
+ public void tick(BooleanSupplier shouldKeepTicking, boolean tickChunks) {
+ ProfilerFiller gameprofilerfiller = Profiler.get();
+
+ gameprofilerfiller.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;
+ 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;
++ // Paper start - PlayerNaturallySpawnCreaturesEvent
++ for (ServerPlayer entityPlayer : this.level.players()) {
++ int chunkRange = Math.min(level.spigotConfig.mobSpawnRange, entityPlayer.getBukkitEntity().getViewDistance());
++ chunkRange = Math.min(chunkRange, 8);
++ entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange);
++ 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
+ } else {
+ list1 = 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);
+ }
+
+@@ -541,10 +645,16 @@
+
+ @Override
+ public void setSpawnSettings(boolean spawnMonsters) {
+- this.spawnEnemies = spawnMonsters;
+- this.spawnFriendlies = this.spawnFriendlies;
++ // CraftBukkit start
++ this.setSpawnSettings(spawnMonsters, this.spawnFriendlies);
+ }
+
++ public void setSpawnSettings(boolean flag, boolean spawnFriendlies) {
++ this.spawnEnemies = flag;
++ this.spawnFriendlies = spawnFriendlies;
++ // CraftBukkit end
++ }
++
+ public String getChunkDebugData(ChunkPos pos) {
+ return this.chunkMap.getChunkDebugData(pos);
+ }
+@@ -618,14 +728,20 @@
+ }
+
+ @Override
+- protected boolean pollTask() {
++ // CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
++ public boolean pollTask() {
++ try {
+ if (ServerChunkCache.this.runDistanceManagerUpdates()) {
+ return true;
+ } else {
+ ServerChunkCache.this.lightEngine.tryScheduleUpdate();
+ return super.pollTask();
+ }
++ } finally {
++ ServerChunkCache.this.chunkMap.callbackExecutor.run();
+ }
++ // CraftBukkit end
++ }
+ }
+
+ private static record ChunkAndHolder(LevelChunk chunk, ChunkHolder holder) {
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
new file mode 100644
index 0000000000..f458422c50
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/level/ServerEntity.java.patch
@@ -0,0 +1,179 @@
+--- 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
new file mode 100644
index 0000000000..d9915e3d7d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/level/ServerLevel.java.patch
@@ -0,0 +1,1261 @@
+--- 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
new file mode 100644
index 0000000000..9daf2af366
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/level/ServerPlayer.java.patch
@@ -0,0 +1,1891 @@
+--- 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
new file mode 100644
index 0000000000..aba7ebeae1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/level/ServerPlayerGameMode.java.patch
@@ -0,0 +1,463 @@
+--- 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/level/TicketType.java.patch b/paper-server/patches/unapplied/net/minecraft/server/level/TicketType.java.patch
new file mode 100644
index 0000000000..ca9484bbeb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/level/TicketType.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/server/level/TicketType.java
++++ b/net/minecraft/server/level/TicketType.java
+@@ -7,6 +7,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 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);
diff --git a/paper-server/patches/unapplied/net/minecraft/server/level/WorldGenRegion.java.patch b/paper-server/patches/unapplied/net/minecraft/server/level/WorldGenRegion.java.patch
new file mode 100644
index 0000000000..415887c78f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/level/WorldGenRegion.java.patch
@@ -0,0 +1,105 @@
+--- 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);
+
+ return k < this.generatingStep.directDependencies().size();
++ }
++
++ // Paper start - if loaded util
++ @Nullable
++ @Override
++ public ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
++ return this.getChunk(x, z, ChunkStatus.FULL, false);
++ }
++
++ @Override
++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++ return chunk == null ? null : chunk.getBlockState(blockposition);
++ }
++
++ @Override
++ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++ return chunk == null ? null : chunk.getFluidState(blockposition);
+ }
++ // Paper end
+
+ @Override
+ public BlockState getBlockState(BlockPos pos) {
+@@ -217,7 +237,8 @@
+ if (iblockdata.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 (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 @@
+ }
+ }
+
++ 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 @@
+
+ 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()));
++ hasSetFarWarned = true;
++ if (this.getServer() != null && this.getServer().isDebugging()) {
++ io.papermc.paper.util.TraceUtil.dumpTraceForThread("far setBlock call");
++ }
++ }
++ // Paper end - Buffer OOB setBlock calls
+ return false;
+ }
+ }
+@@ -294,7 +324,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);
+ }
+ } else {
++ // Paper start - Clear block entity before setting up a DUMMY block entity
++ // The concept of removing a block entity when the block itself changes is generally lifted
++ // from LevelChunk#setBlockState.
++ // It is however to note that this may only run if the block actually changes.
++ // Otherwise a chest block entity generated by a structure template that is later "updated" to
++ // 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);
++ }
++ // 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 @@
+
+ @Override
+ public boolean addFreshEntity(Entity 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) {
++ // CraftBukkit end
+ int i = SectionPos.blockToSectionCoord(entity.getBlockX());
+ int j = SectionPos.blockToSectionCoord(entity.getBlockZ());
+
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
new file mode 100644
index 0000000000..a4f704f9e9
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/network/LegacyQueryHandler.java.patch
@@ -0,0 +1,211 @@
+--- 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/PlayerChunkSender.java.patch b/paper-server/patches/unapplied/net/minecraft/server/network/PlayerChunkSender.java.patch
new file mode 100644
index 0000000000..e1ac133048
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/network/PlayerChunkSender.java.patch
@@ -0,0 +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));
++ // 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();
++ }
++ // Paper end - PlayerChunkUnloadEvent
+ }
+ }
+
+@@ -75,6 +80,11 @@
+
+ private static void sendChunk(ServerGamePacketListenerImpl handler, ServerLevel world, LevelChunk chunk) {
+ handler.send(new ClientboundLevelChunkWithLightPacket(chunk, world.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();
++ }
++ // Paper end - PlayerChunkLoadEvent
+ ChunkPos chunkPos = chunk.getPos();
+ DebugPackets.sendPoiPacketsForChunk(world, chunkPos);
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch b/paper-server/patches/unapplied/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
new file mode 100644
index 0000000000..4ba7794d53
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/network/ServerCommonPacketListenerImpl.java.patch
@@ -0,0 +1,418 @@
+--- 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;
+ 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
+ private static final Logger LOGGER = LogUtils.getLogger();
+ public static final int LATENCY_CHECK_INTERVAL = 15000;
+ private static final int CLOSED_LISTENER_TIMEOUT = 15000;
+ private static final Component TIMEOUT_DISCONNECTION_MESSAGE = Component.translatable("disconnect.timeout");
+ static final Component DISCONNECT_UNEXPECTED_QUERY = Component.translatable("multiplayer.disconnect.unexpected_query_response");
+ protected final MinecraftServer server;
+- protected final Connection connection;
++ public final Connection connection; // Paper
+ private final boolean transferred;
+- private long keepAliveTime;
++ private long keepAliveTime = Util.getMillis(); // Paper
+ private boolean keepAlivePending;
+ private long keepAliveChallenge;
+ private long closedListenerTime;
+ private boolean closed = false;
+ private int latency;
+ private volatile boolean suspendFlushingOnServerThread = false;
++ public final java.util.Map<java.util.UUID, net.kyori.adventure.resource.ResourcePackCallback> packCallbacks = new java.util.concurrent.ConcurrentHashMap<>(); // Paper - adventure resource pack callbacks
++ 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
+
+- 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;
+ this.keepAliveTime = Util.getMillis();
+- this.latency = clientData.latency();
+- this.transferred = clientData.transferred();
++ this.latency = commonlistenercookie.latency();
++ this.transferred = commonlistenercookie.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
++ }
++
+ private void close() {
+ if (!this.closed) {
+ this.closedListenerTime = Util.getMillis();
+@@ -65,6 +116,11 @@
+
+ @Override
+ public void onDisconnect(DisconnectionDetails info) {
++ // Paper start - Fix kick event leave message not being sent
++ this.onDisconnect(info, 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");
+ 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);
+
+ 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
+ }
+
+ }
+@@ -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");
++
+ @Override
+- public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {}
++ public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {
++ // Paper start - Brand support
++ if (packet.payload() instanceof net.minecraft.network.protocol.common.custom.BrandPayload brandPayload) {
++ this.player.clientBrandName = brandPayload.brand();
++ }
++ // Paper end - Brand support
++ if (!(packet.payload() instanceof DiscardedPayload)) {
++ return;
++ }
++ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
++ ResourceLocation identifier = packet.payload().type().id();
++ ByteBuf payload = ((DiscardedPayload)packet.payload()).data();
+
++ if (identifier.equals(ServerCommonPacketListenerImpl.CUSTOM_REGISTER)) {
++ try {
++ String channels = payload.toString(com.google.common.base.Charsets.UTF_8);
++ for (String channel : channels.split("\0")) {
++ 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
++ }
++ } else if (identifier.equals(ServerCommonPacketListenerImpl.CUSTOM_UNREGISTER)) {
++ try {
++ String channels = payload.toString(com.google.common.base.Charsets.UTF_8);
++ for (String channel : channels.split("\0")) {
++ 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
++ }
++ } else {
++ try {
++ byte[] data = new byte[payload.readableBytes()];
++ payload.readBytes(data);
++ // Paper start - Brand support; Retain this incase upstream decides to 'break' the new mechanism in favour of backwards compat...
++ if (identifier.equals(MINECRAFT_BRAND)) {
++ try {
++ this.player.clientBrandName = new net.minecraft.network.FriendlyByteBuf(io.netty.buffer.Unpooled.copiedBuffer(data)).readUtf(256);
++ } catch (StringIndexOutOfBoundsException ex) {
++ this.player.clientBrandName = "illegal";
++ }
++ }
++ // 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
++ }
++ }
++
++ }
++
++ 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);
+ 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
+ }
++ // Paper start - adventure pack callbacks
++ // call the callbacks before the previously-existing event so the event has final say
++ final net.kyori.adventure.resource.ResourcePackCallback callback;
++ if (packet.action().isTerminal()) {
++ callback = this.packCallbacks.remove(packet.id());
++ } else {
++ callback = this.packCallbacks.get(packet.id());
++ }
++ if (callback != null) {
++ callback.packEventReceived(packet.id(), net.kyori.adventure.resource.ResourcePackStatus.valueOf(packet.action().name()), this.getCraftPlayer());
++ }
++ // Paper end
++ // Paper start - store last pack status
++ PlayerResourcePackStatusEvent.Status packStatus = PlayerResourcePackStatusEvent.Status.values()[packet.action().ordinal()];
++ player.getBukkitEntity().resourcePackStatus = packStatus;
++ this.cserver.getPluginManager().callEvent(new PlayerResourcePackStatusEvent(this.getCraftPlayer(), packet.id(), packStatus)); // CraftBukkit
++ // Paper end - store last pack status
+
+ }
+
+ @Override
+ public void handleCookieResponse(ServerboundCookieResponsePacket packet) {
+- this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
++ // CraftBukkit start
++ PacketUtils.ensureRunningOnSameThread(packet, this, (BlockableEventLoop) 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
+ }
+
+ protected void keepConnectionAlive() {
+ Profiler.get().push("keepAlive");
+- long i = Util.getMillis();
++ // 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
+ 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 @@
+ 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
+ }
+
+ return false;
+@@ -156,6 +302,14 @@
+ }
+
+ public void send(Packet<?> packet, @Nullable PacketSendListener callbacks) {
++ // 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());
++ }
++ // CraftBukkit end
+ if (packet.isTerminal()) {
+ this.close();
+ }
+@@ -175,22 +329,109 @@
+ }
+ }
+
++ // Paper start - adventure
++ public void disconnect(final net.kyori.adventure.text.Component reason) {
++ this.disconnect(reason, PlayerKickEvent.Cause.UNKNOWN);
++ }
++ public void disconnect(final net.kyori.adventure.text.Component reason, 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));
++ // Paper start - kick event causes
++ this.disconnect(reason, PlayerKickEvent.Cause.UNKNOWN);
+ }
++ public void disconnect(final Component reason, 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
++ // CraftBukkit start - fire PlayerKickEvent
++ if (this.processedDisconnect) {
++ return;
++ }
++ if (!this.cserver.isPrimaryThread()) {
++ Waitable waitable = new Waitable() {
++ @Override
++ protected Object evaluate() {
++ ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause); // Paper - kick event causes
++ return null;
++ }
++ };
++
++ this.server.processQueue.add(waitable);
++
++ try {
++ waitable.get();
++ } catch (InterruptedException e) {
++ Thread.currentThread().interrupt();
++ } catch (ExecutionException e) {
++ throw new RuntimeException(e);
++ }
++ return;
++ }
++
++ 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
++
++ if (this.cserver.getServer().isRunning()) {
++ this.cserver.getPluginManager().callEvent(event);
++ }
++
++ if (event.isCancelled()) {
++ // Do not kick the player
++ 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
++ }
++
++ 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);
++ // CraftBukkit - Don't wait
++ minecraftserver.scheduleOnMain(networkmanager::handleDisconnection); // Paper
+ }
+
++ // Paper start - add proper async disconnect
++ public void disconnectAsync(net.kyori.adventure.text.Component reason, PlayerKickEvent.Cause cause) {
++ this.disconnectAsync(io.papermc.paper.adventure.PaperAdventure.asVanilla(reason), cause);
++ }
++
++ public void disconnectAsync(Component reason, PlayerKickEvent.Cause cause) {
++ this.disconnectAsync(new DisconnectionDetails(reason), cause);
++ }
++
++ public void disconnectAsync(DisconnectionDetails disconnectionInfo, PlayerKickEvent.Cause cause) {
++ if (this.cserver.isPrimaryThread()) {
++ this.disconnect(disconnectionInfo, cause);
++ return;
++ }
++ this.connection.setReadOnly();
++ this.server.scheduleOnMain(() -> {
++ ServerCommonPacketListenerImpl.this.disconnect(disconnectionInfo, cause);
++ if (ServerCommonPacketListenerImpl.this.player.quitReason == null) {
++ // cancelled
++ ServerCommonPacketListenerImpl.this.connection.enableAutoRead();
++ }
++ });
++ }
++ // Paper end - add proper async disconnect
++
+ protected boolean isSingleplayerOwner() {
+ return this.server.isSingleplayerOwner(this.playerProfile());
+ }
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
new file mode 100644
index 0000000000..813b398d03
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch
@@ -0,0 +1,89 @@
+--- 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
new file mode 100644
index 0000000000..adc3cc15f4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/network/ServerConnectionListener.java.patch
@@ -0,0 +1,156 @@
+--- 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/ServerGamePacketListenerImpl.java.patch b/paper-server/patches/unapplied/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
new file mode 100644
index 0000000000..6c1745ae43
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch
@@ -0,0 +1,2655 @@
+--- 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;
+ 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.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.Slot;
++import net.minecraft.world.item.crafting.RecipeHolder;
++import org.bukkit.Location;
++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;
++import org.bukkit.event.inventory.CraftItemEvent;
++import org.bukkit.event.inventory.InventoryAction;
++import org.bukkit.event.inventory.InventoryClickEvent;
++import org.bukkit.event.inventory.InventoryCreativeEvent;
++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;
++import org.bukkit.event.player.PlayerInteractAtEntityEvent;
++import org.bukkit.event.player.PlayerInteractEntityEvent;
++import org.bukkit.event.player.PlayerItemHeldEvent;
++import org.bukkit.event.player.PlayerMoveEvent;
++import org.bukkit.event.player.PlayerRespawnEvent.RespawnReason;
++import org.bukkit.event.player.PlayerSwapHandItemsEvent;
++import org.bukkit.event.player.PlayerTeleportEvent;
++import org.bukkit.event.player.PlayerToggleFlightEvent;
++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 @@
+ private int tickCount;
+ private int ackBlockChangesUpTo = -1;
+ private final TickThrottler chatSpamThrottler = new TickThrottler(20, 200);
++ private final TickThrottler tabSpamThrottler = new TickThrottler(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamIncrement, io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.tabSpamLimit); // Paper - configurable tab spam limits
+ private final TickThrottler dropSpamThrottler = new TickThrottler(20, 1480);
++ private final TickThrottler recipeSpamPackets = new TickThrottler(io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamIncrement, io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.recipeSpamLimit);
+ private double firstGoodX;
+ private double firstGoodY;
+ private double firstGoodZ;
+@@ -240,14 +307,16 @@
+ 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
++ private int lastTick = MinecraftServer.currentTick;
++ private int allowedPlayerTicks = 1;
++ private int lastDropTick = MinecraftServer.currentTick;
++ private int lastBookTick = MinecraftServer.currentTick;
++ private int dropCount = 0;
++
++ private boolean hasMoved = false;
++ private double lastPosX = Double.MAX_VALUE;
++ private double lastPosY = Double.MAX_VALUE;
++ private double lastPosZ = Double.MAX_VALUE;
++ private float lastPitch = Float.MAX_VALUE;
++ private float lastYaw = Float.MAX_VALUE;
++ private boolean justTeleported = false;
++ // CraftBukkit end
++
+ @Override
+ public void tick() {
+ if (this.ackBlockChangesUpTo > -1) {
+@@ -277,7 +362,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"));
++ 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 @@
+ 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"));
++ 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 @@
+
+ this.keepConnectionAlive();
+ this.chatSpamThrottler.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
++ 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
+ }
+
++ // Paper start - Prevent causing expired keys from impacting new joins
++ if (!hasLoggedExpiry && this.chatSession != null && this.chatSession.profilePublicKey().data().hasExpired()) {
++ LOGGER.info("Player profile key for {} has expired!", this.player.getName().getString());
++ hasLoggedExpiry = true;
++ }
++ // Paper end - Prevent causing expired keys from impacting new joins
++
+ }
+
+ private int getMaximumFlyingTicks(Entity vehicle) {
+@@ -376,6 +471,12 @@
+ @Override
+ public void handlePlayerInput(ServerboundPlayerInputPacket packet) {
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
++ // CraftBukkit start
++ if (!packet.input().equals(this.player.getLastClientInput())) {
++ PlayerInputEvent event = new PlayerInputEvent(this.player.getBukkitEntity(), new CraftInput(packet.input()));
++ this.cserver.getPluginManager().callEvent(event);
++ }
++ // CraftBukkit end
+ this.player.setLastClientInput(packet.input());
+ }
+
+@@ -395,27 +496,84 @@
+ 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
+ } else if (!this.updateAwaitingTeleport() && this.player.hasClientLoaded()) {
+ Entity entity = this.player.getRootVehicle();
++ // Paper start - Don't allow vehicle movement from players while teleporting
++ if (this.awaitingPositionFromClient != null || this.player.isImmobile() || entity.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());
++ // CraftBukkit - store current player position
++ double prevX = this.player.getX();
++ double prevY = this.player.getY();
++ double prevZ = this.player.getZ();
++ 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
+ 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;
++ // 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);
++ // 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);
++ this.lastTick = (int) (System.currentTimeMillis() / 50);
++
++ ++this.receivedMovePacketCount;
++ int i = this.receivedMovePacketCount - this.knownMovePacketCount;
++ if (i > Math.max(this.allowedPlayerTicks, 5)) {
++ ServerGamePacketListenerImpl.LOGGER.debug(this.player.getScoreboardName() + " is sending move packets too frequently (" + i + " packets since last tick)");
++ i = 1;
++ }
++
++ if (d10 > 0) {
++ this.allowedPlayerTicks -= 1;
++ } else {
++ this.allowedPlayerTicks = 20;
++ }
++ double speed;
++ if (this.player.getAbilities().flying) {
++ speed = this.player.getAbilities().flyingSpeed * 20f;
++ } else {
++ speed = this.player.getAbilities().walkingSpeed * 10f;
++ }
++ speed *= 2f; // TODO: Get the speed of the vehicle instead of the player
++
++ // 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));
++ 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)});
+ }
+
+ entity.absMoveTo(d3, d4, d5, f, f1);
++ this.player.absMoveTo(d3, d4, d5, this.player.getYRot(), this.player.getXRot()); // CraftBukkit
+ boolean flag3 = worldserver.noCollision(entity, entity.getBoundingBox().deflate(0.0625D));
+
+ if (flag && (flag2 || !flag3)) {
+ 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));
+ return;
++ }
++
++ // CraftBukkit start - fire PlayerMoveEvent
++ Player player = this.getCraftPlayer();
++ if (!this.hasMoved) {
++ this.lastPosX = prevX;
++ this.lastPosY = prevY;
++ this.lastPosZ = prevZ;
++ this.lastYaw = prevYaw;
++ 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 = CraftLocation.toBukkit(packet.position(), player.getWorld(), packet.yRot(), packet.xRot());
++
++ // Prevent 40 event-calls for less than a single pixel of movement >.>
++ double delta = Math.pow(this.lastPosX - to.getX(), 2) + Math.pow(this.lastPosY - to.getY(), 2) + Math.pow(this.lastPosZ - to.getZ(), 2);
++ float deltaAngle = Math.abs(this.lastYaw - to.getYaw()) + Math.abs(this.lastPitch - to.getPitch());
++
++ if ((delta > 1f / 256 || deltaAngle > 10f) && !this.player.isImmobile()) {
++ this.lastPosX = to.getX();
++ this.lastPosY = to.getY();
++ this.lastPosZ = to.getZ();
++ this.lastYaw = to.getYaw();
++ this.lastPitch = to.getPitch();
++
++ Location oldTo = to.clone();
++ PlayerMoveEvent event = new PlayerMoveEvent(player, from, to);
++ this.cserver.getPluginManager().callEvent(event);
++
++ // If the event is cancelled we move the player back to their old location.
++ if (event.isCancelled()) {
++ this.teleport(from);
++ return;
++ }
++
++ // If a Plugin has changed the To destination then we teleport the Player
++ // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors.
++ // We only do this if the Event was not cancelled.
++ if (!oldTo.equals(event.getTo()) && !event.isCancelled()) {
++ this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN);
++ return;
++ }
++
++ // Check to see if the Players Location has some how changed during the call of the event.
++ // This can happen due to a plugin teleporting the player instead of using .setTo()
++ if (!from.equals(this.getCraftPlayer().getLocation()) && this.justTeleported) {
++ 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 @@
+ 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
+ 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.lastGoodZ = this.awaitingPositionFromClient.z;
+ this.player.hasChangedDimension();
+ this.awaitingPositionFromClient = null;
++ this.player.serverLevel().getChunkSource().move(this.player); // CraftBukkit
+ }
+
+ }
+@@ -528,6 +740,7 @@
+ @Override
+ public void handleRecipeBookChangeSettingsPacket(ServerboundRecipeBookChangeSettingsPacket packet) {
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
++ CraftEventFactory.callRecipeBookSettingsEvent(this.player, packet.getBookType(), packet.isOpen(), packet.isFiltering()); // CraftBukkit
+ this.player.getRecipeBook().setBookSetting(packet.getBookType(), packet.isOpen(), packet.isFiltering());
+ }
+
+@@ -545,21 +758,104 @@
+
+ }
+
++ // 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());
++ // Paper end - AsyncTabCompleteEvent
+ @Override
+ public void handleCustomCommandSuggestions(ServerboundCommandSuggestionPacket packet) {
+- PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
++ // PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel()); // Paper - AsyncTabCompleteEvent; run this async
++ // CraftBukkit start
++ if (!this.tabSpamThrottler.isIncrementAndUnderThreshold() && !this.server.getPlayerList().isOp(this.player.getGameProfile()) && !this.server.isSingleplayerOwner(this.player.getGameProfile())) { // Paper - configurable tab spam limits
++ this.disconnectAsync(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - Kick event cause // Paper - add proper async disconnect
++ return;
++ }
++ // CraftBukkit end
++ // Paper start - Don't suggest if tab-complete is disabled
++ if (org.spigotmc.SpigotConfig.tabComplete < 0) {
++ return;
++ }
++ // Paper end - Don't suggest if tab-complete is disabled
++ // Paper start
++ final int index;
++ if (packet.getCommand().length() > 64 && ((index = packet.getCommand().indexOf(' ')) == -1 || index >= 64)) {
++ this.disconnectAsync(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - add proper async disconnect
++ return;
++ }
++ // Paper end
++ // Paper start - AsyncTabCompleteEvent
++ TAB_COMPLETE_EXECUTOR.execute(() -> this.handleCustomCommandSuggestions0(packet));
++ }
++
++ private void handleCustomCommandSuggestions0(final ServerboundCommandSuggestionPacket packet) {
+ StringReader stringreader = new StringReader(packet.getCommand());
+
+ if (stringreader.canRead() && stringreader.peek() == '/') {
+ stringreader.skip();
+ }
+
++ 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();
++ // If the event isn't handled, we can assume that we have no completions, and so we'll ask the server
++ if (!event.isHandled()) {
++ if (event.isCancelled()) {
++ return;
++ }
++
++ // This needs to be on main
++ 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 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());
++ if (intSuggestion != null) {
++ builder.suggest(intSuggestion, PaperAdventure.asVanilla(completion.tooltip()));
++ } else {
++ builder.suggest(completion.suggestion(), PaperAdventure.asVanilla(completion.tooltip()));
++ }
++ }
++ // Paper start - Brigadier API
++ com.mojang.brigadier.suggestion.Suggestions suggestions = builder.buildFuture().join();
++ 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.connection.send(new ClientboundCommandSuggestionsPacket(packet.getId(), limitTo(suggestEvent.getSuggestions(), ServerGamePacketListenerImpl.MAX_COMMAND_SUGGESTIONS)));
++ }
++ // Paper end - Brigadier API
++ }
++ }
++ // Paper start - brig API
++ private static Suggestions limitTo(final Suggestions suggestions, final int size) {
++ return suggestions.getList().size() <= size ? suggestions : new Suggestions(suggestions.getRange(), suggestions.getList().subList(0, size));
++ }
++ // Paper end - brig API
++
++ private void sendServerSuggestions(final ServerboundCommandSuggestionPacket packet, final StringReader stringreader) {
++ // Paper end - AsyncTabCompleteEvent
+ 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)) {
++ 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
+ });
+ }
+
+@@ -568,7 +864,7 @@
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
+ if (!this.server.isCommandBlockEnabled()) {
+ this.player.sendSystemMessage(Component.translatable("advMode.notEnabled"));
+- } else if (!this.player.canUseGameMasterBlocks()) {
++ } 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 @@
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
+ if (!this.server.isCommandBlockEnabled()) {
+ this.player.sendSystemMessage(Component.translatable("advMode.notEnabled"));
+- } else if (!this.player.canUseGameMasterBlocks()) {
++ } 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()) {
+- if (flag) {
++ if (flag && this.player.getBukkitEntity().hasPermission("minecraft.nbt.copy")) { // Spigot
+ ServerGamePacketListenerImpl.addBlockDataToItem(iblockdata, worldserver, blockposition, itemstack);
+ }
+
+@@ -712,15 +1008,25 @@
+ if (stack.isItemEnabled(this.player.level().enabledFeatures())) {
+ Inventory playerinventory = this.player.getInventory();
+ int i = playerinventory.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 io.papermc.paper.event.player.PlayerPickItemEvent event = new io.papermc.paper.event.player.PlayerPickItemEvent(bukkitPlayer, targetSlot, sourceSlot);
++ if (!event.callEvent()) {
++ return;
++ }
++ i = event.getSourceSlot();
+
+ if (i != -1) {
+- if (Inventory.isHotbarSlot(i)) {
+- playerinventory.selected = i;
++ if (Inventory.isHotbarSlot(i) && Inventory.isHotbarSlot(event.getTargetSlot())) {
++ playerinventory.selected = event.getTargetSlot();
+ } else {
+- playerinventory.pickSlot(i);
++ playerinventory.pickSlot(i, event.getTargetSlot());
+ }
+ } else if (this.player.hasInfiniteMaterials()) {
+- playerinventory.addAndPickItem(stack);
++ playerinventory.addAndPickItem(stack, event.getTargetSlot());
++ // Paper end - Add PlayerPickItemEvent
+ }
+
+ this.player.connection.send(new ClientboundSetHeldSlotPacket(playerinventory.selected));
+@@ -866,6 +1172,13 @@
+ AbstractContainerMenu container = this.player.containerMenu;
+
+ if (container instanceof MerchantMenu containermerchant) {
++ // CraftBukkit start
++ final org.bukkit.event.inventory.TradeSelectEvent tradeSelectEvent = CraftEventFactory.callTradeSelectEvent(this.player, i, containermerchant);
++ if (tradeSelectEvent.isCancelled()) {
++ this.player.getBukkitEntity().updateInventory();
++ return;
++ }
++ // CraftBukkit end
+ if (!containermerchant.stillValid(this.player)) {
+ ServerGamePacketListenerImpl.LOGGER.debug("Player {} interacted with invalid menu {}", this.player, containermerchant);
+ return;
+@@ -879,6 +1192,51 @@
+
+ @Override
+ public void handleEditBook(ServerboundEditBookPacket packet) {
++ // Paper start - Book size limits
++ final io.papermc.paper.configuration.type.number.IntOr.Disabled pageMax = io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.pageMax;
++ if (!this.cserver.isPrimaryThread() && pageMax.enabled()) {
++ final List<String> pageList = packet.pages();
++ long byteTotal = 0;
++ final int maxBookPageSize = pageMax.intValue();
++ final double multiplier = Math.clamp(io.papermc.paper.configuration.GlobalConfiguration.get().itemValidation.bookSize.totalMultiplier, 0.3D, 1D);
++ long byteAllowed = maxBookPageSize;
++ for (final String page : pageList) {
++ final int byteLength = page.getBytes(java.nio.charset.StandardCharsets.UTF_8).length;
++ byteTotal += byteLength;
++ final int length = page.length();
++ int multiByteCharacters = 0;
++ if (byteLength != length) {
++ // Count the number of multi byte characters
++ for (final char c : page.toCharArray()) {
++ if (c > 127) {
++ multiByteCharacters++;
++ }
++ }
++ }
++
++ // Allow pages with fewer characters to consume less of the allowed byte quota
++ byteAllowed += maxBookPageSize * Math.clamp((double) length / 255D, 0.1D, 1) * multiplier;
++
++ if (multiByteCharacters > 1) {
++ // Penalize multibyte characters
++ byteAllowed -= multiByteCharacters;
++ }
++ }
++
++ if (byteTotal > byteAllowed) {
++ ServerGamePacketListenerImpl.LOGGER.warn("{} tried to send a book too large. Book size: {} - Allowed: {} - Pages: {}", this.player.getScoreboardName(), byteTotal, byteAllowed, pageList.size());
++ this.disconnectAsync(Component.literal("Book too large!"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - add proper async disconnect
++ return;
++ }
++ }
++ // Paper end - Book size limits
++ // CraftBukkit start
++ if (this.lastBookTick + 20 > MinecraftServer.currentTick) {
++ this.disconnectAsync(Component.literal("Book edited too quickly!"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause // Paper - add proper async disconnect
++ return;
++ }
++ this.lastBookTick = MinecraftServer.currentTick;
++ // CraftBukkit end
+ int i = packet.slot();
+
+ if (Inventory.isHotbarSlot(i) || i == 40) {
+@@ -899,12 +1257,16 @@
+ }
+
+ private void updateBookContents(List<FilteredText> pages, int slotId) {
+- ItemStack itemstack = this.player.getInventory().getItem(slotId);
++ // CraftBukkit start
++ ItemStack handItem = this.player.getInventory().getItem(slotId);
++ ItemStack itemstack = 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)
+ }
+ }
+
+@@ -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
+ }
+ }
+
+@@ -978,26 +1341,34 @@
+ 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
+ } else {
+ ServerLevel worldserver = this.player.serverLevel();
+
+- if (!this.player.wonGame) {
++ if (!this.player.wonGame && !this.player.isImmobile()) { // CraftBukkit
+ if (this.tickCount == 0) {
+ this.resetPosition();
+ }
+
+ 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()));
+- 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
++ 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);
++ this.allowedPlayerTicks = 20; // CraftBukkit
+ } else {
++ // CraftBukkit - Make sure the move is valid but then reset it for plugins to modify
++ double prevX = this.player.getX();
++ double prevY = this.player.getY();
++ double prevZ = this.player.getZ();
++ 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;
++ // 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;
++ double otherFieldY = d1 - this.lastGoodY;
++ double otherFieldZ = d2 - this.lastGoodZ;
++ d10 = Math.max(d10, (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;
+ 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);
+ i = 1;
+ }
+
++ if (packet.hasRot || d10 > 0) {
++ this.allowedPlayerTicks -= 1;
++ } else {
++ this.allowedPlayerTicks = 20;
++ }
++ double speed;
++ if (this.player.getAbilities().flying) {
++ speed = this.player.getAbilities().flyingSpeed * 20f;
++ } else {
++ 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())))) {
++ // 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);
++ if (!event.isAllowed()) {
++ this.internalTeleport(PositionMoveRotation.of(this.player), Collections.emptySet());
++ return;
++ }
++ // Paper end - Add fail move event
++ }
++ // 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});
+- 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
++ // 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});
++ this.teleport(this.player.getX(), this.player.getY(), this.player.getZ(), this.player.getYRot(), this.player.getXRot());
++ return;
++ }
++ // Paper end - Add fail move event
+ }
+ }
+ }
+
+ 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) {
+- 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.
++ 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.
++ if (packet.hasPos) {
++ to.setX(packet.x);
++ to.setY(packet.y);
++ to.setZ(packet.z);
++ }
++
++ // If the packet contains look information then we update the To location with the correct Yaw & Pitch.
++ if (packet.hasRot) {
++ to.setYaw(packet.yRot);
++ to.setPitch(packet.xRot);
++ }
++
++ com.destroystokyo.paper.event.player.PlayerJumpEvent event = new com.destroystokyo.paper.event.player.PlayerJumpEvent(player, from, to);
++
++ if (event.callEvent()) {
++ this.player.jumpFromGround();
++ } else {
++ from = event.getFrom();
++ this.internalTeleport(new PositionMoveRotation(org.bukkit.craftbukkit.util.CraftLocation.toVec3D(from), Vec3.ZERO, from.getYaw(), from.getPitch()), Collections.emptySet());
++ return;
++ }
++ // Paper end - Add PlayerJumpEvent
+ }
+
+ boolean flag2 = this.player.verticalCollisionBelow;
+
+ this.player.move(MoverType.PLAYER, new Vec3(d6, d7, d8));
++ this.player.onGround = packet.isOnGround(); // CraftBukkit - SPIGOT-5810, SPIGOT-5835, SPIGOT-6828: reset by this.player.move
++ // 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;
+
+ d6 = d0 - this.player.getX();
+@@ -1059,17 +1509,100 @@
+
+ d8 = d2 - this.player.getZ();
+ d10 = d6 * d6 + d7 * d7 + d8 * d8;
+- boolean flag3 = 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
++ // 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
+ }
+
+- 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));
++ 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);
++ if (event.isAllowed()) {
++ 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 {
++ // CraftBukkit start - fire PlayerMoveEvent
++ // Reset to old location first
++ this.player.absMoveTo(prevX, prevY, prevZ, prevYaw, prevPitch);
++
++ Player player = this.getCraftPlayer();
++ if (!this.hasMoved) {
++ this.lastPosX = prevX;
++ this.lastPosY = prevY;
++ this.lastPosZ = prevZ;
++ this.lastYaw = prevYaw;
++ 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.
++
++ // If the packet contains movement information then we update the To location with the correct XYZ.
++ if (packet.hasPos) {
++ to.setX(packet.x);
++ to.setY(packet.y);
++ to.setZ(packet.z);
++ }
++
++ // If the packet contains look information then we update the To location with the correct Yaw & Pitch.
++ if (packet.hasRot) {
++ to.setYaw(packet.yRot);
++ to.setPitch(packet.xRot);
++ }
++
++ // Prevent 40 event-calls for less than a single pixel of movement >.>
++ double delta = Math.pow(this.lastPosX - to.getX(), 2) + Math.pow(this.lastPosY - to.getY(), 2) + Math.pow(this.lastPosZ - to.getZ(), 2);
++ float deltaAngle = Math.abs(this.lastYaw - to.getYaw()) + Math.abs(this.lastPitch - to.getPitch());
++
++ if ((delta > 1f / 256 || deltaAngle > 10f) && !this.player.isImmobile()) {
++ this.lastPosX = to.getX();
++ this.lastPosY = to.getY();
++ this.lastPosZ = to.getZ();
++ this.lastYaw = to.getYaw();
++ this.lastPitch = to.getPitch();
++
++ Location oldTo = to.clone();
++ PlayerMoveEvent event = new PlayerMoveEvent(player, from, to);
++ this.cserver.getPluginManager().callEvent(event);
++
++ // If the event is cancelled we move the player back to their old location.
++ if (event.isCancelled()) {
++ this.teleport(from);
++ return;
++ }
++
++ // If a Plugin has changed the To destination then we teleport the Player
++ // there to avoid any 'Moved wrongly' or 'Moved too quickly' errors.
++ // We only do this if the Event was not cancelled.
++ if (!oldTo.equals(event.getTo()) && !event.isCancelled()) {
++ this.player.getBukkitEntity().teleport(event.getTo(), PlayerTeleportEvent.TeleportCause.PLUGIN);
++ return;
++ }
++
++ // Check to see if the Players Location has some how changed during the call of the event.
++ // This can happen due to a plugin teleporting the player instead of using .setTo()
++ if (!from.equals(this.getCraftPlayer().getLocation()) && this.justTeleported) {
++ this.justTeleported = false;
++ return;
++ }
++ }
++ // 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.allowedPlayerTicks = 20; // CraftBukkit
+
+ return true;
+ } else {
+@@ -1147,23 +1681,98 @@
+ }
+
+ 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) {
++ // CraftBukkit start
++ this.teleport(pos, flags, PlayerTeleportEvent.TeleportCause.UNKNOWN);
++ }
++
++ public boolean teleport(PositionMoveRotation positionmoverotation, Set<Relative> set, PlayerTeleportEvent.TeleportCause cause) { // CraftBukkit - Return event status
++ Player player = this.getCraftPlayer();
++ Location from = player.getLocation();
++ PositionMoveRotation absolutePosition = PositionMoveRotation.calculateAbsolute(PositionMoveRotation.of(this.player), positionmoverotation, set);
++ 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);
++ 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) {
++ final io.papermc.paper.entity.TeleportFlag.Relative flag = org.bukkit.craftbukkit.entity.CraftPlayer.deltaRelativeToAPI(relativeArgument);
++ if (flag != null) relativeFlags.add(flag);
++ }
++ PlayerTeleportEvent event = new PlayerTeleportEvent(player, from.clone(), to.clone(), cause, java.util.Set.copyOf(relativeFlags));
++ // Paper end - Teleport API
++ this.cserver.getPluginManager().callEvent(event);
++
++ 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());
++ }
++
++ this.internalTeleport(positionmoverotation, set);
++ return !event.isCancelled(); // CraftBukkit - Return event status
++ }
++
++ public void teleport(Location dest) {
++ this.internalTeleport(dest.getX(), dest.getY(), dest.getZ(), dest.getYaw(), dest.getPitch());
++ }
++
++ private void internalTeleport(double d0, double d1, double d2, float f, float f1) {
++ this.internalTeleport(new PositionMoveRotation(new Vec3(d0, d1, d2), Vec3.ZERO, f, f1), Collections.emptySet());
++ }
++
++ public void internalTeleport(PositionMoveRotation positionmoverotation, Set<Relative> set) {
++ org.spigotmc.AsyncCatcher.catchOp("teleport"); // Paper
++ // Paper start - Prevent teleporting dead entities
++ if (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");
++ 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(positionmoverotation.xRot())) {
++ positionmoverotation = new PositionMoveRotation(positionmoverotation.position(), positionmoverotation.deltaMovement(), positionmoverotation.yRot(), 0);
++ }
++
++ this.justTeleported = true;
++ // CraftBukkit end
+ this.awaitingTeleportTime = this.tickCount;
+ if (++this.awaitingTeleport == Integer.MAX_VALUE) {
+ this.awaitingTeleport = 0;
+ }
+
+- this.player.teleportSetPosition(pos, flags);
++ this.player.teleportSetPosition(positionmoverotation, set);
+ 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;
++ this.lastPosZ = this.awaitingPositionFromClient.z;
++ this.lastYaw = this.player.getYRot();
++ this.lastPitch = this.player.getXRot();
++ // CraftBukkit end
++ this.player.connection.send(ClientboundPlayerPositionPacket.of(this.awaitingTeleport, positionmoverotation, set));
+ }
+
+ @Override
+ public void handlePlayerAction(ServerboundPlayerActionPacket packet) {
+ 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 @@
+ if (!this.player.isSpectator()) {
+ ItemStack itemstack = 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);
++ // CraftBukkit start - inspiration taken from DispenserRegistry (See SpigotCraft#394)
++ CraftItemStack mainHand = CraftItemStack.asCraftMirror(itemstack);
++ 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);
++ if (swapItemsEvent.isCancelled()) {
++ return;
++ }
++ if (swapItemsEvent.getOffHandItem().equals(offHand)) {
++ this.player.setItemInHand(InteractionHand.OFF_HAND, this.player.getItemInHand(InteractionHand.MAIN_HAND));
++ } else {
++ this.player.setItemInHand(InteractionHand.OFF_HAND, CraftItemStack.asNMSCopy(swapItemsEvent.getOffHandItem()));
++ }
++ if (swapItemsEvent.getMainHandItem().equals(mainHand)) {
++ this.player.setItemInHand(InteractionHand.MAIN_HAND, itemstack);
++ } else {
++ this.player.setItemInHand(InteractionHand.MAIN_HAND, CraftItemStack.asNMSCopy(swapItemsEvent.getMainHandItem()));
++ }
++ // CraftBukkit end
+ this.player.stopUsingItem();
+ }
+
+ return;
+ case DROP_ITEM:
+ if (!this.player.isSpectator()) {
++ // limit how quickly items can be dropped
++ // If the ticks aren't the same then the count starts from 0 and we update the lastDropTick.
++ if (this.lastDropTick != MinecraftServer.currentTick) {
++ this.dropCount = 0;
++ this.lastDropTick = MinecraftServer.currentTick;
++ } else {
++ // Else we increment the drop count and check the amount.
++ this.dropCount++;
++ if (this.dropCount >= 20) {
++ ServerGamePacketListenerImpl.LOGGER.warn(this.player.getScoreboardName() + " dropped their items too quickly!");
++ this.disconnect(Component.literal("You dropped your items too quickly (Hacking?)"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - kick event cause
++ return;
++ }
++ }
++ // CraftBukkit end
+ this.player.drop(false);
+ }
+
+@@ -1199,8 +1840,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) {
++ 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;
++
++ this.player.gameMode.capturedBlockEntity = false;
++ BlockEntity tileentity = this.player.level().getBlockEntity(blockposition);
++ if (tileentity != null) {
++ this.player.connection.send(tileentity.getUpdatePacket());
++ }
++ }
++ // Paper end - Send block entities after destroy prediction
+ return;
+ default:
+ throw new IllegalArgumentException("Invalid player action");
+@@ -1215,12 +1882,34 @@
+ Item item = stack.getItem();
+
+ return (item instanceof BlockItem || item instanceof BucketItem) && !player.getCooldowns().isOnCooldown(stack);
++ }
++ }
++
++ // Spigot start - limit place/interactions
++ private int limitedPackets;
++ private long lastLimitedPacket = -1;
++ private static int getSpamThreshold() { return io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.incomingPacketThreshold; } // Paper - Configurable threshold
++
++ private boolean checkLimit(long timestamp) {
++ if (this.lastLimitedPacket != -1 && timestamp - this.lastLimitedPacket < getSpamThreshold() && this.limitedPackets++ >= 8) { // Paper - Configurable threshold; raise packet limit to 8
++ return false;
++ }
++
++ if (this.lastLimitedPacket == -1 || timestamp - this.lastLimitedPacket >= getSpamThreshold()) { // Paper - Configurable threshold
++ this.lastLimitedPacket = timestamp;
++ this.limitedPackets = 0;
++ return true;
+ }
++
++ return true;
+ }
++ // Spigot end
+
+ @Override
+ public void handleUseItemOn(ServerboundUseItemOnPacket packet) {
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
++ if (this.player.isImmobile()) return; // CraftBukkit
++ 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
++ 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);
+ }
+ }
+- }
++ } 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 @@
+ @Override
+ public void handleUseItem(ServerboundUseItemPacket packet) {
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
++ if (this.player.isImmobile()) return; // CraftBukkit
++ 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 @@
+ 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);
++
++ float f3 = Mth.cos(-f * 0.017453292F - 3.1415927F);
++ float f4 = Mth.sin(-f * 0.017453292F - 3.1415927F);
++ float f5 = -Mth.cos(-f1 * 0.017453292F);
++ float f6 = Mth.sin(-f1 * 0.017453292F);
++ 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));
++
++ 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);
++ 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)) {
++ 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());
++ 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
++ return;
++ }
++ itemstack = this.player.getItemInHand(enumhand); // Update in case it was changed in the event
++ if (itemstack.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 (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
+ return;
+ }
+ }
+@@ -1342,22 +2081,52 @@
+
+ @Override
+ public void onDisconnect(DisconnectionDetails info) {
++ // Paper start - Fix kick event leave message not being sent
++ this.onDisconnect(info, null);
++ }
++ @Override
++ public void onDisconnect(DisconnectionDetails info, @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) {
++ return;
++ } else {
++ this.processedDisconnect = true;
++ }
++ // CraftBukkit end
+ ServerGamePacketListenerImpl.LOGGER.info("{} lost connection: {}", this.player.getName().getString(), info.reason().getString());
+- this.removePlayerFromWorld();
+- super.onDisconnect(info);
++ 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
+ }
+
++ // Paper start - Fix kick event leave message not being sent
+ private void removePlayerFromWorld() {
++ this.removePlayerFromWorld(null);
++ }
++
++ private void removePlayerFromWorld(@Nullable net.kyori.adventure.text.Component quitMessage) {
++ // Paper end - Fix kick event leave message not being sent
+ this.chatMessageChain.close();
++ // 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.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
++ }
++ // CraftBukkit end
+ this.player.getTextFilter().leave();
+ }
+
+ public void ackBlockChangesUpTo(int sequence) {
+ if (sequence < 0) {
++ this.disconnect(Component.literal("Expected packet sequence nr >= 0"), org.bukkit.event.player.PlayerKickEvent.Cause.ILLEGAL_ACTION); // Paper - Treat sequence violations like they should be
+ throw new IllegalArgumentException("Expected packet sequence nr >= 0");
+ } else {
+ this.ackBlockChangesUpTo = Math.max(sequence, this.ackBlockChangesUpTo);
+@@ -1367,7 +2136,17 @@
+ @Override
+ public void handleSetCarriedItem(ServerboundSetCarriedItemPacket packet) {
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
++ if (this.player.isImmobile()) return; // CraftBukkit
+ if (packet.getSlot() >= 0 && packet.getSlot() < Inventory.getSelectionSize()) {
++ if (packet.getSlot() == this.player.getInventory().selected) { return; } // Paper - don't fire itemheldevent when there wasn't a slot change
++ PlayerItemHeldEvent event = new PlayerItemHeldEvent(this.getCraftPlayer(), this.player.getInventory().selected, packet.getSlot());
++ this.cserver.getPluginManager().callEvent(event);
++ if (event.isCancelled()) {
++ this.send(new ClientboundSetHeldSlotPacket(this.player.getInventory().selected));
++ this.player.resetLastActionTime();
++ return;
++ }
++ // CraftBukkit end
+ if (this.player.getInventory().selected != packet.getSlot() && this.player.getUsedItemHand() == InteractionHand.MAIN_HAND) {
+ this.player.stopUsingItem();
+ }
+@@ -1376,11 +2155,18 @@
+ this.player.resetLastActionTime();
+ } else {
+ ServerGamePacketListenerImpl.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
+ }
+ }
+
+ @Override
+ public void handleChat(ServerboundChatPacket packet) {
++ // CraftBukkit start - async chat
++ // SPIGOT-3638
++ if (this.server.isStopped()) {
++ return;
++ }
++ // CraftBukkit end
+ Optional<LastSeenMessages> optional = this.unpackAndApplyLastSeen(packet.lastSeenMessages());
+
+ if (!optional.isEmpty()) {
+@@ -1394,27 +2180,46 @@
+ 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);
+ });
+- });
++ }, false); // CraftBukkit - async chat
+ }
+ }
+
+ @Override
+ public void handleChatCommand(ServerboundChatCommandPacket packet) {
+ this.tryHandleChat(packet.command(), () -> {
++ // CraftBukkit start - SPIGOT-7346: Prevent disconnected players from executing commands
++ if (this.player.hasDisconnected()) {
++ return;
++ }
++ // CraftBukkit end
+ this.performUnsignedChatCommand(packet.command());
+- this.detectRateSpam();
+- });
++ this.detectRateSpam("/" + packet.command()); // Spigot
++ }, true); // CraftBukkit - sync commands
+ }
+
+ private void performUnsignedChatCommand(String command) {
++ // CraftBukkit start
++ String command1 = "/" + command;
++ if (org.spigotmc.SpigotConfig.logCommands) { // Paper - Add missing SpigotConfig logCommands check
++ ServerGamePacketListenerImpl.LOGGER.info(this.player.getScoreboardName() + " issued server command: " + command1);
++ }
++
++ PlayerCommandPreprocessEvent event = new PlayerCommandPreprocessEvent(this.getCraftPlayer(), command1, new LazyPlayerSet(this.server));
++ this.cserver.getPluginManager().callEvent(event);
++
++ if (event.isCancelled()) {
++ return;
++ }
++ command = event.getMessage().substring(1);
++ // CraftBukkit end
+ ParseResults<CommandSourceStack> parseresults = this.parseCommand(command);
+
+ if (this.server.enforceSecureProfile() && SignableCommand.hasSignableArguments(parseresults)) {
+@@ -1431,30 +2236,58 @@
+
+ if (!optional.isEmpty()) {
+ this.tryHandleChat(packet.command(), () -> {
++ // CraftBukkit start - SPIGOT-7346: Prevent disconnected players from executing commands
++ if (this.player.hasDisconnected()) {
++ return;
++ }
++ // CraftBukkit end
+ this.performSignedChatCommand(packet, (LastSeenMessages) optional.get());
+- this.detectRateSpam();
+- });
++ this.detectRateSpam("/" + packet.command()); // Spigot
++ }, true); // CraftBukkit - sync commands
+ }
+ }
+
+ 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);
++ } // 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;
+ 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);
+ return;
+ }
+
++ if (event.isCancelled()) {
++ // Only now are we actually good to return
++ return;
++ }
++
++ // Remove signed parts if the command was changed
++ if (!command.equals(packet.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
+ }
+
+ private void handleMessageDecodeFailure(SignedMessageChain.DecodeException exception) {
+@@ -1530,14 +2363,20 @@
+ return com_mojang_brigadier_commanddispatcher.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"));
+- } 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
++ } 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);
++ // CraftBukkit start
++ if (sync) {
++ this.server.execute(runnable);
++ } else {
++ runnable.run();
++ }
++ // CraftBukkit end
+ }
+ }
+
+@@ -1549,7 +2388,7 @@
+
+ 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
+ }
+
+ return optional;
+@@ -1566,6 +2405,117 @@
+ return false;
+ }
+
++ // CraftBukkit start - add method
++ public void chat(String s, PlayerChatMessage original, boolean async) {
++ if (s.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);
++ } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) {
++ // Do nothing, this is coming from a plugin
++ // Paper start
++ } else if (true) {
++ if (!async && !org.bukkit.Bukkit.isPrimaryThread()) {
++ org.spigotmc.AsyncCatcher.catchOp("Asynchronous player chat is not allowed here");
++ }
++ final ChatProcessor cp = new ChatProcessor(this.server, this.player, original, async);
++ cp.process();
++ // Paper end
++ } else if (false) { // Paper
++ Player player = this.getCraftPlayer();
++ AsyncPlayerChatEvent event = new AsyncPlayerChatEvent(async, player, s, new LazyPlayerSet(this.server));
++ String originalFormat = event.getFormat(), originalMessage = event.getMessage();
++ this.cserver.getPluginManager().callEvent(event);
++
++ if (PlayerChatEvent.getHandlerList().getRegisteredListeners().length != 0) {
++ // 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() {
++ @Override
++ protected Object evaluate() {
++ org.bukkit.Bukkit.getPluginManager().callEvent(queueEvent);
++
++ if (queueEvent.isCancelled()) {
++ return null;
++ }
++
++ String message = String.format(queueEvent.getFormat(), queueEvent.getPlayer().getDisplayName(), queueEvent.getMessage());
++ if (((LazyPlayerSet) queueEvent.getRecipients()).isLazy()) {
++ if (!org.spigotmc.SpigotConfig.bungee && originalFormat.equals(queueEvent.getFormat()) && originalMessage.equals(queueEvent.getMessage()) && queueEvent.getPlayer().getName().equalsIgnoreCase(queueEvent.getPlayer().getDisplayName())) { // Spigot
++ ServerGamePacketListenerImpl.this.server.getPlayerList().broadcastChatMessage(original, ServerGamePacketListenerImpl.this.player, ChatType.bind(ChatType.CHAT, (Entity) ServerGamePacketListenerImpl.this.player));
++ return null;
++ }
++
++ for (ServerPlayer recipient : ServerGamePacketListenerImpl.this.server.getPlayerList().players) {
++ recipient.getBukkitEntity().sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), message);
++ }
++ } else {
++ for (Player player : queueEvent.getRecipients()) {
++ player.sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), message);
++ }
++ }
++ ServerGamePacketListenerImpl.this.server.console.sendMessage(message);
++
++ return null;
++ }};
++ if (async) {
++ this.server.processQueue.add(waitable);
++ } else {
++ waitable.run();
++ }
++ try {
++ waitable.get();
++ } catch (InterruptedException e) {
++ Thread.currentThread().interrupt(); // This is proper habit for java. If we aren't handling it, pass it on!
++ } catch (ExecutionException e) {
++ throw new RuntimeException("Exception processing chat event", e.getCause());
++ }
++ } else {
++ if (event.isCancelled()) {
++ return;
++ }
++
++ s = 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));
++ return;
++ }
++
++ for (ServerPlayer recipient : this.server.getPlayerList().players) {
++ recipient.getBukkitEntity().sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), s);
++ }
++ } else {
++ for (Player recipient : event.getRecipients()) {
++ recipient.sendMessage(ServerGamePacketListenerImpl.this.player.getUUID(), s);
++ }
++ }
++ this.server.console.sendMessage(s);
++ }
++ }
++ }
++
++ @Deprecated // Paper
++ public void handleCommand(String s) { // Paper - private -> public
++ // Paper start - Remove all this old duplicated logic
++ if (s.startsWith("/")) {
++ s = s.substring(1);
++ }
++ /*
++ It should be noted that this represents the "legacy" command execution path.
++ Api can call commands even if there is no additional context provided.
++ This method should ONLY be used if you need to execute a command WITHOUT
++ an actual player's input.
++ */
++ this.performUnsignedChatCommand(s);
++ // Paper end
++ }
++ // 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 @@
+ }
+
+ private void broadcastChatMessage(PlayerChatMessage message) {
+- this.server.getPlayerList().broadcastChatMessage(message, this.player, ChatType.bind(ChatType.CHAT, (Entity) 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");
++ } else if (this.getCraftPlayer().isConversing()) {
++ final String conversationInput = s;
++ this.server.processQueue.add(new Runnable() {
++ @Override
++ public void run() {
++ 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.server.getPlayerList().broadcastChatMessage(playerchatmessage, this.player, ChatMessageType.bind(ChatMessageType.CHAT, (Entity) this.player));
++ // CraftBukkit end
++ this.detectRateSpam(s); // 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"));
++ // Spigot start - spam exclusions
++ private void detectRateSpam(String s) {
++ // CraftBukkit start - replaced with thread safe throttle
++ for ( String exclude : org.spigotmc.SpigotConfig.spamExclusions )
++ {
++ if ( exclude != null && s.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())) {
++ // 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
++ }
+
+ }
+
+@@ -1592,7 +2571,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
+ }
+
+ }
+@@ -1601,7 +2580,40 @@
+ @Override
+ public void handleAnimate(ServerboundSwingPacket packet) {
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
++ if (this.player.isImmobile()) return; // CraftBukkit
+ this.player.resetLastActionTime();
++ // CraftBukkit start - Raytrace to look for 'rogue armswings'
++ float f1 = this.player.getXRot();
++ float f2 = this.player.getYRot();
++ double d0 = this.player.getX();
++ double d1 = this.player.getY() + (double) this.player.getEyeHeight();
++ double d2 = this.player.getZ();
++ Location origin = new Location(this.player.level().getWorld(), d0, d1, d2, f2, f1);
++
++ double d3 = Math.max(this.player.blockInteractionRange(), this.player.entityInteractionRange());
++ // SPIGOT-5607: Only call interact event if no block or entity is being clicked. Use bukkit ray trace method, because it handles blocks and entities at the same time
++ // SPIGOT-7429: Make sure to call PlayerInteractEvent for spectators and non-pickable entities
++ org.bukkit.util.RayTraceResult result = this.player.level().getWorld().rayTrace(origin, origin.getDirection(), d3, org.bukkit.FluidCollisionMode.NEVER, false, 0.0, entity -> { // Paper - Call interact event; change raySize from 0.1 to 0.0
++ Entity handle = ((CraftEntity) entity).getHandle();
++ return entity != this.player.getBukkitEntity() && this.player.getBukkitEntity().canSee(entity) && !handle.isSpectator() && handle.isPickable() && !handle.isPassengerOfSameVehicle(this.player);
++ });
++ if (result == null) {
++ CraftEventFactory.callPlayerInteractEvent(this.player, Action.LEFT_CLICK_AIR, this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND);
++ } else { // Paper start - Call interact event
++ GameType gameType = this.player.gameMode.getGameModeForPlayer();
++ if (gameType == GameType.ADVENTURE && result.getHitBlock() != null) {
++ CraftEventFactory.callPlayerInteractEvent(this.player, Action.LEFT_CLICK_BLOCK, ((org.bukkit.craftbukkit.block.CraftBlock) result.getHitBlock()).getPosition(), org.bukkit.craftbukkit.block.CraftBlock.blockFaceToNotch(result.getHitBlockFace()), this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND);
++ } else if (gameType != GameType.CREATIVE && result.getHitEntity() != null && origin.toVector().distanceSquared(result.getHitPosition()) > this.player.entityInteractionRange() * this.player.entityInteractionRange()) {
++ CraftEventFactory.callPlayerInteractEvent(this.player, Action.LEFT_CLICK_AIR, this.player.getInventory().getSelected(), InteractionHand.MAIN_HAND);
++ }
++ } // Paper end - Call interact event
++
++ // Arm swing animation
++ io.papermc.paper.event.player.PlayerArmSwingEvent event = new io.papermc.paper.event.player.PlayerArmSwingEvent(this.getCraftPlayer(), org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(packet.getHand())); // Paper - Add PlayerArmSwingEvent
++ this.cserver.getPluginManager().callEvent(event);
++
++ if (event.isCancelled()) return;
++ // CraftBukkit end
+ this.player.swing(packet.getHand());
+ }
+
+@@ -1609,6 +2621,29 @@
+ public void handlePlayerCommand(ServerboundPlayerCommandPacket packet) {
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
+ if (this.player.hasClientLoaded()) {
++ // CraftBukkit start
++ if (this.player.isRemoved()) return;
++ switch (packet.getAction()) {
++ case PRESS_SHIFT_KEY:
++ case RELEASE_SHIFT_KEY:
++ PlayerToggleSneakEvent event = new PlayerToggleSneakEvent(this.getCraftPlayer(), packet.getAction() == ServerboundPlayerCommandPacket.Action.PRESS_SHIFT_KEY);
++ this.cserver.getPluginManager().callEvent(event);
++
++ if (event.isCancelled()) {
++ 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);
++
++ if (e2.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);
++ // Paper start - Add option to make parrots stay
++ if (this.player.level().paperConfig().entities.behavior.parrotsAreUnaffectedByPlayerMovement) {
++ this.player.removeEntitiesOnShoulder();
++ }
++ // Paper end - Add option to make parrots stay
+ break;
+ case RELEASE_SHIFT_KEY:
+ this.player.setShiftKeyDown(false);
+@@ -1684,15 +2724,25 @@
+ }
+
+ 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
+ }
+
+ }
+ }
+
+ public void sendPlayerChatMessage(PlayerChatMessage message, ChatType.Bound params) {
++ // 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);
++ 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);
++ }
++ // 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 @@
+ return this.connection.getRemoteAddress();
+ }
+
++ // 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) {
++ return new java.net.InetSocketAddress(java.net.InetAddress.getLoopbackAddress(), 0);
++ }
++ // Paper end - Unix domain socket support
++ return this.connection.channel.remoteAddress();
++ }
++ // Spigot End
++
+ public void switchToConfig() {
+ this.waitingForSwitchToConfig = true;
+ this.removePlayerFromWorld();
+@@ -1718,9 +2780,17 @@
+ @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
++ return;
++ }
++ // Spigot End
+
+ this.player.resetLastActionTime();
+ this.player.setShiftKeyDown(packet.isUsingSecondaryAction());
+@@ -1731,22 +2801,61 @@
+
+ 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);
++
++ // 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();
++ }
++
++ 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
++ }
++
++ ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote(); // Paper - fix slot desync - always refresh player inventory
++ }
++
++ if (event.isCancelled()) {
++ return;
++ }
++ // CraftBukkit end
++ InteractionResult enuminteractionresult = playerconnection_a.run(ServerGamePacketListenerImpl.this.player, entity, enumhand);
++
++ // CraftBukkit start
++ if (!itemInHand.isEmpty() && itemInHand.getCount() <= -1) {
++ ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote();
++ }
++ // CraftBukkit end
++
+ 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) {
++ ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote();
++ }
++ // CraftBukkit end
+ return;
+ }
+ }
+
+- 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());
+ }
+ });
+ }
+ }
++ // Paper start - PlayerUseUnknownEntityEvent
++ else {
++ packet.dispatch(new net.minecraft.network.protocol.game.ServerboundInteractPacket.Handler() {
++ @Override
++ public void onInteraction(net.minecraft.world.InteractionHand hand) {
++ CraftEventFactory.callPlayerUseUnknownEntityEvent(ServerGamePacketListenerImpl.this.player, packet, hand, null);
++ }
+
++ @Override
++ public void onInteraction(net.minecraft.world.InteractionHand hand, net.minecraft.world.phys.Vec3 pos) {
++ CraftEventFactory.callPlayerUseUnknownEntityEvent(ServerGamePacketListenerImpl.this.player, packet, hand, pos);
++ }
++
++ @Override
++ public void onAttack() {
++ CraftEventFactory.callPlayerUseUnknownEntityEvent(ServerGamePacketListenerImpl.this.player, packet, net.minecraft.world.InteractionHand.MAIN_HAND, null);
++ }
++ });
++ }
++ // Paper end - PlayerUseUnknownEntityEvent
+ }
+ }
+
+@@ -1809,7 +2943,7 @@
+ case PERFORM_RESPAWN:
+ if (this.player.wonGame) {
+ this.player.wonGame = false;
+- this.player = this.server.getPlayerList().respawn(this.player, true, Entity.RemovalReason.CHANGED_DIMENSION);
++ this.player = this.server.getPlayerList().respawn(this.player, true, Entity.RemovalReason.CHANGED_DIMENSION, RespawnReason.END_PORTAL); // CraftBukkit
+ this.resetPosition();
+ CriteriaTriggers.CHANGED_DIMENSION.trigger(this.player, Level.END, Level.OVERWORLD);
+ } else {
+@@ -1817,11 +2951,11 @@
+ return;
+ }
+
+- this.player = this.server.getPlayerList().respawn(this.player, false, Entity.RemovalReason.KILLED);
++ this.player = this.server.getPlayerList().respawn(this.player, false, Entity.RemovalReason.KILLED, RespawnReason.DEATH); // CraftBukkit
+ 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.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
+ }
+ }
+ break;
+@@ -1833,16 +2967,27 @@
+
+ @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());
++
++ if (this.player.isImmobile()) return; // CraftBukkit
++ CraftEventFactory.handleInventoryCloseEvent(this.player, reason); // CraftBukkit // Paper
++
+ this.player.doCloseContainer();
+ }
+
+ @Override
+ public void handleContainerClick(ServerboundContainerClickPacket packet) {
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
++ if (this.player.isImmobile()) return; // CraftBukkit
+ this.player.resetLastActionTime();
+- if (this.player.containerMenu.containerId == packet.getContainerId()) {
+- if (this.player.isSpectator()) {
++ if (this.player.containerMenu.containerId == packet.getContainerId() && this.player.containerMenu.stillValid(this.player)) { // CraftBukkit
++ boolean cancelled = this.player.isSpectator(); // CraftBukkit - see below if
++ 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 @@
+ boolean flag = packet.getStateId() != this.player.containerMenu.getStateId();
+
+ this.player.containerMenu.suppressRemoteUpdates();
+- this.player.containerMenu.clicked(i, packet.getButtonNum(), packet.getClickType(), this.player);
++ // CraftBukkit start - Call InventoryClickEvent
++ if (packet.getSlotNum() < -1 && packet.getSlotNum() != -999) {
++ return;
++ }
++
++ InventoryView inventory = this.player.containerMenu.getBukkitView();
++ SlotType type = inventory.getSlotType(packet.getSlotNum());
++
++ InventoryClickEvent event;
++ ClickType click = ClickType.UNKNOWN;
++ InventoryAction action = InventoryAction.UNKNOWN;
++
++ ItemStack itemstack = ItemStack.EMPTY;
++
++ switch (packet.getClickType()) {
++ case PICKUP:
++ if (packet.getButtonNum() == 0) {
++ click = ClickType.LEFT;
++ } else if (packet.getButtonNum() == 1) {
++ click = ClickType.RIGHT;
++ }
++ if (packet.getButtonNum() == 0 || packet.getButtonNum() == 1) {
++ action = InventoryAction.NOTHING; // Don't want to repeat ourselves
++ if (packet.getSlotNum() == -999) {
++ if (!this.player.containerMenu.getCarried().isEmpty()) {
++ action = packet.getButtonNum() == 0 ? InventoryAction.DROP_ALL_CURSOR : InventoryAction.DROP_ONE_CURSOR;
++ }
++ } else if (packet.getSlotNum() < 0) {
++ action = InventoryAction.NOTHING;
++ } else {
++ Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
++ if (slot != null) {
++ ItemStack clickedItem = slot.getItem();
++ ItemStack cursor = this.player.containerMenu.getCarried();
++ if (clickedItem.isEmpty()) {
++ if (!cursor.isEmpty()) {
++ action = packet.getButtonNum() == 0 ? InventoryAction.PLACE_ALL : InventoryAction.PLACE_ONE;
++ }
++ } else if (slot.mayPickup(this.player)) {
++ if (cursor.isEmpty()) {
++ action = packet.getButtonNum() == 0 ? InventoryAction.PICKUP_ALL : InventoryAction.PICKUP_HALF;
++ } else if (slot.mayPlace(cursor)) {
++ if (ItemStack.isSameItemSameComponents(clickedItem, cursor)) {
++ int toPlace = packet.getButtonNum() == 0 ? cursor.getCount() : 1;
++ toPlace = Math.min(toPlace, clickedItem.getMaxStackSize() - clickedItem.getCount());
++ toPlace = Math.min(toPlace, slot.container.getMaxStackSize() - clickedItem.getCount());
++ if (toPlace == 1) {
++ action = InventoryAction.PLACE_ONE;
++ } else if (toPlace == cursor.getCount()) {
++ action = InventoryAction.PLACE_ALL;
++ } else if (toPlace < 0) {
++ action = toPlace != -1 ? InventoryAction.PICKUP_SOME : InventoryAction.PICKUP_ONE; // this happens with oversized stacks
++ } else if (toPlace != 0) {
++ action = InventoryAction.PLACE_SOME;
++ }
++ } else if (cursor.getCount() <= slot.getMaxStackSize()) {
++ action = InventoryAction.SWAP_WITH_CURSOR;
++ }
++ } else if (ItemStack.isSameItemSameComponents(cursor, clickedItem)) {
++ if (clickedItem.getCount() >= 0) {
++ if (clickedItem.getCount() + cursor.getCount() <= cursor.getMaxStackSize()) {
++ // As of 1.5, this is result slots only
++ action = InventoryAction.PICKUP_ALL;
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ break;
++ // TODO check on updates
++ case QUICK_MOVE:
++ if (packet.getButtonNum() == 0) {
++ click = ClickType.SHIFT_LEFT;
++ } else if (packet.getButtonNum() == 1) {
++ click = ClickType.SHIFT_RIGHT;
++ }
++ if (packet.getButtonNum() == 0 || packet.getButtonNum() == 1) {
++ if (packet.getSlotNum() < 0) {
++ action = InventoryAction.NOTHING;
++ } else {
++ Slot slot = this.player.containerMenu.getSlot(packet.getSlotNum());
++ if (slot != null && slot.mayPickup(this.player) && slot.hasItem()) {
++ action = InventoryAction.MOVE_TO_OTHER_INVENTORY;
++ } else {
++ action = InventoryAction.NOTHING;
++ }
++ }
++ }
++ 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;
++ } else {
++ action = InventoryAction.NOTHING;
++ }
++ } else {
++ action = InventoryAction.NOTHING;
++ }
++ }
++ break;
++ case CLONE:
++ if (packet.getButtonNum() == 2) {
++ click = ClickType.MIDDLE;
++ if (packet.getSlotNum() < 0) {
++ 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;
++ }
++ }
++ } 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;
++ }
++ 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;
++ }
++ }
++ }
++ // 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;
++ }
++
++ 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());
++ } else {
++ event = new InventoryClickEvent(inventory, type, packet.getSlotNum(), click, action);
++ }
++
++ org.bukkit.inventory.Inventory top = inventory.getTopInventory();
++ if (packet.getSlotNum() == 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());
++ } else {
++ event = new CraftItemEvent(recipe, inventory, type, packet.getSlotNum(), click, action);
++ }
++ }
++ }
++
++ if (packet.getSlotNum() == 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());
++ } else {
++ event = new SmithItemEvent(inventory, type, packet.getSlotNum(), click, action);
++ }
++ }
++ }
++
++ // Paper start - cartography item event
++ if (packet.getSlotNum() == 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());
++ } else {
++ event = new io.papermc.paper.event.player.CartographyItemEvent(inventory, type, packet.getSlotNum(), click, action);
++ }
++ }
++ }
++ // Paper end - cartography item event
++
++ event.setCancelled(cancelled);
++ AbstractContainerMenu oldContainer = this.player.containerMenu; // SPIGOT-1224
++ this.cserver.getPluginManager().callEvent(event);
++ if (this.player.containerMenu != oldContainer) {
++ return;
++ }
++
++ switch (event.getResult()) {
++ case ALLOW:
++ case DEFAULT:
++ this.player.containerMenu.clicked(i, packet.getButtonNum(), packet.getClickType(), this.player);
++ break;
++ case DENY:
++ /* Needs enum constructor in InventoryAction
++ if (action.modifiesOtherSlots()) {
++
++ } else {
++ if (action.modifiesCursor()) {
++ this.player.playerConnection.sendPacket(new Packet103SetSlot(-1, -1, this.player.inventory.getCarried()));
++ }
++ if (action.modifiesClicked()) {
++ this.player.playerConnection.sendPacket(new Packet103SetSlot(this.player.activeContainer.windowId, packet102windowclick.slot, this.player.activeContainer.getSlot(packet102windowclick.slot).getItem()));
++ }
++ }*/
++ switch (action) {
++ // Modified other slots
++ case PICKUP_ALL:
++ case MOVE_TO_OTHER_INVENTORY:
++ case HOTBAR_MOVE_AND_READD:
++ case HOTBAR_SWAP:
++ case COLLECT_TO_CURSOR:
++ case UNKNOWN:
++ this.player.containerMenu.sendAllDataToRemote();
++ break;
++ // Modified cursor and clicked
++ case PICKUP_SOME:
++ case PICKUP_HALF:
++ case PICKUP_ONE:
++ case PLACE_ALL:
++ case PLACE_SOME:
++ 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()));
++ 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()));
++ break;
++ // Modified cursor only
++ case DROP_ALL_CURSOR:
++ case DROP_ONE_CURSOR:
++ case CLONE_STACK:
++ this.player.connection.send(new net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket(this.player.containerMenu.getCarried().copy())); // Paper - correctly set cursor
++ break;
++ // Nothing
++ case NOTHING:
++ break;
++ }
++ }
++
++ if (event instanceof CraftItemEvent || event instanceof SmithItemEvent) {
++ // Need to update the inventory on crafting to
++ // correctly support custom recipes
++ this.player.containerMenu.sendAllDataToRemote();
++ }
++ }
++ // CraftBukkit end
+ ObjectIterator objectiterator = Int2ObjectMaps.fastIterable(packet.getChangedSlots()).iterator();
+
+ while (objectiterator.hasNext()) {
+@@ -1879,6 +3332,14 @@
+
+ @Override
+ public void handlePlaceRecipe(ServerboundPlaceRecipePacket packet) {
++ // Paper start - auto recipe limit
++ if (!org.bukkit.Bukkit.isPrimaryThread()) {
++ if (!this.recipeSpamPackets.isIncrementAndUnderThreshold()) {
++ this.disconnectAsync(net.minecraft.network.chat.Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Paper - add proper async disconnect
++ return;
++ }
++ }
++ // Paper end - auto recipe limit
+ 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());
+ return;
+ }
++ // Paper start - Add PlayerRecipeBookClickEvent
++ 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
++ );
++ if (!paperEvent.callEvent()) {
++ return;
++ }
++ 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();
++ }
++ 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) {
++ 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 @@
+ @Override
+ public void handleContainerButtonClick(ServerboundContainerButtonClickPacket packet) {
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
++ if (this.player.isImmobile()) return; // CraftBukkit
+ this.player.resetLastActionTime();
+ if (this.player.containerMenu.containerId == packet.containerId() && !this.player.isSpectator()) {
+ if (!this.player.containerMenu.stillValid(this.player)) {
+@@ -1945,7 +3441,44 @@
+
+ boolean flag1 = packet.slotNum() >= 1 && packet.slotNum() <= 45;
+ 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;
++ } else if (packet.slotNum() < 36) {
++ if (packet.slotNum() >= 5 && packet.slotNum() < 9) {
++ type = SlotType.ARMOR;
++ } else {
++ type = SlotType.CONTAINER;
++ }
++ }
++ InventoryCreativeEvent event = new InventoryCreativeEvent(inventory, type, flag ? -999 : packet.slotNum(), item);
++ this.cserver.getPluginManager().callEvent(event);
++
++ 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;
++ }
++ }
++ // CraftBukkit end
++
+ if (flag1 && flag2) {
+ this.player.inventoryMenu.getSlot(packet.slotNum()).setByPlayer(itemstack);
+ this.player.inventoryMenu.setRemoteSlot(packet.slotNum(), itemstack);
+@@ -1964,7 +3497,19 @@
+
+ @Override
+ public void handleSignUpdate(ServerboundSignUpdatePacket packet) {
+- List<String> list = (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) {
++ if (MAX_SIGN_LINE_LENGTH > 0 && lines[i].length() > MAX_SIGN_LINE_LENGTH) {
++ // This handles multibyte characters as 1
++ int offset = lines[i].codePoints().limit(MAX_SIGN_LINE_LENGTH).map(Character::charCount).sum();
++ if (offset < lines[i].length()) {
++ lines[i] = lines[i].substring(0, offset); // this will break any filtering, but filtering is NYI as of 1.17
++ }
++ }
++ }
++ List<String> list = (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 @@
+ }
+
+ private void updateSignText(ServerboundSignUpdatePacket packet, List<FilteredText> signText) {
++ if (this.player.isImmobile()) return; // CraftBukkit
+ this.player.resetLastActionTime();
+ ServerLevel worldserver = this.player.serverLevel();
+ BlockPos blockposition = packet.getPos();
+@@ -1993,15 +3539,33 @@
+ @Override
+ public void handlePlayerAbilities(ServerboundPlayerAbilitiesPacket packet) {
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
+- this.player.getAbilities().flying = packet.isFlying() && this.player.getAbilities().mayfly;
++ // CraftBukkit start
++ if (this.player.getAbilities().mayfly && this.player.getAbilities().flying != packet.isFlying()) {
++ PlayerToggleFlightEvent event = new PlayerToggleFlightEvent(this.player.getBukkitEntity(), packet.isFlying());
++ this.cserver.getPluginManager().callEvent(event);
++ if (!event.isCancelled()) {
++ this.player.getAbilities().flying = packet.isFlying(); // Actually set the player's flying status
++ } else {
++ this.player.onUpdateAbilities(); // Tell the player their ability was reverted
++ }
++ }
++ // CraftBukkit end
+ }
+
+ @Override
+ public void handleClientInformation(ServerboundClientInformationPacket packet) {
+ 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());
++ 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);
+
+ this.player.updateOptions(packet.information());
++ this.connection.channel.attr(io.papermc.paper.adventure.PaperAdventure.LOCALE_ATTRIBUTE).set(net.kyori.adventure.translation.Translator.parseLocale(packet.information().language())); // Paper
+ if (this.player.isModelPartShown(PlayerModelPart.HAT) != flag) {
+ this.server.getPlayerList().broadcastAll(new ClientboundPlayerInfoUpdatePacket(ClientboundPlayerInfoUpdatePacket.Action.UPDATE_HAT, this.player));
+ }
+@@ -2012,7 +3576,7 @@
+ public void handleChangeDifficulty(ServerboundChangeDifficultyPacket packet) {
+ PacketUtils.ensureRunningOnSameThread(packet, this, this.player.serverLevel());
+ if (this.player.hasPermissions(2) || this.isSingleplayerOwner()) {
+- this.server.setDifficulty(packet.getDifficulty(), false);
++ // this.server.setDifficulty(packet.getDifficulty(), false); // Paper - per level difficulty; don't allow clients to change this
+ }
+ }
+
+@@ -2033,7 +3597,7 @@
+
+ if (!Objects.equals(profilepublickey_a, profilepublickey_a1)) {
+ if (profilepublickey_a != null && profilepublickey_a1.expiresAt().isBefore(profilepublickey_a.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 @@
+
+ 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
+ }
+
+ }
+@@ -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
+ }
+ }
+
+@@ -2076,15 +3640,18 @@
+
+ private void resetPlayerChatState(RemoteChatSession session) {
+ this.chatSession = session;
++ 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
+ });
+ }
+
+- @Override
+- public void handleCustomPayload(ServerboundCustomPayloadPacket packet) {}
++ // CraftBukkit start - handled in super
++ // @Override
++ // public void handleCustomPayload(ServerboundCustomPayloadPacket serverboundcustompayloadpacket) {}
++ // CraftBukkit end
+
+ @Override
+ public void handleClientTickEnd(ServerboundClientTickEndPacket packet) {
+@@ -2115,4 +3682,17 @@
+
+ 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();
++ 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,
++ false, logWarning, from, to);
++ event.callEvent();
++ return event;
++ }
++ // Paper end - Add fail move event
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java.patch b/paper-server/patches/unapplied/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java.patch
new file mode 100644
index 0000000000..bf3a6ca144
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java.patch
@@ -0,0 +1,169 @@
+--- a/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java
++++ b/net/minecraft/server/network/ServerHandshakePacketListenerImpl.java
+@@ -13,11 +13,26 @@
+ import net.minecraft.network.protocol.status.StatusProtocols;
+ import net.minecraft.server.MinecraftServer;
+
++// CraftBukkit start
++import java.net.InetAddress;
++import java.util.HashMap;
++// CraftBukkit end
++
+ public class ServerHandshakePacketListenerImpl implements ServerHandshakePacketListener {
+
++ // 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 int throttleCounter = 0;
++ // CraftBukkit end
+ private static final Component IGNORE_STATUS_REASON = Component.translatable("disconnect.ignoring_status_request");
+ 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 @@
+
+ @Override
+ public void handleIntention(ClientIntentionPacket packet) {
++ this.connection.hostname = packet.hostName() + ":" + packet.port(); // CraftBukkit - set hostname
+ switch (packet.intention()) {
+ case LOGIN:
+ this.beginLogin(packet, false);
+@@ -55,23 +71,127 @@
+ throw new UnsupportedOperationException("Invalid intention " + String.valueOf(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) {
+ 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();
++
++ 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.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();
++ }
++ }
++ }
++ }
++ } // 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
+
+- if (packet.protocolVersion() < 754) {
+- ichatmutablecomponent = Component.translatable("multiplayer.disconnect.outdated_client", SharedConstants.getCurrentVersion().getName());
++ 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
+ } 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 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);
+ } else {
+ this.connection.setupInboundProtocol(LoginProtocols.SERVERBOUND, new ServerLoginPacketListenerImpl(this.server, this.connection, transfer));
++ // Paper start - PlayerHandshakeEvent
++ boolean proxyLogicEnabled = org.spigotmc.SpigotConfig.bungee;
++ boolean handledByEvent = false;
++ // Try and handle the handshake through the event
++ if (com.destroystokyo.paper.event.player.PlayerHandshakeEvent.getHandlerList().getRegisteredListeners().length != 0) { // Hello? Can you hear me?
++ java.net.SocketAddress socketAddress = this.connection.address;
++ String hostnameOfRemote = socketAddress instanceof java.net.InetSocketAddress ? ((java.net.InetSocketAddress) socketAddress).getHostString() : InetAddress.getLoopbackAddress().getHostAddress();
++ com.destroystokyo.paper.event.player.PlayerHandshakeEvent event = new com.destroystokyo.paper.event.player.PlayerHandshakeEvent(packet.hostName(), hostnameOfRemote, !proxyLogicEnabled);
++ if (event.callEvent()) {
++ // If we've failed somehow, let the client know so and go no further.
++ if (event.isFailed()) {
++ Component component = io.papermc.paper.adventure.PaperAdventure.asVanilla(event.failMessage());
++ this.connection.send(new ClientboundLoginDisconnectPacket(component));
++ this.connection.disconnect(component);
++ return;
++ }
++
++ if (event.getServerHostname() != null) {
++ // change hostname
++ packet = new ClientIntentionPacket(
++ packet.protocolVersion(),
++ event.getServerHostname(),
++ packet.port(),
++ packet.intention()
++ );
++ }
++ if (event.getSocketAddressHostname() != null) this.connection.address = new java.net.InetSocketAddress(event.getSocketAddressHostname(), socketAddress instanceof java.net.InetSocketAddress ? ((java.net.InetSocketAddress) socketAddress).getPort() : 0);
++ this.connection.spoofedUUID = event.getUniqueId();
++ this.connection.spoofedProfile = gson.fromJson(event.getPropertiesJson(), com.mojang.authlib.properties.Property[].class);
++ handledByEvent = true; // Hooray, we did it!
++ }
++ }
++ // Paper end
++ // Spigot Start
++ String[] split = packet.hostName().split("\00");
++ if (!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
++ // 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);
++ return;
++ }
++ 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;
++ }
++ // Spigot End
+ }
+
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch b/paper-server/patches/unapplied/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
new file mode 100644
index 0000000000..2f73ee117e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/network/ServerLoginPacketListenerImpl.java.patch
@@ -0,0 +1,424 @@
+--- 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 @@
+ import net.minecraft.util.StringUtil;
+ import org.apache.commons.lang3.Validate;
+ import org.slf4j.Logger;
++import net.minecraft.network.protocol.Packet;
++import net.minecraft.network.protocol.PacketUtils;
++import org.bukkit.craftbukkit.entity.CraftPlayer;
++import org.bukkit.craftbukkit.util.Waitable;
++import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
++import org.bukkit.event.player.PlayerPreLoginEvent;
+
+-public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, TickablePacketListener {
++public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, TickablePacketListener, CraftPlayer.TransferCookieConnection {
+
++ @Override
++ public boolean isTransferred() {
++ return this.transferred;
++ }
++
++ @Override
++ public ConnectionProtocol getProtocol() {
++ return ConnectionProtocol.LOGIN;
++ }
++
++ @Override
++ public void sendPacket(Packet<?> packet) {
++ this.connection.send(packet);
++ }
++
++ @Override
++ public void kickPlayer(Component reason, org.bukkit.event.player.PlayerKickEvent.Cause cause) { // Paper - kick event causes - during login, no event can be called.
++ 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()) {
++ 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));
++ } // 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 @@
+
+ }
+
++ // 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
++ }
++ // CraftBukkit end
++
+ @Override
+ public boolean isAcceptingMessages() {
+ return this.connection.isConnected();
+@@ -120,7 +173,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]);
++ // 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]);
++ }
++ // Paper end - Validate usernames
+ this.requestedUsername = packet.name();
+ GameProfile gameprofile = this.server.getSingleplayerProfile();
+
+@@ -131,7 +190,36 @@
+ this.state = ServerLoginPacketListenerImpl.State.KEY;
+ this.connection.send(new ClientboundHelloPacket("", this.server.getKeyPair().getPublic().getEncoded(), this.challenge, true));
+ } else {
+- this.startClientVerification(UUIDUtil.createOfflineProfile(this.requestedUsername));
++ // Paper start - Add Velocity IP Forwarding Support
++ if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) {
++ this.velocityLoginMessageId = java.util.concurrent.ThreadLocalRandom.current().nextInt();
++ net.minecraft.network.FriendlyByteBuf buf = new net.minecraft.network.FriendlyByteBuf(io.netty.buffer.Unpooled.buffer());
++ buf.writeByte(com.destroystokyo.paper.proxy.VelocityProxy.MAX_SUPPORTED_FORWARDING_VERSION);
++ net.minecraft.network.protocol.login.ClientboundCustomQueryPacket packet1 = new net.minecraft.network.protocol.login.ClientboundCustomQueryPacket(this.velocityLoginMessageId, new net.minecraft.network.protocol.login.ClientboundCustomQueryPacket.PlayerInfoChannelPayload(com.destroystokyo.paper.proxy.VelocityProxy.PLAYER_INFO_CHANNEL, buf));
++ this.connection.send(packet1);
++ return;
++ }
++ // Paper end - Add Velocity IP Forwarding Support
++ // CraftBukkit start
++ // Paper start - Cache authenticator threads
++ authenticatorPool.execute(new Runnable() {
++
++ @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);
++ }
++ }
++ });
++ // Paper end - Cache authenticator threads
++ // CraftBukkit end
+ }
+
+ }
+@@ -144,10 +232,24 @@
+
+ private void verifyLoginAndFinishConnectionSetup(GameProfile profile) {
+ PlayerList playerlist = this.server.getPlayerList();
+- Component ichatbasecomponent = playerlist.canPlayerLogin(this.connection.getRemoteAddress(), profile);
++ // CraftBukkit start - fire PlayerLoginEvent
++ this.player = playerlist.canPlayerLogin(this, profile); // CraftBukkit
+
+- if (ichatbasecomponent != null) {
+- this.disconnect(ichatbasecomponent);
++ if (this.player != null) {
++ if (this.player.getBukkitEntity().isAwaitingCookies()) {
++ this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_COOKIES;
++ } else {
++ this.postCookies(profile);
++ }
++ }
++ }
++
++ private void postCookies(GameProfile gameprofile) {
++ PlayerList playerlist = this.server.getPlayerList();
++
++ if (this.player == null) {
++ // this.disconnect(ichatbasecomponent);
++ // 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 @@
+ }));
+ }
+
+- boolean flag = playerlist.disconnectAllPlayersWithProfile(profile);
++ boolean flag = playerlist.disconnectAllPlayersWithProfile(gameprofile, this.player); // CraftBukkit - add player reference
+
+ if (flag) {
+ this.state = ServerLoginPacketListenerImpl.State.WAITING_FOR_DUPE_DISCONNECT;
+ } else {
+- this.finishLoginAndWaitForClient(profile);
++ this.finishLoginAndWaitForClient(gameprofile);
+ }
+ }
+
+@@ -195,7 +297,8 @@
+ throw new IllegalStateException("Protocol error", cryptographyexception);
+ }
+
+- Thread thread = new Thread("User Authenticator #" + ServerLoginPacketListenerImpl.UNIQUE_THREAD_ID.incrementAndGet()) {
++ // Paper start - Cache authenticator threads
++ authenticatorPool.execute(new Runnable() {
+ 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();
+
++ // CraftBukkit start - fire PlayerPreLoginEvent
++ if (!ServerLoginPacketListenerImpl.this.connection.isConnected()) {
++ return;
++ }
++ 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);
+ } 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
+ } 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) {
+ 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
+ } 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) {
++ ServerLoginPacketListenerImpl.this.disconnect("Failed to verify username!");
++ ServerLoginPacketListenerImpl.this.server.server.getLogger().log(java.util.logging.Level.WARNING, "Exception verifying " + s1, exception);
++ // CraftBukkit end
+ }
+
+ }
+@@ -232,23 +346,118 @@
+
+ return ServerLoginPacketListenerImpl.this.server.getPreventProxyConnections() && socketaddress instanceof InetSocketAddress ? ((InetSocketAddress) socketaddress).getAddress() : null;
+ }
+- };
++ });
++ // 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.");
++ return gameprofile;
++ }
++ // Paper end - Add Velocity IP Forwarding Support
++ String playerName = gameprofile.getName();
++ java.net.InetAddress address = ((java.net.InetSocketAddress) this.connection.getRemoteAddress()).getAddress();
++ java.util.UUID uniqueId = gameprofile.getId();
++ final org.bukkit.craftbukkit.CraftServer server = ServerLoginPacketListenerImpl.this.server.server;
++
++ // Paper start - Add more fields to AsyncPlayerPreLoginEvent
++ final InetAddress rawAddress = ((InetSocketAddress) this.connection.channel.remoteAddress()).getAddress();
++ com.destroystokyo.paper.profile.PlayerProfile profile = com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitMirror(gameprofile); // Paper - setPlayerProfileAPI
++ AsyncPlayerPreLoginEvent asyncEvent = new AsyncPlayerPreLoginEvent(playerName, address, rawAddress, uniqueId, this.transferred, profile, this.connection.hostname);
++ server.getPluginManager().callEvent(asyncEvent);
++ profile = asyncEvent.getPlayerProfile();
++ profile.complete(true); // Paper - setPlayerProfileAPI
++ gameprofile = com.destroystokyo.paper.profile.CraftPlayerProfile.asAuthlibCopy(profile);
++ playerName = gameprofile.getName();
++ uniqueId = gameprofile.getId();
++ // Paper end - Add more fields to AsyncPlayerPreLoginEvent
++
++ if (PlayerPreLoginEvent.getHandlerList().getRegisteredListeners().length != 0) {
++ final PlayerPreLoginEvent event = new PlayerPreLoginEvent(playerName, address, uniqueId);
++ if (asyncEvent.getResult() != PlayerPreLoginEvent.Result.ALLOWED) {
++ event.disallow(asyncEvent.getResult(), asyncEvent.kickMessage()); // Paper - Adventure
++ }
++ Waitable<PlayerPreLoginEvent.Result> waitable = new Waitable<PlayerPreLoginEvent.Result>() {
++ @Override
++ protected PlayerPreLoginEvent.Result evaluate() {
++ server.getPluginManager().callEvent(event);
++ return event.getResult();
++ }
++ };
++
++ ServerLoginPacketListenerImpl.this.server.processQueue.add(waitable);
++ if (waitable.get() != PlayerPreLoginEvent.Result.ALLOWED) {
++ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(event.kickMessage())); // Paper - Adventure
++ }
++ } else {
++ if (asyncEvent.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) {
++ this.disconnect(io.papermc.paper.adventure.PaperAdventure.asVanilla(asyncEvent.kickMessage())); // Paper - Adventure
++ }
++ }
++ return gameprofile; // Paper - Add more fields to AsyncPlayerPreLoginEvent
+ }
++ // CraftBukkit end
+
+ @Override
+ public void handleCustomQueryPacket(ServerboundCustomQueryAnswerPacket packet) {
++ // Paper start - Add Velocity IP Forwarding Support
++ if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled && packet.transactionId() == this.velocityLoginMessageId) {
++ ServerboundCustomQueryAnswerPacket.QueryAnswerPayload payload = (ServerboundCustomQueryAnswerPacket.QueryAnswerPayload)packet.payload();
++ if (payload == null) {
++ this.disconnect("This server requires you to connect with Velocity.");
++ return;
++ }
++
++ net.minecraft.network.FriendlyByteBuf buf = payload.buffer;
++
++ if (!com.destroystokyo.paper.proxy.VelocityProxy.checkIntegrity(buf)) {
++ this.disconnect("Unable to verify player details");
++ return;
++ }
++
++ int version = buf.readVarInt();
++ if (version > com.destroystokyo.paper.proxy.VelocityProxy.MAX_SUPPORTED_FORWARDING_VERSION) {
++ throw new IllegalStateException("Unsupported forwarding version " + version + ", wanted upto " + com.destroystokyo.paper.proxy.VelocityProxy.MAX_SUPPORTED_FORWARDING_VERSION);
++ }
++
++ java.net.SocketAddress listening = this.connection.getRemoteAddress();
++ int port = 0;
++ if (listening instanceof java.net.InetSocketAddress) {
++ port = ((java.net.InetSocketAddress) listening).getPort();
++ }
++ this.connection.address = new java.net.InetSocketAddress(com.destroystokyo.paper.proxy.VelocityProxy.readAddress(buf), port);
++
++ this.authenticatedProfile = com.destroystokyo.paper.proxy.VelocityProxy.createProfile(buf);
++
++ //TODO Update handling for lazy sessions, might not even have to do anything?
++
++ // Proceed with login
++ authenticatorPool.execute(() -> {
++ try {
++ final GameProfile gameprofile = this.callPlayerPreLoginEvents(this.authenticatedProfile);
++ ServerLoginPacketListenerImpl.LOGGER.info("UUID of player {} is {}", gameprofile.getName(), gameprofile.getId());
++ ServerLoginPacketListenerImpl.this.startClientVerification(gameprofile);
++ } catch (Exception ex) {
++ disconnect("Failed to verify username!");
++ server.server.getLogger().log(java.util.logging.Level.WARNING, "Exception verifying " + this.authenticatedProfile.getName(), ex);
++ }
++ });
++ return;
++ }
++ // Paper end - Add Velocity IP Forwarding Support
+ this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
+ }
+
+ @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]);
+ 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 @@
+
+ @Override
+ public void handleCookieResponse(ServerboundCookieResponsePacket packet) {
++ // CraftBukkit start
++ PacketUtils.ensureRunningOnSameThread(packet, this, this.server);
++ if (this.player != null && this.player.getBukkitEntity().handleCookieResponse(packet)) {
++ return;
++ }
++ // CraftBukkit end
+ this.disconnect(ServerCommonPacketListenerImpl.DISCONNECT_UNEXPECTED_QUERY);
+ }
+
++ // Spigot start
++ protected GameProfile createOfflineProfile(String s) {
++ java.util.UUID uuid;
++ if ( this.connection.spoofedUUID != null )
++ {
++ uuid = this.connection.spoofedUUID;
++ } else
++ {
++ uuid = UUIDUtil.createOfflinePlayerUUID( 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 );
++ }
++ }
++
++ 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() {}
+ }
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
new file mode 100644
index 0000000000..7ea7e46622
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/network/ServerStatusPacketListenerImpl.java.patch
@@ -0,0 +1,137 @@
+--- 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/packs/PathPackResources.java.patch b/paper-server/patches/unapplied/net/minecraft/server/packs/PathPackResources.java.patch
new file mode 100644
index 0000000000..4c21d9802f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/packs/PathPackResources.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/server/packs/PathPackResources.java
++++ b/net/minecraft/server/packs/PathPackResources.java
+@@ -103,6 +103,12 @@
+ try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path)) {
+ for (Path path2 : directoryStream) {
+ String string = path2.getFileName().toString();
++ // Paper start - Improve logging and errors
++ if (!Files.isDirectory(path2)) {
++ LOGGER.error("Invalid directory entry: {} in {}.", string, this.root, new java.nio.file.NotDirectoryException(string));
++ continue;
++ }
++ // Paper end - Improve logging and errors
+ if (ResourceLocation.isValidNamespace(string)) {
+ set.add(string);
+ } else {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/packs/VanillaPackResourcesBuilder.java.patch b/paper-server/patches/unapplied/net/minecraft/server/packs/VanillaPackResourcesBuilder.java.patch
new file mode 100644
index 0000000000..5f4418300b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/packs/VanillaPackResourcesBuilder.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/server/packs/VanillaPackResourcesBuilder.java
++++ b/net/minecraft/server/packs/VanillaPackResourcesBuilder.java
+@@ -138,6 +138,15 @@
+
+ public VanillaPackResourcesBuilder applyDevelopmentConfig() {
+ developmentConfig.accept(this);
++ if (Boolean.getBoolean("Paper.pushPaperAssetsRoot")) {
++ try {
++ this.pushAssetPath(net.minecraft.server.packs.PackType.SERVER_DATA, net.minecraft.server.packs.VanillaPackResourcesBuilder.safeGetPath(java.util.Objects.requireNonNull(
++ // Important that this is a patched class
++ VanillaPackResourcesBuilder.class.getResource("/data/.paperassetsroot"), "Missing required .paperassetsroot file").toURI()).getParent());
++ } catch (java.net.URISyntaxException | IOException ex) {
++ throw new RuntimeException(ex);
++ }
++ }
+ return this;
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/server/packs/repository/ServerPacksSource.java.patch b/paper-server/patches/unapplied/net/minecraft/server/packs/repository/ServerPacksSource.java.patch
new file mode 100644
index 0000000000..a5d66bbff8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/packs/repository/ServerPacksSource.java.patch
@@ -0,0 +1,31 @@
+--- a/net/minecraft/server/packs/repository/ServerPacksSource.java
++++ b/net/minecraft/server/packs/repository/ServerPacksSource.java
+@@ -48,7 +48,7 @@
+ public static VanillaPackResources createVanillaPackSource() {
+ return new VanillaPackResourcesBuilder()
+ .setMetadata(BUILT_IN_METADATA)
+- .exposeNamespace("minecraft")
++ .exposeNamespace("minecraft", ResourceLocation.PAPER_NAMESPACE) // Paper
+ .applyDevelopmentConfig()
+ .pushJarResources()
+ .build(VANILLA_PACK_INFO);
+@@ -68,7 +68,18 @@
+ @Nullable
+ @Override
+ protected Pack createBuiltinPack(String fileName, Pack.ResourcesSupplier packFactory, Component displayName) {
+- return Pack.readMetaAndCreate(createBuiltInPackLocation(fileName, displayName), packFactory, PackType.SERVER_DATA, FEATURE_SELECTION_CONFIG);
++ // Paper start - custom built-in pack
++ final PackLocationInfo info;
++ final PackSelectionConfig packConfig;
++ if ("paper".equals(fileName)) {
++ info = new PackLocationInfo(fileName, displayName, PackSource.BUILT_IN, Optional.empty());
++ packConfig = new PackSelectionConfig(true, Pack.Position.TOP, true);
++ } else {
++ info = createBuiltInPackLocation(fileName, displayName);
++ packConfig = FEATURE_SELECTION_CONFIG;
++ }
++ return Pack.readMetaAndCreate(info, packFactory, PackType.SERVER_DATA, packConfig);
++ // Paper end - custom built-in pack
+ }
+
+ public static PackRepository createPackRepository(Path dataPacksPath, DirectoryValidator symlinkFinder) {
diff --git a/paper-server/patches/unapplied/net/minecraft/server/players/BanListEntry.java.patch b/paper-server/patches/unapplied/net/minecraft/server/players/BanListEntry.java.patch
new file mode 100644
index 0000000000..205eaad746
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/players/BanListEntry.java.patch
@@ -0,0 +1,34 @@
+--- a/net/minecraft/server/players/BanListEntry.java
++++ b/net/minecraft/server/players/BanListEntry.java
+@@ -27,7 +27,7 @@
+ }
+
+ protected BanListEntry(@Nullable T key, JsonObject json) {
+- super(key);
++ super(BanListEntry.checkExpiry(key, json)); // 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);
+ }
++
++ // CraftBukkit start
++ private static <T> T checkExpiry(T object, JsonObject jsonobject) {
++ Date expires = null;
++
++ try {
++ expires = jsonobject.has("expires") ? BanListEntry.DATE_FORMAT.parse(jsonobject.get("expires").getAsString()) : null;
++ } catch (ParseException ex) {
++ // Guess we don't have a date
++ }
++
++ if (expires == null || expires.after(new Date())) {
++ return object;
++ } else {
++ return null;
++ }
++ }
++ // CraftBukkit 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
new file mode 100644
index 0000000000..8b31e2142b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/players/GameProfileCache.java.patch
@@ -0,0 +1,217 @@
+--- 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/OldUsersConverter.java.patch b/paper-server/patches/unapplied/net/minecraft/server/players/OldUsersConverter.java.patch
new file mode 100644
index 0000000000..7f3147ad37
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/players/OldUsersConverter.java.patch
@@ -0,0 +1,98 @@
+--- a/net/minecraft/server/players/OldUsersConverter.java
++++ b/net/minecraft/server/players/OldUsersConverter.java
+@@ -21,6 +21,9 @@
+ import java.util.UUID;
+ import javax.annotation.Nullable;
+ import net.minecraft.core.UUIDUtil;
++import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.NbtAccounter;
++import net.minecraft.nbt.NbtIo;
+ import net.minecraft.server.MinecraftServer;
+ import net.minecraft.server.dedicated.DedicatedServer;
+ import net.minecraft.util.StringUtil;
+@@ -62,7 +65,8 @@
+ return new String[i];
+ });
+
+- 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);
+ } else {
+ String[] astring1 = astring;
+@@ -85,7 +89,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
+ }
+ }
+
+@@ -143,7 +147,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
+ }
+ }
+
+@@ -184,7 +188,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
+ }
+ }
+
+@@ -228,7 +232,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
+ }
+ }
+
+@@ -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");
++
++ // CraftBukkit start - Use old file name to seed lastKnownName
++ CompoundTag root = null;
++
++ try {
++ root = NbtIo.readCompressed(new java.io.FileInputStream(file5), NbtAccounter.unlimitedHeap());
++ } catch (Exception exception) {
++ // Paper start
++ io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(exception);
++ exception.printStackTrace();
++ 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);
++
++ try {
++ NbtIo.writeCompressed(root, new java.io.FileOutputStream(file2));
++ } catch (Exception exception) {
++ // Paper start
++ io.papermc.paper.util.StacktraceDeobfuscator.INSTANCE.deobfuscateThrowable(exception);
++ exception.printStackTrace();
++ com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception);
++ // Paper end
++ }
++ }
++ // CraftBukkit end
++
+ OldUsersConverter.ensureDirectoryExists(playerDataFolder);
+ if (!file5.renameTo(file6)) {
+ throw new OldUsersConverter.ConversionError("Could not convert file for " + fileName);
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
new file mode 100644
index 0000000000..06288fc4a0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/players/PlayerList.java.patch
@@ -0,0 +1,1276 @@
+--- 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
new file mode 100644
index 0000000000..568224fe25
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/players/SleepStatus.java.patch
@@ -0,0 +1,44 @@
+--- 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
new file mode 100644
index 0000000000..bea194db95
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/players/StoredUserList.java.patch
@@ -0,0 +1,96 @@
+--- 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/players/UserBanListEntry.java.patch b/paper-server/patches/unapplied/net/minecraft/server/players/UserBanListEntry.java.patch
new file mode 100644
index 0000000000..df038411ce
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/players/UserBanListEntry.java.patch
@@ -0,0 +1,42 @@
+--- 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 @@
+
+ @Nullable
+ private static GameProfile createGameProfile(JsonObject json) {
+- if (json.has("uuid") && json.has("name")) {
++ // Spigot start
++ // this whole method has to be reworked to account for the fact Bukkit only accepts UUID bans and gives no way for usernames to be stored!
++ UUID uuid = null;
++ String name = null;
++ if (json.has("uuid")) {
+ String s = json.get("uuid").getAsString();
+
+- UUID uuid;
+-
+ try {
+ uuid = UUID.fromString(s);
+ } catch (Throwable throwable) {
+- return null;
+ }
+
+- return new GameProfile(uuid, json.get("name").getAsString());
++ }
++ if ( json.has("name"))
++ {
++ name = json.get("name").getAsString();
++ }
++ if ( uuid != null || name != null )
++ {
++ return new GameProfile( uuid, name );
+ } else {
+ return null;
+ }
++ // Spigot End
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/server/players/UserWhiteList.java.patch b/paper-server/patches/unapplied/net/minecraft/server/players/UserWhiteList.java.patch
new file mode 100644
index 0000000000..4cef2c2bc4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/players/UserWhiteList.java.patch
@@ -0,0 +1,26 @@
+--- 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();
+ }
++ // Paper start - Add whitelist events
++ @Override
++ public void add(UserWhiteListEntry entry) {
++ if (!new io.papermc.paper.event.server.WhitelistStateUpdateEvent(com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitCopy(entry.getUser()), io.papermc.paper.event.server.WhitelistStateUpdateEvent.WhitelistStatus.ADDED).callEvent()) {
++ return;
++ }
++
++ super.add(entry);
++ }
++
++ @Override
++ public void remove(GameProfile profile) {
++ if (!new io.papermc.paper.event.server.WhitelistStateUpdateEvent(com.destroystokyo.paper.profile.CraftPlayerProfile.asBukkitCopy(profile), io.papermc.paper.event.server.WhitelistStateUpdateEvent.WhitelistStatus.REMOVED).callEvent()) {
++ return;
++ }
++
++ super.remove(profile);
++ }
++ // Paper end - Add whitelist events
+ }
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
new file mode 100644
index 0000000000..589c29e384
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/rcon/RconConsoleSource.java.patch
@@ -0,0 +1,49 @@
+--- 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/QueryThreadGs4.java.patch b/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/QueryThreadGs4.java.patch
new file mode 100644
index 0000000000..6a8d04320c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/QueryThreadGs4.java.patch
@@ -0,0 +1,126 @@
+--- a/net/minecraft/server/rcon/thread/QueryThreadGs4.java
++++ b/net/minecraft/server/rcon/thread/QueryThreadGs4.java
+@@ -106,13 +106,32 @@
+ NetworkDataOutputStream networkDataOutputStream = new NetworkDataOutputStream(1460);
+ networkDataOutputStream.write(0);
+ networkDataOutputStream.writeBytes(this.getIdentBytes(packet.getSocketAddress()));
+- networkDataOutputStream.writeString(this.serverName);
++
++ 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()
++ .motd(this.serverName)
++ .map(this.worldName)
++ .currentPlayers(this.serverInterface.getPlayerCount())
++ .maxPlayers(this.maxPlayers)
++ .port(this.serverPort)
++ .hostname(this.hostIp)
++ .gameVersion(this.serverInterface.getServerVersion())
++ .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);
++ queryEvent.callEvent();
++ queryResponse = queryEvent.getResponse();
++
++ networkDataOutputStream.writeString(queryResponse.getMotd());
+ networkDataOutputStream.writeString("SMP");
+- networkDataOutputStream.writeString(this.worldName);
+- networkDataOutputStream.writeString(Integer.toString(this.serverInterface.getPlayerCount()));
+- networkDataOutputStream.writeString(Integer.toString(this.maxPlayers));
+- networkDataOutputStream.writeShort((short)this.serverPort);
+- networkDataOutputStream.writeString(this.hostIp);
++ networkDataOutputStream.writeString(queryResponse.getMap());
++ networkDataOutputStream.writeString(Integer.toString(queryResponse.getCurrentPlayers()));
++ networkDataOutputStream.writeString(Integer.toString(queryResponse.getMaxPlayers()));
++ networkDataOutputStream.writeShort((short) queryResponse.getPort());
++ networkDataOutputStream.writeString(queryResponse.getHostname());
++ // Paper end
+ this.sendTo(networkDataOutputStream.toByteArray(), packet);
+ LOGGER.debug("Status [{}]", socketAddress);
+ }
+@@ -147,31 +166,75 @@
+ this.rulesResponse.writeString("splitnum");
+ this.rulesResponse.write(128);
+ this.rulesResponse.write(0);
++ // Paper start
++ // Pack plugins
++ java.util.List<com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse.PluginInformation> plugins = java.util.Collections.emptyList();
++ org.bukkit.plugin.Plugin[] bukkitPlugins;
++ if (((net.minecraft.server.dedicated.DedicatedServer) this.serverInterface).server.getQueryPlugins() && (bukkitPlugins = org.bukkit.Bukkit.getPluginManager().getPlugins()).length > 0) {
++ plugins = java.util.stream.Stream.of(bukkitPlugins)
++ .map(plugin -> com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse.PluginInformation.of(plugin.getName(), plugin.getDescription().getVersion()))
++ .collect(java.util.stream.Collectors.toList());
++ }
++
++ com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse queryResponse = com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse.builder()
++ .motd(this.serverName)
++ .map(this.worldName)
++ .currentPlayers(this.serverInterface.getPlayerCount())
++ .maxPlayers(this.maxPlayers)
++ .port(this.serverPort)
++ .hostname(this.hostIp)
++ .plugins(plugins)
++ .players(this.serverInterface.getPlayerNames())
++ .gameVersion(this.serverInterface.getServerVersion())
++ .serverVersion(org.bukkit.Bukkit.getServer().getName() + " on " + org.bukkit.Bukkit.getServer().getBukkitVersion())
++ .build();
++ 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);
++ queryEvent.callEvent();
++ queryResponse = queryEvent.getResponse();
+ this.rulesResponse.writeString("hostname");
+- this.rulesResponse.writeString(this.serverName);
++ this.rulesResponse.writeString(queryResponse.getMotd());
+ this.rulesResponse.writeString("gametype");
+ this.rulesResponse.writeString("SMP");
+ this.rulesResponse.writeString("game_id");
+ this.rulesResponse.writeString("MINECRAFT");
+ this.rulesResponse.writeString("version");
+- this.rulesResponse.writeString(this.serverInterface.getServerVersion());
++ this.rulesResponse.writeString(queryResponse.getGameVersion());
+ this.rulesResponse.writeString("plugins");
+- this.rulesResponse.writeString(this.serverInterface.getPluginNames());
++ java.lang.StringBuilder pluginsString = new java.lang.StringBuilder();
++ pluginsString.append(queryResponse.getServerVersion());
++ if (!queryResponse.getPlugins().isEmpty()) {
++ pluginsString.append(": ");
++ java.util.Iterator<com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse.PluginInformation> iter = queryResponse.getPlugins().iterator();
++ while (iter.hasNext()) {
++ com.destroystokyo.paper.event.server.GS4QueryEvent.QueryResponse.PluginInformation info = iter.next();
++ pluginsString.append(info.getName());
++ if (info.getVersion() != null) {
++ pluginsString.append(' ').append(info.getVersion().replace(";", ","));
++ }
++ if (iter.hasNext()) {
++ pluginsString.append(';').append(' ');
++ }
++ }
++ }
++ this.rulesResponse.writeString(pluginsString.toString());
+ this.rulesResponse.writeString("map");
+- this.rulesResponse.writeString(this.worldName);
++ this.rulesResponse.writeString(queryResponse.getMap());
+ this.rulesResponse.writeString("numplayers");
+- this.rulesResponse.writeString(this.serverInterface.getPlayerCount() + "");
++ this.rulesResponse.writeString(Integer.toString(queryResponse.getCurrentPlayers()));
+ this.rulesResponse.writeString("maxplayers");
+- this.rulesResponse.writeString(this.maxPlayers + "");
++ this.rulesResponse.writeString(Integer.toString(queryResponse.getMaxPlayers()));
+ this.rulesResponse.writeString("hostport");
+- this.rulesResponse.writeString(this.serverPort + "");
++ this.rulesResponse.writeString(Integer.toString(queryResponse.getPort()));
+ this.rulesResponse.writeString("hostip");
+- this.rulesResponse.writeString(this.hostIp);
++ this.rulesResponse.writeString(queryResponse.getHostname());
+ this.rulesResponse.write(0);
+ this.rulesResponse.write(1);
+ this.rulesResponse.writeString("player_");
+ this.rulesResponse.write(0);
+- String[] strings = this.serverInterface.getPlayerNames();
++ String[] strings = queryResponse.getPlayers().toArray(String[]::new);
+
+ for (String string : strings) {
+ this.rulesResponse.writeString(string);
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
new file mode 100644
index 0000000000..1aa09ff9d7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/RconClient.java.patch
@@ -0,0 +1,80 @@
+--- 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/server/rcon/thread/RconThread.java.patch b/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/RconThread.java.patch
new file mode 100644
index 0000000000..5bc2ff1f90
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/server/rcon/thread/RconThread.java.patch
@@ -0,0 +1,26 @@
+--- a/net/minecraft/server/rcon/thread/RconThread.java
++++ b/net/minecraft/server/rcon/thread/RconThread.java
+@@ -57,7 +57,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";
+ }
+@@ -104,6 +104,14 @@
+
+ this.clients.clear();
+ }
++ // Paper start - don't wait for remote connections
++ public void stopNonBlocking() {
++ this.running = false;
++ for (RconClient client : this.clients) {
++ client.running = false;
++ }
++ }
++ // Paper end - don't wait for remote connections
+
+ private void closeSocket(ServerSocket socket) {
+ LOGGER.debug("closeSocket: {}", socket);
diff --git a/paper-server/patches/unapplied/net/minecraft/stats/ServerRecipeBook.java.patch b/paper-server/patches/unapplied/net/minecraft/stats/ServerRecipeBook.java.patch
new file mode 100644
index 0000000000..f95e98f66a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/stats/ServerRecipeBook.java.patch
@@ -0,0 +1,38 @@
+--- 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
new file mode 100644
index 0000000000..f82072300e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/stats/ServerStatsCounter.java.patch
@@ -0,0 +1,58 @@
+--- 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/stats/StatsCounter.java.patch b/paper-server/patches/unapplied/net/minecraft/stats/StatsCounter.java.patch
new file mode 100644
index 0000000000..51c6680579
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/stats/StatsCounter.java.patch
@@ -0,0 +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);
+
++ // CraftBukkit start - fire Statistic events
++ org.bukkit.event.Cancellable cancellable = org.bukkit.craftbukkit.event.CraftEventFactory.handleStatisticsIncrease(player, stat, this.getValue(stat), j);
++ if (cancellable != null && cancellable.isCancelled()) {
++ return;
++ }
++ // CraftBukkit end
+ this.setValue(player, stat, j);
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/tags/TagLoader.java.patch b/paper-server/patches/unapplied/net/minecraft/tags/TagLoader.java.patch
new file mode 100644
index 0000000000..10e1ff5cb3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/tags/TagLoader.java.patch
@@ -0,0 +1,66 @@
+--- 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);
+ }
+
+- public Map<ResourceLocation, List<T>> build(Map<ResourceLocation, List<TagLoader.EntryWithSource>> tags) {
++ // 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
+ 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))
+ );
+- 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 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 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
+ .flatMap(Optional::stream)
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ public static <T> void loadTagsForRegistry(ResourceManager resourceManager, WritableRegistry<T> registry) {
++ // 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
+ 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
+ }
+
+ 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));
+ }
+
+- private static <T> Optional<Registry.PendingTags<T>> loadPendingTags(ResourceManager resourceManager, Registry<T> registry) {
++ private static <T> Optional<Registry.PendingTags<T>> loadPendingTags(ResourceManager resourceManager, Registry<T> registry, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause cause) { // Paper - add registrar event cause
+ ResourceKey<? extends Registry<T>> resourceKey = registry.key();
+ TagLoader<Holder<T>> tagLoader = new TagLoader<>(
+ (TagLoader.ElementLookup<Holder<T>>)TagLoader.ElementLookup.fromFrozenRegistry(registry), Registries.tagsDirPath(resourceKey)
+ );
+- TagLoader.LoadResult<T> loadResult = new TagLoader.LoadResult<>(resourceKey, wrapTags(registry.key(), tagLoader.build(tagLoader.load(resourceManager))));
++ TagLoader.LoadResult<T> loadResult = new TagLoader.LoadResult<>(resourceKey, wrapTags(registry.key(), tagLoader.build(tagLoader.load(resourceManager), io.papermc.paper.tag.PaperTagListenerManager.INSTANCE.createEventConfig(registry, cause)))); // Paper - add registrar event cause
+ return loadResult.tags().isEmpty() ? Optional.empty() : Optional.of(registry.prepareTagReload(loadResult));
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/util/SimpleBitStorage.java.patch b/paper-server/patches/unapplied/net/minecraft/util/SimpleBitStorage.java.patch
new file mode 100644
index 0000000000..d3a669c38d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/util/SimpleBitStorage.java.patch
@@ -0,0 +1,70 @@
+--- a/net/minecraft/util/SimpleBitStorage.java
++++ b/net/minecraft/util/SimpleBitStorage.java
+@@ -204,8 +204,8 @@
+ private final long mask;
+ private final int size;
+ private final int valuesPerLong;
+- private final int divideMul;
+- private final int divideAdd;
++ private final int divideMul; private final long divideMulUnsigned; // Paper - Perf: Optimize SimpleBitStorage; referenced in b(int) with 2 Integer.toUnsignedLong calls
++ private final int divideAdd; private final long divideAddUnsigned; // Paper - Perf: Optimize SimpleBitStorage
+ private final int divideShift;
+
+ public SimpleBitStorage(int elementBits, int size, int[] data) {
+@@ -248,8 +248,8 @@
+ this.mask = (1L << elementBits) - 1L;
+ this.valuesPerLong = (char)(64 / elementBits);
+ int i = 3 * (this.valuesPerLong - 1);
+- this.divideMul = MAGIC[i + 0];
+- this.divideAdd = MAGIC[i + 1];
++ this.divideMul = MAGIC[i + 0]; this.divideMulUnsigned = Integer.toUnsignedLong(this.divideMul); // Paper - Perf: Optimize SimpleBitStorage
++ this.divideAdd = MAGIC[i + 1]; this.divideAddUnsigned = Integer.toUnsignedLong(this.divideAdd); // Paper - Perf: Optimize SimpleBitStorage
+ this.divideShift = MAGIC[i + 2];
+ int j = (size + this.valuesPerLong - 1) / this.valuesPerLong;
+ if (data != null) {
+@@ -264,15 +264,15 @@
+ }
+
+ private int cellIndex(int index) {
+- long l = Integer.toUnsignedLong(this.divideMul);
+- long m = Integer.toUnsignedLong(this.divideAdd);
+- return (int)((long)index * l + m >> 32 >> this.divideShift);
++ //long l = Integer.toUnsignedLong(this.divideMul); // Paper - Perf: Optimize SimpleBitStorage
++ //long m = Integer.toUnsignedLong(this.divideAdd); // Paper - Perf: Optimize SimpleBitStorage
++ return (int) (index * this.divideMulUnsigned + this.divideAddUnsigned >> 32 >> this.divideShift); // Paper - Perf: Optimize SimpleBitStorage
+ }
+
+ @Override
+- public int getAndSet(int index, int value) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
+- Validate.inclusiveBetween(0L, this.mask, (long)value);
++ public final int getAndSet(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage
+ int i = this.cellIndex(index);
+ long l = this.data[i];
+ int j = (index - i * this.valuesPerLong) * this.bits;
+@@ -282,9 +282,9 @@
+ }
+
+ @Override
+- public void set(int index, int value) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
+- Validate.inclusiveBetween(0L, this.mask, (long)value);
++ public final void set(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, this.mask, (long)value); // Paper - Perf: Optimize SimpleBitStorage
+ int i = this.cellIndex(index);
+ long l = this.data[i];
+ int j = (index - i * this.valuesPerLong) * this.bits;
+@@ -292,8 +292,8 @@
+ }
+
+ @Override
+- public int get(int index) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
++ public final int get(int index) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
+ int i = this.cellIndex(index);
+ long l = this.data[i];
+ int j = (index - i * this.valuesPerLong) * this.bits;
diff --git a/paper-server/patches/unapplied/net/minecraft/util/SortedArraySet.java.patch b/paper-server/patches/unapplied/net/minecraft/util/SortedArraySet.java.patch
new file mode 100644
index 0000000000..00af73f775
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/util/SortedArraySet.java.patch
@@ -0,0 +1,11 @@
+--- 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
new file mode 100644
index 0000000000..803fd149f1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/util/SpawnUtil.java.patch
@@ -0,0 +1,60 @@
+--- 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/StringUtil.java.patch b/paper-server/patches/unapplied/net/minecraft/util/StringUtil.java.patch
new file mode 100644
index 0000000000..d0f9b93e86
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/util/StringUtil.java.patch
@@ -0,0 +1,28 @@
+--- 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();
+ }
+
++ // Paper start - Username validation
++ public static boolean isReasonablePlayerName(final String name) {
++ if (name.isEmpty() || name.length() > 16) {
++ return false;
++ }
++
++ for (int i = 0, len = name.length(); i < len; ++i) {
++ final char c = name.charAt(i);
++ if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || (c == '_' || c == '.')) {
++ continue;
++ }
++
++ return false;
++ }
++
++ return true;
++ }
++ // Paper end - Username validation
++
+ public static String filterText(String string) {
+ return filterText(string, false);
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/util/TickThrottler.java.patch b/paper-server/patches/unapplied/net/minecraft/util/TickThrottler.java.patch
new file mode 100644
index 0000000000..b829a1224e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/util/TickThrottler.java.patch
@@ -0,0 +1,53 @@
+--- 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
++
+ public class TickThrottler {
+
+ private final int incrementStep;
+ private final int threshold;
+- private int count;
++ private final AtomicInteger count = new AtomicInteger(); // CraftBukkit - multithreaded field
+
+ public TickThrottler(int increment, int threshold) {
+ this.incrementStep = increment;
+@@ -12,17 +16,32 @@
+ }
+
+ public void increment() {
+- this.count += this.incrementStep;
++ this.count.addAndGet(this.incrementStep); // CraftBukkit - use thread-safe field access instead
+ }
+
+ public void tick() {
++ // CraftBukkit start
++ 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;
+ }
++ */
++ // 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);
++ }
++
++ 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/unapplied/net/minecraft/util/ZeroBitStorage.java.patch
new file mode 100644
index 0000000000..a9e7cdc67b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/util/ZeroBitStorage.java.patch
@@ -0,0 +1,32 @@
+--- a/net/minecraft/util/ZeroBitStorage.java
++++ b/net/minecraft/util/ZeroBitStorage.java
+@@ -13,21 +13,21 @@
+ }
+
+ @Override
+- public int getAndSet(int index, int value) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
+- Validate.inclusiveBetween(0L, 0L, (long)value);
++ public final int getAndSet(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, 0L, (long)value); // Paper - Perf: Optimize SimpleBitStorage
+ return 0;
+ }
+
+ @Override
+- public void set(int index, int value) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
+- Validate.inclusiveBetween(0L, 0L, (long)value);
++ public final void set(int index, int value) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, 0L, (long)value); // Paper - Perf: Optimize SimpleBitStorage
+ }
+
+ @Override
+- public int get(int index) {
+- Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index);
++ public final int get(int index) { // Paper - Perf: Optimize SimpleBitStorage
++ //Validate.inclusiveBetween(0L, (long)(this.size - 1), (long)index); // Paper - Perf: Optimize SimpleBitStorage
+ return 0;
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/util/datafix/DataFixers.java.patch b/paper-server/patches/unapplied/net/minecraft/util/datafix/DataFixers.java.patch
new file mode 100644
index 0000000000..86e8fe50b7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/util/datafix/DataFixers.java.patch
@@ -0,0 +1,82 @@
+--- 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
new file mode 100644
index 0000000000..139090af75
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..a56a92ae47
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/thread/BlockableEventLoop.java.patch b/paper-server/patches/unapplied/net/minecraft/util/thread/BlockableEventLoop.java.patch
new file mode 100644
index 0000000000..9692da77b5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/util/thread/BlockableEventLoop.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/util/thread/BlockableEventLoop.java
++++ b/net/minecraft/util/thread/BlockableEventLoop.java
+@@ -82,6 +82,13 @@
+ runnable.run();
+ }
+ }
++ // Paper start
++ public void scheduleOnMain(Runnable runnable) {
++ // postToMainThread does not work the same as older versions of mc
++ // This method is actually used to create a TickTask, which can then be posted onto main
++ this.schedule(this.wrapRunnable(runnable));
++ }
++ // Paper end
+
+ @Override
+ public void schedule(R runnable) {
diff --git a/paper-server/patches/unapplied/net/minecraft/util/worldupdate/WorldUpgrader.java.patch b/paper-server/patches/unapplied/net/minecraft/util/worldupdate/WorldUpgrader.java.patch
new file mode 100644
index 0000000000..de3432baee
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/util/worldupdate/WorldUpgrader.java.patch
@@ -0,0 +1,32 @@
+--- 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/BossEvent.java.patch b/paper-server/patches/unapplied/net/minecraft/world/BossEvent.java.patch
new file mode 100644
index 0000000000..3210ddc320
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/BossEvent.java.patch
@@ -0,0 +1,86 @@
+--- a/net/minecraft/world/BossEvent.java
++++ b/net/minecraft/world/BossEvent.java
+@@ -13,6 +13,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 Component getName() {
++ if (this.adventure != null) return io.papermc.paper.adventure.PaperAdventure.asVanilla(this.adventure.name()); // Paper
+ return this.name;
+ }
+
+ public void setName(Component name) {
++ if (this.adventure != null) this.adventure.name(io.papermc.paper.adventure.PaperAdventure.asAdventure(name)); // Paper
+ this.name = name;
+ }
+
+ public float getProgress() {
++ if (this.adventure != null) return this.adventure.progress(); // Paper
+ return this.progress;
+ }
+
+ public void setProgress(float percent) {
++ if (this.adventure != null) this.adventure.progress(percent); // Paper
+ this.progress = percent;
+ }
+
+ public BossEvent.BossBarColor getColor() {
++ if (this.adventure != null) return io.papermc.paper.adventure.PaperAdventure.asVanilla(this.adventure.color()); // Paper
+ return this.color;
+ }
+
+ public void setColor(BossEvent.BossBarColor color) {
++ if (this.adventure != null) this.adventure.color(io.papermc.paper.adventure.PaperAdventure.asAdventure(color)); // Paper
+ this.color = color;
+ }
+
+ public BossEvent.BossBarOverlay getOverlay() {
++ if (this.adventure != null) return io.papermc.paper.adventure.PaperAdventure.asVanilla(this.adventure.overlay()); // Paper
+ return this.overlay;
+ }
+
+ public void setOverlay(BossEvent.BossBarOverlay style) {
++ if (this.adventure != null) this.adventure.overlay(io.papermc.paper.adventure.PaperAdventure.asAdventure(style)); // Paper
+ this.overlay = style;
+ }
+
+ public boolean shouldDarkenScreen() {
++ if (this.adventure != null) return this.adventure.hasFlag(net.kyori.adventure.bossbar.BossBar.Flag.DARKEN_SCREEN); // Paper
+ return this.darkenScreen;
+ }
+
+ public BossEvent setDarkenScreen(boolean darkenSky) {
++ if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.DARKEN_SCREEN, darkenSky); // Paper
+ this.darkenScreen = darkenSky;
+ return this;
+ }
+
+ public boolean shouldPlayBossMusic() {
++ if (this.adventure != null) return this.adventure.hasFlag(net.kyori.adventure.bossbar.BossBar.Flag.PLAY_BOSS_MUSIC); // Paper
+ return this.playBossMusic;
+ }
+
+ public BossEvent setPlayBossMusic(boolean dragonMusic) {
++ if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.PLAY_BOSS_MUSIC, dragonMusic); // Paper
+ this.playBossMusic = dragonMusic;
+ return this;
+ }
+
+ public BossEvent setCreateWorldFog(boolean thickenFog) {
++ if (this.adventure != null) io.papermc.paper.adventure.PaperAdventure.setFlag(this.adventure, net.kyori.adventure.bossbar.BossBar.Flag.CREATE_WORLD_FOG, thickenFog); // Paper
+ this.createWorldFog = thickenFog;
+ return this;
+ }
+
+ public boolean shouldCreateWorldFog() {
++ if (this.adventure != null) return this.adventure.hasFlag(net.kyori.adventure.bossbar.BossBar.Flag.CREATE_WORLD_FOG); // Paper
+ return this.createWorldFog;
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/CompoundContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/CompoundContainer.java.patch
new file mode 100644
index 0000000000..1a360b99ad
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/CompoundContainer.java.patch
@@ -0,0 +1,74 @@
+--- 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
new file mode 100644
index 0000000000..cc552af92d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/Container.java.patch
@@ -0,0 +1,49 @@
+--- 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();
++
++ [email protected] 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/RandomizableContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/RandomizableContainer.java.patch
new file mode 100644
index 0000000000..a79a33da44
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/RandomizableContainer.java.patch
@@ -0,0 +1,94 @@
+--- a/net/minecraft/world/RandomizableContainer.java
++++ b/net/minecraft/world/RandomizableContainer.java
+@@ -28,7 +28,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);
+ }
+@@ -50,14 +50,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"));
+ } else {
+ this.setLootTableSeed(0L);
+ }
+
+- return true;
++ return this.lootableData() == null; // Paper - only track the loot table if there is chance for replenish
+ } else {
+ return false;
+ }
+@@ -69,26 +70,44 @@
+ return false;
+ } else {
+ nbt.putString("LootTable", resourceKey.location().toString());
++ if (this.lootableData() != null) this.lootableData().saveNbt(nbt); // Paper - LootTable API
+ long l = this.getLootTableSeed();
+ if (l != 0L) {
+ nbt.putLong("LootTableSeed", l);
+ }
+
+- return true;
++ return this.lootableData() == null; // Paper - only track the loot table if there is chance for replenish
+ }
+ }
+
+ default void unpackLootTable(@Nullable Player player) {
++ // Paper start - LootTable API
++ this.unpackLootTable(player, false);
++ }
++ default void unpackLootTable(@Nullable final Player player, final boolean forceClearLootTable) {
++ // Paper end - LootTable API
+ Level level = this.getLevel();
+ BlockPos blockPos = this.getBlockPos();
+ ResourceKey<LootTable> resourceKey = this.getLootTable();
+- if (resourceKey != null && level != null && level.getServer() != null) {
++ // Paper start - LootTable API
++ lootReplenish: if (resourceKey != null && level != null && level.getServer() != null) {
++ if (this.lootableData() != null && !this.lootableData().shouldReplenish(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.CONTAINER, player)) {
++ if (forceClearLootTable) {
++ this.setLootTable(null);
++ }
++ break lootReplenish;
++ }
++ // Paper end - LootTable API
+ LootTable lootTable = level.getServer().reloadableRegistries().getLootTable(resourceKey);
+ if (player instanceof ServerPlayer) {
+ CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, resourceKey);
+ }
+
+- this.setLootTable(null);
++ // Paper start - LootTable API
++ if (forceClearLootTable || this.lootableData() == null || this.lootableData().shouldClearLootTable(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.CONTAINER, player)) {
++ this.setLootTable(null);
++ }
++ // Paper end - LootTable API
+ LootParams.Builder builder = new LootParams.Builder((ServerLevel)level).withParameter(LootContextParams.ORIGIN, Vec3.atCenterOf(blockPos));
+ if (player != null) {
+ builder.withLuck(player.getLuck()).withParameter(LootContextParams.THIS_ENTITY, player);
+@@ -97,4 +116,16 @@
+ lootTable.fill(this, builder.create(LootContextParamSets.CHEST), this.getLootTableSeed());
+ }
+ }
++
++ // Paper start - LootTable API
++ @Nullable @org.jetbrains.annotations.Contract(pure = true)
++ default com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ return null; // some containers don't really have a "replenish" ability like decorated pots
++ }
++
++ default com.destroystokyo.paper.loottable.PaperLootableInventory getLootableInventory() {
++ final org.bukkit.block.Block block = org.bukkit.craftbukkit.block.CraftBlock.at(java.util.Objects.requireNonNull(this.getLevel(), "Cannot manage loot tables on block entities not in world"), this.getBlockPos());
++ return (com.destroystokyo.paper.loottable.PaperLootableInventory) block.getState(false);
++ }
++ // Paper end - LootTable API
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/SimpleContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/SimpleContainer.java.patch
new file mode 100644
index 0000000000..ff3587753c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/SimpleContainer.java.patch
@@ -0,0 +1,103 @@
+--- 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;
+ @Nullable
+ private List<ContainerListener> listeners;
++
++ // CraftBukkit start - add fields and methods
++ public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
++ private int maxStack = MAX_STACK;
++ protected @Nullable org.bukkit.inventory.InventoryHolder bukkitOwner; // Paper - annotation
++
++ 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 i) {
++ this.maxStack = i;
++ }
++
++ public org.bukkit.inventory.InventoryHolder getOwner() {
++ // Paper start - Add missing InventoryHolders
++ if (this.bukkitOwner == null && this.bukkitOwnerCreator != null) {
++ this.bukkitOwner = this.bukkitOwnerCreator.get();
++ }
++ // Paper end - Add missing InventoryHolders
++ return this.bukkitOwner;
++ }
+
++ @Override
++ public 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) {
++ return blockInventoryHolder.getBlock().getLocation();
++ }
++ // When the bukkit owner is a bukkit entity, but does not implement Container itself, e. g. horses
++ if (this.bukkitOwner instanceof org.bukkit.entity.Entity entity) {
++ return entity.getLocation();
++ }
++ // Paper end - Fix inventories returning null Locations
++ return null;
++ }
++
++ public SimpleContainer(SimpleContainer original) {
++ this(original.size);
++ for (int slot = 0; slot < original.size; slot++) {
++ this.items.set(slot, original.items.get(slot).copy());
++ }
++ }
++
+ 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);
++ }
++
+ public SimpleContainer(ItemStack... items) {
+ this.size = items.length;
+ this.items = NonNullList.of(ItemStack.EMPTY, items);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSource.java.patch b/paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSource.java.patch
new file mode 100644
index 0000000000..65bbca9531
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSource.java.patch
@@ -0,0 +1,128 @@
+--- a/net/minecraft/world/damagesource/DamageSource.java
++++ b/net/minecraft/world/damagesource/DamageSource.java
+@@ -21,7 +21,106 @@
+ private final Entity directEntity;
+ @Nullable
+ private final Vec3 damageSourcePosition;
++ // CraftBukkit start
++ @Nullable
++ private org.bukkit.block.Block directBlock; // The block that caused the damage. damageSourcePosition is not used for all block damages
++ @Nullable
++ private org.bukkit.block.BlockState directBlockState; // The block state of the block relevant to this damage source
++ private boolean sweep = false;
++ private boolean melting = false;
++ 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;
++ }
++
++ public boolean isSweep() {
++ return this.sweep;
++ }
++
++ public DamageSource melting() {
++ this.melting = true;
++ return this;
++ }
++
++ public boolean isMelting() {
++ return this.melting;
++ }
++
++ public DamageSource poison() {
++ this.poison = true;
++ return this;
++ }
++
++ public boolean isPoison() {
++ return this.poison;
++ }
++
++ // Paper start - fix DamageSource API
++ @Nullable
++ public Entity getCustomEventDamager() {
++ return (this.customEventDamager != null) ? this.customEventDamager : this.directEntity;
++ }
++
++ public DamageSource customEventDamager(Entity entity) {
++ if (this.directEntity != null) {
++ throw new IllegalStateException("Cannot set custom event damager when direct entity is already set (report a bug to Paper)");
++ }
++ DamageSource damageSource = this.cloneInstance();
++ damageSource.customEventDamager = entity;
++ // Paper end - fix DamageSource API
++ return damageSource;
++ }
++
++ public org.bukkit.block.Block getDirectBlock() {
++ return this.directBlock;
++ }
++
++ public DamageSource directBlock(net.minecraft.world.level.Level world, net.minecraft.core.BlockPos blockPosition) {
++ if (blockPosition == null || world == null) {
++ return this;
++ }
++ return this.directBlock(org.bukkit.craftbukkit.block.CraftBlock.at(world, blockPosition));
++ }
++
++ public DamageSource directBlock(org.bukkit.block.Block block) {
++ if (block == null) {
++ return this;
++ }
++ // Cloning the instance lets us return unique instances of DamageSource without affecting constants defined in DamageSources
++ DamageSource damageSource = this.cloneInstance();
++ damageSource.directBlock = block;
++ return damageSource;
++ }
++
++ public org.bukkit.block.BlockState getDirectBlockState() {
++ return this.directBlockState;
++ }
++
++ public DamageSource directBlockState(org.bukkit.block.BlockState blockState) {
++ if (blockState == null) {
++ return this;
++ }
++ // Cloning the instance lets us return unique instances of DamageSource without affecting constants defined in DamageSources
++ DamageSource damageSource = this.cloneInstance();
++ damageSource.directBlockState = blockState;
++ return damageSource;
++ }
++
++ private DamageSource cloneInstance() {
++ DamageSource damageSource = new DamageSource(this.type, this.directEntity, this.causingEntity, this.damageSourcePosition);
++ damageSource.directBlock = this.getDirectBlock();
++ damageSource.directBlockState = this.getDirectBlockState();
++ damageSource.sweep = this.isSweep();
++ damageSource.poison = this.isPoison();
++ damageSource.melting = this.isMelting();
++ return damageSource;
++ }
++ // CraftBukkit end
++
+ public String toString() {
+ return "DamageSource (" + this.type().msgId() + ")";
+ }
+@@ -163,4 +262,18 @@
+ public Holder<DamageType> typeHolder() {
+ return this.type;
+ }
++
++ // Paper start - add critical damage API
++ private boolean critical;
++ public boolean isCritical() {
++ return this.critical;
++ }
++ public DamageSource critical() {
++ return this.critical(true);
++ }
++ public DamageSource critical(boolean critical) {
++ this.critical = critical;
++ return this;
++ }
++ // Paper end - add critical damage API
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSources.java.patch b/paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSources.java.patch
new file mode 100644
index 0000000000..ef590e6374
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/damagesource/DamageSources.java.patch
@@ -0,0 +1,62 @@
+--- a/net/minecraft/world/damagesource/DamageSources.java
++++ b/net/minecraft/world/damagesource/DamageSources.java
+@@ -43,9 +43,15 @@
+ private final DamageSource stalagmite;
+ private final DamageSource outsideBorder;
+ private final DamageSource genericKill;
++ // CraftBukkit start
++ private final DamageSource melting;
++ private final DamageSource poison;
+
+ public DamageSources(RegistryAccess registryManager) {
+ this.damageTypes = registryManager.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 @@
+
+ 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;
++ }
++
++ 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 @@
+ }
+
+ 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;
+ }
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
new file mode 100644
index 0000000000..d8a62a429d
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..9757b68e1a
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..98497613dc
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..6c259deb9b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/effect/MobEffectUtil.java.patch
@@ -0,0 +1,39 @@
+--- 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
new file mode 100644
index 0000000000..4a2bef9952
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..367358c2d8
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/RegenerationMobEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/effect/RegenerationMobEffect.java.patch
new file mode 100644
index 0000000000..9a24fc0330
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/effect/RegenerationMobEffect.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/effect/RegenerationMobEffect.java
++++ b/net/minecraft/world/effect/RegenerationMobEffect.java
+@@ -12,7 +12,7 @@
+ @Override
+ public boolean applyEffectTick(ServerLevel world, 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
+ }
+
+ 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
new file mode 100644
index 0000000000..5d38f090c8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/effect/SaturationMobEffect.java.patch
@@ -0,0 +1,30 @@
+--- 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/effect/WeavingMobEffect.java.patch b/paper-server/patches/unapplied/net/minecraft/world/effect/WeavingMobEffect.java.patch
new file mode 100644
index 0000000000..8beb585d3f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/effect/WeavingMobEffect.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/effect/WeavingMobEffect.java
++++ b/net/minecraft/world/effect/WeavingMobEffect.java
+@@ -25,11 +25,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
+ }
+ }
+
+- 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
+ Set<BlockPos> set = Sets.newHashSet();
+ int i = this.maxCobwebs.applyAsInt(random);
+
+@@ -46,6 +46,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);
+ }
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
new file mode 100644
index 0000000000..5748ed58a8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/AgeableMob.java.patch
@@ -0,0 +1,74 @@
+--- 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
new file mode 100644
index 0000000000..835489e22c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/AreaEffectCloud.java.patch
@@ -0,0 +1,160 @@
+--- 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/ConversionParams.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ConversionParams.java.patch
new file mode 100644
index 0000000000..3673cd3667
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ConversionParams.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/entity/ConversionParams.java
++++ b/net/minecraft/world/entity/ConversionParams.java
+@@ -12,4 +12,11 @@
+ public interface AfterConversion<T extends Mob> {
+ void finalizeConversion(T convertedEntity);
+ }
++
++ // Paper start - entity zap event - allow conversion to be cancelled during finalization
++ @FunctionalInterface
++ public interface CancellingAfterConversion<T extends Mob> {
++ boolean finalizeConversionOrCancel(final T convertedEntity);
++ }
++ // Paper start - entity zap event - allow conversion to be cancelled during finalization
+ }
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
new file mode 100644
index 0000000000..ed6daf39bf
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ConversionType.java.patch
@@ -0,0 +1,46 @@
+--- 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
new file mode 100644
index 0000000000..893b7a911c
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..a274089f56
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/Entity.java.patch
@@ -0,0 +1,1994 @@
+--- 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
new file mode 100644
index 0000000000..e79a8f240b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/EntitySelector.java.patch
@@ -0,0 +1,49 @@
+--- 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
new file mode 100644
index 0000000000..1ff8d4806a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/EntityType.java.patch
@@ -0,0 +1,179 @@
+--- 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/ExperienceOrb.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ExperienceOrb.java.patch
new file mode 100644
index 0000000000..2e0228c9d3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ExperienceOrb.java.patch
@@ -0,0 +1,267 @@
+--- 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;
+ 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;
++import org.bukkit.event.entity.EntityTargetLivingEntityEvent;
++import org.bukkit.event.entity.EntityTargetEvent;
++import org.bukkit.event.player.PlayerExpCooldownChangeEvent;
++// CraftBukkit end
+
+ public class ExperienceOrb extends Entity {
+
+@@ -37,9 +44,63 @@
+ public int value;
+ public int count;
+ 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;
++ }
++ CompoundTag comp = tag.getCompound("Paper.ExpData");
++ if (comp.hasUUID("source")) {
++ this.sourceEntityId = comp.getUUID("source");
++ }
++ if (comp.hasUUID("trigger")) {
++ this.triggerEntityId = comp.getUUID("trigger");
++ }
++ if (comp.contains("reason")) {
++ String reason = comp.getString("reason");
++ try {
++ this.spawnReason = org.bukkit.entity.ExperienceOrb.SpawnReason.valueOf(reason);
++ } catch (Exception e) {
++ this.level().getCraftServer().getLogger().warning("Invalid spawnReason set for experience orb: " + e.getMessage() + " - " + reason);
++ }
++ }
++ }
++ private void savePaperNBT(CompoundTag tag) {
++ CompoundTag comp = new CompoundTag();
++ if (this.sourceEntityId != null) {
++ comp.putUUID("source", this.sourceEntityId);
++ }
++ if (this.triggerEntityId != null) {
++ comp.putUUID("trigger", triggerEntityId);
++ }
++ if (this.spawnReason != null && this.spawnReason != org.bukkit.entity.ExperienceOrb.SpawnReason.UNKNOWN) {
++ comp.putString("reason", this.spawnReason.name());
++ }
++ tag.put("Paper.ExpData", comp);
++ }
++
++ @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 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 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);
++ 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 @@
+ @Override
+ public void tick() {
+ super.tick();
++ Player prevTarget = this.followingPlayer;// CraftBukkit - store old target
+ this.xo = this.getX();
+ this.yo = this.getY();
+ this.zo = this.getZ();
+@@ -93,7 +155,22 @@
+ this.followingPlayer = null;
+ }
+
+- if (this.followingPlayer != null) {
++ // CraftBukkit start
++ boolean cancelled = false;
++ if (this.followingPlayer != prevTarget) {
++ EntityTargetLivingEntityEvent event = CraftEventFactory.callEntityTargetLivingEvent(this, this.followingPlayer, (this.followingPlayer != null) ? EntityTargetEvent.TargetReason.CLOSEST_PLAYER : EntityTargetEvent.TargetReason.FORGOT_TARGET);
++ LivingEntity target = (event.getTarget() == null) ? null : ((org.bukkit.craftbukkit.entity.CraftLivingEntity) event.getTarget()).getHandle();
++ cancelled = event.isCancelled();
++
++ if (cancelled) {
++ this.followingPlayer = prevTarget;
++ } else {
++ this.followingPlayer = (target instanceof Player) ? (Player) target : null;
++ }
++ }
++
++ 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();
+
+@@ -121,7 +198,7 @@
+
+ ++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) {
++ // Paper start - add reasons for orbs
++ award(world, 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 world, 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
+ }
+ }
+
+ }
+
+ private static boolean tryMergeToExisting(ServerLevel world, 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 @@
+ }
+
+ private void merge(ExperienceOrb other) {
++ // 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()) {
++ 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
+ }
+
+ private void setUnderwaterMovement() {
+@@ -215,7 +306,7 @@
+ this.markHurt();
+ this.health = (int) ((float) 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
+ }
+
+ @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
+ }
+
+ @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);
+
+ if (i > 0) {
+- player.giveExperiencePoints(i);
++ player.giveExperiencePoints(CraftEventFactory.callPlayerExpChangeEvent(player, this).getAmount()); // CraftBukkit - this.value -> event.getAmount() // Paper - supply experience orb object
+ }
+
+ --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());
++ // 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);
++ // Paper end - mending event
++ k = event.getRepairAmount();
++ if (event.isCancelled()) {
++ return amount;
++ }
++ // 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) {
++ // 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);
+ }
+ }
+@@ -291,6 +395,24 @@
+ }
+
+ public static int getExperienceValue(int value) {
++ // 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;
++ // 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)))))))));
+ }
+
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
new file mode 100644
index 0000000000..4ee0117128
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/Interaction.java.patch
@@ -0,0 +1,42 @@
+--- 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/ItemBasedSteering.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ItemBasedSteering.java.patch
new file mode 100644
index 0000000000..2f4eab4bc8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ItemBasedSteering.java.patch
@@ -0,0 +1,17 @@
+--- 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);
+ }
+
++ // CraftBukkit add setBoostTicks(int)
++ public void setBoostTicks(int ticks) {
++ this.boosting = true;
++ this.boostTime = 0;
++ this.entityData.set(this.boostTimeAccessor, ticks);
++ }
++ // CraftBukkit end
++
+ public void addAdditionalSaveData(CompoundTag nbt) {
+ nbt.putBoolean("Saddle", this.hasSaddle());
+ }
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
new file mode 100644
index 0000000000..aa36549d47
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/Leashable.java.patch
@@ -0,0 +1,157 @@
+--- 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
new file mode 100644
index 0000000000..1dc2b6bf90
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/LightningBolt.java.patch
@@ -0,0 +1,160 @@
+--- 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/LivingEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/LivingEntity.java.patch
new file mode 100644
index 0000000000..c1e602ad26
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/LivingEntity.java.patch
@@ -0,0 +1,2034 @@
+--- a/net/minecraft/world/entity/LivingEntity.java
++++ b/net/minecraft/world/entity/LivingEntity.java
+@@ -42,6 +42,8 @@
+ import net.minecraft.core.particles.ParticleOptions;
+ import net.minecraft.core.particles.ParticleTypes;
+ import net.minecraft.nbt.CompoundTag;
++import net.minecraft.nbt.FloatTag;
++import net.minecraft.nbt.IntTag;
+ 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;
+ import net.minecraft.world.scores.Scoreboard;
+ import org.slf4j.Logger;
++
++// CraftBukkit start
++import java.util.ArrayList;
++import java.util.HashSet;
++import java.util.Set;
++import java.util.LinkedList;
++import java.util.UUID;
++import net.minecraft.world.item.component.Consumable;
++import org.bukkit.Location;
++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;
++import org.bukkit.event.entity.EntityKnockbackEvent;
++import org.bukkit.event.entity.EntityPotionEffectEvent;
++import org.bukkit.event.entity.EntityRegainHealthEvent;
++import org.bukkit.event.entity.EntityRemoveEvent;
++import org.bukkit.event.entity.EntityResurrectEvent;
++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;
++ // CraftBukkit start
++ public int expToDrop;
++ public ArrayList<DefaultDrop> drops = new ArrayList<>(); // Paper - Restore vanilla drops behavior
++ public final org.bukkit.craftbukkit.attribute.CraftAttributeMap craftAttributes;
++ public boolean collides = true;
++ public Set<UUID> collidableExemptions = new HashSet<>();
++ public boolean bukkitPickUpLoot;
++ public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper
++ 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));
+- 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());
+ this.blocksBuilding = true;
+ this.rotA = (float) ((Math.random() + 1.0D) * 0.009999999776482582D);
+ 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
+ }
+ }
+ }
+@@ -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;
+ if (this.deathTime >= 20 && !this.level().isClientSide() && !this.isRemoved()) {
+ 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 @@
+ }
+
+ public boolean shouldDiscardFriction() {
+- return this.discardFriction;
++ return !this.frictionState.toBooleanOrElse(!this.discardFriction); // Paper - Friction API
+ }
+
+ public void setDiscardFriction(boolean noDrag) {
+@@ -679,17 +735,23 @@
+ }
+
+ 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);
++ // CraftBukkit start
++ this.onEquipItem(slot, oldStack, newStack, 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) {
++ // 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 @@
+
+ @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) {
++ // 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);
+ }
+ }
+
+- super.remove(reason);
++ super.remove(entity_removalreason, cause); // CraftBukkit
+ this.brain.clearMemories();
+ }
+
+@@ -722,11 +791,17 @@
+ mobeffect.onMobRemoved(world, this, reason);
+ }
+
++ this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.DEATH); // CraftBukkit
+ this.activeEffects.clear();
+ }
+
+ @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.putFloat("Health", this.getHealth());
+ nbt.putShort("HurtTime", (short) this.hurtTime);
+ nbt.putInt("HurtByTimestamp", this.lastHurtByMobTimestamp);
+@@ -763,7 +838,23 @@
+
+ @Override
+ public void readAdditionalSaveData(CompoundTag nbt) {
+- this.internalSetAbsorptionAmount(nbt.getFloat("AbsorptionAmount"));
++ // Paper start - Check for NaN
++ float absorptionAmount = nbt.getFloat("AbsorptionAmount");
++ if (Float.isNaN(absorptionAmount)) {
++ absorptionAmount = 0;
++ }
++ this.internalSetAbsorptionAmount(absorptionAmount);
++ // Paper end - Check for NaN
++ // 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) {
++ LOGGER.error("Unknown friction state " + fs + " for " + this);
++ }
++ }
++ // Paper end - Friction API
+ if (nbt.contains("attributes", 9) && this.level() != null && !this.level().isClientSide) {
+ this.getAttributes().load(nbt.getList("attributes", 10));
+ }
+@@ -781,6 +872,17 @@
+ }
+ }
+
++ // 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());
++ }
++ }
++ // CraftBukkit end
++
+ if (nbt.contains("Health", 99)) {
+ this.setHealth(nbt.getFloat("Health"));
+ }
+@@ -792,6 +894,7 @@
+ String s = nbt.getString("Team");
+ Scoreboard scoreboard = this.level().getScoreboard();
+ PlayerTeam scoreboardteam = scoreboard.getPlayerTeam(s);
++ if (!this.level().paperConfig().scoreboards.allowNonPlayerEntitiesOnScoreboards && !(this instanceof net.minecraft.world.entity.player.Player)) { scoreboardteam = null; } // Paper - Perf: Disable Scoreboards for non players by default
+ boolean flag = scoreboardteam != null && scoreboard.addPlayerToTeam(this.getStringUUID(), scoreboardteam);
+
+ if (!flag) {
+@@ -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"));
+
++ 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 (!this.firstTick) {
+ this.setPosToBed(blockposition);
+ }
++ } // 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 @@
+
+ }
+
++ // CraftBukkit start
++ private boolean isTickingEffects = false;
++ private List<ProcessableEffect> effectsToProcess = Lists.newArrayList();
++
++ private static class ProcessableEffect {
++
++ private Holder<MobEffect> type;
++ private MobEffectInstance effect;
++ private final EntityPotionEffectEvent.Cause cause;
++
++ private ProcessableEffect(MobEffectInstance effect, EntityPotionEffectEvent.Cause cause) {
++ this.effect = effect;
++ this.cause = cause;
++ }
++
++ private ProcessableEffect(Holder<MobEffect> type, EntityPotionEffectEvent.Cause cause) {
++ this.type = type;
++ this.cause = cause;
++ }
++ }
++ // CraftBukkit end
++
+ protected void tickEffects() {
+ Iterator<Holder<MobEffect>> iterator = this.activeEffects.keySet().iterator();
+
++ this.isTickingEffects = true; // CraftBukkit
+ try {
+ while (iterator.hasNext()) {
+ Holder<MobEffect> holder = (Holder) iterator.next();
+@@ -831,6 +959,12 @@
+ this.onEffectUpdated(mobeffect, true, (Entity) null);
+ })) {
+ if (!this.level().isClientSide) {
++ // CraftBukkit start
++ EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, mobeffect, null, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.EXPIRATION);
++ if (event.isCancelled()) {
++ continue;
++ }
++ // CraftBukkit end
+ iterator.remove();
+ this.onEffectsRemoved(List.of(mobeffect));
+ }
+@@ -841,6 +975,17 @@
+ } catch (ConcurrentModificationException concurrentmodificationexception) {
+ ;
+ }
++ // CraftBukkit start
++ this.isTickingEffects = false;
++ for (ProcessableEffect e : this.effectsToProcess) {
++ if (e.effect != null) {
++ this.addEffect(e.effect, e.cause);
++ } else {
++ this.removeEffect(e.type, e.cause);
++ }
++ }
++ this.effectsToProcess.clear();
++ // CraftBukkit end
+
+ if (this.effectsDirty) {
+ if (!this.level().isClientSide) {
+@@ -921,7 +1066,7 @@
+ }
+
+ 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() {
++ return this.removeAllEffects(org.bukkit.event.entity.EntityPotionEffectEvent.Cause.UNKNOWN);
++ }
++
++ public boolean removeAllEffects(EntityPotionEffectEvent.Cause cause) {
++ // CraftBukkit end
+ if (this.level().isClientSide) {
+ return false;
+ } else if (this.activeEffects.isEmpty()) {
+ 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;
++ while (iterator.hasNext()) {
++ MobEffectInstance effect = iterator.next();
++ EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, effect, null, cause, EntityPotionEffectEvent.Action.CLEARED);
++ if (event.isCancelled()) {
++ continue;
++ }
++
++ iterator.remove();
++ toRemove.add(effect);
++ }
++
++ this.onEffectsRemoved(toRemove);
++ return !toRemove.isEmpty();
++ // CraftBukkit end
+ }
+ }
+
+@@ -987,24 +1151,63 @@
+ return this.addEffect(effect, (Entity) null);
+ }
+
++ // CraftBukkit start
++ public boolean addEffect(MobEffectInstance mobeffect, EntityPotionEffectEvent.Cause cause) {
++ return this.addEffect(mobeffect, (Entity) null, cause);
++ }
++
+ 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) {
++ // Paper start - Don't fire sync event during generation
++ return this.addEffect(mobeffect, entity, cause, true);
++ }
++ public boolean addEffect(MobEffectInstance mobeffect, @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));
++ return true;
++ }
++ // CraftBukkit end
++
++ if (!this.canBeAffected(mobeffect)) {
+ return false;
+ } else {
+- MobEffectInstance mobeffect1 = (MobEffectInstance) this.activeEffects.get(effect.getEffect());
++ MobEffectInstance mobeffect1 = (MobEffectInstance) this.activeEffects.get(mobeffect.getEffect());
+ boolean flag = false;
+
++ // CraftBukkit start
++ boolean override = false;
++ if (mobeffect1 != null) {
++ override = new MobEffectInstance(mobeffect1).update(mobeffect);
++ }
++
++ if (fireEvent) { // Paper - Don't fire sync event during generation
++ EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, mobeffect1, mobeffect, 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);
+ flag = true;
+- effect.onEffectAdded(this);
+- } else if (mobeffect1.update(effect)) {
+- this.onEffectUpdated(mobeffect1, true, source);
++ mobeffect.onEffectAdded(this);
++ // CraftBukkit start
++ } else if (override) { // Paper - Don't fire sync event during generation
++ mobeffect1.update(mobeffect);
++ this.onEffectUpdated(mobeffect1, true, entity);
++ // CraftBukkit end
+ flag = true;
+ }
+
+- 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.removeEffectNoUpdate(effect, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.UNKNOWN);
+ }
+
++ @Nullable
++ public MobEffectInstance removeEffectNoUpdate(Holder<MobEffect> holder, EntityPotionEffectEvent.Cause cause) {
++ if (this.isTickingEffects) {
++ this.effectsToProcess.add(new ProcessableEffect(holder, cause));
++ return null;
++ }
++
++ MobEffectInstance effect = this.activeEffects.get(holder);
++ if (effect == null) {
++ return null;
++ }
++
++ EntityPotionEffectEvent event = CraftEventFactory.callEntityPotionEffectChangeEvent(this, effect, 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 @@
+
+ }
+
++ // 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 void heal(float f, EntityRegainHealthEvent.RegainReason regainReason) {
++ // Paper start - Forward
++ heal(f, regainReason, false);
++ }
++
++ public void heal(float f, EntityRegainHealthEvent.RegainReason regainReason, boolean isFastRegen) {
++ // Paper end
+ float f1 = this.getHealth();
+
+ if (f1 > 0.0F) {
+- this.setHealth(f1 + amount);
++ EntityRegainHealthEvent event = new EntityRegainHealthEvent(this.getBukkitEntity(), f, regainReason, isFastRegen); // Paper
++ // Suppress during worldgen
++ if (this.valid) {
++ this.level().getCraftServer().getPluginManager().callEvent(event);
++ }
++
++ if (!event.isCancelled()) {
++ this.setHealth((float) (this.getHealth() + event.getAmount()));
++ }
++ // CraftBukkit end
+ }
+
+ }
+
+ public float getHealth() {
++ // CraftBukkit start - Use unscaled health
++ if (this instanceof ServerPlayer) {
++ return (float) ((ServerPlayer) this).getBukkitEntity().getHealth();
++ }
++ // CraftBukkit end
+ return (Float) this.entityData.get(LivingEntity.DATA_HEALTH_ID);
+ }
+
+ public void setHealth(float health) {
++ // Paper start - Check for NaN
++ if (Float.isNaN(health)) { health = getMaxHealth(); if (this.valid) {
++ System.err.println("[NAN-HEALTH] " + getScoreboardName() + " had NaN health set");
++ } } // Paper end - Check for NaN
++ // CraftBukkit start - Handle scaled health
++ if (this instanceof ServerPlayer) {
++ org.bukkit.craftbukkit.entity.CraftPlayer player = ((ServerPlayer) this).getBukkitEntity();
++ // Squeeze
++ if (health < 0.0F) {
++ player.setRealHealth(0.0D);
++ } else if (health > player.getMaxHealth()) {
++ player.setRealHealth(player.getMaxHealth());
++ } else {
++ player.setRealHealth(health);
++ }
++
++ player.updateScaledHealth(false);
++ return;
++ }
++ // CraftBukkit end
+ this.entityData.set(LivingEntity.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)) {
+ 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)) {
+ return false;
+@@ -1181,11 +1455,12 @@
+ amount = 0.0F;
+ }
+
+- float f1 = 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)) {
++ // 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)) {
+ this.hurtCurrentlyUsedShield(amount);
+ f2 = amount;
+ amount = 0.0F;
+@@ -1202,15 +1477,21 @@
+ flag = true;
+ }
+
+- if (source.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)) {
+ amount *= 5.0F;
+ }
+
+- if (source.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);
+ amount *= 0.75F;
+ }
+
++ // CraftBukkit start
++ EntityDamageEvent event; // Paper - move this into the actual invuln check....
++ // CraftBukkit end
++
+ 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 (amount <= this.lastHurt) {
+ return false;
+ }
+
+- this.actuallyHurt(world, source, 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
++ 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
++ 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.
++ // CraftBukkit end
+ this.lastHurt = amount;
+ 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)
++ 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)) {
++ 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.invulnerableTime = this.invulnerableDuration; // CraftBukkit - restore use of maxNoDamageTicks
++ // this.actuallyHurt(worldserver, damagesource, f);
++ // CraftBukkit end
+ this.hurtDuration = 10;
+ this.hurtTime = this.hurtDuration;
+ }
+@@ -1243,7 +1544,7 @@
+ world.broadcastDamageEvent(this, source);
+ }
+
+- 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
+ this.markHurt();
+ }
+
+@@ -1263,7 +1564,7 @@
+ d1 = source.getSourcePosition().z() - this.getZ();
+ }
+
+- 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
+ if (!flag) {
+ this.indicateDamage(d0, d1);
+ }
+@@ -1272,17 +1573,18 @@
+
+ if (this.isDeadOrDying()) {
+ if (!this.checkTotemDeathProtection(source)) {
+- if (flag1) {
+- this.makeSound(this.getDeathSound());
+- }
++ // Paper start - moved into CraftEventFactory event caller for cancellable death event
++ this.silentDeath = !flag1; // mark entity as dying silently
++ // Paper end
+
+ this.die(source);
++ this.silentDeath = false; // Paper - cancellable death event - reset to default
+ }
+ } else if (flag1) {
+ this.playHurtSound(source);
+ }
+
+- 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 @@
+ }
+ }
+
++ // Paper start - only call damage event when actuallyHurt will be called - move out amount computation logic
++ private float computeAmountFromEntityDamageEvent(final EntityDamageEvent event) {
++ // Taken from hurt()'s craftbukkit diff.
++ float amount = 0;
++ amount += (float) event.getDamage(DamageModifier.BASE);
++ amount += (float) event.getDamage(DamageModifier.BLOCKING);
++ amount += (float) event.getDamage(DamageModifier.FREEZING);
++ amount += (float) event.getDamage(DamageModifier.HARD_HAT);
++ return amount;
++ }
++ // Paper end - only call damage event when actuallyHurt will be called - move out amount computation logic
++
+ protected void blockUsingShield(LivingEntity attacker) {
+ 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
+ }
+
+ private boolean checkTotemDeathProtection(DamageSource source) {
+@@ -1375,20 +1689,39 @@
+ InteractionHand[] aenumhand = InteractionHand.values();
+ int i = aenumhand.length;
+
++ // 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
+ break;
+ }
+ }
+
+- if (itemstack != null) {
+- if (this instanceof 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);
++ 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);
++ }
++ // 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;
++ }
++ // Paper end - fix NPE when pre-cancelled EntityResurrectEvent is uncancelled
++ if (itemstack != null && this instanceof ServerPlayer) {
++ // CraftBukkit end
+ ServerPlayer entityplayer = (ServerPlayer) this;
+
+ entityplayer.awardStat(Stats.ITEM_USED.get(itemstack.getItem()));
+@@ -1468,6 +1801,7 @@
+ Entity entity = damageSource.getEntity();
+ LivingEntity entityliving = this.getKillCredit();
+
++ /* // Paper - move down to make death event cancellable - this is the awardKillScore below
+ if (entityliving != null) {
+ entityliving.awardKillScore(this, damageSource);
+ }
+@@ -1477,26 +1811,61 @@
+ }
+
+ if (!this.level().isClientSide && this.hasCustomName()) {
+- LivingEntity.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
+
+ this.dead = true;
+- this.getCombatTracker().recheckStatus();
++ // Paper - moved into if below
+ Level world = this.level();
+
+ if (world instanceof ServerLevel) {
+ ServerLevel worldserver = (ServerLevel) world;
++ // 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);
++ 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);
++ //}
++ // Paper start - clear equipment if event is not cancelled
++ if (this instanceof Mob) {
++ for (EquipmentSlot slot : this.clearedEquipmentSlots) {
++ this.setItemSlot(slot, ItemStack.EMPTY);
++ }
++ this.clearedEquipmentSlots.clear();
++ }
++ // Paper end
++
++ if (this.isSleeping()) {
++ this.stopSleeping();
++ }
++
++ if (!this.level().isClientSide && this.hasCustomName()) {
++ if (org.spigotmc.SpigotConfig.logNamedDeaths) LivingEntity.LOGGER.info("Named entity {} died: {}", this, this.getCombatTracker().getDeathMessage().getString()); // Spigot
++ }
++
++ this.getCombatTracker().recheckStatus();
++ if (entity != null) {
++ entity.killedEntity((ServerLevel) this.level(), this);
++ }
+ this.gameEvent(GameEvent.ENTITY_DIE);
+- this.dropAllDeathLoot(worldserver, damageSource);
++ } else {
++ this.dead = false;
++ this.setHealth((float) deathEvent.getReviveHealth());
++ }
++ // Paper end
+ this.createWitherRose(entityliving);
+ }
+
++ // Paper start
++ if (this.dead) { // Paper
+ this.level().broadcastEntityEvent(this, (byte) 3);
+- }
+
+ this.setPose(Pose.DYING);
++ }
++ // Paper end
+ }
+ }
+
+@@ -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
+ }
+ }
+
+ if (!flag) {
+ ItemEntity entityitem = 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());
++ CraftEventFactory.callEvent(event);
++ if (event.isCancelled()) {
++ return;
++ }
++ // CraftBukkit end
+ this.level().addFreshEntity(entityitem);
+ }
+ }
+@@ -1527,27 +1904,60 @@
+ }
+ }
+
+- protected void dropAllDeathLoot(ServerLevel world, 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) {
++ // 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);
++ // Paper start
++ final boolean prev = this.clearEquipmentSlots;
++ this.clearEquipmentSlots = false;
++ this.clearedEquipmentSlots.clear();
++ // Paper end
+ this.dropCustomDeathLoot(world, damageSource, flag);
++ this.clearEquipmentSlots = prev; // Paper
+ }
+-
+- this.dropEquipment(world);
++ // 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();
++ if (entityliving != null) {
++ entityliving.awardKillScore(this, damageSource);
++ }
++ }); // Paper end
++ this.postDeathDropItems(deathEvent); // Paper
++ this.drops = new ArrayList<>();
++ // CraftBukkit end
++
++ // this.dropEquipment(worldserver);// CraftBukkit - moved up
+ this.dropExperience(world, damageSource.getEntity());
++ return deathEvent; // Paper
+ }
+
+ protected void dropEquipment(ServerLevel world) {}
++ 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 }
+ }
+
++ return 0; // CraftBukkit
+ }
+
++ protected void dropExperience(ServerLevel world, @Nullable Entity attacker) {
++ // 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
++ this.expToDrop = 0;
++ }
++ // CraftBukkit end
++ }
++
+ protected void dropCustomDeathLoot(ServerLevel world, DamageSource source, boolean causedByPlayer) {}
+
+ public long getLootTableSeed() {
+@@ -1612,19 +2022,35 @@
+ }
+
+ 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;
+
+- 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;
+ }
+
+- 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);
++ // 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);
++ // 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
++ // CraftBukkit end
+ }
+ }
+
+@@ -1683,6 +2109,20 @@
+ return new LivingEntity.Fallsounds(SoundEvents.GENERIC_SMALL_FALL, SoundEvents.GENERIC_BIG_FALL);
+ }
+
++ // CraftBukkit start - Add delegate methods
++ public SoundEvent getHurtSound0(DamageSource damagesource) {
++ return this.getHurtSound(damagesource);
++ }
++
++ public SoundEvent getDeathSound0() {
++ return this.getDeathSound();
++ }
++
++ public SoundEvent getFallDamageSound0(int fallHeight) {
++ return this.getFallDamageSound(fallHeight);
++ }
++ // CraftBukkit end
++
+ public Optional<BlockPos> getLastClimbablePos() {
+ return this.lastClimbablePos;
+ }
+@@ -1718,7 +2158,7 @@
+
+ @Override
+ public boolean isAlive() {
+- return !this.isRemoved() && this.getHealth() > 0.0F;
++ 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);
+
+ if (i > 0) {
++ // CraftBukkit start
++ if (!this.hurtServer((ServerLevel) this.level(), damageSource, (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
+ return true;
+ } else {
+ return flag;
+@@ -1830,7 +2275,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;
+ } else {
+- if (this.hasEffect(MobEffects.DAMAGE_RESISTANCE) && !source.is(DamageTypeTags.BYPASSES_RESISTANCE)) {
++ // CraftBukkit - Moved to handleEntityDamage(DamageSource, float)
++ if (false && this.hasEffect(MobEffects.DAMAGE_RESISTANCE) && !source.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 @@
+ }
+ }
+
+- 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;
++ // CraftBukkit start
++ private EntityDamageEvent handleEntityDamage(final DamageSource damagesource, float f, final float invulnerabilityRelatedLastDamage) { // Paper - fix invulnerability reduction in EntityDamageEvent
++ float originalDamage = f;
++ // 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
++ // last damage existed, this means the reduction *technically* is (new damage - last damage).
++ // If the event damage was changed to something less than invul damage, hard lock it at 0.
++ if (d < invulnerabilityRelatedLastDamage) return 0D;
++ return (double) -invulnerabilityRelatedLastDamage;
++ };
++ final float originalInvulnerabilityReduction = invulnerabilityReductionEquation.apply((double) f).floatValue();
++ f += 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) {
++ if (damagesource.is(DamageTypeTags.IS_FREEZING) && LivingEntity.this.getType().is(EntityTypeTags.FREEZE_HURTS_EXTRA_TYPES)) {
++ return -(f - (f * 5.0F));
++ }
++ return -0.0;
++ }
++ };
++ float freezingModifier = freezing.apply((double) f).floatValue();
++ f += freezingModifier;
+
++ com.google.common.base.Function<Double, Double> hardHat = new com.google.common.base.Function<Double, Double>() {
++ @Override
++ public Double apply(Double f) {
++ if (damagesource.is(DamageTypeTags.DAMAGES_HELMET) && !LivingEntity.this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) {
++ return -(f - (f * 0.75F));
++ }
++ return -0.0;
++ }
++ };
++ float hardHatModifier = hardHat.apply((double) f).floatValue();
++ f += hardHatModifier;
++
++ com.google.common.base.Function<Double, Double> blocking = new com.google.common.base.Function<Double, Double>() {
++ @Override
++ public Double apply(Double f) {
++ return -((LivingEntity.this.isDamageSourceBlocked(damagesource)) ? f : 0.0);
++ }
++ };
++ float blockingModifier = blocking.apply((double) f).floatValue();
++ f += blockingModifier;
++
++ com.google.common.base.Function<Double, Double> armor = new com.google.common.base.Function<Double, Double>() {
++ @Override
++ public Double apply(Double f) {
++ return -(f - LivingEntity.this.getDamageAfterArmorAbsorb(damagesource, f.floatValue()));
++ }
++ };
++ float armorModifier = armor.apply((double) f).floatValue();
++ f += armorModifier;
++
++ com.google.common.base.Function<Double, Double> resistance = new com.google.common.base.Function<Double, Double>() {
++ @Override
++ public Double apply(Double f) {
++ if (!damagesource.is(DamageTypeTags.BYPASSES_EFFECTS) && LivingEntity.this.hasEffect(MobEffects.DAMAGE_RESISTANCE) && !damagesource.is(DamageTypeTags.BYPASSES_RESISTANCE)) {
++ int i = (LivingEntity.this.getEffect(MobEffects.DAMAGE_RESISTANCE).getAmplifier() + 1) * 5;
++ int j = 25 - i;
++ float f1 = f.floatValue() * (float) j;
++
++ return -(f - Math.max(f1 / 25.0F, 0.0F));
++ }
++ return -0.0;
++ }
++ };
++ float resistanceModifier = resistance.apply((double) f).floatValue();
++ f += resistanceModifier;
++
++ com.google.common.base.Function<Double, Double> magic = new com.google.common.base.Function<Double, Double>() {
++ @Override
++ public Double apply(Double f) {
++ return -(f - LivingEntity.this.getDamageAfterMagicAbsorb(damagesource, f.floatValue()));
++ }
++ };
++ float magicModifier = magic.apply((double) f).floatValue();
++ f += magicModifier;
++
++ com.google.common.base.Function<Double, Double> absorption = new com.google.common.base.Function<Double, Double>() {
++ @Override
++ public Double apply(Double f) {
++ return -(Math.max(f - Math.max(f - LivingEntity.this.getAbsorptionAmount(), 0.0F), 0.0F));
++ }
++ };
++ float absorptionModifier = absorption.apply((double) f).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) -> {
++ damageModifierFunctionMap.put(DamageModifier.INVULNERABILITY_REDUCTION, invulnerabilityReductionEquation);
++ damageModifierDoubleMap.put(DamageModifier.INVULNERABILITY_REDUCTION, (double) originalInvulnerabilityReduction);
++ });
++ // 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)) {
++ if (event.isCancelled()) {
++ return false;
++ }
++
++ 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 (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();
++ }
++ // Paper end - PlayerAttackEntityCooldownResetEvent
++ }
++
++ // Resistance
++ if (event.getDamage(DamageModifier.RESISTANCE) < 0) {
++ float f3 = (float) -event.getDamage(DamageModifier.RESISTANCE);
++ 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));
++ }
++ }
++ }
++
++ // Apply damage to helmet
++ if (damagesource.is(DamageTypeTags.DAMAGES_HELMET) && !this.getItemBySlot(EquipmentSlot.HEAD).isEmpty()) {
++ this.hurtHelmet(damagesource, f);
++ }
++
++ // Apply damage to 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);
++ }
++
++ // Apply blocking code // PAIL: steal from above
++ if (event.getDamage(DamageModifier.BLOCKING) < 0) {
++ 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
++ this.blockUsingShield((LivingEntity) entity);
++ }
++ }
++
++ boolean human = this instanceof net.minecraft.world.entity.player.Player;
++ float originalDamage = (float) event.getDamage();
++ float absorptionModifier = (float) -event.getDamage(DamageModifier.ABSORPTION);
++ this.setAbsorptionAmount(Math.max(this.getAbsorptionAmount() - absorptionModifier, 0.0F));
++ float f2 = 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));
++ }
++ // 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 (amount != 0.0F) {
+- this.getCombatTracker().recordDamage(source, amount);
+- this.setHealth(this.getHealth() - amount);
+- this.setAbsorptionAmount(this.getAbsorptionAmount() - amount);
++ // CraftBukkit start
++ if (f > 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));
++ }
++ }
++ // CraftBukkit end
++ this.getCombatTracker().recordDamage(damagesource, f);
++ this.setHealth(this.getHealth() - f);
++ // CraftBukkit start
++ if (!human) {
++ this.setAbsorptionAmount(this.getAbsorptionAmount() - f);
++ }
+ 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) {
++ ((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
++ }
++
++ 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".
++ } else {
++ return true; // Paper - return false ONLY if event was cancelled
++ }
++ // CraftBukkit end
+ }
+ }
++ return true; // CraftBukkit // Paper - return false ONLY if event was cancelled
+ }
+
+ public CombatTracker getCombatTracker() {
+@@ -1935,8 +2568,18 @@
+ }
+
+ public final void setArrowCount(int stuckArrowCount) {
+- this.entityData.set(LivingEntity.DATA_ARROW_COUNT_ID, stuckArrowCount);
++ // CraftBukkit start
++ this.setArrowCount(stuckArrowCount, false);
++ }
++
++ public final void setArrowCount(int i, boolean flag) {
++ ArrowBodyCountChangeEvent event = CraftEventFactory.callArrowBodyCountChangeEvent(this, this.getArrowCount(), i, flag);
++ if (event.isCancelled()) {
++ return;
++ }
++ this.entityData.set(LivingEntity.DATA_ARROW_COUNT_ID, event.getNewAmount());
+ }
++ // 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 @@
+
+ @Override
+ protected void onBelowWorld() {
+- this.hurt(this.damageSources().fellOutOfWorld(), 4.0F);
++ this.hurt(this.damageSources().fellOutOfWorld(), this.level().getWorld().getVoidDamageAmount()); // Paper - use configured void damage amount
+ }
+
+ protected void updateSwingTime() {
+@@ -2182,6 +2825,12 @@
+
+ public abstract ItemStack getItemBySlot(EquipmentSlot slot);
+
++ // CraftBukkit start
++ public void setItemSlot(EquipmentSlot enumitemslot, ItemStack itemstack, boolean silent) {
++ this.setItemSlot(enumitemslot, itemstack);
++ }
++ // CraftBukkit end
++
+ 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;
+ }
+
++ 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();
++ // Paper start - Prevent excessive velocity through repeated crits
++ long time = System.nanoTime();
++ boolean canCrit = true;
++ if (this instanceof net.minecraft.world.entity.player.Player) {
++ canCrit = false;
++ if (time - this.lastJumpTime > (long)(0.250e9)) {
++ this.lastJumpTime = time;
++ canCrit = true;
++ }
++ }
++ // Paper end - Prevent excessive velocity through repeated crits
+
+ this.setDeltaMovement(vec3d.x, Math.max((double) f, vec3d.y), vec3d.z);
+ if (this.isSprinting()) {
+ float f1 = this.getYRot() * 0.017453292F;
+-
++ 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));
+ }
+
+@@ -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 @@
+ }
+
+ protected float getFlyingSpeed() {
+- return this.getControllingPassenger() instanceof Player ? this.getSpeed() * 0.1F : 0.02F;
++ return this.getControllingPassenger() instanceof net.minecraft.world.entity.player.Player ? this.getSpeed() * 0.1F : 0.02F;
+ }
+
+ public float getSpeed() {
+@@ -2634,7 +3295,7 @@
+ }
+ }
+
+- this.detectEquipmentUpdates();
++ this.detectEquipmentUpdatesPublic(); // CraftBukkit
+ if (this.tickCount % 20 == 0) {
+ this.getCombatTracker().recheckStatus();
+ }
+@@ -2687,38 +3348,16 @@
+ gameprofilerfiller.pop();
+ gameprofilerfiller.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;
+- }
+-
+- while (this.getXRot() - this.xRotO < -180.0F) {
+- this.xRotO -= 360.0F;
+- }
+-
+- 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;
+- }
+-
+- while (this.yHeadRot - this.yHeadRotO >= 180.0F) {
+- this.yHeadRotO += 360.0F;
+- }
+-
+ gameprofilerfiller.pop();
+ this.animStep += f2;
+ if (this.isFallFlying()) {
+@@ -2741,7 +3380,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)) {
++ // 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();
++ }
++ // Paper end - PlayerArmorChangeEvent
+ if (map == null) {
+ map = Maps.newEnumMap(EquipmentSlot.class);
+ }
+@@ -2864,7 +3510,7 @@
+ }
+
+ });
+- ((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 @@
+ this.calculateEntityAnimation(this instanceof FlyingAnimal);
+ gameprofilerfiller.pop();
+ gameprofilerfiller.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();
+
+ if (this.isInPowderSnow && this.canFreeze()) {
+@@ -3046,6 +3694,20 @@
+
+ this.pushEntities();
+ gameprofilerfiller.pop();
++ // Paper start - Add EntityMoveEvent
++ if (((ServerLevel) this.level()).hasEntityMoveEvent && !(this instanceof net.minecraft.world.entity.player.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());
++ io.papermc.paper.event.entity.EntityMoveEvent event = new io.papermc.paper.event.entity.EntityMoveEvent(this.getBukkitLivingEntity(), from, to.clone());
++ if (!event.callEvent()) {
++ this.absMoveTo(from.getX(), from.getY(), from.getZ(), from.getYaw(), from.getPitch());
++ } else if (!to.equals(event.getTo())) {
++ this.absMoveTo(event.getTo().getX(), event.getTo().getY(), event.getTo().getZ(), event.getTo().getYaw(), event.getTo().getPitch());
++ }
++ }
++ }
++ // Paper end - Add EntityMoveEvent
+ world = this.level();
+ if (world instanceof ServerLevel worldserver) {
+ if (this.isSensitiveToWater() && this.isInWaterRainOrBubble()) {
+@@ -3063,6 +3725,7 @@
+ this.checkSlowFallDistance();
+ if (!this.level().isClientSide) {
+ if (!this.canGlide()) {
++ if (this.getSharedFlag(7) != false && !CraftEventFactory.callToggleGlideEvent(this, false).isCancelled()) // CraftBukkit
+ 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);
+ } else {
+- List list = this.level().getEntities((Entity) 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) {
++ 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);
++ // Paper - don't run getEntities if we're not going to use its result; moved up
+
+ if (i > 0 && list.size() > i - 1 && this.random.nextInt(4) == 0) {
+ int j = 0;
+@@ -3138,10 +3815,12 @@
+ }
+
+ 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
+ this.doPush(entity1);
+ }
+ }
+@@ -3190,10 +3869,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();
+
+- super.stopRiding();
+- if (entity != null && entity != 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);
+ }
+
+@@ -3258,7 +3943,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
+ }
+
+ }
+@@ -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;
++ // 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
+ }
+ }
+
+@@ -3305,13 +3991,27 @@
+
+ @Override
+ public boolean isPickable() {
+- return !this.isRemoved();
++ return !this.isRemoved() && this.collides; // CraftBukkit
+ }
+
++ // Paper start - Climbing should not bypass cramming gamerule
+ @Override
+ public boolean isPushable() {
+- return this.isAlive() && !this.isSpectator() && !this.onClimbable();
++ return this.isCollidable(this.level().paperConfig().collisions.fixClimbingBypassingCrammingRule);
++ }
++
++ @Override
++ public boolean isCollidable(boolean ignoreClimbing) {
++ return this.isAlive() && !this.isSpectator() && (ignoreClimbing || !this.onClimbable()) && this.collides; // CraftBukkit
++ // Paper end - Climbing should not bypass cramming gamerule
++ }
++
++ // CraftBukkit start - collidable API
++ @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 @@
+ }
+
+ public final void setAbsorptionAmount(float absorptionAmount) {
+- this.internalSetAbsorptionAmount(Mth.clamp(absorptionAmount, 0.0F, this.getMaxAbsorption()));
++ this.internalSetAbsorptionAmount(!Float.isNaN(absorptionAmount) ? Mth.clamp(absorptionAmount, 0.0F, this.getMaxAbsorption()) : 0.0F); // Paper - Check for NaN
+ }
+
+ protected void internalSetAbsorptionAmount(float absorptionAmount) {
+@@ -3367,6 +4067,11 @@
+ return ((Byte) this.entityData.get(LivingEntity.DATA_LIVING_ENTITY_FLAGS) & 2) > 0 ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
+ }
+
++ // Paper start - Properly cancel usable items
++ public void resyncUsingItem(ServerPlayer serverPlayer) {
++ this.resendPossiblyDesyncedDataValues(java.util.List.of(DATA_LIVING_ENTITY_FLAGS), serverPlayer);
++ }
++ // Paper end - Properly cancel usable items
+ private void updatingUsingItem() {
+ if (this.isUsingItem()) {
+ if (ItemStack.isSameItem(this.getItemInHand(this.getUsedItemHand()), this.useItem)) {
+@@ -3410,9 +4115,14 @@
+ }
+
+ public void startUsingItem(InteractionHand hand) {
++ // Paper start - Prevent consuming the wrong itemstack
++ this.startUsingItem(hand, false);
++ }
++ 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);
+ if (!this.level().isClientSide) {
+@@ -3483,13 +4193,50 @@
+ this.releaseUsingItem();
+ } else {
+ if (!this.useItem.isEmpty() && this.isUsingItem()) {
+- 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;
++ PlayerItemConsumeEvent event = null; // Paper
++ if (this instanceof ServerPlayer entityPlayer) {
++ org.bukkit.inventory.ItemStack craftItem = CraftItemStack.asBukkitCopy(this.useItem);
++ org.bukkit.inventory.EquipmentSlot hand = org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(enumhand);
++ event = new PlayerItemConsumeEvent((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);
++ }
++ entityPlayer.getBukkitEntity().updateInventory();
++ entityPlayer.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);
++ } else {
++ itemstack = this.useItem.finishUsingItem(this.level(), this);
++ }
++ // Paper start - save the default replacement item and change it if necessary
++ final ItemStack defaultReplacement = itemstack;
++ if (event != null && event.getReplacement() != null) {
++ itemstack = CraftItemStack.asNMSCopy(event.getReplacement());
++ }
++ // Paper end
++ // CraftBukkit end
++
+ if (itemstack != this.useItem) {
+ this.setItemInHand(enumhand, itemstack);
+ }
+
+ this.stopUsingItem();
++ // Paper start
++ if (this instanceof ServerPlayer) {
++ ((ServerPlayer) this).getBukkitEntity().updateInventory();
++ }
++ // Paper end
+ }
+
+ }
+@@ -3512,6 +4259,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
+ 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();
+
+- 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
+ } else {
+ return null;
+ }
++ }
++
++ // Paper start - Make shield blocking delay configurable
++ public HitResult getRayTrace(int maxDistance, ClipContext.Fluid fluidCollisionOption) {
++ if (maxDistance < 1 || maxDistance > 120) {
++ throw new IllegalArgumentException("maxDistance must be between 1-120");
++ }
++
++ Vec3 start = new Vec3(getX(), getY() + getEyeHeight(), getZ());
++ org.bukkit.util.Vector dir = getBukkitEntity().getLocation().getDirection().multiply(maxDistance);
++ Vec3 end = new Vec3(start.x + dir.getX(), start.y + dir.getY(), start.z + dir.getZ());
++ ClipContext raytrace = new ClipContext(start, end, ClipContext.Block.OUTLINE, fluidCollisionOption, this);
++
++ return this.level().clip(raytrace);
++ }
++
++ public @Nullable net.minecraft.world.phys.EntityHitResult getTargetEntity(int maxDistance) {
++ if (maxDistance < 1 || maxDistance > 120) {
++ throw new IllegalArgumentException("maxDistance must be between 1-120");
++ }
++
++ Vec3 start = this.getEyePosition(1.0F);
++ Vec3 direction = this.getLookAngle();
++ Vec3 end = start.add(direction.x * maxDistance, direction.y * maxDistance, direction.z * maxDistance);
++
++ List<Entity> entityList = this.level().getEntities(this, getBoundingBox().expandTowards(direction.x * maxDistance, direction.y * maxDistance, direction.z * maxDistance).inflate(1.0D, 1.0D, 1.0D), EntitySelector.NO_SPECTATORS.and(Entity::isPickable));
++
++ double distance = 0.0D;
++ net.minecraft.world.phys.EntityHitResult result = null;
++
++ for (Entity entity : entityList) {
++ final double inflationAmount = (double) entity.getPickRadius();
++ AABB aabb = entity.getBoundingBox().inflate(inflationAmount, inflationAmount, inflationAmount);
++ Optional<Vec3> rayTraceResult = aabb.clip(start, end);
++
++ if (rayTraceResult.isPresent()) {
++ Vec3 rayTrace = rayTraceResult.get();
++ double distanceTo = start.distanceToSqr(rayTrace);
++ if (distanceTo < distance || distance == 0.0D) {
++ result = new net.minecraft.world.phys.EntityHitResult(entity, rayTrace);
++ distance = distanceTo;
++ }
++ }
++ }
++
++ return result;
++ }
++
++ public int shieldBlockingDelay = this.level().paperConfig().misc.shieldBlockingDelay;
++
++ 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 @@
+ }
+
+ public boolean randomTeleport(double x, double y, double z, boolean particleEffects) {
++ // CraftBukkit start
++ return this.randomTeleport(x, y, z, particleEffects, 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) {
++ // 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 @@
+ }
+
+ if (flag2) {
+- this.teleportTo(x, d6, 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;
+ }
++ // now revert and call event if the teleport place is valid
++ this.setPos(d3, d4, d5);
++
++ if (flag1) {
++ 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));
++ this.level().getCraftServer().getPluginManager().callEvent(teleport);
++ if (!teleport.isCancelled() && teleport.getTo() != null) { // Paper
++ Location to = teleport.getTo();
++ this.teleportTo(to.getX(), to.getY(), to.getZ());
++ } else {
++ return Optional.empty();
++ }
++ } else {
++ // player teleport event is called in the underlining code
++ if (!((ServerPlayer) this).connection.teleport(d0, d6, d2, this.getYRot(), this.getXRot(), cause)) {
++ return Optional.empty();
++ }
++ }
++ }
++ // CraftBukkit end
+ }
+ }
+
+ if (!flag1) {
+- this.teleportTo(d3, d4, d5);
+- return false;
++ // this.enderTeleportTo(d3, d4, d5); // 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();
+ }
+
+- return true;
++ return Optional.of(true); // CraftBukkit
+ }
+ }
+
+@@ -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/unapplied/net/minecraft/world/entity/Mob.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/Mob.java.patch
new file mode 100644
index 0000000000..aeaafc3a1b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/Mob.java.patch
@@ -0,0 +1,472 @@
+--- 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
new file mode 100644
index 0000000000..2088822118
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/NeutralMob.java.patch
@@ -0,0 +1,86 @@
+--- 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/OminousItemSpawner.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/OminousItemSpawner.java.patch
new file mode 100644
index 0000000000..c390374ad2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/OminousItemSpawner.java.patch
@@ -0,0 +1,29 @@
+--- a/net/minecraft/world/entity/OminousItemSpawner.java
++++ b/net/minecraft/world/entity/OminousItemSpawner.java
+@@ -76,7 +76,7 @@
+ entity = this.spawnProjectile(serverLevel, projectileItem, itemStack);
+ } else {
+ entity = new ItemEntity(serverLevel, this.getX(), this.getY(), this.getZ(), itemStack);
+- 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));
+ 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,
+ stack,
+@@ -99,7 +99,7 @@
+ (double)direction.getStepZ(),
+ dispenseConfig.power(),
+ dispenseConfig.uncertainty()
+- );
++ ).spawn(org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.OMINOUS_ITEM_SPAWNER); // Paper - fixes and addition to spawn reason API
+ projectile.setOwner(this);
+ return projectile;
+ }
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
new file mode 100644
index 0000000000..24a880fc47
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/PathfinderMob.java.patch
@@ -0,0 +1,12 @@
+--- 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/Shearable.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/Shearable.java.patch
new file mode 100644
index 0000000000..aa779fc65d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/Shearable.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/world/entity/Shearable.java
++++ b/net/minecraft/world/entity/Shearable.java
+@@ -5,7 +5,15 @@
+ 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);
+
+ 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
++ default java.util.List<net.minecraft.world.item.ItemStack> generateDefaultDrops(final ServerLevel serverLevel, final ItemStack shears) {
++ return java.util.Collections.emptyList();
++ }
++ // Paper end - custom shear drops
+ }
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
new file mode 100644
index 0000000000..ae8be7d5d0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/TamableAnimal.java.patch
@@ -0,0 +1,85 @@
+--- 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
new file mode 100644
index 0000000000..d6d3313220
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/AttributeMap.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/AttributeMap.java.patch
new file mode 100644
index 0000000000..08b12ec883
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/AttributeMap.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/world/entity/ai/attributes/AttributeMap.java
++++ b/net/minecraft/world/entity/ai/attributes/AttributeMap.java
+@@ -162,4 +162,12 @@
+ }
+ }
+ }
++
++ // Paper - start - living entity allow attribute registration
++ public void registerAttribute(Holder<Attribute> attributeBase) {
++ AttributeInstance attributeModifiable = new AttributeInstance(attributeBase, AttributeInstance::getAttribute);
++ attributes.put(attributeBase, attributeModifiable);
++ }
++ // Paper - end - living entity allow attribute registration
++
+ }
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
new file mode 100644
index 0000000000..51565ac321
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/attributes/Attributes.java.patch
@@ -0,0 +1,31 @@
+--- 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
new file mode 100644
index 0000000000..64bab57963
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..a3aec14777
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/AssignProfessionFromJobSite.java.patch
@@ -0,0 +1,31 @@
+--- 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
new file mode 100644
index 0000000000..fe0de69d00
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/BabyFollowAdult.java.patch
@@ -0,0 +1,37 @@
+--- 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/Behavior.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/Behavior.java.patch
new file mode 100644
index 0000000000..dab10a1e20
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/Behavior.java.patch
@@ -0,0 +1,40 @@
+--- a/net/minecraft/world/entity/ai/behavior/Behavior.java
++++ b/net/minecraft/world/entity/ai/behavior/Behavior.java
+@@ -14,6 +14,9 @@
+ 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
+
+ public Behavior(Map<MemoryModuleType<?>, MemoryStatus> requiredMemoryState) {
+ this(requiredMemoryState, 60);
+@@ -27,6 +30,14 @@
+ this.minDuration = minRunTime;
+ this.maxDuration = maxRunTime;
+ this.entryCondition = requiredMemoryState;
++ // 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('.');
++ if (lastSeparator != -1) {
++ key = key.substring(lastSeparator + 1);
++ }
++ this.configKey = key.toLowerCase(java.util.Locale.ROOT);
++ // Paper end - configurable behavior tick rate and timings
+ }
+
+ @Override
+@@ -36,6 +47,12 @@
+
+ @Override
+ public final boolean tryStart(ServerLevel world, E entity, long time) {
++ // 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) {
++ return false;
++ }
++ // Paper end - configurable behavior tick rate and timings
+ if (this.hasRequiredMemories(entity) && this.checkExtraStartConditions(world, entity)) {
+ this.status = Behavior.Status.RUNNING;
+ int i = this.minDuration + world.getRandom().nextInt(this.maxDuration + 1 - this.minDuration);
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
new file mode 100644
index 0000000000..35afe8f0a4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java.patch
@@ -0,0 +1,64 @@
+--- 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/GateBehavior.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/GateBehavior.java.patch
new file mode 100644
index 0000000000..fdb4490626
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/GateBehavior.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ai/behavior/GateBehavior.java
++++ b/net/minecraft/world/entity/ai/behavior/GateBehavior.java
+@@ -18,7 +18,7 @@
+ private final Set<MemoryModuleType<?>> exitErasedMemories;
+ private final GateBehavior.OrderPolicy orderPolicy;
+ private final GateBehavior.RunningPolicy runningPolicy;
+- private final ShufflingList<BehaviorControl<? super E>> behaviors = new ShufflingList<>();
++ private final ShufflingList<BehaviorControl<? super E>> behaviors = new ShufflingList<>(false); // Paper - Fix Concurrency issue in ShufflingList during worldgen
+ private Behavior.Status status = Behavior.Status.STOPPED;
+
+ public GateBehavior(
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
new file mode 100644
index 0000000000..d9692605cb
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..066bfd1863
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/HarvestFarmland.java.patch
@@ -0,0 +1,63 @@
+--- 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
new file mode 100644
index 0000000000..7348471be5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/InteractWithDoor.java.patch
@@ -0,0 +1,39 @@
+--- 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
new file mode 100644
index 0000000000..18aef70f25
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/PrepareRamNearestTarget.java.patch
@@ -0,0 +1,73 @@
+--- 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
new file mode 100644
index 0000000000..3c23484d4f
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..f5dc7ba366
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/ResetProfession.java.patch
@@ -0,0 +1,31 @@
+--- 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/ShufflingList.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/ShufflingList.java.patch
new file mode 100644
index 0000000000..3f1d97092e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/ShufflingList.java.patch
@@ -0,0 +1,44 @@
+--- a/net/minecraft/world/entity/ai/behavior/ShufflingList.java
++++ b/net/minecraft/world/entity/ai/behavior/ShufflingList.java
+@@ -16,12 +16,25 @@
+ public class ShufflingList<U> implements Iterable<U> {
+ protected final List<ShufflingList.WeightedEntry<U>> entries;
+ private final RandomSource random = RandomSource.create();
++ private final boolean isUnsafe; // Paper - Fix Concurrency issue in ShufflingList during worldgen
+
+ public ShufflingList() {
++ // Paper start - Fix Concurrency issue in ShufflingList during worldgen
++ this(true);
++ }
++ public ShufflingList(boolean isUnsafe) {
++ this.isUnsafe = isUnsafe;
++ // Paper end - Fix Concurrency issue in ShufflingList during worldgen
+ this.entries = Lists.newArrayList();
+ }
+
+ private ShufflingList(List<ShufflingList.WeightedEntry<U>> list) {
++ // Paper start - Fix Concurrency issue in ShufflingList during worldgen
++ this(list, true);
++ }
++ private ShufflingList(List<ShufflingList.WeightedEntry<U>> list, boolean isUnsafe) {
++ this.isUnsafe = isUnsafe;
++ // Paper end - Fix Concurrency issue in ShufflingList during worldgen
+ this.entries = Lists.newArrayList(list);
+ }
+
+@@ -35,9 +48,12 @@
+ }
+
+ public ShufflingList<U> shuffle() {
+- this.entries.forEach(entry -> entry.setRandom(this.random.nextFloat()));
+- this.entries.sort(Comparator.comparingDouble(ShufflingList.WeightedEntry::getRandWeight));
+- return this;
++ // Paper start - Fix Concurrency issue in ShufflingList during worldgen
++ List<ShufflingList.WeightedEntry<U>> list = this.isUnsafe ? Lists.newArrayList(this.entries) : this.entries;
++ list.forEach(entry -> entry.setRandom(this.random.nextFloat()));
++ list.sort(Comparator.comparingDouble(ShufflingList.WeightedEntry::getRandWeight));
++ return this.isUnsafe ? new ShufflingList<>(list, this.isUnsafe) : this;
++ // Paper end - Fix Concurrency issue in ShufflingList during worldgen
+ }
+
+ public Stream<U> stream() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/SleepInBed.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/SleepInBed.java.patch
new file mode 100644
index 0000000000..d905f5c1d7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/SleepInBed.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/entity/ai/behavior/SleepInBed.java
++++ b/net/minecraft/world/entity/ai/behavior/SleepInBed.java
+@@ -42,7 +42,8 @@
+ }
+ }
+
+- BlockState blockState = world.getBlockState(globalPos.pos());
++ BlockState blockState = world.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);
+ }
+ }
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
new file mode 100644
index 0000000000..0a02e4eb2c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/StartAttacking.java.patch
@@ -0,0 +1,36 @@
+--- 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
new file mode 100644
index 0000000000..c156a35921
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/StopAttackingIfTargetInvalid.java.patch
@@ -0,0 +1,46 @@
+--- 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
new file mode 100644
index 0000000000..e0fa77caef
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/VillagerGoalPackages.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java.patch
new file mode 100644
index 0000000000..c8260c2305
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java
++++ b/net/minecraft/world/entity/ai/behavior/VillagerGoalPackages.java
+@@ -42,7 +42,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(
+ 6,
+ AcquirePoi.create(
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
new file mode 100644
index 0000000000..9875c853b9
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/VillagerMakeLove.java.patch
@@ -0,0 +1,42 @@
+--- 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/WorkAtComposter.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/WorkAtComposter.java.patch
new file mode 100644
index 0000000000..3d8d9ce4bd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/WorkAtComposter.java.patch
@@ -0,0 +1,12 @@
+--- 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));
+ if (!itemStack.isEmpty()) {
++ villager.forceDrops = true; // Paper - Add missing forceDrop toggles
+ villager.spawnAtLocation(world, itemStack, 0.5F);
++ villager.forceDrops = false; // Paper - Add missing forceDrop toggles
+ }
+ }
+ }
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
new file mode 100644
index 0000000000..6f3b4a357c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/behavior/warden/Digging.java.patch
@@ -0,0 +1,22 @@
+--- 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
new file mode 100644
index 0000000000..4fbd06f256
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/BreakDoorGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java.patch
new file mode 100644
index 0000000000..8323bfa7ee
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java
++++ b/net/minecraft/world/entity/ai/goal/BreakDoorGoal.java
+@@ -72,9 +72,16 @@
+ }
+
+ if (this.breakTime == this.getDoorBreakTime() && this.isValidDifficulty(this.mob.level().getDifficulty())) {
++ // CraftBukkit start
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityBreakDoorEvent(this.mob, this.doorPos, this.mob.level().getFluidState(this.doorPos).createLegacyBlock()).isCancelled()) { // Paper - fix wrong block state
++ this.start();
++ return;
++ }
++ // CraftBukkit end
++ final net.minecraft.world.level.block.state.BlockState oldState = this.mob.level().getBlockState(this.doorPos); // Paper - fix MC-263999
+ this.mob.level().removeBlock(this.doorPos, false);
+ this.mob.level().levelEvent(1021, this.doorPos, 0);
+- 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/unapplied/net/minecraft/world/entity/ai/goal/EatBlockGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/EatBlockGoal.java.patch
new file mode 100644
index 0000000000..72de1e0703
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/EatBlockGoal.java.patch
@@ -0,0 +1,46 @@
+--- 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/FloatGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/FloatGoal.java.patch
new file mode 100644
index 0000000000..ed90ccea87
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/FloatGoal.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/entity/ai/goal/FloatGoal.java
++++ b/net/minecraft/world/entity/ai/goal/FloatGoal.java
+@@ -9,6 +9,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
+ 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/unapplied/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch
new file mode 100644
index 0000000000..4cdf53ef91
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java
++++ b/net/minecraft/world/entity/ai/goal/FollowOwnerGoal.java
+@@ -72,7 +72,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
+ }
+
+ if (--this.timeToRecalcPath <= 0) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/Goal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/Goal.java.patch
new file mode 100644
index 0000000000..a354e63b19
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/Goal.java.patch
@@ -0,0 +1,39 @@
+--- a/net/minecraft/world/entity/ai/goal/Goal.java
++++ b/net/minecraft/world/entity/ai/goal/Goal.java
+@@ -46,6 +46,16 @@
+ return this.flags;
+ }
+
++ // Paper start - Mob Goal API
++ public boolean hasFlag(final Goal.Flag flag) {
++ return this.flags.contains(flag);
++ }
++
++ public void addFlag(final Goal.Flag flag) {
++ this.flags.add(flag);
++ }
++ // Paper end - Mob Goal API
++
+ protected int adjustedTickDelay(int ticks) {
+ return this.requiresUpdateEveryTick() ? ticks : reducedTickDelay(ticks);
+ }
+@@ -62,7 +72,19 @@
+ return (ServerLevel)world;
+ }
+
++ // 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) {
++ this.vanillaGoal = new com.destroystokyo.paper.entity.ai.PaperVanillaGoal<>(this);
++ }
++ //noinspection unchecked
++ return (com.destroystokyo.paper.entity.ai.Goal<T>) this.vanillaGoal;
++ }
++ // Paper end - Mob goal api
++
+ public static enum Flag {
++ UNKNOWN_BEHAVIOR, // Paper - add UNKNOWN_BEHAVIOR
+ MOVE,
+ LOOK,
+ JUMP,
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
new file mode 100644
index 0000000000..2111952674
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/RemoveBlockGoal.java.patch
@@ -0,0 +1,55 @@
+--- 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
new file mode 100644
index 0000000000..4f41b508ea
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/RunAroundLikeCrazyGoal.java.patch
@@ -0,0 +1,22 @@
+--- 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/SitWhenOrderedToGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java.patch
new file mode 100644
index 0000000000..64830b4a5b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java
++++ b/net/minecraft/world/entity/ai/goal/SitWhenOrderedToGoal.java
+@@ -22,7 +22,7 @@
+ @Override
+ public boolean canUse() {
+ if (!this.mob.isTame()) {
+- return false;
++ return this.mob.isOrderedToSit() && this.mob.getTarget() == null; // CraftBukkit - Allow sitting for wild animals
+ } else if (this.mob.isInWaterOrBubble()) {
+ return false;
+ } else if (!this.mob.onGround()) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SwellGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SwellGoal.java.patch
new file mode 100644
index 0000000000..885e0fd98c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/SwellGoal.java.patch
@@ -0,0 +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;
+ }
+
++ // Paper start - Fix MC-179072
+ @Override
++ public boolean canContinueToUse() {
++ return !net.minecraft.world.entity.EntitySelector.NO_CREATIVE_OR_SPECTATOR.test(this.creeper.getTarget()) && canUse();
++ }
++ // Paper end
++
++ @Override
+ public void start() {
+ this.creeper.getNavigation().stop();
+ this.target = this.creeper.getTarget();
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
new file mode 100644
index 0000000000..8d33e61ef6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/TemptGoal.java.patch
@@ -0,0 +1,44 @@
+--- 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/DefendVillageTargetGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java.patch
new file mode 100644
index 0000000000..3a2b688e7b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java
++++ b/net/minecraft/world/entity/ai/goal/target/DefendVillageTargetGoal.java
+@@ -61,7 +61,7 @@
+
+ @Override
+ public void start() {
+- this.golem.setTarget(this.potentialTarget);
++ this.golem.setTarget(this.potentialTarget, org.bukkit.event.entity.EntityTargetEvent.TargetReason.DEFEND_VILLAGE, true); // CraftBukkit - reason
+ super.start();
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java.patch
new file mode 100644
index 0000000000..3b4092a6f3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java
++++ b/net/minecraft/world/entity/ai/goal/target/HurtByTargetGoal.java
+@@ -67,7 +67,7 @@
+
+ @Override
+ public void start() {
+- this.mob.setTarget(this.mob.getLastHurtByMob());
++ this.mob.setTarget(this.mob.getLastHurtByMob(), org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_ATTACKED_ENTITY, true); // CraftBukkit - reason
+ this.targetMob = this.mob.getTarget();
+ this.timestamp = this.mob.getLastHurtByMobTimestamp();
+ this.unseenMemoryTicks = 300;
+@@ -114,6 +114,6 @@
+ }
+
+ protected void alertOther(Mob mob, LivingEntity target) {
+- mob.setTarget(target);
++ mob.setTarget(target, org.bukkit.event.entity.EntityTargetEvent.TargetReason.TARGET_ATTACKED_NEARBY_ENTITY, true); // CraftBukkit - reason
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java.patch
new file mode 100644
index 0000000000..5d304f4ac8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java
++++ b/net/minecraft/world/entity/ai/goal/target/NearestAttackableTargetGoal.java
+@@ -70,7 +70,7 @@
+
+ @Override
+ public void start() {
+- this.mob.setTarget(this.target);
++ this.mob.setTarget(this.target, this.target instanceof ServerPlayer ? org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_PLAYER : org.bukkit.event.entity.EntityTargetEvent.TargetReason.CLOSEST_ENTITY, true); // CraftBukkit - reason
+ super.start();
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java.patch
new file mode 100644
index 0000000000..edaee8e182
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtByTargetGoal.java.patch
@@ -0,0 +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 @@
+
+ @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) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java.patch
new file mode 100644
index 0000000000..696e8ea9fa
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/OwnerHurtTargetGoal.java.patch
@@ -0,0 +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 @@
+
+ @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) {
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
new file mode 100644
index 0000000000..ab51c6b369
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/goal/target/TargetGoal.java.patch
@@ -0,0 +1,30 @@
+--- 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/gossip/GossipContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/gossip/GossipContainer.java.patch
new file mode 100644
index 0000000000..55d0a4951d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/gossip/GossipContainer.java.patch
@@ -0,0 +1,46 @@
+--- a/net/minecraft/world/entity/ai/gossip/GossipContainer.java
++++ b/net/minecraft/world/entity/ai/gossip/GossipContainer.java
+@@ -216,6 +216,43 @@
+ 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()) {
++ map.put(toApi(type.getKey()), type.getIntValue());
++ }
++
++ return new com.destroystokyo.paper.entity.villager.Reputation(map);
++ }
++
++ public void assignFromPaperReputation(com.destroystokyo.paper.entity.villager.Reputation rep) {
++ for (GossipType type : TYPES) {
++ com.destroystokyo.paper.entity.villager.ReputationType api = toApi(type);
++
++ if (rep.hasReputationSet(api)) {
++ int reputation = rep.getReputation(api);
++ if (reputation == 0) {
++ this.entries.removeInt(type);
++ } else {
++ this.entries.put(type, reputation);
++ }
++ }
++ }
++ }
++
++ private static com.destroystokyo.paper.entity.villager.ReputationType toApi(GossipType type) {
++ return switch (type) {
++ case MAJOR_NEGATIVE -> com.destroystokyo.paper.entity.villager.ReputationType.MAJOR_NEGATIVE;
++ case MINOR_NEGATIVE -> com.destroystokyo.paper.entity.villager.ReputationType.MINOR_NEGATIVE;
++ case MINOR_POSITIVE -> com.destroystokyo.paper.entity.villager.ReputationType.MINOR_POSITIVE;
++ case MAJOR_POSITIVE -> com.destroystokyo.paper.entity.villager.ReputationType.MAJOR_POSITIVE;
++ case TRADING -> com.destroystokyo.paper.entity.villager.ReputationType.TRADING;
++ };
++ }
++ // Paper end - Add villager reputation API
+ }
+
+ static 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/unapplied/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java.patch
new file mode 100644
index 0000000000..ac1686a6c3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java
++++ b/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java
+@@ -39,7 +39,7 @@
+
+ @Override
+ public Path createPath(Entity entity, int distance) {
+- return this.createPath(entity.blockPosition(), distance);
++ return this.createPath(entity.blockPosition(), entity, distance); // Paper - EntityPathfindEvent
+ }
+
+ @Override
diff --git a/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
new file mode 100644
index 0000000000..36cc381dc3
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/PathNavigation.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch
new file mode 100644
index 0000000000..124ed48519
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/navigation/PathNavigation.java.patch
@@ -0,0 +1,104 @@
+--- a/net/minecraft/world/entity/ai/navigation/PathNavigation.java
++++ b/net/minecraft/world/entity/ai/navigation/PathNavigation.java
+@@ -125,8 +125,14 @@
+
+ @Nullable
+ public Path createPath(BlockPos target, int distance) {
+- return this.createPath(ImmutableSet.of(target), 8, false, distance);
++ // Paper start - EntityPathfindEvent
++ return this.createPath(target, null, distance);
+ }
++ @Nullable
++ public Path createPath(BlockPos target, @Nullable Entity entity, int distance) {
++ return this.createPath(ImmutableSet.of(target), entity, 8, false, distance);
++ // Paper end - EntityPathfindEvent
++ }
+
+ @Nullable
+ 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
+@@ -145,6 +151,17 @@
+
+ @Nullable
+ protected Path createPath(Set<BlockPos> positions, int range, boolean useHeadPos, int distance, float followRange) {
++ // Paper start - EntityPathfindEvent
++ return this.createPath(positions, null, range, useHeadPos, distance, followRange);
++ }
++
++ @Nullable
++ protected Path createPath(Set<BlockPos> positions, @Nullable Entity target, int range, boolean useHeadPos, int distance) {
++ return this.createPath(positions, target, range, useHeadPos, distance, (float) this.mob.getAttributeValue(Attributes.FOLLOW_RANGE));
++ }
++
++ @Nullable protected Path createPath(Set<BlockPos> positions, @Nullable Entity target, int range, boolean useHeadPos, int distance, float followRange) {
++ // Paper end - EntityPathfindEvent
+ if (positions.isEmpty()) {
+ return null;
+ } else if (this.mob.getY() < (double)this.level.getMinY()) {
+@@ -154,6 +171,23 @@
+ } else if (this.path != null && !this.path.isDone() && positions.contains(this.targetPos)) {
+ return this.path;
+ } else {
++ // Paper start - EntityPathfindEvent
++ boolean copiedSet = false;
++ for (BlockPos possibleTarget : positions) {
++ if (!this.mob.getCommandSenderWorld().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);
++ }
++ // note: since we copy the set this remove call is safe, since we're iterating over the old copy
++ positions.remove(possibleTarget);
++ if (positions.isEmpty()) {
++ return null;
++ }
++ }
++ }
++ // Paper end - EntityPathfindEvent
+ ProfilerFiller profilerFiller = Profiler.get();
+ profilerFiller.push("pathfind");
+ BlockPos blockPos = useHeadPos ? this.mob.blockPosition().above() : this.mob.blockPosition();
+@@ -175,13 +209,33 @@
+ return this.moveTo(this.createPath(x, y, z, 1), speed);
+ }
+
++ // Paper start - Perf: Optimise pathfinding
++ private int lastFailure = 0;
++ 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(Entity entity, double speed) {
++ // Paper start - Perf: Optimise pathfinding
++ if (this.pathfindFailures > 10 && this.path == null && net.minecraft.server.MinecraftServer.currentTick < this.lastFailure + 40) {
++ return false;
++ }
++ // Paper end - Perf: Optimise pathfinding
+ Path path = this.createPath(entity, 1);
+- return path != null && this.moveTo(path, speed);
++ // Paper start - Perf: Optimise pathfinding
++ if (path != null && this.moveTo(path, speed)) {
++ this.lastFailure = 0;
++ this.pathfindFailures = 0;
++ return true;
++ } else {
++ this.pathfindFailures++;
++ this.lastFailure = net.minecraft.server.MinecraftServer.currentTick;
++ return false;
++ }
++ // Paper end - Perf: Optimise pathfinding
+ }
+
+ public boolean moveTo(@Nullable Path path, double speed) {
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
new file mode 100644
index 0000000000..b2662d7401
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/Sensor.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/sensing/Sensor.java.patch
new file mode 100644
index 0000000000..7d220911a0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/sensing/Sensor.java.patch
@@ -0,0 +1,34 @@
+--- a/net/minecraft/world/entity/ai/sensing/Sensor.java
++++ b/net/minecraft/world/entity/ai/sensing/Sensor.java
+@@ -29,8 +29,19 @@
+ .ignoreInvisibilityTesting();
+ private final int scanRate;
+ private long timeToTick;
++ // Paper start - configurable sensor tick rate and timings
++ private final String configKey;
++ // Paper end
+
+ public Sensor(int senseInterval) {
++ // 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('.');
++ if (lastSeparator != -1) {
++ key = key.substring(lastSeparator + 1);
++ }
++ this.configKey = key.toLowerCase(java.util.Locale.ROOT);
++ // Paper end
+ this.scanRate = senseInterval;
+ this.timeToTick = (long)RANDOM.nextInt(senseInterval);
+ }
+@@ -41,8 +52,10 @@
+
+ public final void tick(ServerLevel world, 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.updateTargetingConditionRanges(entity);
++ // Paper end
+ this.doTick(world, entity);
+ }
+ }
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
new file mode 100644
index 0000000000..2bd53d40cd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ai/sensing/TemptingSensor.java.patch
@@ -0,0 +1,45 @@
+--- 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
new file mode 100644
index 0000000000..4b4a523928
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..acfaa378fe
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/ambient/Bat.java.patch
@@ -0,0 +1,55 @@
+--- 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/AbstractSchoolingFish.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/AbstractSchoolingFish.java.patch
new file mode 100644
index 0000000000..69bd0b2aef
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/AbstractSchoolingFish.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/entity/animal/AbstractSchoolingFish.java
++++ b/net/minecraft/world/entity/animal/AbstractSchoolingFish.java
+@@ -51,6 +51,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
+ this.leader.removeFollower();
+ this.leader = null;
+ }
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
new file mode 100644
index 0000000000..f8c533216c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Animal.java.patch
@@ -0,0 +1,141 @@
+--- 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
new file mode 100644
index 0000000000..748c94de8e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Bee.java.patch
@@ -0,0 +1,205 @@
+--- 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
new file mode 100644
index 0000000000..804bc3e693
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Bucketable.java.patch
@@ -0,0 +1,46 @@
+--- 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
new file mode 100644
index 0000000000..d829874cb7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Cat.java.patch
@@ -0,0 +1,96 @@
+--- 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
new file mode 100644
index 0000000000..78c5696f2a
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..4e23f7179b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Cow.java.patch
@@ -0,0 +1,33 @@
+--- 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
new file mode 100644
index 0000000000..c16913343e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Dolphin.java.patch
@@ -0,0 +1,87 @@
+--- 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
new file mode 100644
index 0000000000..7052ecb52a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Fox.java.patch
@@ -0,0 +1,168 @@
+--- 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
new file mode 100644
index 0000000000..4562593cc0
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..a7cff6ad18
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/MushroomCow.java.patch
@@ -0,0 +1,85 @@
+--- 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
new file mode 100644
index 0000000000..e217641cf1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Ocelot.java.patch
@@ -0,0 +1,34 @@
+--- 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
new file mode 100644
index 0000000000..911c02e2c0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Panda.java.patch
@@ -0,0 +1,122 @@
+--- 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/Parrot.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Parrot.java.patch
new file mode 100644
index 0000000000..e42863c2b1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Parrot.java.patch
@@ -0,0 +1,47 @@
+--- a/net/minecraft/world/entity/animal/Parrot.java
++++ b/net/minecraft/world/entity/animal/Parrot.java
+@@ -248,7 +248,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);
+ } else {
+@@ -269,7 +269,7 @@
+ }
+ } else {
+ this.usePlayerItem(player, hand, itemstack);
+- 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 @@
+ }
+
+ @Override
+- public boolean isPushable() {
+- return true;
++ public boolean isCollidable(boolean ignoreClimbing) { // Paper - Climbing should not bypass cramming gamerule
++ return super.isCollidable(ignoreClimbing); // CraftBukkit - collidable API // Paper - Climbing should not bypass cramming gamerule
+ }
+
+ @Override
+@@ -378,8 +378,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
+ }
+ }
+
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
new file mode 100644
index 0000000000..a5cdaa0866
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Pig.java.patch
@@ -0,0 +1,29 @@
+--- 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/Pufferfish.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Pufferfish.java.patch
new file mode 100644
index 0000000000..32042a8ba6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Pufferfish.java.patch
@@ -0,0 +1,60 @@
+--- a/net/minecraft/world/entity/animal/Pufferfish.java
++++ b/net/minecraft/world/entity/animal/Pufferfish.java
+@@ -102,25 +102,39 @@
+ public void tick() {
+ if (!this.level().isClientSide && this.isAlive() && this.isEffectiveAi()) {
+ if (this.inflateCounter > 0) {
++ boolean increase = true; // Paper - Add PufferFishStateChangeEvent
+ if (this.getPuffState() == 0) {
++ if (new io.papermc.paper.event.entity.PufferFishStateChangeEvent((org.bukkit.entity.PufferFish) getBukkitEntity(), 1).callEvent()) { // Paper - Add PufferFishStateChangeEvent
+ this.makeSound(SoundEvents.PUFFER_FISH_BLOW_UP);
+ this.setPuffState(1);
++ } else { increase = false; } // Paper - Add PufferFishStateChangeEvent
+ } else if (this.inflateCounter > 40 && this.getPuffState() == 1) {
++ if (new io.papermc.paper.event.entity.PufferFishStateChangeEvent((org.bukkit.entity.PufferFish) getBukkitEntity(), 2).callEvent()) { // Paper - Add PufferFishStateChangeEvent
+ this.makeSound(SoundEvents.PUFFER_FISH_BLOW_UP);
+ this.setPuffState(2);
++ } else { increase = false; } // Paper - Add PufferFishStateChangeEvent
+ }
+
++ if (increase) { // Paper - Add PufferFishStateChangeEvent
+ ++this.inflateCounter;
++ } // Paper - Add PufferFishStateChangeEvent
+ } else if (this.getPuffState() != 0) {
++ boolean increase = true; // Paper - Add PufferFishStateChangeEvent
+ if (this.deflateTimer > 60 && this.getPuffState() == 2) {
++ if (new io.papermc.paper.event.entity.PufferFishStateChangeEvent((org.bukkit.entity.PufferFish) getBukkitEntity(), 1).callEvent()) { // Paper - Add PufferFishStateChangeEvent
+ this.makeSound(SoundEvents.PUFFER_FISH_BLOW_OUT);
+ this.setPuffState(1);
++ } else { increase = false; } // Paper - Add PufferFishStateChangeEvent
+ } else if (this.deflateTimer > 100 && this.getPuffState() == 1) {
++ if (new io.papermc.paper.event.entity.PufferFishStateChangeEvent((org.bukkit.entity.PufferFish) getBukkitEntity(), 0).callEvent()) { // Paper - Add PufferFishStateChangeEvent
+ this.makeSound(SoundEvents.PUFFER_FISH_BLOW_OUT);
+ this.setPuffState(0);
++ } else { increase = false; } // Paper - Add PufferFishStateChangeEvent
+ }
+
++ if (increase) { // Paper - Add PufferFishStateChangeEvent
+ ++this.deflateTimer;
++ } // Paper - Add PufferFishStateChangeEvent
+ }
+ }
+
+@@ -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
+ 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
+ }
+ }
+
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
new file mode 100644
index 0000000000..1f8f84be30
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Rabbit.java.patch
@@ -0,0 +1,40 @@
+--- 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
new file mode 100644
index 0000000000..ce52e0ef57
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Sheep.java.patch
@@ -0,0 +1,90 @@
+--- 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
new file mode 100644
index 0000000000..40307984a0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/ShoulderRidingEntity.java.patch
@@ -0,0 +1,21 @@
+--- 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
new file mode 100644
index 0000000000..c07f86335e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/SnowGolem.java.patch
@@ -0,0 +1,86 @@
+--- 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
new file mode 100644
index 0000000000..d67bfc9654
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..49bba2e107
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Turtle.java.patch
@@ -0,0 +1,74 @@
+--- 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
new file mode 100644
index 0000000000..5089fd98c0
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..50375e02de
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/Wolf.java.patch
@@ -0,0 +1,126 @@
+--- 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
new file mode 100644
index 0000000000..8c853dc764
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/allay/Allay.java.patch
@@ -0,0 +1,102 @@
+--- 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
new file mode 100644
index 0000000000..2e72adb5ea
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/armadillo/Armadillo.java.patch
@@ -0,0 +1,81 @@
+--- 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
new file mode 100644
index 0000000000..3923bd7b5c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/axolotl/Axolotl.java.patch
@@ -0,0 +1,52 @@
+--- 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/camel/Camel.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/camel/Camel.java.patch
new file mode 100644
index 0000000000..e2b9543713
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/camel/Camel.java.patch
@@ -0,0 +1,81 @@
+--- 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 @@
+ 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
+ }
+
+ boolean flag2 = this.isBaby();
+@@ -454,9 +457,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;
++ }
++ // CraftBukkit end
+ this.standUpInstantly();
+- super.actuallyHurt(world, source, amount);
++ return true; // CraftBukkit
+ }
+
+ @Override
+@@ -563,7 +572,7 @@
+ }
+
+ public void sitDown() {
+- if (!this.isCamelSitting()) {
++ if (!this.isCamelSitting() && new io.papermc.paper.event.entity.EntityToggleSitEvent(this.getBukkitEntity(), true).callEvent()) { // Paper - Add EntityToggleSitEvent
+ this.makeSound(SoundEvents.CAMEL_SIT);
+ this.setPose(Pose.SITTING);
+ this.gameEvent(GameEvent.ENTITY_ACTION);
+@@ -572,7 +581,7 @@
+ }
+
+ public void standUp() {
+- if (this.isCamelSitting()) {
++ if (this.isCamelSitting() && new io.papermc.paper.event.entity.EntityToggleSitEvent(this.getBukkitEntity(), false).callEvent()) { // Paper - Add EntityToggleSitEvent
+ this.makeSound(SoundEvents.CAMEL_STAND);
+ this.setPose(Pose.STANDING);
+ this.gameEvent(GameEvent.ENTITY_ACTION);
+@@ -581,6 +590,7 @@
+ }
+
+ public void standUpInstantly() {
++ if (this.isCamelSitting() && !new io.papermc.paper.event.entity.EntityToggleSitEvent(this.getBukkitEntity(), false).callEvent()) return; // Paper - Add EntityToggleSitEvent
+ this.setPose(Pose.STANDING);
+ this.gameEvent(GameEvent.ENTITY_ACTION);
+ this.resetLastPoseChangeTickToFullStand(this.level().getGameTime());
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Frog.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Frog.java.patch
new file mode 100644
index 0000000000..f849bb1ffc
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Frog.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/entity/animal/frog/Frog.java
++++ b/net/minecraft/world/entity/animal/frog/Frog.java
+@@ -270,7 +270,12 @@
+
+ @Override
+ public void spawnChildFromBreeding(ServerLevel world, Animal other) {
+- this.finalizeSpawnChildFromBreeding(world, other, null);
++ // 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;
++
++ this.finalizeSpawnChildFromBreeding(world, other, 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/unapplied/net/minecraft/world/entity/animal/frog/ShootTongue.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/ShootTongue.java.patch
new file mode 100644
index 0000000000..354d8a45c5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/ShootTongue.java.patch
@@ -0,0 +1,39 @@
+--- 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
new file mode 100644
index 0000000000..636405ea80
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/frog/Tadpole.java.patch
@@ -0,0 +1,87 @@
+--- 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
new file mode 100644
index 0000000000..44290161de
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/goat/Goat.java.patch
@@ -0,0 +1,76 @@
+--- 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
new file mode 100644
index 0000000000..78095235f3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/AbstractChestedHorse.java.patch
@@ -0,0 +1,19 @@
+--- 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/AbstractHorse.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/AbstractHorse.java.patch
new file mode 100644
index 0000000000..e975349f91
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/AbstractHorse.java.patch
@@ -0,0 +1,203 @@
+--- a/net/minecraft/world/entity/animal/horse/AbstractHorse.java
++++ b/net/minecraft/world/entity/animal/horse/AbstractHorse.java
+@@ -79,6 +79,17 @@
+ import net.minecraft.world.phys.Vec3;
+ import net.minecraft.world.ticks.ContainerSingleItem;
+
++// CraftBukkit start
++import java.util.Arrays;
++import java.util.List;
++import org.bukkit.Location;
++import org.bukkit.craftbukkit.entity.CraftHumanEntity;
++import org.bukkit.craftbukkit.event.CraftEventFactory;
++import org.bukkit.entity.HumanEntity;
++import org.bukkit.event.entity.EntityRegainHealthEvent;
++import org.bukkit.inventory.InventoryHolder;
++// 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 boolean stillValid(Player player) {
+ return player.getVehicle() == AbstractHorse.this || player.canInteractWithEntity((Entity) AbstractHorse.this, 4.0D);
++ }
++
++ // CraftBukkit start - add fields and methods
++ public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
++ private int maxStack = MAX_STACK;
++
++ @Override
++ public List<ItemStack> getContents() {
++ return Arrays.asList(this.getTheItem());
++ }
++
++ @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 InventoryHolder getOwner() {
++ return (org.bukkit.entity.AbstractHorse) AbstractHorse.this.getBukkitEntity();
++ }
++
++ @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 @@
+ }
+
+ @Override
+- public boolean isPushable() {
++ public boolean isCollidable(boolean ignoreClimbing) { // Paper - Climbing should not bypass cramming gamerule
+ return !this.isVehicle();
+ }
+
+@@ -366,7 +423,7 @@
+ public void createInventory() {
+ SimpleContainer inventorysubcontainer = 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 @@
+ }
+
+ public int getMaxTemper() {
+- return 100;
++ return this.maxDomestication; // CraftBukkit - return stored max domestication instead of 100
+ }
+
+ @Override
+@@ -528,7 +585,7 @@
+ b0 = 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
+ }
+ } else if (item.is(Items.GOLDEN_APPLE) || item.is(Items.ENCHANTED_GOLDEN_APPLE)) {
+ f = 10.0F;
+@@ -536,12 +593,12 @@
+ b0 = 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
+ }
+ }
+
+ if (this.getHealth() < this.getMaxHealth() && f > 0.0F) {
+- this.heal(f);
++ this.heal(f, EntityRegainHealthEvent.RegainReason.EATING); // CraftBukkit
+ 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 @@
+ }
+ }
+
++ }
++
++ // 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);
+ }
+
++ // Paper start - Horse API
++ public void setForceStanding(boolean standing) {
++ this.setFlag(FLAG_STANDING, standing);
++ }
++ // Paper end - Horse API
+ public void setStanding(boolean angry) {
+ if (angry) {
+ this.setEating(false);
+@@ -883,6 +954,7 @@
+ if (this.getOwnerUUID() != null) {
+ nbt.putUUID("Owner", this.getOwnerUUID());
+ }
++ nbt.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 @@
+
+ if (uuid != null) {
+ this.setOwnerUUID(uuid);
++ }
++ // CraftBukkit start
++ if (nbt.contains("Bukkit.MaxDomestication")) {
++ this.maxDomestication = nbt.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 @@
+
+ @Override
+ public void handleStartJump(int height) {
++ // CraftBukkit start
++ float power;
++ if (height >= 90) {
++ power = 1.0F;
++ } else {
++ power = 0.4F + 0.4F * (float) height / 90.0F;
++ }
++ if (!CraftEventFactory.callHorseJumpEvent(this, power)) {
++ return;
++ }
++ // CraftBukkit end
+ this.allowStandSliding = true;
+ this.standIfPossible();
+ this.playJumpSound();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/Llama.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/Llama.java.patch
new file mode 100644
index 0000000000..df44c55d01
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/Llama.java.patch
@@ -0,0 +1,51 @@
+--- a/net/minecraft/world/entity/animal/horse/Llama.java
++++ b/net/minecraft/world/entity/animal/horse/Llama.java
+@@ -71,17 +71,23 @@
+ @Nullable
+ private Llama caravanHead;
+ @Nullable
+- private Llama caravanTail;
++ public Llama caravanTail; // Paper
+
+ public Llama(EntityType<? extends Llama> type, Level world) {
+ super(type, world);
+ this.getNavigation().setRequiredPathLength(40.0F);
++ this.maxDomestication = 30; // Paper - Missing entity API; configure max temper instead of a hardcoded value
+ }
+
+ public boolean isTraderLlama() {
+ return false;
+ }
+
++ // CraftBukkit start
++ public void setStrengthPublic(int i) {
++ this.setStrength(i);
++ }
++ // CraftBukkit end
+ private void setStrength(int strength) {
+ this.entityData.set(Llama.DATA_STRENGTH_ID, Math.max(1, Math.min(5, strength)));
+ }
+@@ -171,12 +177,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
+ }
+ }
+
+ if (this.getHealth() < this.getMaxHealth() && f > 0.0F) {
+- this.heal(f);
++ this.heal(f, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.EATING); // Paper - Add missing regain reason
+ flag = true;
+ }
+
+@@ -289,7 +295,7 @@
+
+ @Override
+ public int getMaxTemper() {
+- return 30;
++ return super.getMaxTemper(); // Paper - Missing entity API; delegate to parent
+ }
+
+ @Override
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
new file mode 100644
index 0000000000..a0ae868b36
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/SkeletonHorse.java.patch
@@ -0,0 +1,21 @@
+--- 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
new file mode 100644
index 0000000000..8f22bd7fd2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/SkeletonTrapGoal.java.patch
@@ -0,0 +1,49 @@
+--- 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
new file mode 100644
index 0000000000..1edf3cd1bd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/horse/TraderLlama.java.patch
@@ -0,0 +1,30 @@
+--- 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
new file mode 100644
index 0000000000..a6567df470
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/animal/sniffer/Sniffer.java.patch
@@ -0,0 +1,56 @@
+--- 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
new file mode 100644
index 0000000000..9f6803d4ee
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java.patch
@@ -0,0 +1,91 @@
+--- 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
new file mode 100644
index 0000000000..01ecc5bc3d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java.patch
@@ -0,0 +1,331 @@
+--- 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
new file mode 100644
index 0000000000..2dcf3ac15b
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/DragonHoldingPatternPhase.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonHoldingPatternPhase.java.patch
new file mode 100644
index 0000000000..f07b2b70e3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonHoldingPatternPhase.java.patch
@@ -0,0 +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 @@
+
+ private void findNewTarget(ServerLevel world) {
+ 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
+ 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/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
new file mode 100644
index 0000000000..ce24273215
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/DragonLandingPhase.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingPhase.java.patch
new file mode 100644
index 0000000000..0d9035a506
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonLandingPhase.java.patch
@@ -0,0 +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) {
+ 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
+ );
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java.patch
new file mode 100644
index 0000000000..9477328964
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonSittingFlamingPhase.java.patch
@@ -0,0 +1,35 @@
+--- 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 @@
+ 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);
++ // Paper start - EnderDragon Events
++ } else {
++ this.end();
++ }
++ // Paper end - EnderDragon Events
+ }
+
+ }
+@@ -100,7 +109,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 = null;
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java.patch
new file mode 100644
index 0000000000..7a5a567fd1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java
++++ b/net/minecraft/world/entity/boss/enderdragon/phases/DragonStrafePlayerPhase.java
+@@ -79,8 +79,11 @@
+ }
+
+ DragonFireball dragonFireball = new DragonFireball(world, this.dragon, vec34.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);
++ else dragonFireball.discard(null); // Paper - EnderDragon Events
+ this.fireballCharge = 0;
+ if (this.currentPath != null) {
+ while (!this.currentPath.isDone()) {
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
new file mode 100644
index 0000000000..54fe962df5
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..d39507da58
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/enderdragon/phases/EnderDragonPhaseManager.java.patch
@@ -0,0 +1,42 @@
+--- 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
new file mode 100644
index 0000000000..67144bc046
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/boss/wither/WitherBoss.java.patch
@@ -0,0 +1,165 @@
+--- 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
new file mode 100644
index 0000000000..edb69ea66e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/ArmorStand.java.patch
@@ -0,0 +1,513 @@
+--- 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
new file mode 100644
index 0000000000..2f102c86b9
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/BlockAttachedEntity.java.patch
@@ -0,0 +1,140 @@
+--- 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
new file mode 100644
index 0000000000..f6d7c47bb4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/ItemFrame.java.patch
@@ -0,0 +1,150 @@
+--- 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/LeashFenceKnotEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java.patch
new file mode 100644
index 0000000000..c569c75fde
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/LeashFenceKnotEntity.java.patch
@@ -0,0 +1,87 @@
+--- 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();
+
+ 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()));
++ flag = true; // Also set true when the event is cancelled otherwise it tries to unleash the entities
++ continue;
++ }
++ }
++ // CraftBukkit end
+ leashable.setLeashedTo(this, true);
+ flag = true;
+ }
+@@ -93,18 +107,43 @@
+ boolean flag1 = false;
+
+ if (!flag) {
+- this.discard();
+- if (player.getAbilities().instabuild) {
++ // CraftBukkit start - Move below
++ // this.discard();
++ 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();
+
+ 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);
++ dropLeash = event.isDropLeash();
++ if (event.isCancelled()) {
++ // Paper end - Expand EntityUnleashEvent
++ die = false;
++ continue;
++ }
++ }
++ if (!dropLeash) { // Paper - Expand EntityUnleashEvent
++ leashable1.removeLeash();
++ } else {
++ leashable1.dropLeash();
++ }
++ // CraftBukkit end
+ flag1 = true;
+ }
+ }
++ // CraftBukkit start
++ if (die) {
++ this.discard(EntityRemoveEvent.Cause.DROP); // CraftBukkit - add Bukkit remove cause
++ }
++ // CraftBukkit end
+ }
+ }
+
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
new file mode 100644
index 0000000000..122466b765
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/decoration/Painting.java.patch
@@ -0,0 +1,54 @@
+--- 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
new file mode 100644
index 0000000000..8f36430b05
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/item/FallingBlockEntity.java.patch
@@ -0,0 +1,162 @@
+--- 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
new file mode 100644
index 0000000000..0bc136847c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/item/ItemEntity.java.patch
@@ -0,0 +1,389 @@
+--- 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/item/PrimedTnt.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/item/PrimedTnt.java.patch
new file mode 100644
index 0000000000..1120fe2bdf
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/item/PrimedTnt.java.patch
@@ -0,0 +1,116 @@
+--- a/net/minecraft/world/entity/item/PrimedTnt.java
++++ b/net/minecraft/world/entity/item/PrimedTnt.java
+@@ -27,6 +27,12 @@
+ import net.minecraft.world.level.material.FluidState;
+ import net.minecraft.world.level.portal.TeleportTransition;
+
++// 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 @@
+ public LivingEntity owner;
+ private boolean usedPortal;
+ public float explosionPower;
++ 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);
+ 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);
+ this.setFuse(80);
+@@ -94,10 +101,17 @@
+
+ @Override
+ public void tick() {
++ if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().spigotConfig.currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot
+ this.handlePortal();
+ this.applyGravity();
+ this.move(MoverType.SELF, this.getDeltaMovement());
+ this.applyEffectsFromBlocks();
++ // Paper start - Configurable TNT height nerf
++ if (this.level().paperConfig().fixes.tntEntityHeightNerf.test(v -> this.getY() > v)) {
++ this.discard(EntityRemoveEvent.Cause.OUT_OF_WORLD);
++ return;
++ }
++ // Paper end - Configurable TNT height nerf
+ this.setDeltaMovement(this.getDeltaMovement().scale(0.98D));
+ if (this.onGround()) {
+ this.setDeltaMovement(this.getDeltaMovement().multiply(0.7D, -0.5D, 0.7D));
+@@ -107,10 +121,13 @@
+
+ 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();
+ if (!this.level().isClientSide) {
+ this.explode();
+ }
++ this.discard(EntityRemoveEvent.Cause.EXPLODE); // CraftBukkit - add Bukkit remove cause
++ // CraftBukkit end
+ } else {
+ this.updateInWaterStateAndDoFluidPushing();
+ if (this.level().isClientSide) {
+@@ -118,10 +135,37 @@
+ }
+ }
+
++ // Paper start - Option to prevent TNT from moving in water
++ if (!this.isRemoved() && this.wasTouchingWater && this.level().paperConfig().fixes.preventTntFromMovingInWater) {
++ /*
++ * Author: Jedediah Smith <[email protected]>
++ */
++ // Send position and velocity updates to nearby players on every tick while the TNT is in water.
++ // This does pretty well at keeping their clients in sync with the server.
++ net.minecraft.server.level.ChunkMap.TrackedEntity ete = ((net.minecraft.server.level.ServerLevel) this.level()).getChunkSource().chunkMap.entityMap.get(this.getId());
++ if (ete != null) {
++ net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket velocityPacket = new net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket(this);
++ net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket positionPacket = net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket.teleport(this.getId(), net.minecraft.world.entity.PositionMoveRotation.of(this), java.util.Set.of(), this.onGround);
++
++ ete.seenBy.stream()
++ .filter(viewer -> (viewer.getPlayer().getX() - this.getX()) * (viewer.getPlayer().getY() - this.getY()) * (viewer.getPlayer().getZ() - this.getZ()) < 16 * 16)
++ .forEach(viewer -> {
++ viewer.send(velocityPacket);
++ viewer.send(positionPacket);
++ });
++ }
++ }
++ // Paper end - Option to prevent TNT from moving in water
+ }
+
+ private void explode() {
+- 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
+ }
+
+ @Override
+@@ -198,4 +242,11 @@
+ public final boolean hurtServer(ServerLevel world, DamageSource source, float amount) {
+ return false;
+ }
++
++ // Paper start - Option to prevent TNT from moving in water
++ @Override
++ public boolean isPushedByFluid() {
++ return !level().paperConfig().fixes.preventTntFromMovingInWater && super.isPushedByFluid();
++ }
++ // Paper end - Option to prevent TNT from moving in water
+ }
diff --git a/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
new file mode 100644
index 0000000000..d726409faf
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/AbstractSkeleton.java.patch
@@ -0,0 +1,74 @@
+--- 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
new file mode 100644
index 0000000000..1cd68614bd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Bogged.java.patch
@@ -0,0 +1,72 @@
+--- 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
new file mode 100644
index 0000000000..c2449e2257
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..ea4e74957f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Creeper.java.patch
@@ -0,0 +1,132 @@
+--- 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/Drowned.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Drowned.java.patch
new file mode 100644
index 0000000000..938442254b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Drowned.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/monster/Drowned.java
++++ b/net/minecraft/world/entity/monster/Drowned.java
+@@ -85,7 +85,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(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
+ 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/unapplied/net/minecraft/world/entity/monster/ElderGuardian.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ElderGuardian.java.patch
new file mode 100644
index 0000000000..b270c246e4
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..512e3e45cf
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/EnderMan.java.patch
@@ -0,0 +1,157 @@
+--- 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
new file mode 100644
index 0000000000..9a32ade2bd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Endermite.java.patch
@@ -0,0 +1,21 @@
+--- 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
new file mode 100644
index 0000000000..d8921518a6
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..e91f483915
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Ghast.java.patch
@@ -0,0 +1,25 @@
+--- 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/Guardian.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Guardian.java.patch
new file mode 100644
index 0000000000..078be667c0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Guardian.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/entity/monster/Guardian.java
++++ b/net/minecraft/world/entity/monster/Guardian.java
+@@ -60,6 +60,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);
+- 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(7, this.randomStrollGoal);
+ this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
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
new file mode 100644
index 0000000000..0df55d8f37
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/Illusioner.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Illusioner.java.patch
new file mode 100644
index 0000000000..b839923b98
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Illusioner.java.patch
@@ -0,0 +1,38 @@
+--- 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) {
++ // 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);
++ 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));
++ }
++ // 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 @@
+
+ @Override
+ protected void performSpellCasting() {
+- Illusioner.this.getTarget().addEffect(new MobEffectInstance(MobEffects.BLINDNESS, 400), Illusioner.this);
++ Illusioner.this.getTarget().addEffect(new MobEffectInstance(MobEffects.BLINDNESS, 400), Illusioner.this, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.ATTACK); // CraftBukkit
+ }
+
+ @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Monster.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Monster.java.patch
new file mode 100644
index 0000000000..bae67fa57b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Monster.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/monster/Monster.java
++++ b/net/minecraft/world/entity/monster/Monster.java
+@@ -92,7 +92,7 @@
+ return false;
+ } else {
+ DimensionType dimensionType = world.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) {
+ return false;
+ } else {
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
new file mode 100644
index 0000000000..ca42f84c2e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Phantom.java.patch
@@ -0,0 +1,78 @@
+--- 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
new file mode 100644
index 0000000000..43bd8b72e6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Pillager.java.patch
@@ -0,0 +1,21 @@
+--- 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
new file mode 100644
index 0000000000..1cbab8cd90
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Ravager.java.patch
@@ -0,0 +1,41 @@
+--- 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
new file mode 100644
index 0000000000..fb73125321
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Shulker.java.patch
@@ -0,0 +1,59 @@
+--- 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
new file mode 100644
index 0000000000..b031571b09
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Silverfish.java.patch
@@ -0,0 +1,51 @@
+--- 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/Skeleton.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Skeleton.java.patch
new file mode 100644
index 0000000000..d17fea360f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Skeleton.java.patch
@@ -0,0 +1,24 @@
+--- a/net/minecraft/world/entity/monster/Skeleton.java
++++ b/net/minecraft/world/entity/monster/Skeleton.java
+@@ -94,12 +94,19 @@
+ }
+
+ 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
+ if (!this.isSilent()) {
+ this.level().levelEvent((Player) 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
++ if (stray == null) {
++ this.conversionTime = 300;
++ }
++ // Paper end - Fix issues with mob conversion
++
+ }
+
+ @Override
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
new file mode 100644
index 0000000000..4ee5279ca5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Slime.java.patch
@@ -0,0 +1,239 @@
+--- 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/SpellcasterIllager.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/SpellcasterIllager.java.patch
new file mode 100644
index 0000000000..7e02fcf3a8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/SpellcasterIllager.java.patch
@@ -0,0 +1,24 @@
+--- 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 @@
+ public void tick() {
+ --this.attackWarmupDelay;
+ if (this.attackWarmupDelay == 0) {
++ // CraftBukkit start
++ if (!CraftEventFactory.handleEntitySpellCastEvent(SpellcasterIllager.this, this.getSpell())) {
++ return;
++ }
++ // CraftBukkit end
+ this.performSpellCasting();
+ SpellcasterIllager.this.playSound(SpellcasterIllager.this.getCastingSoundEvent(), 1.0F, 1.0F);
+ }
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
new file mode 100644
index 0000000000..1dae9b0f57
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/Strider.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Strider.java.patch
new file mode 100644
index 0000000000..fbc85b5bbe
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Strider.java.patch
@@ -0,0 +1,18 @@
+--- 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);
++ // CraftBukkit start
++ boolean suffocating = !flag || flag2;
++ if (suffocating ^ this.isSuffocating()) {
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callStriderTemperatureChangeEvent(this, suffocating)) {
++ this.setSuffocating(suffocating);
++ }
++ }
++ // CraftBukkit end
+ }
+
+ super.tick();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vex.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vex.java.patch
new file mode 100644
index 0000000000..c2c4e27616
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vex.java.patch
@@ -0,0 +1,23 @@
+--- 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 @@
+
+ @Override
+ public void start() {
+- Vex.this.setTarget(Vex.this.owner.getTarget());
++ Vex.this.setTarget(Vex.this.owner.getTarget(), org.bukkit.event.entity.EntityTargetEvent.TargetReason.OWNER_ATTACKED_TARGET, true); // CraftBukkit
+ super.start();
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vindicator.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vindicator.java.patch
new file mode 100644
index 0000000000..e652ce7a7e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Vindicator.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/entity/monster/Vindicator.java
++++ b/net/minecraft/world/entity/monster/Vindicator.java
+@@ -184,7 +184,7 @@
+
+ static class VindicatorBreakDoorGoal extends BreakDoorGoal {
+ public VindicatorBreakDoorGoal(Mob mob) {
+- super(mob, 6, Vindicator.DOOR_BREAKING_PREDICATE);
++ super(mob, 6, com.google.common.base.Predicates.in(mob.level().paperConfig().entities.behavior.doorBreakingDifficulty.getOrDefault(mob.getType(), mob.level().paperConfig().entities.behavior.doorBreakingDifficulty.get(EntityType.VINDICATOR)))); // Paper - Configurable door breaking difficulty
+ this.setFlags(EnumSet.of(Goal.Flag.MOVE));
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Witch.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Witch.java.patch
new file mode 100644
index 0000000000..6939fca599
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Witch.java.patch
@@ -0,0 +1,77 @@
+--- a/net/minecraft/world/entity/monster/Witch.java
++++ b/net/minecraft/world/entity/monster/Witch.java
+@@ -124,9 +124,15 @@
+
+ this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
+ PotionContents potioncontents = (PotionContents) itemstack.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;
++ }
++ // 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
+ }
+
+ this.gameEvent(GameEvent.DRINK);
+@@ -146,17 +152,7 @@
+ }
+
+ if (holder != null) {
+- this.setItemSlot(EquipmentSlot.MAINHAND, PotionContents.createItemStack(Items.POTION, holder));
+- 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);
+- }
+-
+- AttributeInstance attributemodifiable = this.getAttribute(Attributes.MOVEMENT_SPEED);
+-
+- attributemodifiable.removeModifier(Witch.SPEED_MODIFIER_DRINKING_ID);
+- attributemodifiable.addTransientModifier(Witch.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 @@
+ }
+
+ super.aiStep();
++ }
++
++ // Paper start - moved to its own method
++ public void setDrinkingPotion(ItemStack potion) {
++ potion = org.bukkit.craftbukkit.event.CraftEventFactory.handleWitchReadyPotionEvent(this, potion);
++ this.setItemSlot(EquipmentSlot.MAINHAND, potion);
++ 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);
++ }
++
++ AttributeInstance attributemodifiable = this.getAttribute(Attributes.MOVEMENT_SPEED);
++
++ attributemodifiable.removeModifier(Witch.SPEED_MODIFIER_DRINKING_ID);
++ attributemodifiable.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);
+
++ // 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));
++ 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);
+ }
+
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
new file mode 100644
index 0000000000..61b7e05667
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..4b95368547
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/Zombie.java.patch
@@ -0,0 +1,304 @@
+--- 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
new file mode 100644
index 0000000000..d025644b62
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ZombieVillager.java.patch
@@ -0,0 +1,149 @@
+--- 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
new file mode 100644
index 0000000000..0b3d7e043c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/ZombifiedPiglin.java.patch
@@ -0,0 +1,67 @@
+--- 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/breeze/Breeze.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/breeze/Breeze.java.patch
new file mode 100644
index 0000000000..b3c69c8fbb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/breeze/Breeze.java.patch
@@ -0,0 +1,19 @@
+--- 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 @@
+
+ @Override
+ public boolean canAttackType(EntityType<?> type) {
++ if (this.getTarget() != null) return this.getTarget().getType() == type; // SPIGOT-7957: Allow attack if target from brain was set
+ return type == EntityType.PLAYER || type == EntityType.IRON_GOLEM;
+ }
+
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
new file mode 100644
index 0000000000..d3d16a6b28
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/creaking/Creaking.java.patch
@@ -0,0 +1,60 @@
+--- 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
new file mode 100644
index 0000000000..345420cb17
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/hoglin/Hoglin.java.patch
@@ -0,0 +1,48 @@
+--- 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
new file mode 100644
index 0000000000..634a383e72
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..68e189739c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/AbstractPiglin.java.patch
@@ -0,0 +1,20 @@
+--- 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
new file mode 100644
index 0000000000..d238fb349f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/Piglin.java.patch
@@ -0,0 +1,140 @@
+--- 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
new file mode 100644
index 0000000000..66919128bd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/piglin/PiglinAi.java.patch
@@ -0,0 +1,210 @@
+--- 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
new file mode 100644
index 0000000000..f65a699a17
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..c6378460b2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/monster/warden/Warden.java.patch
@@ -0,0 +1,59 @@
+--- 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/AbstractVillager.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/AbstractVillager.java.patch
new file mode 100644
index 0000000000..ba7ad87dd7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/AbstractVillager.java.patch
@@ -0,0 +1,106 @@
+--- a/net/minecraft/world/entity/npc/AbstractVillager.java
++++ b/net/minecraft/world/entity/npc/AbstractVillager.java
+@@ -40,8 +40,21 @@
+ import net.minecraft.world.phys.Vec3;
+ import org.slf4j.Logger;
+
++// CraftBukkit start
++import org.bukkit.Bukkit;
++import org.bukkit.craftbukkit.inventory.CraftMerchant;
++import org.bukkit.craftbukkit.inventory.CraftMerchantRecipe;
++import org.bukkit.event.entity.VillagerAcquireTradeEvent;
++// CraftBukkit end
++
+ public abstract class AbstractVillager extends AgeableMob implements InventoryCarrier, Npc, Merchant {
+
++ // CraftBukkit start
++ @Override
++ public CraftMerchant getCraftMerchant() {
++ return (org.bukkit.craftbukkit.entity.CraftAbstractVillager) this.getBukkitEntity();
++ }
++ // CraftBukkit end
+ 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 @@
+ 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
+
+ public AbstractVillager(EntityType<? extends AbstractVillager> type, Level world) {
+ super(type, world);
+@@ -101,6 +114,13 @@
+ return this.tradingPlayer != null;
+ }
+
++ // Paper start - Villager#resetOffers
++ public void resetOffers() {
++ this.offers = new MerchantOffers();
++ this.updateTrades();
++ }
++ // Paper end - Villager#resetOffers
++
+ @Override
+ public MerchantOffers getOffers() {
+ if (this.level().isClientSide) {
+@@ -121,11 +141,24 @@
+ @Override
+ public void overrideXp(int experience) {}
+
++ // 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
++ if (event == null || event.willIncreaseTradeUses()) {
++ recipe.increaseUses();
++ }
++ if (event == null || event.isRewardingExp()) {
++ this.rewardTradeXp(recipe);
++ }
++ this.notifyTrade(recipe);
++ }
++ // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent
++
++ @Override
++ public void notifyTrade(MerchantOffer offer) {
++ // 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());
+ }
+@@ -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);
++ // CraftBukkit start
++ VillagerAcquireTradeEvent event = new VillagerAcquireTradeEvent((org.bukkit.entity.AbstractVillager) this.getBukkitEntity(), merchantrecipe.asBukkit());
++ // Suppress during worldgen
++ if (this.valid) {
++ Bukkit.getPluginManager().callEvent(event);
++ }
++ if (!event.isCancelled()) {
++ // Paper start - Fix crash from invalid ingredient list
++ final CraftMerchantRecipe craftMerchantRecipe = CraftMerchantRecipe.fromBukkit(event.getRecipe());
++ if (craftMerchantRecipe.getIngredients().isEmpty()) return;
++ recipeList.add(craftMerchantRecipe.toMinecraft());
++ // Paper end - Fix crash from invalid ingredient list
++ }
++ // CraftBukkit end
+ ++j;
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/CatSpawner.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/CatSpawner.java.patch
new file mode 100644
index 0000000000..6d93d621f8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/CatSpawner.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/entity/npc/CatSpawner.java
++++ b/net/minecraft/world/entity/npc/CatSpawner.java
+@@ -82,8 +82,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.moveTo(pos, 0.0F, 0.0F);
+ world.addFreshEntityWithPassengers(cat);
+ return 1;
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/entity/npc/InventoryCarrier.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/InventoryCarrier.java.patch
new file mode 100644
index 0000000000..d6c445bea3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/InventoryCarrier.java.patch
@@ -0,0 +1,35 @@
+--- a/net/minecraft/world/entity/npc/InventoryCarrier.java
++++ b/net/minecraft/world/entity/npc/InventoryCarrier.java
+@@ -8,6 +8,10 @@
+ import net.minecraft.world.entity.item.ItemEntity;
+ import net.minecraft.world.item.ItemStack;
+
++// CraftBukkit start
++import org.bukkit.event.entity.EntityRemoveEvent;
++// CraftBukkit end
++
+ public interface InventoryCarrier {
+
+ String TAG_INVENTORY = "Inventory";
+@@ -25,13 +29,20 @@
+ return;
+ }
+
++ // CraftBukkit start
++ ItemStack remaining = new SimpleContainer(inventorysubcontainer).addItem(itemstack);
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callEntityPickupItemEvent(entity, item, 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
+ } else {
+ itemstack.setCount(itemstack1.getCount());
+ }
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
new file mode 100644
index 0000000000..cb4983a642
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/Villager.java.patch
@@ -0,0 +1,211 @@
+--- 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/VillagerTrades.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/VillagerTrades.java.patch
new file mode 100644
index 0000000000..a21a5e780e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/VillagerTrades.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/entity/npc/VillagerTrades.java
++++ b/net/minecraft/world/entity/npc/VillagerTrades.java
+@@ -1834,7 +1834,8 @@
+ return null;
+ } else {
+ ServerLevel serverLevel = (ServerLevel)entity.level();
+- BlockPos blockPos = serverLevel.findNearestMapStructure(this.destination, entity.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
+ 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/unapplied/net/minecraft/world/entity/npc/WanderingTrader.java.patch
new file mode 100644
index 0000000000..c3bde269c7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/WanderingTrader.java.patch
@@ -0,0 +1,80 @@
+--- a/net/minecraft/world/entity/npc/WanderingTrader.java
++++ b/net/minecraft/world/entity/npc/WanderingTrader.java
+@@ -48,25 +48,38 @@
+ import net.minecraft.world.phys.Vec3;
+ import org.apache.commons.lang3.tuple.Pair;
+
+-public class WanderingTrader extends AbstractVillager implements Consumable.OverrideConsumeSound {
++// CraftBukkit start
++import org.bukkit.Bukkit;
++import org.bukkit.craftbukkit.inventory.CraftMerchantRecipe;
++import org.bukkit.entity.AbstractVillager;
++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 {
++
+ private static final int NUMBER_OF_TRADE_OFFERS = 5;
+ @Nullable
+ private BlockPos wanderTarget;
+ private int despawnDelay;
++ // Paper start - Add more WanderingTrader API
++ public boolean canDrinkPotion = true;
++ 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
+ }));
+ 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);
++ // CraftBukkit start
++ VillagerAcquireTradeEvent event = new VillagerAcquireTradeEvent((AbstractVillager) this.getBukkitEntity(), merchantrecipe.asBukkit());
++ // Suppress during worldgen
++ if (this.valid) {
++ Bukkit.getPluginManager().callEvent(event);
++ }
++ if (!event.isCancelled()) {
++ merchantrecipelist.add(CraftMerchantRecipe.fromBukkit(event.getRecipe()).toMinecraft());
++ }
++ // CraftBukkit end
+ }
+
+ }
+@@ -190,7 +212,7 @@
+ 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
+ }
+
+ }
+@@ -244,7 +266,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/unapplied/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch
new file mode 100644
index 0000000000..5b4f2d10a7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/npc/WanderingTraderSpawner.java.patch
@@ -0,0 +1,100 @@
+--- 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
new file mode 100644
index 0000000000..be3c5cccdb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/player/Inventory.java.patch
@@ -0,0 +1,130 @@
+--- 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
new file mode 100644
index 0000000000..3f8a17b363
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/player/Player.java.patch
@@ -0,0 +1,735 @@
+--- 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/player/ProfilePublicKey.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/player/ProfilePublicKey.java.patch
new file mode 100644
index 0000000000..a8a26d91a8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/player/ProfilePublicKey.java.patch
@@ -0,0 +1,28 @@
+--- a/net/minecraft/world/entity/player/ProfilePublicKey.java
++++ b/net/minecraft/world/entity/player/ProfilePublicKey.java
+@@ -24,7 +24,7 @@
+
+ public static ProfilePublicKey createValidated(SignatureValidator servicesSignatureVerifier, UUID playerUuid, ProfilePublicKey.Data publicKeyData) throws ProfilePublicKey.ValidationException {
+ if (!publicKeyData.validateSignature(servicesSignatureVerifier, playerUuid)) {
+- 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);
+ }
+@@ -88,8 +88,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) {
++ // Paper start
++ this(messageText, org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN);
++ }
++ public ValidationException(Component messageText, org.bukkit.event.player.PlayerKickEvent.Cause kickCause) {
++ // Paper end
+ super(messageText);
++ this.kickCause = kickCause; // Paper
+ }
+ }
+ }
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
new file mode 100644
index 0000000000..d2cc2fa1a9
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/AbstractArrow.java.patch
@@ -0,0 +1,320 @@
+--- 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
new file mode 100644
index 0000000000..b53dd9f069
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/AbstractHurtingProjectile.java.patch
@@ -0,0 +1,38 @@
+--- 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
new file mode 100644
index 0000000000..c19bed3b4d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Arrow.java.patch
@@ -0,0 +1,20 @@
+--- 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
new file mode 100644
index 0000000000..6278d9b3bb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/DragonFireball.java.patch
@@ -0,0 +1,26 @@
+--- 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
new file mode 100644
index 0000000000..fc93ad07be
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/EvokerFangs.java.patch
@@ -0,0 +1,30 @@
+--- 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/EyeOfEnder.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/EyeOfEnder.java.patch
new file mode 100644
index 0000000000..67aa21fe00
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/EyeOfEnder.java.patch
@@ -0,0 +1,58 @@
+--- 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 @@
+ }
+
+ public void signalTo(BlockPos pos) {
++ // Paper start - Change EnderEye target without changing other things
++ this.signalTo(pos, true);
++ }
++ 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 @@
+ this.tz = d1;
+ }
+
++ if (update) { // Paper - Change EnderEye target without changing other things
+ this.life = 0;
+ this.surviveAfterDeath = this.random.nextInt(5) > 0;
++ } // Paper - Change EnderEye target without changing other things
+ }
+
+ @Override
+@@ -153,7 +163,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
+ if (this.surviveAfterDeath) {
+ this.level().addFreshEntity(new ItemEntity(this.level(), this.getX(), this.getY(), this.getZ(), this.getItem()));
+ } else {
+@@ -174,7 +184,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()));
++ // 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);
++ }
++ // CraftBukkit end
+ } else {
+ this.setItem(this.getDefaultItem());
+ }
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
new file mode 100644
index 0000000000..6cc358f744
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..91715aeb75
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/FireworkRocketEntity.java.patch
@@ -0,0 +1,132 @@
+--- 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
new file mode 100644
index 0000000000..27f0995455
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/FishingHook.java.patch
@@ -0,0 +1,352 @@
+--- 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
new file mode 100644
index 0000000000..494d25292d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/LargeFireball.java.patch
@@ -0,0 +1,56 @@
+--- 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
new file mode 100644
index 0000000000..370c83f5da
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/LlamaSpit.java.patch
@@ -0,0 +1,42 @@
+--- 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
new file mode 100644
index 0000000000..4e0aeff12d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Projectile.java.patch
@@ -0,0 +1,231 @@
+--- 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
new file mode 100644
index 0000000000..bac5f4b417
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ShulkerBullet.java.patch
@@ -0,0 +1,100 @@
+--- 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
new file mode 100644
index 0000000000..65522ba256
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/SmallFireball.java.patch
@@ -0,0 +1,63 @@
+--- 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
new file mode 100644
index 0000000000..8d2ce7e8d8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/Snowball.java.patch
@@ -0,0 +1,21 @@
+--- 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
new file mode 100644
index 0000000000..629d60da70
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/ThrowableItemProjectile.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java.patch
new file mode 100644
index 0000000000..6471dbc99e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java
++++ b/net/minecraft/world/entity/projectile/ThrowableItemProjectile.java
+@@ -34,6 +34,12 @@
+
+ protected abstract Item getDefaultItem();
+
++ // CraftBukkit start
++ public Item getDefaultItemPublic() {
++ return this.getDefaultItem();
++ }
++ // CraftBukkit end
++
+ @Override
+ public ItemStack getItem() {
+ return (ItemStack) this.getEntityData().get(ThrowableItemProjectile.DATA_ITEM_STACK);
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
new file mode 100644
index 0000000000..db8a43fd04
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..5df84b160a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownEgg.java.patch
@@ -0,0 +1,121 @@
+--- 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
new file mode 100644
index 0000000000..f24cfeb088
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownEnderpearl.java.patch
@@ -0,0 +1,95 @@
+--- 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
new file mode 100644
index 0000000000..40cd201036
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownExperienceBottle.java.patch
@@ -0,0 +1,36 @@
+--- 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
new file mode 100644
index 0000000000..babee054f3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownPotion.java.patch
@@ -0,0 +1,322 @@
+--- 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
new file mode 100644
index 0000000000..fff7300b9e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/ThrownTrident.java.patch
@@ -0,0 +1,86 @@
+--- 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
new file mode 100644
index 0000000000..96234163f1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/WitherSkull.java.patch
@@ -0,0 +1,55 @@
+--- 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
new file mode 100644
index 0000000000..01844591e6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/projectile/windcharge/AbstractWindCharge.java.patch
@@ -0,0 +1,48 @@
+--- 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/Raid.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raid.java.patch
new file mode 100644
index 0000000000..789e719f42
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raid.java.patch
@@ -0,0 +1,174 @@
+--- a/net/minecraft/world/entity/raid/Raid.java
++++ b/net/minecraft/world/entity/raid/Raid.java
+@@ -107,6 +107,11 @@
+ private Raid.RaidStatus status;
+ private int celebrationTicks;
+ private Optional<BlockPos> waveSpawnPos;
++ // 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));
+ }
+ }
++ // Paper start
++ if (nbt.contains(PDC_NBT_KEY, net.minecraft.nbt.Tag.TAG_COMPOUND)) {
++ this.persistentDataContainer.putAll(nbt.getCompound(PDC_NBT_KEY));
++ }
++ // Paper end
+
+ }
+
+@@ -177,6 +187,12 @@
+ return this.status == Raid.RaidStatus.LOSS;
+ }
+
++ // CraftBukkit start
++ public boolean isInProgress() {
++ return this.status == RaidStatus.ONGOING;
++ }
++ // CraftBukkit end
++
+ public float getTotalHealth() {
+ return this.totalHealth;
+ }
+@@ -281,6 +297,7 @@
+
+ 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 @@
+ if (!this.level.isVillage(this.center)) {
+ if (this.groupsSpawned > 0) {
+ this.status = Raid.RaidStatus.LOSS;
++ org.bukkit.craftbukkit.event.CraftEventFactory.callRaidFinishEvent(this, new java.util.ArrayList<>()); // CraftBukkit
+ } else {
++ org.bukkit.craftbukkit.event.CraftEventFactory.callRaidStopEvent(this, org.bukkit.event.raid.RaidStopEvent.Reason.NOT_IN_VILLAGE); // CraftBukkit
+ this.stop();
+ }
+ }
+
+ ++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 @@
+ }
+
+ if (j > 5) {
++ org.bukkit.craftbukkit.event.CraftEventFactory.callRaidStopEvent(this, org.bukkit.event.raid.RaidStopEvent.Reason.UNSPAWNABLE); // CraftBukkit
+ this.stop();
+ break;
+ }
+@@ -386,6 +407,7 @@
+ 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();
+ 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
+ }
+ }
+ }
+ }
++ org.bukkit.craftbukkit.event.CraftEventFactory.callRaidFinishEvent(this, winners); // CraftBukkit
+ }
+ }
+
+@@ -411,6 +435,7 @@
+ } else if (this.isOver()) {
+ ++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;
+
++ // 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
+ }
+
+ 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.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);
+ 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 @@
+ }
+
+ nbt.put("HeroesOfTheVillage", nbttaglist);
++ // Paper start
++ if (!this.persistentDataContainer.isEmpty()) {
++ nbt.put(PDC_NBT_KEY, this.persistentDataContainer.toTagCompound());
++ }
++ // Paper end
+ return nbt;
+ }
+
+@@ -865,6 +903,12 @@
+ this.heroesOfTheVillage.add(entity.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;
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
new file mode 100644
index 0000000000..7f33d0dc93
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raider.java.patch
@@ -0,0 +1,83 @@
+--- 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/raid/Raids.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raids.java.patch
new file mode 100644
index 0000000000..b787b60fa1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/raid/Raids.java.patch
@@ -0,0 +1,27 @@
+--- 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);
+
++ /* 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)) {
++ player.removeEffect(net.minecraft.world.effect.MobEffects.RAID_OMEN);
++ return null;
++ }
++
++ if (!raid.isStarted() && !this.raidMap.containsKey(raid.getId())) {
++ this.raidMap.put(raid.getId(), raid);
++ }
++ // CraftBukkit end
+ raid.absorbRaidOmen(player);
+ }
+
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
new file mode 100644
index 0000000000..28b20b3791
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractBoat.java.patch
@@ -0,0 +1,125 @@
+--- 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
new file mode 100644
index 0000000000..f23f862b79
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractChestBoat.java.patch
@@ -0,0 +1,121 @@
+--- 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/AbstractMinecart.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch
new file mode 100644
index 0000000000..e7b99cb727
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecart.java.patch
@@ -0,0 +1,196 @@
+--- 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));
+ });
+
++ // CraftBukkit start
++ public boolean slowWhenEmpty = true;
++ private double derailedX = 0.5;
++ private double derailedY = 0.5;
++ private double derailedZ = 0.5;
++ private double flyingX = 0.95;
++ 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 @@
+
+ @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 @@
+
+ @Override
+ public boolean canCollideWith(Entity other) {
+- return AbstractBoat.canVehicleCollide(this, other);
++ // Paper start - fix VehicleEntityCollisionEvent not called when colliding with player
++ boolean collides = AbstractBoat.canVehicleCollide(this, other);
++ if (!collides) {
++ return false;
++ }
++ org.bukkit.event.vehicle.VehicleEntityCollisionEvent collisionEvent = new org.bukkit.event.vehicle.VehicleEntityCollisionEvent((org.bukkit.entity.Vehicle) getBukkitEntity(), other.getBukkitEntity());
++
++ return collisionEvent.callEvent();
++ // Paper end - fix VehicleEntityCollisionEvent not called when colliding with player
+ }
+
+ @Override
+- public boolean isPushable() {
++ public boolean isCollidable(boolean ignoreClimbing) { // Paper - Climbing should not bypass cramming gamerule
+ return true;
+ }
+
+@@ -262,6 +289,14 @@
+
+ @Override
+ public void tick() {
++ // CraftBukkit start
++ double prevX = this.getX();
++ double prevY = this.getY();
++ double prevZ = this.getZ();
++ float prevYaw = this.getYRot();
++ float prevPitch = this.getXRot();
++ // CraftBukkit end
++
+ if (this.getHurtTime() > 0) {
+ this.setHurtTime(this.getHurtTime() - 1);
+ }
+@@ -271,8 +306,20 @@
+ }
+
+ this.checkBelowWorld();
+- this.handlePortal();
++ // this.handlePortal(); // CraftBukkit - handled in postTick
+ 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();
++
++ this.level().getCraftServer().getPluginManager().callEvent(new org.bukkit.event.vehicle.VehicleUpdateEvent(vehicle));
++
++ if (!from.equals(to)) {
++ this.level().getCraftServer().getPluginManager().callEvent(new org.bukkit.event.vehicle.VehicleMoveEvent(vehicle, from, to));
++ }
++ // 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));
+ if (this.onGround()) {
+- this.setDeltaMovement(this.getDeltaMovement().scale(0.5D));
++ // 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
+ }
+
+ this.move(MoverType.SELF, this.getDeltaMovement());
+ if (!this.onGround()) {
+- this.setDeltaMovement(this.getDeltaMovement().scale(0.95D));
++ // 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");
++ // 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
+ }
+
+ @Override
+@@ -514,13 +575,28 @@
+
+ nbt.putBoolean("FlippedRotation", this.flipped);
+ nbt.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());
++ }
++ // Paper end - Friction API
+ }
+
+ @Override
+ public void push(Entity entity) {
+ if (!this.level().isClientSide) {
+ if (!entity.noPhysics && !this.noPhysics) {
++ 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;
++ }
++ // CraftBukkit end
+ double d0 = entity.getX() - this.getX();
+ double d1 = entity.getZ() - this.getZ();
+ double d2 = d0 * d0 + d1 * d1;
+@@ -645,4 +721,27 @@
+ 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 void setFlyingVelocityMod(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 void setDerailedVelocityMod(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/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch
new file mode 100644
index 0000000000..b33136b346
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java.patch
@@ -0,0 +1,98 @@
+--- 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/ContainerEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch
new file mode 100644
index 0000000000..3ac071ee7d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/ContainerEntity.java.patch
@@ -0,0 +1,81 @@
+--- 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) {
+ if (this.getContainerLootTable() != null) {
+ nbt.putString("LootTable", this.getContainerLootTable().location().toString());
++ this.lootableData().saveNbt(nbt); // Paper
+ if (this.getContainerLootTableSeed() != 0L) {
+ nbt.putLong("LootTableSeed", this.getContainerLootTableSeed());
+ }
+- } else {
+- ContainerHelper.saveAllItems(nbt, this.getItemStacks(), registries);
+ }
++ ContainerHelper.saveAllItems(nbt, this.getItemStacks(), registries); // Paper - always save the items, table may still remain
+ }
+
+ default void readChestVehicleSaveData(CompoundTag nbt, HolderLookup.Provider registries) {
+ this.clearItemStacks();
+ if (nbt.contains("LootTable", 8)) {
+- this.setContainerLootTable(ResourceKey.create(Registries.LOOT_TABLE, ResourceLocation.parse(nbt.getString("LootTable"))));
++ this.setContainerLootTable(net.minecraft.Optionull.map(ResourceLocation.tryParse(nbt.getString("LootTable")), rl -> ResourceKey.create(Registries.LOOT_TABLE, rl))); // Paper - Validate ResourceLocation
++ // Paper start - LootTable API
++ if (this.getContainerLootTable() != null) {
++ this.lootableData().loadNbt(nbt);
++ }
++ // Paper end - LootTable API
+ this.setContainerLootTableSeed(nbt.getLong("LootTableSeed"));
+- } else {
+- ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registries);
+ }
++ ContainerHelper.loadAllItems(nbt, this.getItemStacks(), registries); // Paper - always save the items, table may still remain
+ }
+
+ default void chestVehicleDestroyed(DamageSource source, ServerLevel world, Entity vehicle) {
+@@ -91,19 +95,28 @@
+ }
+
+ default InteractionResult interactWithContainerVehicle(Player player) {
+- player.openMenu(this);
++ // Paper start - Fix InventoryOpenEvent cancellation
++ if (player.openMenu(this).isEmpty()) {
++ return InteractionResult.PASS;
++ }
++ // Paper end - Fix InventoryOpenEvent cancellation
+ return InteractionResult.SUCCESS;
+ }
+
+ default void unpackChestVehicleLootTable(@Nullable Player player) {
+ MinecraftServer minecraftServer = this.level().getServer();
+- if (this.getContainerLootTable() != null && minecraftServer != null) {
++ if (minecraftServer != null && this.lootableData().shouldReplenish(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.ENTITY, player)) { // Paper - LootTable API
+ LootTable lootTable = minecraftServer.reloadableRegistries().getLootTable(this.getContainerLootTable());
+ if (player != null) {
+ CriteriaTriggers.GENERATE_LOOT.trigger((ServerPlayer)player, this.getContainerLootTable());
+ }
+
+- this.setContainerLootTable(null);
++ // Paper start - LootTable API
++ if (this.lootableData().shouldClearLootTable(this, com.destroystokyo.paper.loottable.PaperLootableInventoryData.ENTITY, player)) {
++ this.setContainerLootTable(null);
++ }
++ // Paper end - LootTable API
++
+ LootParams.Builder builder = new LootParams.Builder((ServerLevel)this.level()).withParameter(LootContextParams.ORIGIN, this.position());
+ if (player != null) {
+ builder.withLuck(player.getLuck()).withParameter(LootContextParams.THIS_ENTITY, player);
+@@ -173,4 +186,14 @@
+ default boolean isChestVehicleStillValid(Player player) {
+ return !this.isRemoved() && player.canInteractWithEntity(this.getBoundingBox(), 4.0);
+ }
++
++ // Paper start - LootTable API
++ default com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ throw new UnsupportedOperationException("Implement this method");
++ }
++
++ default com.destroystokyo.paper.loottable.PaperLootableInventory getLootableInventory() {
++ return ((com.destroystokyo.paper.loottable.PaperLootableInventory) ((net.minecraft.world.entity.Entity) this).getBukkitEntity());
++ }
++ // Paper end - LootTable API
+ }
diff --git a/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
new file mode 100644
index 0000000000..7c789e80ad
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/MinecartCommandBlock.java.patch
@@ -0,0 +1,23 @@
+--- 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
new file mode 100644
index 0000000000..7fdf216d8a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/MinecartTNT.java.patch
@@ -0,0 +1,53 @@
+--- 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
new file mode 100644
index 0000000000..8bed619b55
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/NewMinecartBehavior.java.patch
@@ -0,0 +1,83 @@
+--- 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
new file mode 100644
index 0000000000..5f791f5901
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/OldMinecartBehavior.java.patch
@@ -0,0 +1,75 @@
+--- 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
new file mode 100644
index 0000000000..10fce15f79
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/entity/vehicle/VehicleEntity.java.patch
@@ -0,0 +1,64 @@
+--- 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/FoodData.java.patch b/paper-server/patches/unapplied/net/minecraft/world/food/FoodData.java.patch
new file mode 100644
index 0000000000..68a35b8aa4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/food/FoodData.java.patch
@@ -0,0 +1,101 @@
+--- 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 @@
+ public float saturationLevel = 5.0F;
+ public float exhaustionLevel;
+ private int tickTimer;
++ // CraftBukkit start
++ public int saturatedRegenRate = 10;
++ public int unsaturatedRegenRate = 80;
++ public int starvationRate = 80;
++ // CraftBukkit end
+
+ public FoodData() {}
+
+@@ -29,6 +37,20 @@
+ this.add(foodComponent.nutrition(), foodComponent.saturation());
+ }
+
++ // CraftBukkit start
++ public void eat(FoodProperties foodinfo, ItemStack itemstack, ServerPlayer entityplayer) {
++ int oldFoodLevel = this.foodLevel;
++
++ org.bukkit.event.entity.FoodLevelChangeEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callFoodLevelChangeEvent(entityplayer, foodinfo.nutrition() + oldFoodLevel, itemstack);
++
++ if (!event.isCancelled()) {
++ this.add(event.getFoodLevel() - oldFoodLevel, foodinfo.saturation());
++ }
++
++ entityplayer.getBukkitEntity().sendHealthUpdate();
++ }
++ // CraftBukkit end
++
+ public void tick(ServerPlayer player) {
+ ServerLevel worldserver = player.serverLevel();
+ Difficulty enumdifficulty = worldserver.getDifficulty();
+@@ -38,7 +60,15 @@
+ if (this.saturationLevel > 0.0F) {
+ this.saturationLevel = Math.max(this.saturationLevel - 1.0F, 0.0F);
+ } else if (enumdifficulty != 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));
++
++ if (!event.isCancelled()) {
++ this.foodLevel = event.getFoodLevel();
++ }
++
++ player.connection.send(new 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;
+- 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
+ this.tickTimer = 0;
+ }
+ } else if (flag && this.foodLevel >= 18 && player.isHurt()) {
+ ++this.tickTimer;
+- if (this.tickTimer >= 80) {
+- player.heal(1.0F);
+- this.addExhaustion(6.0F);
++ if (this.tickTimer >= this.unsaturatedRegenRate) { // CraftBukkit - add regen rate manipulation
++ player.heal(1.0F, org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason.SATIATED); // CraftBukkit - added RegainReason
++ // this.addExhaustion(6.0F); CraftBukkit - EntityExhaustionEvent
++ player.causeFoodExhaustion(player.level().spigotConfig.regenExhaustion, org.bukkit.event.entity.EntityExhaustionEvent.ExhaustionReason.REGEN); // CraftBukkit - EntityExhaustionEvent // Spigot - Change to use configurable value
+ this.tickTimer = 0;
+ }
+ } else if (this.foodLevel <= 0) {
+ ++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);
+ }
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
new file mode 100644
index 0000000000..a8c85757de
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/food/FoodProperties.java.patch
@@ -0,0 +1,19 @@
+--- 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
new file mode 100644
index 0000000000..a0c2f5bf6b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractContainerMenu.java.patch
@@ -0,0 +1,310 @@
+--- 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
new file mode 100644
index 0000000000..bda213246a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractCraftingMenu.java.patch
@@ -0,0 +1,44 @@
+--- 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
new file mode 100644
index 0000000000..42a568418f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/AbstractFurnaceMenu.java.patch
@@ -0,0 +1,60 @@
+--- 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/AnvilMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/AnvilMenu.java.patch
new file mode 100644
index 0000000000..1c6f48d25b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/AnvilMenu.java.patch
@@ -0,0 +1,166 @@
+--- 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 @@
+ 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;
++ // 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 @@
+
+ @Override
+ protected boolean mayPickup(Player player, boolean present) {
+- 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
+ }
+
+ @Override
+@@ -94,7 +104,7 @@
+ 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);
+
++ // 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);
++ if (!event.callEvent()) {
++ return;
++ } else if (event.getDamageState() == com.destroystokyo.paper.event.block.AnvilDamagedEvent.DamageState.BROKEN) {
++ iblockdata1 = null;
++ } else {
++ iblockdata1 = ((org.bukkit.craftbukkit.block.data.CraftBlockData) event.getDamageState().getMaterial().createBlockData()).getState().setValue(AnvilBlock.FACING, iblockdata.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) {
+- this.resultSlots.setItem(0, ItemStack.EMPTY);
+- this.cost.set(0);
++ org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), ItemStack.EMPTY); // CraftBukkit
++ this.cost.set(AnvilMenu.DEFAULT_DENIED_COST); // CraftBukkit - use a variable for set a cost for denied item
+ return;
+ }
+
+@@ -158,8 +178,8 @@
+ this.repairItemCountCost = i1;
+ } else {
+ if (!flag && (!itemstack1.is(itemstack2.getItem()) || !itemstack1.isDamageableItem())) {
+- this.resultSlots.setItem(0, ItemStack.EMPTY);
+- this.cost.set(0);
++ org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), ItemStack.EMPTY); // CraftBukkit
++ this.cost.set(AnvilMenu.DEFAULT_DENIED_COST); // CraftBukkit - use a variable for set a cost for denied item
+ return;
+ }
+
+@@ -214,7 +234,7 @@
+ flag2 = true;
+ } else {
+ flag1 = true;
+- if (i2 > enchantment.getMaxLevel()) {
++ if (i2 > enchantment.getMaxLevel() && !this.bypassEnchantmentLevelRestriction) { // Paper - bypass anvil level restrictions
+ i2 = enchantment.getMaxLevel();
+ }
+
+@@ -233,8 +253,8 @@
+ }
+
+ if (flag2 && !flag1) {
+- this.resultSlots.setItem(0, ItemStack.EMPTY);
+- this.cost.set(0);
++ org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), ItemStack.EMPTY); // CraftBukkit
++ this.cost.set(AnvilMenu.DEFAULT_DENIED_COST); // CraftBukkit - use a variable for set a cost for denied item
+ return;
+ }
+ }
+@@ -260,14 +280,14 @@
+ }
+
+ if (b0 == i && b0 > 0) {
+- if (this.cost.get() >= 40) {
+- this.cost.set(39);
++ if (this.cost.get() >= this.maximumRepairCost) { // CraftBukkit
++ this.cost.set(this.maximumRepairCost - 1); // CraftBukkit
+ }
+
+ this.onlyRenaming = true;
+ }
+
+- if (this.cost.get() >= 40 && !this.player.getAbilities().instabuild) {
++ if (this.cost.get() >= this.maximumRepairCost && !this.player.getAbilities().instabuild) { // CraftBukkit
+ itemstack1 = ItemStack.EMPTY;
+ }
+
+@@ -285,12 +305,13 @@
+ EnchantmentHelper.setEnchantments(itemstack1, itemenchantments_a.toImmutable());
+ }
+
+- this.resultSlots.setItem(0, itemstack1);
++ org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), itemstack1); // CraftBukkit
+ this.broadcastChanges();
+ } else {
+- this.resultSlots.setItem(0, ItemStack.EMPTY);
+- this.cost.set(0);
++ org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareAnvilEvent(this.getBukkitView(), ItemStack.EMPTY); // CraftBukkit
++ this.cost.set(AnvilMenu.DEFAULT_DENIED_COST); // CraftBukkit - use a variable for set a cost for denied item
+ }
++ 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 @@
+ }
+
+ this.createResult();
++ org.bukkit.craftbukkit.event.CraftEventFactory.callPrepareResultEvent(this, RESULT_SLOT); // Paper - Add PrepareResultEvent
+ return true;
+ } else {
+ return false;
+@@ -329,4 +351,19 @@
+ public int getCost() {
+ return this.cost.get();
+ }
++
++ // CraftBukkit start
++ @Override
++ public 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.updateFromLegacy(inventory);
++ return this.bukkitEntity;
++ }
++ // CraftBukkit end
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/inventory/BeaconMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/BeaconMenu.java.patch
new file mode 100644
index 0000000000..587f354b49
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/BeaconMenu.java.patch
@@ -0,0 +1,109 @@
+--- 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;
+ private final ContainerLevelAccess access;
+ private final ContainerData beaconData;
++ // CraftBukkit start
++ private CraftBeaconView bukkitEntity = null;
++ private 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 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;
+ }
++ // Paper start - Fix inventories returning null Locations
++ @Override
++ public org.bukkit.Location getLocation() {
++ return context.getLocation();
++ }
++ // Paper end - Fix inventories returning null Locations
+ };
+ checkContainerDataCount(propertyDelegate, 3);
+ this.beaconData = propertyDelegate;
+@@ -69,6 +83,7 @@
+
+ @Override
+ public boolean stillValid(Player player) {
++ if (!this.checkReachable) return true; // CraftBukkit
+ return stillValid(this.access, player, Blocks.BEACON);
+ }
+
+@@ -148,12 +163,30 @@
+ return BeaconMenu.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) {
++ // 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();
++ }
++ // 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)));
++ // 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());
++ 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
++ this.beaconData.set(2, BeaconMenu.encodeEffect(event.getSecondary() == null ? null : org.bukkit.craftbukkit.potion.CraftPotionEffectType.bukkitToMinecraftHolder(event.getSecondary())));// CraftBukkit - decompile error
++ if (event.willConsumeItem()) { // Paper
+ this.paymentSlot.remove(1);
++ } // Paper
+ this.access.execute(Level::blockEntityChanged);
++ } // Paper end - Add PlayerChangeBeaconEffectEvent
+ }
+
+ }
+@@ -178,4 +211,17 @@
+ return 1;
+ }
+ }
++
++ // CraftBukkit start
++ @Override
++ public 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);
++ return this.bukkitEntity;
++ }
++ // CraftBukkit end
+ }
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
new file mode 100644
index 0000000000..36dcdb7277
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/BrewingStandMenu.java.patch
@@ -0,0 +1,127 @@
+--- 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
new file mode 100644
index 0000000000..b46cd6b42c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/CartographyTableMenu.java.patch
@@ -0,0 +1,146 @@
+--- 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
new file mode 100644
index 0000000000..8ba0e1a6a1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/ChestMenu.java.patch
@@ -0,0 +1,64 @@
+--- 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/ContainerLevelAccess.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerLevelAccess.java.patch
new file mode 100644
index 0000000000..3202223f24
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerLevelAccess.java.patch
@@ -0,0 +1,69 @@
+--- a/net/minecraft/world/inventory/ContainerLevelAccess.java
++++ b/net/minecraft/world/inventory/ContainerLevelAccess.java
+@@ -8,16 +8,66 @@
+
+ public interface ContainerLevelAccess {
+
++ // CraftBukkit start
++ default Level getWorld() {
++ throw new UnsupportedOperationException("Not supported yet.");
++ }
++
++ default BlockPos getPosition() {
++ throw new UnsupportedOperationException("Not supported yet.");
++ }
++
++ default org.bukkit.Location getLocation() {
++ return new org.bukkit.Location(this.getWorld().getWorld(), this.getPosition().getX(), this.getPosition().getY(), this.getPosition().getZ());
++ }
++ // CraftBukkit end
++ // Paper start - Add missing InventoryHolders
++ default boolean isBlock() {
++ return false;
++ }
++
++ default [email protected] BlockInventoryHolder createBlockHolder(AbstractContainerMenu menu) {
++ if (!this.isBlock()) {
++ return null;
++ }
++ 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/unapplied/net/minecraft/world/inventory/ContainerListener.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerListener.java.patch
new file mode 100644
index 0000000000..7d9e714dfe
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerListener.java.patch
@@ -0,0 +1,14 @@
+--- 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
new file mode 100644
index 0000000000..a76ea56e99
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/ContainerSynchronizer.java.patch
@@ -0,0 +1,10 @@
+--- 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
new file mode 100644
index 0000000000..e998f9ae45
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/CrafterMenu.java.patch
@@ -0,0 +1,38 @@
+--- 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
new file mode 100644
index 0000000000..0c5babe735
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/CraftingContainer.java.patch
@@ -0,0 +1,29 @@
+--- 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
new file mode 100644
index 0000000000..9f0941d40b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/CraftingMenu.java.patch
@@ -0,0 +1,74 @@
+--- 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
new file mode 100644
index 0000000000..18ed7c2049
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/DispenserMenu.java.patch
@@ -0,0 +1,62 @@
+--- 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
new file mode 100644
index 0000000000..531e6792cc
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/EnchantmentMenu.java.patch
@@ -0,0 +1,270 @@
+--- 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
new file mode 100644
index 0000000000..488614cbf6
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..0ba3910b94
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/GrindstoneMenu.java.patch
@@ -0,0 +1,141 @@
+--- 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
new file mode 100644
index 0000000000..2174c38e81
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/HopperMenu.java.patch
@@ -0,0 +1,51 @@
+--- 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
new file mode 100644
index 0000000000..2657cf4ccb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/HorseInventoryMenu.java.patch
@@ -0,0 +1,53 @@
+--- 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
new file mode 100644
index 0000000000..78a5d31f7d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/InventoryMenu.java.patch
@@ -0,0 +1,64 @@
+--- 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/ItemCombinerMenu.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/ItemCombinerMenu.java.patch
new file mode 100644
index 0000000000..7d50fbdafb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/ItemCombinerMenu.java.patch
@@ -0,0 +1,65 @@
+--- a/net/minecraft/world/inventory/ItemCombinerMenu.java
++++ b/net/minecraft/world/inventory/ItemCombinerMenu.java
+@@ -17,12 +17,7 @@
+ protected final ContainerLevelAccess access;
+ protected final Player player;
+ protected final Container inputSlots;
+- protected final ResultContainer resultSlots = new ResultContainer() {
+- @Override
+- public void setChanged() {
+- ItemCombinerMenu.this.slotsChanged(this);
+- }
+- };
++ 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;
++ // Paper start - Add missing InventoryHolders; delay field init
++ this.resultSlots = new ResultContainer(this.createBlockHolder(this.access)) {
++ @Override
++ public void setChanged() {
++ ItemCombinerMenu.this.slotsChanged(this);
++ }
++ };
++ // 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 @@
+ public abstract void createResult();
+
+ private SimpleContainer createContainer(int size) {
+- return new SimpleContainer(size) {
++ return new SimpleContainer(this.createBlockHolder(this.access), size) {
+ @Override
+ public void setChanged() {
+ super.setChanged();
+@@ -96,6 +99,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 @@
+
+ @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);
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
new file mode 100644
index 0000000000..667c188455
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/LecternMenu.java.patch
@@ -0,0 +1,143 @@
+--- 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
new file mode 100644
index 0000000000..1d534db5e0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/LoomMenu.java.patch
@@ -0,0 +1,203 @@
+--- 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
new file mode 100644
index 0000000000..ac3dc03175
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..82bbe5e4c7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantContainer.java.patch
@@ -0,0 +1,70 @@
+--- 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
new file mode 100644
index 0000000000..9fecebd95a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantMenu.java.patch
@@ -0,0 +1,92 @@
+--- 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/MerchantResultSlot.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantResultSlot.java.patch
new file mode 100644
index 0000000000..f363fb2ef5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/MerchantResultSlot.java.patch
@@ -0,0 +1,37 @@
+--- a/net/minecraft/world/inventory/MerchantResultSlot.java
++++ b/net/minecraft/world/inventory/MerchantResultSlot.java
+@@ -47,13 +47,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();
++ // 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 (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);
++ } 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);
++ }
++ if (event != null) {
++ if (!event.callEvent()) {
++ stack.setCount(0);
++ event.getPlayer().updateInventory();
++ return;
++ }
++ merchantOffer = 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
+ player.awardStat(Stats.TRADED_WITH_VILLAGER);
+ this.slots.setItem(0, itemStack);
+ this.slots.setItem(1, itemStack2);
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
new file mode 100644
index 0000000000..e83e892d8e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/PlayerEnderChestContainer.java.patch
@@ -0,0 +1,36 @@
+--- 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/ResultContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/inventory/ResultContainer.java.patch
new file mode 100644
index 0000000000..a26bc470cf
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/ResultContainer.java.patch
@@ -0,0 +1,67 @@
+--- 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;
+ @Nullable
+ private RecipeHolder<?> recipeUsed;
+
++ // CraftBukkit start
++ private int maxStack = MAX_STACK;
++
++ public java.util.List<ItemStack> getContents() {
++ return this.itemStacks;
++ }
++
++ public org.bukkit.inventory.InventoryHolder getOwner() {
++ // Paper start - Add missing InventoryHolders
++ if (this.holder == null && this.holderCreator != null) {
++ this.holder = this.holderCreator.get();
++ }
++ return this.holder; // Result slots don't get an owner
++ // Paper end - Add missing InventoryHolders
++ }
++
++ // 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>();
++ }
++
++ @Override
++ public int getMaxStackSize() {
++ return this.maxStack;
++ }
++
++ public void setMaxStackSize(int size) {
++ this.maxStack = size;
++ }
++
++ @Override
++ public Location getLocation() {
++ return null;
++ }
++ // CraftBukkit end
++ // Paper start - Add missing InventoryHolders
++ 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);
+ }
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
new file mode 100644
index 0000000000..4efe4724bc
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/ShulkerBoxMenu.java.patch
@@ -0,0 +1,49 @@
+--- 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
new file mode 100644
index 0000000000..46bc8ef32c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/SmithingMenu.java.patch
@@ -0,0 +1,66 @@
+--- 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
new file mode 100644
index 0000000000..95cd1f3220
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/StonecutterMenu.java.patch
@@ -0,0 +1,188 @@
+--- 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
new file mode 100644
index 0000000000..67ed6a0fb4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/inventory/TransientCraftingContainer.java.patch
@@ -0,0 +1,93 @@
+--- 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
new file mode 100644
index 0000000000..5be546c2b6
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/AxeItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/AxeItem.java.patch
new file mode 100644
index 0000000000..858ea3302d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/AxeItem.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/item/AxeItem.java
++++ b/net/minecraft/world/item/AxeItem.java
+@@ -67,6 +67,11 @@
+ return InteractionResult.PASS;
+ } else {
+ ItemStack itemStack = context.getItemInHand();
++ // Paper start - EntityChangeBlockEvent
++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, blockPos, optional.get())) {
++ return InteractionResult.PASS;
++ }
++ // Paper end
+ if (player instanceof ServerPlayer) {
+ CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger((ServerPlayer)player, blockPos, itemStack);
+ }
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
new file mode 100644
index 0000000000..52e43aa9e4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/BlockItem.java.patch
@@ -0,0 +1,109 @@
+--- 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
new file mode 100644
index 0000000000..c8502fcea3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/BoatItem.java.patch
@@ -0,0 +1,33 @@
+--- 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
new file mode 100644
index 0000000000..4cf7f7eebf
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/BoneMealItem.java.patch
@@ -0,0 +1,41 @@
+--- 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
new file mode 100644
index 0000000000..6e51878d93
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/BucketItem.java.patch
@@ -0,0 +1,168 @@
+--- 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
new file mode 100644
index 0000000000..d6f5a8f499
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/CrossbowItem.java.patch
@@ -0,0 +1,48 @@
+--- 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
new file mode 100644
index 0000000000..b81a2063de
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/DebugStickItem.java.patch
@@ -0,0 +1,25 @@
+--- 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
new file mode 100644
index 0000000000..ca96f3388a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/DyeItem.java.patch
@@ -0,0 +1,29 @@
+--- 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
new file mode 100644
index 0000000000..6281e950e3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/EggItem.java.patch
@@ -0,0 +1,40 @@
+--- 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
new file mode 100644
index 0000000000..89844fdef2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/EndCrystalItem.java.patch
@@ -0,0 +1,31 @@
+--- 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
new file mode 100644
index 0000000000..c343069562
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..e025880d4f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/EnderpearlItem.java.patch
@@ -0,0 +1,39 @@
+--- 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
new file mode 100644
index 0000000000..e16430df8d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/ExperienceBottleItem.java.patch
@@ -0,0 +1,54 @@
+--- 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
new file mode 100644
index 0000000000..ee72808622
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..9d8f816a26
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/FireworkRocketItem.java.patch
@@ -0,0 +1,51 @@
+--- 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
new file mode 100644
index 0000000000..6c64321f95
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/FishingRodItem.java.patch
@@ -0,0 +1,50 @@
+--- 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
new file mode 100644
index 0000000000..0d633ff507
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..79e498f9a5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/HangingEntityItem.java.patch
@@ -0,0 +1,67 @@
+--- 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/HoneycombItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/HoneycombItem.java.patch
new file mode 100644
index 0000000000..02d0f745ad
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/HoneycombItem.java.patch
@@ -0,0 +1,17 @@
+--- a/net/minecraft/world/item/HoneycombItem.java
++++ b/net/minecraft/world/item/HoneycombItem.java
+@@ -74,6 +74,14 @@
+ return getWaxed(blockState).map(state -> {
+ Player player = context.getPlayer();
+ ItemStack itemStack = context.getItemInHand();
++ // Paper start - EntityChangeBlockEvent
++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(player, blockPos, state)) {
++ if (!player.isCreative()) {
++ player.containerMenu.sendAllDataToRemote();
++ }
++ return InteractionResult.PASS;
++ }
++ // Paper end
+ if (player instanceof ServerPlayer serverPlayer) {
+ CriteriaTriggers.ITEM_USED_ON_BLOCK.trigger(serverPlayer, blockPos, itemStack);
+ }
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
new file mode 100644
index 0000000000..f8d61e11c7
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..4409d94819
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/ItemStack.java.patch
@@ -0,0 +1,630 @@
+--- 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
new file mode 100644
index 0000000000..163c53a0d4
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..e45ad43774
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/LeadItem.java.patch
@@ -0,0 +1,92 @@
+--- 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/LingeringPotionItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/LingeringPotionItem.java.patch
new file mode 100644
index 0000000000..04fd231da8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/LingeringPotionItem.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/item/LingeringPotionItem.java
++++ b/net/minecraft/world/item/LingeringPotionItem.java
+@@ -24,6 +24,10 @@
+
+ @Override
+ public InteractionResult use(Level world, Player user, InteractionHand hand) {
++ // Paper start - PlayerLaunchProjectileEvent
++ final InteractionResult wrapper = super.use(world, user, hand);
++ if (wrapper instanceof InteractionResult.Fail) return wrapper;
++ // Paper end - PlayerLaunchProjectileEvent
+ world.playSound(
+ null,
+ user.getX(),
+@@ -34,6 +38,6 @@
+ 0.5F,
+ 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F)
+ );
+- return super.use(world, user, hand);
++ return wrapper; // Paper - PlayerLaunchProjectileEvent
+ }
+ }
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
new file mode 100644
index 0000000000..5374da2aa0
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..f97004f5f9
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/NameTagItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/NameTagItem.java.patch
new file mode 100644
index 0000000000..735436752e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/NameTagItem.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/world/item/NameTagItem.java
++++ b/net/minecraft/world/item/NameTagItem.java
+@@ -18,8 +18,13 @@
+ 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) {
++ // 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);
++ 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
+ mob.setPersistenceRequired();
+ }
+
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
new file mode 100644
index 0000000000..185bcf8992
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..dfdb8d27c6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/ProjectileWeaponItem.java.patch
@@ -0,0 +1,52 @@
+--- 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/ServerItemCooldowns.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/ServerItemCooldowns.java.patch
new file mode 100644
index 0000000000..b9e17d595b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/ServerItemCooldowns.java.patch
@@ -0,0 +1,43 @@
+--- a/net/minecraft/world/item/ServerItemCooldowns.java
++++ b/net/minecraft/world/item/ServerItemCooldowns.java
+@@ -11,7 +11,40 @@
+ this.player = player;
+ }
+
++ // Paper start - Add PlayerItemCooldownEvent
+ @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(
++ this.player.getBukkitEntity(),
++ org.bukkit.craftbukkit.inventory.CraftItemType.minecraftToBukkit(item.getItem()),
++ org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(cooldownGroup),
++ duration
++ );
++ if (event.callEvent()) {
++ super.addCooldown(cooldownGroup, event.getCooldown(), false);
++ }
++ }
++
++ @Override
++ public void addCooldown(ResourceLocation groupId, int duration, boolean callEvent) {
++ if (callEvent) {
++ final io.papermc.paper.event.player.PlayerItemGroupCooldownEvent event = new io.papermc.paper.event.player.PlayerItemGroupCooldownEvent(
++ this.player.getBukkitEntity(),
++ org.bukkit.craftbukkit.util.CraftNamespacedKey.fromMinecraft(groupId),
++ duration
++ );
++ if (!event.callEvent()) {
++ return;
++ }
++
++ duration = event.getCooldown();
++ }
++ super.addCooldown(groupId, duration, false);
++ }
++ // Paper end - Add PlayerItemCooldownEvent
++
++ @Override
+ protected void onCooldownStarted(ResourceLocation groupId, int duration) {
+ super.onCooldownStarted(groupId, duration);
+ this.player.connection.send(new ClientboundCooldownPacket(groupId, duration));
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
new file mode 100644
index 0000000000..0840475938
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..50c0cdf7d1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/SignItem.java.patch
@@ -0,0 +1,23 @@
+--- 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
new file mode 100644
index 0000000000..bb5a1788d6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/SnowballItem.java.patch
@@ -0,0 +1,37 @@
+--- 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
new file mode 100644
index 0000000000..a33a635c7a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/SpawnEggItem.java.patch
@@ -0,0 +1,24 @@
+--- 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/SplashPotionItem.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/SplashPotionItem.java.patch
new file mode 100644
index 0000000000..bdf8aca4d5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/SplashPotionItem.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/item/SplashPotionItem.java
++++ b/net/minecraft/world/item/SplashPotionItem.java
+@@ -14,6 +14,10 @@
+
+ @Override
+ public InteractionResult use(Level world, Player user, InteractionHand hand) {
++ // Paper start - PlayerLaunchProjectileEvent
++ final InteractionResult wrapper = super.use(world, user, hand);
++ if (wrapper instanceof InteractionResult.Fail) return wrapper;
++ // Paper end - PlayerLaunchProjectileEvent
+ world.playSound(
+ null,
+ user.getX(),
+@@ -24,6 +28,6 @@
+ 0.5F,
+ 0.4F / (world.getRandom().nextFloat() * 0.4F + 0.8F)
+ );
+- return super.use(world, user, hand);
++ return wrapper; // Paper - PlayerLaunchProjectileEvent
+ }
+ }
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
new file mode 100644
index 0000000000..9e1a576a36
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/StandingAndWallBlockItem.java.patch
@@ -0,0 +1,41 @@
+--- 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
new file mode 100644
index 0000000000..13c480eabd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/ThrowablePotionItem.java.patch
@@ -0,0 +1,34 @@
+--- 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
new file mode 100644
index 0000000000..a23995cfe3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/TridentItem.java.patch
@@ -0,0 +1,51 @@
+--- 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
new file mode 100644
index 0000000000..8fd7be1352
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/WindChargeItem.java.patch
@@ -0,0 +1,42 @@
+--- 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
new file mode 100644
index 0000000000..f29d9ca860
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/PotionBrewing.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionBrewing.java.patch
new file mode 100644
index 0000000000..c9c0b22f57
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionBrewing.java.patch
@@ -0,0 +1,96 @@
+--- a/net/minecraft/world/item/alchemy/PotionBrewing.java
++++ b/net/minecraft/world/item/alchemy/PotionBrewing.java
+@@ -19,6 +19,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 @@
+ }
+
+ public boolean isIngredient(ItemStack stack) {
+- return this.isContainerIngredient(stack) || this.isPotionIngredient(stack);
++ return this.isContainerIngredient(stack) || this.isPotionIngredient(stack) || this.isCustomIngredient(stack); // Paper - Custom Potion Mixes
+ }
+
+ private boolean isContainer(ItemStack stack) {
+@@ -71,6 +72,11 @@
+ }
+
+ public boolean hasMix(ItemStack input, ItemStack ingredient) {
++ // Paper start - Custom Potion Mixes
++ if (this.hasCustomMix(input, ingredient)) {
++ return true;
++ }
++ // Paper end - Custom Potion Mixes
+ return this.isContainer(input) && (this.hasContainerMix(input, ingredient) || this.hasPotionMix(input, ingredient));
+ }
+
+@@ -103,6 +109,13 @@
+ if (input.isEmpty()) {
+ return input;
+ } 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)) {
++ return mix.result().copy();
++ }
++ }
++ // Paper end - Custom Potion Mixes
+ Optional<Holder<Potion>> optional = input.getOrDefault(DataComponents.POTION_CONTENTS, PotionContents.EMPTY).potion();
+ if (optional.isEmpty()) {
+ return input;
+@@ -190,6 +203,50 @@
+ 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()) {
++ if (mix.ingredient().test(stack)) {
++ return true;
++ }
++ }
++ return false;
++ }
++
++ public boolean isCustomInput(ItemStack stack) {
++ for (io.papermc.paper.potion.PaperPotionMix mix : this.customMixes.values()) {
++ if (mix.input().test(stack)) {
++ return true;
++ }
++ }
++ return false;
++ }
++
++ private boolean hasCustomMix(ItemStack input, ItemStack ingredient) {
++ for (io.papermc.paper.potion.PaperPotionMix mix : this.customMixes.values()) {
++ if (mix.input().test(input) && mix.ingredient().test(ingredient)) {
++ return true;
++ }
++ }
++ return false;
++ }
++
++ public void addPotionMix(io.papermc.paper.potion.PotionMix mix) {
++ if (this.customMixes.containsKey(mix.getKey())) {
++ throw new IllegalArgumentException("Duplicate recipe ignored with ID " + mix.getKey());
++ }
++ this.customMixes.putAndMoveToFirst(mix.getKey(), new io.papermc.paper.potion.PaperPotionMix(mix));
++ }
++
++ public boolean removePotionMix(org.bukkit.NamespacedKey key) {
++ return this.customMixes.remove(key) != null;
++ }
++
++ public PotionBrewing reload(FeatureFlagSet flags) {
++ 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/unapplied/net/minecraft/world/item/alchemy/PotionContents.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionContents.java.patch
new file mode 100644
index 0000000000..163fbb5056
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/alchemy/PotionContents.java.patch
@@ -0,0 +1,20 @@
+--- 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
new file mode 100644
index 0000000000..a163ec68c0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/component/Consumable.java.patch
@@ -0,0 +1,53 @@
+--- 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
new file mode 100644
index 0000000000..2e31354cba
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/CustomData.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/component/CustomData.java.patch
new file mode 100644
index 0000000000..73089c0170
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/component/CustomData.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/item/component/CustomData.java
++++ b/net/minecraft/world/item/component/CustomData.java
+@@ -34,7 +34,17 @@
+ private static final Logger LOGGER = LogUtils.getLogger();
+ public static final CustomData EMPTY = new CustomData(new CompoundTag());
+ private static final String TYPE_TAG = "id";
+- public static final Codec<CustomData> CODEC = Codec.withAlternative(CompoundTag.CODEC, TagParser.AS_CODEC)
++ // Paper start - Item serialization as json
++ public static ThreadLocal<Boolean> SERIALIZE_CUSTOM_AS_SNBT = ThreadLocal.withInitial(() -> false);
++ public static final Codec<CustomData> CODEC = Codec.either(CompoundTag.CODEC, TagParser.AS_CODEC)
++ .xmap(com.mojang.datafixers.util.Either::unwrap, data -> { // Both will be used for deserialization, but we decide which one to use for serialization
++ if (!SERIALIZE_CUSTOM_AS_SNBT.get()) {
++ return com.mojang.datafixers.util.Either.left(data); // First codec
++ } else {
++ return com.mojang.datafixers.util.Either.right(data); // Second codec
++ }
++ })
++ // Paper end - Item serialization as json
+ .xmap(CustomData::new, component -> component.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)
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
new file mode 100644
index 0000000000..8516839b42
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/component/DeathProtection.java.patch
@@ -0,0 +1,22 @@
+--- 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/LodestoneTracker.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/component/LodestoneTracker.java.patch
new file mode 100644
index 0000000000..79d33fc6bf
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/component/LodestoneTracker.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/item/component/LodestoneTracker.java
++++ b/net/minecraft/world/item/component/LodestoneTracker.java
+@@ -29,7 +29,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
+ ? this
+ : new LodestoneTracker(Optional.empty(), true);
+ }
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
new file mode 100644
index 0000000000..57338edcbf
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/component/OminousBottleAmplifier.java.patch
@@ -0,0 +1,18 @@
+--- 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/ResolvableProfile.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/component/ResolvableProfile.java.patch
new file mode 100644
index 0000000000..c0d2bc1d79
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/component/ResolvableProfile.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/world/item/component/ResolvableProfile.java
++++ b/net/minecraft/world/item/component/ResolvableProfile.java
+@@ -20,9 +20,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
+ );
+ 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 @@
+ if (this.isResolved()) {
+ return CompletableFuture.completedFuture(this);
+ } else {
+- return this.id.isPresent() ? SkullBlockEntity.fetchGameProfile(this.id.get()).thenApply(optional -> {
++ 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 -> {
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
new file mode 100644
index 0000000000..2836856f9c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/component/SuspiciousStewEffects.java.patch
@@ -0,0 +1,28 @@
+--- 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
new file mode 100644
index 0000000000..2551be5478
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ApplyStatusEffectsConsumeEffect.java.patch
@@ -0,0 +1,32 @@
+--- 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
new file mode 100644
index 0000000000..35168c5339
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ClearAllStatusEffectsConsumeEffect.java.patch
@@ -0,0 +1,24 @@
+--- 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
new file mode 100644
index 0000000000..c5d56842c7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/ConsumeEffect.java.patch
@@ -0,0 +1,30 @@
+--- 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
new file mode 100644
index 0000000000..c5eacaef69
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/RemoveStatusEffectsConsumeEffect.java.patch
@@ -0,0 +1,29 @@
+--- 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
new file mode 100644
index 0000000000..b419363bd1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/consume_effects/TeleportRandomlyConsumeEffect.java.patch
@@ -0,0 +1,20 @@
+--- 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
new file mode 100644
index 0000000000..26a37bbc25
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/BlastingRecipe.java.patch
@@ -0,0 +1,35 @@
+--- 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
new file mode 100644
index 0000000000..039e214990
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/CampfireCookingRecipe.java.patch
@@ -0,0 +1,35 @@
+--- 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
new file mode 100644
index 0000000000..773c84fb8b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/CustomRecipe.java.patch
@@ -0,0 +1,54 @@
+--- 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
new file mode 100644
index 0000000000..1d00386837
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/Ingredient.java.patch
@@ -0,0 +1,66 @@
+--- 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/Recipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/Recipe.java.patch
new file mode 100644
index 0000000000..ad97a94316
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/Recipe.java.patch
@@ -0,0 +1,9 @@
+--- a/net/minecraft/world/item/crafting/Recipe.java
++++ b/net/minecraft/world/item/crafting/Recipe.java
+@@ -44,4 +44,6 @@
+ }
+
+ RecipeBookCategory recipeBookCategory();
++
++ org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id); // CraftBukkit
+ }
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
new file mode 100644
index 0000000000..818a4d266e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeHolder.java.patch
@@ -0,0 +1,26 @@
+--- 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
new file mode 100644
index 0000000000..83a2caa10c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeManager.java.patch
@@ -0,0 +1,111 @@
+--- 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
new file mode 100644
index 0000000000..171f3d8e88
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/RecipeMap.java.patch
@@ -0,0 +1,72 @@
+--- 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/ShapedRecipe.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/ShapedRecipe.java.patch
new file mode 100644
index 0000000000..05fee5cb09
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/ShapedRecipe.java.patch
@@ -0,0 +1,86 @@
+--- 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);
+ }
+
++ // CraftBukkit start
+ @Override
++ public org.bukkit.inventory.ShapedRecipe toBukkitRecipe(NamespacedKey id) {
++ CraftItemStack result = CraftItemStack.asCraftMirror(this.result);
++ CraftShapedRecipe recipe = new CraftShapedRecipe(id, result, this);
++ recipe.setGroup(this.group);
++ recipe.setCategory(CraftRecipe.getCategory(this.category()));
++
++ switch (this.pattern.height()) {
++ case 1:
++ switch (this.pattern.width()) {
++ case 1:
++ recipe.shape("a");
++ break;
++ case 2:
++ recipe.shape("ab");
++ break;
++ case 3:
++ recipe.shape("abc");
++ break;
++ }
++ break;
++ case 2:
++ switch (this.pattern.width()) {
++ case 1:
++ recipe.shape("a","b");
++ break;
++ case 2:
++ recipe.shape("ab","cd");
++ break;
++ case 3:
++ recipe.shape("abc","def");
++ break;
++ }
++ break;
++ case 3:
++ switch (this.pattern.width()) {
++ case 1:
++ recipe.shape("a","b","c");
++ break;
++ case 2:
++ recipe.shape("ab","cd","ef");
++ break;
++ case 3:
++ recipe.shape("abc","def","ghi");
++ break;
++ }
++ break;
++ }
++ char c = 'a';
++ for (Optional<Ingredient> list : this.pattern.ingredients()) {
++ RecipeChoice choice = CraftRecipe.toBukkit(list);
++ if (choice != RecipeChoice.empty()) { // Paper
++ recipe.setIngredient(c, choice);
++ }
++
++ c++;
++ }
++ return recipe;
++ }
++ // CraftBukkit end
++
++ @Override
+ public RecipeSerializer<? extends ShapedRecipe> getSerializer() {
+ return RecipeSerializer.SHAPED_RECIPE;
+ }
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
new file mode 100644
index 0000000000..167de63903
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/ShapelessRecipe.java.patch
@@ -0,0 +1,39 @@
+--- 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
new file mode 100644
index 0000000000..4b26fa911a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmeltingRecipe.java.patch
@@ -0,0 +1,35 @@
+--- 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
new file mode 100644
index 0000000000..173f836106
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmithingTransformRecipe.java.patch
@@ -0,0 +1,61 @@
+--- 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
new file mode 100644
index 0000000000..a5e660382a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmithingTrimRecipe.java.patch
@@ -0,0 +1,69 @@
+--- 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
new file mode 100644
index 0000000000..7c099a0fca
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/SmokingRecipe.java.patch
@@ -0,0 +1,35 @@
+--- 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
new file mode 100644
index 0000000000..fd7dc9145c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/StonecutterRecipe.java.patch
@@ -0,0 +1,34 @@
+--- 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
new file mode 100644
index 0000000000..7cc6f6f6d9
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/crafting/TransmuteRecipe.java.patch
@@ -0,0 +1,31 @@
+--- 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/ItemEnchantments.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/ItemEnchantments.java.patch
new file mode 100644
index 0000000000..519f69ad9b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/ItemEnchantments.java.patch
@@ -0,0 +1,60 @@
+--- 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;
+ import net.minecraft.world.item.component.TooltipProvider;
++// Paper start
++import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap;
++// Paper end
+
+ public class ItemEnchantments implements TooltipProvider {
+- public static final ItemEnchantments EMPTY = new ItemEnchantments(new Object2IntOpenHashMap<>(), true);
++ // Paper start
++ private static final java.util.Comparator<Holder<Enchantment>> ENCHANTMENT_ORDER = java.util.Comparator.comparing(Holder::getRegisteredName);
++ public static final ItemEnchantments EMPTY = new ItemEnchantments(new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER), true);
++ // Paper end
+ private static final Codec<Integer> LEVEL_CODEC = Codec.intRange(1, 255);
+- private static final Codec<Object2IntOpenHashMap<Holder<Enchantment>>> LEVELS_CODEC = Codec.unboundedMap(Enchantment.CODEC, LEVEL_CODEC)
+- .xmap(Object2IntOpenHashMap::new, Function.identity());
++ private static final Codec<Object2IntAVLTreeMap<Holder<Enchantment>>> LEVELS_CODEC = Codec.unboundedMap(
++ Enchantment.CODEC, LEVEL_CODEC
++ )// Paper start - sort enchantments
++ .xmap(m -> {
++ final Object2IntAVLTreeMap<Holder<Enchantment>> map = new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER);
++ map.putAll(m);
++ return map;
++ }, Function.identity());
++ // Paper end - sort enchantments
+ private static final Codec<ItemEnchantments> FULL_CODEC = RecordCodecBuilder.create(
+ instance -> instance.group(
+ LEVELS_CODEC.fieldOf("levels").forGetter(component -> component.enchantments),
+@@ -41,16 +54,16 @@
+ );
+ public static final Codec<ItemEnchantments> CODEC = Codec.withAlternative(FULL_CODEC, LEVELS_CODEC, map -> new ItemEnchantments(map, true));
+ public static final StreamCodec<RegistryFriendlyByteBuf, ItemEnchantments> STREAM_CODEC = StreamCodec.composite(
+- ByteBufCodecs.map(Object2IntOpenHashMap::new, Enchantment.STREAM_CODEC, ByteBufCodecs.VAR_INT),
++ ByteBufCodecs.map((v) -> new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER), Enchantment.STREAM_CODEC, ByteBufCodecs.VAR_INT),
+ component -> component.enchantments,
+ ByteBufCodecs.BOOL,
+ component -> component.showInTooltip,
+ ItemEnchantments::new
+ );
+- final Object2IntOpenHashMap<Holder<Enchantment>> enchantments;
++ final Object2IntAVLTreeMap<Holder<Enchantment>> enchantments; // Paper
+ public final boolean showInTooltip;
+
+- ItemEnchantments(Object2IntOpenHashMap<Holder<Enchantment>> enchantments, boolean showInTooltip) {
++ ItemEnchantments(Object2IntAVLTreeMap<Holder<Enchantment>> enchantments, boolean showInTooltip) { // Paper
+ this.enchantments = enchantments;
+ this.showInTooltip = showInTooltip;
+
+@@ -139,7 +152,7 @@
+ }
+
+ public static class Mutable {
+- private final Object2IntOpenHashMap<Holder<Enchantment>> enchantments = new Object2IntOpenHashMap<>();
++ private final Object2IntAVLTreeMap<Holder<Enchantment>> enchantments = new Object2IntAVLTreeMap<>(ENCHANTMENT_ORDER); // Paper
+ public boolean showInTooltip;
+
+ public Mutable(ItemEnchantments enchantmentsComponent) {
diff --git a/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
new file mode 100644
index 0000000000..abe8774935
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..711eebddb2
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..80efcc0872
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/Ignite.java.patch
@@ -0,0 +1,36 @@
+--- 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
new file mode 100644
index 0000000000..78dc09a7de
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..2a0ad9e549
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..8ae0ac6d6e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/enchantment/effects/SummonEntityEffect.java.patch
@@ -0,0 +1,35 @@
+--- 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/Merchant.java.patch b/paper-server/patches/unapplied/net/minecraft/world/item/trading/Merchant.java.patch
new file mode 100644
index 0000000000..a8626b121e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/trading/Merchant.java.patch
@@ -0,0 +1,17 @@
+--- a/net/minecraft/world/item/trading/Merchant.java
++++ b/net/minecraft/world/item/trading/Merchant.java
+@@ -20,6 +20,7 @@
+
+ void overrideOffers(MerchantOffers offers);
+
++ default void processTrade(MerchantOffer merchantRecipe, @Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { this.notifyTrade(merchantRecipe); } // Paper
+ void notifyTrade(MerchantOffer offer);
+
+ void notifyTradeUpdated(ItemStack stack);
+@@ -54,4 +55,6 @@
+ boolean isClientSide();
+
+ boolean stillValid(Player player);
++
++ org.bukkit.craftbukkit.inventory.CraftMerchant getCraftMerchant(); // CraftBukkit
+ }
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
new file mode 100644
index 0000000000..bf956ac4ae
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/item/trading/MerchantOffer.java.patch
@@ -0,0 +1,90 @@
+--- 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/BaseCommandBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/BaseCommandBlock.java.patch
new file mode 100644
index 0000000000..3c091f17a7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/BaseCommandBlock.java.patch
@@ -0,0 +1,39 @@
+--- a/net/minecraft/world/level/BaseCommandBlock.java
++++ b/net/minecraft/world/level/BaseCommandBlock.java
+@@ -33,6 +33,10 @@
+ private String command = "";
+ @Nullable
+ private Component customName;
++ // CraftBukkit start
++ @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 @@
+ @Override
+ public void sendSystemMessage(Component message) {
+ 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 @@
+ }
+
+ public InteractionResult usedBy(Player player) {
+- if (!player.canUseGameMasterBlocks()) {
++ if (!player.canUseGameMasterBlocks() && (!player.isCreative() || !player.getBukkitEntity().hasPermission("minecraft.commandblock"))) { // Paper - command block permission
+ return InteractionResult.PASS;
+ } else {
+ if (player.getCommandSenderWorld().isClientSide) {
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
new file mode 100644
index 0000000000..c836ef12e6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/BaseSpawner.java.patch
@@ -0,0 +1,167 @@
+--- 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
new file mode 100644
index 0000000000..b3ea51c1e2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/BlockGetter.java.patch
@@ -0,0 +1,90 @@
+--- 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/ChunkPos.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/ChunkPos.java.patch
new file mode 100644
index 0000000000..be7a951951
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/ChunkPos.java.patch
@@ -0,0 +1,39 @@
+--- a/net/minecraft/world/level/ChunkPos.java
++++ b/net/minecraft/world/level/ChunkPos.java
+@@ -46,6 +46,7 @@
+ public static final int REGION_MAX_INDEX = 31;
+ public final int x;
+ public final int z;
++ public final long longKey; // Paper
+ private static final int HASH_A = 1664525;
+ private static final int HASH_C = 1013904223;
+ private static final int HASH_Z_XOR = -559038737;
+@@ -53,16 +54,19 @@
+ public ChunkPos(int x, int z) {
+ this.x = x;
+ this.z = z;
++ this.longKey = asLong(this.x, this.z); // Paper
+ }
+
+ public ChunkPos(BlockPos pos) {
+ this.x = SectionPos.blockToSectionCoord(pos.getX());
+ this.z = SectionPos.blockToSectionCoord(pos.getZ());
++ this.longKey = asLong(this.x, this.z); // Paper
+ }
+
+ public ChunkPos(long pos) {
+ this.x = (int)pos;
+ this.z = (int)(pos >> 32);
++ this.longKey = asLong(this.x, this.z); // Paper
+ }
+
+ public static ChunkPos minFromRegion(int x, int z) {
+@@ -74,7 +78,7 @@
+ }
+
+ public long toLong() {
+- return asLong(this.x, this.z);
++ return longKey; // Paper
+ }
+
+ public static long asLong(int chunkX, int chunkZ) {
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
new file mode 100644
index 0000000000..20d8ff1d94
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/ClipContext.java.patch
@@ -0,0 +1,20 @@
+--- 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/EmptyBlockGetter.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/EmptyBlockGetter.java.patch
new file mode 100644
index 0000000000..b4743ab862
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/EmptyBlockGetter.java.patch
@@ -0,0 +1,22 @@
+--- a/net/minecraft/world/level/EmptyBlockGetter.java
++++ b/net/minecraft/world/level/EmptyBlockGetter.java
+@@ -17,7 +17,19 @@
+ return null;
+ }
+
++ // Paper start - If loaded util
+ @Override
++ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++ return Fluids.EMPTY.defaultFluidState();
++ }
++
++ @Override
++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
++ return Blocks.AIR.defaultBlockState();
++ }
++ // Paper end
++
++ @Override
+ public BlockState getBlockState(BlockPos pos) {
+ return Blocks.AIR.defaultBlockState();
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/EntityGetter.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/EntityGetter.java.patch
new file mode 100644
index 0000000000..09a6133595
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/EntityGetter.java.patch
@@ -0,0 +1,76 @@
+--- a/net/minecraft/world/level/EntityGetter.java
++++ b/net/minecraft/world/level/EntityGetter.java
+@@ -71,6 +71,11 @@
+ }
+ }
+
++ // Paper start - Affects Spawning API
++ default @Nullable Player findNearbyPlayer(Entity entity, double maxDistance, @Nullable Predicate<Entity> predicate) {
++ return this.getNearestPlayer(entity.getX(), entity.getY(), entity.getZ(), maxDistance, predicate);
++ }
++ // Paper end - Affects Spawning API
+ @Nullable
+ default Player getNearestPlayer(double x, double y, double z, double maxDistance, @Nullable Predicate<Entity> targetPredicate) {
+ double d = -1.0;
+@@ -89,6 +94,28 @@
+ return player;
+ }
+
++ // Paper start
++ default List<org.bukkit.entity.HumanEntity> findNearbyBukkitPlayers(double x, double y, double z, double radius, boolean notSpectator) {
++ return findNearbyBukkitPlayers(x, y, z, radius, notSpectator ? EntitySelector.NO_SPECTATORS : net.minecraft.world.entity.EntitySelector.NO_CREATIVE_OR_SPECTATOR);
++ }
++
++ default List<org.bukkit.entity.HumanEntity> findNearbyBukkitPlayers(double x, double y, double z, double radius, @Nullable Predicate<Entity> predicate) {
++ com.google.common.collect.ImmutableList.Builder<org.bukkit.entity.HumanEntity> builder = com.google.common.collect.ImmutableList.builder();
++
++ for (Player human : this.players()) {
++ if (predicate == null || predicate.test(human)) {
++ double distanceSquared = human.distanceToSqr(x, y, z);
++
++ if (radius < 0.0D || distanceSquared < radius * radius) {
++ builder.add(human.getBukkitEntity());
++ }
++ }
++ }
++
++ return builder.build();
++ }
++ // 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);
+ }
+
++ // Paper start - Affects Spawning API
++ default boolean hasNearbyAlivePlayerThatAffectsSpawning(double x, double y, double z, double range) {
++ for (Player player : this.players()) {
++ if (EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) { // combines NO_SPECTATORS and LIVING_ENTITY_STILL_ALIVE with an "affects spawning" check
++ double distanceSqr = player.distanceToSqr(x, y, z);
++ if (range < 0.0D || distanceSqr < range * range) {
++ return true;
++ }
++ }
++ }
++ return false;
++ }
++ // Paper end - Affects Spawning API
++
+ default boolean hasNearbyAlivePlayer(double x, double y, double z, double range) {
+ for (Player player : this.players()) {
+ if (EntitySelector.NO_SPECTATORS.test(player) && EntitySelector.LIVING_ENTITY_STILL_ALIVE.test(player)) {
+@@ -124,4 +165,11 @@
+
+ return null;
+ }
++
++ // Paper start - check global player list where appropriate
++ @Nullable
++ default Player getGlobalPlayerByUUID(UUID uuid) {
++ return this.getPlayerByUUID(uuid);
++ }
++ // Paper end - check global player list where appropriate
+ }
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
new file mode 100644
index 0000000000..13a583766e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/GameRules.java.patch
@@ -0,0 +1,321 @@
+--- 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/Level.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/Level.java.patch
new file mode 100644
index 0000000000..6a3ac30d97
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/Level.java.patch
@@ -0,0 +1,710 @@
+--- a/net/minecraft/world/level/Level.java
++++ b/net/minecraft/world/level/Level.java
+@@ -25,8 +25,10 @@
+ import net.minecraft.network.protocol.Packet;
+ import net.minecraft.resources.ResourceKey;
+ import net.minecraft.resources.ResourceLocation;
++import io.papermc.paper.util.MCUtil;
+ import net.minecraft.server.MinecraftServer;
+ import net.minecraft.server.level.FullChunkStatus;
++import net.minecraft.server.level.ServerLevel;
+ 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 @@
+ import net.minecraft.world.phys.Vec3;
+ import net.minecraft.world.scores.Scoreboard;
+
++// CraftBukkit start
++import java.util.HashMap;
++import java.util.Map;
++import net.minecraft.network.protocol.game.ClientboundSetBorderCenterPacket;
++import net.minecraft.network.protocol.game.ClientboundSetBorderLerpSizePacket;
++import net.minecraft.network.protocol.game.ClientboundSetBorderSizePacket;
++import net.minecraft.network.protocol.game.ClientboundSetBorderWarningDelayPacket;
++import net.minecraft.network.protocol.game.ClientboundSetBorderWarningDistancePacket;
++import org.bukkit.Bukkit;
++import org.bukkit.craftbukkit.CraftServer;
++import org.bukkit.craftbukkit.CraftWorld;
++import org.bukkit.craftbukkit.block.CapturedBlockState;
++import org.bukkit.craftbukkit.block.CraftBlockState;
++import org.bukkit.craftbukkit.block.data.CraftBlockData;
++import org.bukkit.craftbukkit.util.CraftSpawnCategory;
++import org.bukkit.entity.SpawnCategory;
++import org.bukkit.event.block.BlockPhysicsEvent;
++// 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 int TICKS_PER_DAY = 24000;
+ public static final int MAX_ENTITY_SPAWN_Y = 20000000;
+ public static final int MIN_ENTITY_SPAWN_Y = -20000000;
+- protected final List<TickingBlockEntity> blockEntityTickers = Lists.newArrayList();
++ public final List<TickingBlockEntity> blockEntityTickers = Lists.newArrayList(); // Paper - public
+ protected final NeighborUpdater neighborUpdater;
+ private final List<TickingBlockEntity> pendingBlockEntityTickers = Lists.newArrayList();
+ private boolean tickingBlockEntities;
+@@ -121,23 +145,91 @@
+ 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 final it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<SpawnCategory> ticksPerSpawnCategory = new it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap<>();
++ public boolean populating;
++ public final org.spigotmc.SpigotWorldConfig spigotConfig; // Spigot
++ // Paper start - add paper world config
++ private final io.papermc.paper.configuration.WorldConfiguration paperConfig;
++ public io.papermc.paper.configuration.WorldConfiguration paperConfig() {
++ return this.paperConfig;
++ }
++ // Paper end - add paper world config
++
++ public static BlockPos lastPhysicsProblem; // Spigot
++ private org.spigotmc.TickLimiter entityLimiter;
++ private org.spigotmc.TickLimiter tileLimiter;
++ private int tileTickPosition;
++ public final Map<ServerExplosion.CacheKey, Float> explosionDensityCache = new HashMap<>(); // Paper - Optimize explosions
++ public java.util.ArrayDeque<net.minecraft.world.level.block.RedstoneTorchBlock.Toggle> redstoneUpdateInfos; // Paper - Faster redstone torch rapid clock removal; Move from Map in BlockRedstoneTorch to here
++
++ public CraftWorld getWorld() {
++ return this.world;
++ }
++
++ public CraftServer getCraftServer() {
++ return (CraftServer) Bukkit.getServer();
++ }
++ // Paper start - Use getChunkIfLoadedImmediately
++ @Override
++ public boolean hasChunk(int chunkX, int chunkZ) {
++ return this.getChunkIfLoaded(chunkX, chunkZ) != null;
++ }
++ // Paper end - Use getChunkIfLoadedImmediately
++ // Paper start - per world ticks per spawn
++ private int getTicksPerSpawn(SpawnCategory spawnCategory) {
++ final int perWorld = this.paperConfig().entities.spawning.ticksPerSpawn.getInt(CraftSpawnCategory.toNMS(spawnCategory));
++ if (perWorld >= 0) {
++ return perWorld;
++ }
++ return this.getCraftServer().getTicksPerSpawns(spawnCategory);
++ }
++ // Paper end
++
++
++ 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
++ this.paperConfig = paperWorldConfigCreator.apply(this.spigotConfig); // Paper - create paper world config
++ this.generator = gen;
++ this.world = new CraftWorld((ServerLevel) this, gen, biomeProvider, env);
++
++ // CraftBukkit Ticks things
++ for (SpawnCategory spawnCategory : SpawnCategory.values()) {
++ if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
++ this.ticksPerSpawnCategory.put(spawnCategory, this.getTicksPerSpawn(spawnCategory)); // Paper
++ }
++ }
++
++ // 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
+ @Override
+ public double getCenterX() {
+- return super.getCenterX() / dimensionmanager.coordinateScale();
++ return super.getCenterX(); // CraftBukkit
+ }
+
+ @Override
+ public double getCenterZ() {
+- return super.getCenterZ() / dimensionmanager.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);
++ // CraftBukkit start
++ this.getWorldBorder().world = (ServerLevel) this;
++ // From PlayerList.setPlayerFileData
++ this.getWorldBorder().addListener(new BorderChangeListener() {
++ @Override
++ public void onBorderSizeSet(WorldBorder border, double size) {
++ Level.this.getCraftServer().getHandle().broadcastAll(new ClientboundSetBorderSizePacket(border), border.world);
++ }
++
++ @Override
++ public void onBorderSizeLerping(WorldBorder border, double fromSize, double toSize, long time) {
++ Level.this.getCraftServer().getHandle().broadcastAll(new ClientboundSetBorderLerpSizePacket(border), border.world);
++ }
++
++ @Override
++ public void onBorderCenterSet(WorldBorder border, double centerX, double centerZ) {
++ Level.this.getCraftServer().getHandle().broadcastAll(new ClientboundSetBorderCenterPacket(border), border.world);
++ }
++
++ @Override
++ public void onBorderSetWarningTime(WorldBorder border, int warningTime) {
++ Level.this.getCraftServer().getHandle().broadcastAll(new ClientboundSetBorderWarningDelayPacket(border), border.world);
++ }
++
++ @Override
++ public void onBorderSetWarningBlocks(WorldBorder border, int warningBlockDistance) {
++ Level.this.getCraftServer().getHandle().broadcastAll(new ClientboundSetBorderWarningDistancePacket(border), border.world);
++ }
++
++ @Override
++ public void onBorderSetDamagePerBlock(WorldBorder border, double damagePerBlock) {}
++
++ @Override
++ public void onBorderSetDamageSafeZOne(WorldBorder border, double safeZoneRadius) {}
++ });
++ // 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,
++ BlockPos position, boolean checkCanSee) {
++ // Copied from IWorldReader#a(IBlockData, BlockPosition, VoxelShapeCollision) & EntityAccess#a(Entity, VoxelShape)
++ net.minecraft.world.phys.shapes.VoxelShape voxelshape = data.getCollisionShape(this, position, voxelshapedcollision);
++ if (voxelshape.isEmpty()) {
++ return true;
++ }
++
++ voxelshape = voxelshape.move((double) position.getX(), (double) position.getY(), (double) position.getZ());
++ if (voxelshape.isEmpty()) {
++ return true;
++ }
++
++ List<Entity> entities = this.getEntities(null, voxelshape.bounds());
++ for (int i = 0, len = entities.size(); i < len; ++i) {
++ Entity entity = entities.get(i);
++
++ if (checkCanSee && source instanceof net.minecraft.server.level.ServerPlayer && entity instanceof net.minecraft.server.level.ServerPlayer
++ && !((net.minecraft.server.level.ServerPlayer) source).getBukkitEntity().canSee(((net.minecraft.server.level.ServerPlayer) entity).getBukkitEntity())) {
++ continue;
++ }
++
++ // !entity1.dead && entity1.i && (entity == null || !entity1.x(entity));
++ // elide the last check since vanilla calls with entity = null
++ // only we care about the source for the canSee check
++ if (entity.isRemoved() || !entity.blocksBuilding) {
++ continue;
++ }
++
++ if (net.minecraft.world.phys.shapes.Shapes.joinIsNotEmpty(voxelshape, net.minecraft.world.phys.shapes.Shapes.create(entity.getBoundingBox()), net.minecraft.world.phys.shapes.BooleanOp.AND)) {
++ return false;
++ }
++ }
++
++ return true;
++ }
++ // Paper end - Cancel hit for vanished players
+ @Override
+ public boolean isClientSide() {
+ return this.isClientSide;
+@@ -163,6 +332,13 @@
+ return null;
+ }
+
++ // Paper start
++ public net.minecraft.world.phys.BlockHitResult.Type clipDirect(Vec3 start, Vec3 end, net.minecraft.world.phys.shapes.CollisionContext context) {
++ // To be patched over
++ return this.clip(new ClipContext(start, end, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, context)).getType();
++ }
++ // Paper end
++
+ public boolean isInWorldBounds(BlockPos pos) {
+ return !this.isOutsideBuildHeight(pos) && Level.isInWorldBoundsHorizontal(pos);
+ }
+@@ -172,25 +348,87 @@
+ }
+
+ 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()
+ }
+
+ private static boolean isOutsideSpawnableHeight(int y) {
+ return y < -20000000 || y >= 20000000;
+ }
+
+- public LevelChunk getChunkAt(BlockPos pos) {
++ public final LevelChunk getChunkAt(BlockPos pos) { // Paper - help inline
+ return this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
+ }
+
+ @Override
+- public LevelChunk getChunk(int chunkX, int chunkZ) {
+- return (LevelChunk) this.getChunk(chunkX, chunkZ, ChunkStatus.FULL);
++ public final LevelChunk getChunk(int chunkX, int chunkZ) { // Paper - final to help inline
++ // Paper start - Perf: make sure loaded chunks get the inlined variant of this function
++ net.minecraft.server.level.ServerChunkCache cps = ((ServerLevel)this).getChunkSource();
++ LevelChunk ifLoaded = cps.getChunkAtIfLoadedImmediately(chunkX, chunkZ);
++ if (ifLoaded != null) {
++ return ifLoaded;
++ }
++ 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
++ public final ChunkAccess getChunkIfLoadedImmediately(int x, int z) {
++ return ((ServerLevel)this).chunkSource.getChunkAtIfLoadedImmediately(x, z);
++ }
++
++ @Override
++ @Nullable
++ public final BlockState getBlockStateIfLoaded(BlockPos pos) {
++ // CraftBukkit start - tree generation
++ if (this.captureTreeGeneration) {
++ CraftBlockState previous = this.capturedBlockStates.get(pos);
++ if (previous != null) {
++ return previous.getHandle();
++ }
++ }
++ // CraftBukkit end
++ if (this.isOutsideBuildHeight(pos)) {
++ return Blocks.VOID_AIR.defaultBlockState();
++ } else {
++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);
++
++ return chunk == null ? null : chunk.getBlockState(pos);
++ }
++ }
++
++ @Override
++ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++ ChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++
++ return chunk == null ? null : chunk.getFluidState(blockposition);
++ }
++
++ @Override
++ public final boolean hasChunkAt(BlockPos pos) {
++ return getChunkIfLoaded(pos.getX() >> 4, pos.getZ() >> 4) != null; // Paper - Perf: Optimize Level.hasChunkAt(BlockPosition)Z
++ }
++
++ public final boolean isLoadedAndInBounds(BlockPos blockposition) { // Paper - final for inline
++ return getWorldBorder().isWithinBounds(blockposition) && getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4) != null;
++ }
++
++ public @Nullable LevelChunk getChunkIfLoaded(int x, int z) { // Overridden in WorldServer for ABI compat which has final
++ return ((ServerLevel) this).getChunkSource().getChunkAtIfLoadedImmediately(x, z);
++ }
++ public final @Nullable LevelChunk getChunkIfLoaded(BlockPos blockposition) {
++ return ((ServerLevel) this).getChunkSource().getChunkAtIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++ }
++
++ // reduces need to do isLoaded before getType
++ public final @Nullable BlockState getBlockStateIfLoadedAndInBounds(BlockPos blockposition) {
++ return getWorldBorder().isWithinBounds(blockposition) ? getBlockStateIfLoaded(blockposition) : null;
++ }
++
++ @Override
+ public ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create) {
++ // Paper end
+ ChunkAccess ichunkaccess = this.getChunkSource().getChunk(chunkX, chunkZ, leastStatus, create);
+
+ if (ichunkaccess == null && create) {
+@@ -207,6 +445,22 @@
+
+ @Override
+ public boolean setBlock(BlockPos pos, BlockState state, int flags, int maxUpdateDepth) {
++ // CraftBukkit start - tree generation
++ if (this.captureTreeGeneration) {
++ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed
++ BlockState type = getBlockState(pos);
++ if (!type.isDestroyable()) return false;
++ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
++ CraftBlockState blockstate = this.capturedBlockStates.get(pos);
++ if (blockstate == null) {
++ blockstate = CapturedBlockState.getTreeBlockState(this, pos, flags);
++ this.capturedBlockStates.put(pos.immutable(), blockstate);
++ }
++ blockstate.setData(state);
++ blockstate.setFlag(flags);
++ return true;
++ }
++ // CraftBukkit end
+ if (this.isOutsideBuildHeight(pos)) {
+ return false;
+ } else if (!this.isClientSide && this.isDebug()) {
+@@ -214,44 +468,125 @@
+ } else {
+ LevelChunk chunk = this.getChunkAt(pos);
+ Block block = state.getBlock();
+- BlockState iblockdata1 = chunk.setBlockState(pos, state, (flags & 64) != 0);
+
++ // CraftBukkit start - capture blockstates
++ boolean captured = false;
++ if (this.captureBlockStates && !this.capturedBlockStates.containsKey(pos)) {
++ CraftBlockState blockstate = (CraftBlockState) world.getBlockAt(pos.getX(), pos.getY(), pos.getZ()).getState(); // Paper - use CB getState to get a suitable snapshot
++ blockstate.setFlag(flags); // Paper - set flag
++ this.capturedBlockStates.put(pos.immutable(), blockstate);
++ captured = true;
++ }
++ // CraftBukkit end
++
++ BlockState iblockdata1 = chunk.setBlockState(pos, state, (flags & 64) != 0, (flags & 1024) == 0); // CraftBukkit custom NO_PLACE flag
++
+ if (iblockdata1 == null) {
++ // CraftBukkit start - remove blockstate if failed (or the same)
++ if (this.captureBlockStates && captured) {
++ this.capturedBlockStates.remove(pos);
++ }
++ // CraftBukkit end
+ return false;
+ } else {
+ BlockState iblockdata2 = this.getBlockState(pos);
+
+- if (iblockdata2 == state) {
++ /*
++ if (iblockdata2 == iblockdata) {
+ if (iblockdata1 != iblockdata2) {
+- this.setBlocksDirty(pos, iblockdata1, iblockdata2);
++ this.setBlocksDirty(blockposition, iblockdata1, iblockdata2);
+ }
+
+- 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);
+ }
++ */
+
++ // 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);
++ } 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;
++ BlockState iblockdata1 = oldBlock;
++ BlockState iblockdata2 = actualBlock;
++ if (iblockdata2 == iblockdata) {
++ if (iblockdata1 != iblockdata2) {
++ this.setBlocksDirty(blockposition, iblockdata1, iblockdata2);
++ }
++
++ if ((i & 2) != 0 && (!this.isClientSide || (i & 4) == 0) && (this.isClientSide || chunk == null || (chunk.getFullStatus() != null && chunk.getFullStatus().isOrAfter(FullChunkStatus.BLOCK_TICKING)))) { // allow chunk to be null here as chunk.isReady() is false when we send our notification during block placement
++ this.sendBlockUpdated(blockposition, iblockdata1, iblockdata, i);
++ }
++
++ if ((i & 1) != 0) {
++ this.blockUpdated(blockposition, iblockdata1.getBlock());
++ if (!this.isClientSide && iblockdata.hasAnalogOutputSignal()) {
++ this.updateNeighbourForOutputSignal(blockposition, newBlock.getBlock());
++ }
++ }
++
++ if ((i & 16) == 0 && j > 0) {
++ int k = i & -34;
++
++ // CraftBukkit start
++ iblockdata1.updateIndirectNeighbourShapes(this, blockposition, k, j - 1); // Don't call an event for the old block to limit event spam
++ CraftWorld world = ((ServerLevel) this).getWorld();
++ boolean cancelledUpdates = false; // Paper - Fix block place logic
++ if (world != null && ((ServerLevel)this).hasPhysicsEvent) { // Paper - BlockPhysicsEvent
++ BlockPhysicsEvent event = new BlockPhysicsEvent(world.getBlockAt(blockposition.getX(), blockposition.getY(), blockposition.getZ()), CraftBlockData.fromData(iblockdata));
++ this.getCraftServer().getPluginManager().callEvent(event);
++
++ cancelledUpdates = event.isCancelled(); // Paper - Fix block place logic
++ }
++ // CraftBukkit end
++ if (!cancelledUpdates) { // Paper - Fix block place logic
++ 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 oldBlock, BlockState newBlock) {}
+
+@@ -270,15 +605,33 @@
+ return false;
+ } else {
+ FluidState fluid = this.getFluidState(pos);
++ // 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);
++ 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);
++ if (!event.callEvent()) {
++ return false;
++ }
++ effectType = ((CraftBlockData) event.getEffectBlock()).getState();
++ playEffect = event.playEffect();
++ drop = 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
++ 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
+ }
+
+ boolean flag1 = this.setBlock(pos, fluid.createLegacyBlock(), 3, maxUpdateDepth);
+@@ -340,10 +693,18 @@
+
+ @Override
+ public BlockState getBlockState(BlockPos pos) {
++ // CraftBukkit start - tree generation
++ if (this.captureTreeGeneration) {
++ CraftBlockState previous = this.capturedBlockStates.get(pos); // Paper
++ if (previous != null) {
++ return previous.getHandle();
++ }
++ }
++ // CraftBukkit end
+ if (this.isOutsideBuildHeight(pos)) {
+ return Blocks.VOID_AIR.defaultBlockState();
+ } else {
+- LevelChunk chunk = this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()));
++ ChunkAccess chunk = this.getChunk(pos.getX() >> 4, pos.getZ() >> 4, ChunkStatus.FULL, true); // Paper - manually inline to reduce hops and avoid unnecessary null check to reduce total byte code size, this should never return null and if it does we will see it the next line but the real stack trace will matter in the chunk engine
+
+ return chunk.getBlockState(pos);
+ }
+@@ -446,34 +807,53 @@
+ this.pendingBlockEntityTickers.clear();
+ }
+
+- Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
++ // Spigot start
++ // Iterator<TickingBlockEntity> iterator = this.blockEntityTickers.iterator();
+ boolean flag = this.tickRateManager().runsNormally();
+
+- while (iterator.hasNext()) {
+- TickingBlockEntity tickingblockentity = (TickingBlockEntity) iterator.next();
++ 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
++ for (tileTickPosition = 0; tileTickPosition < this.blockEntityTickers.size(); tileTickPosition++) { // Paper - Disable tick limiters
++ this.tileTickPosition = (this.tileTickPosition < this.blockEntityTickers.size()) ? this.tileTickPosition : 0;
++ TickingBlockEntity tickingblockentity = (TickingBlockEntity) this.blockEntityTickers.get(this.tileTickPosition);
++ // Spigot end
+
+ 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();
+ }
+ }
++ this.blockEntityTickers.removeAll(toRemove); // Paper - Fix MC-117075
+
+ this.tickingBlockEntities = false;
+ gameprofilerfiller.pop();
++ this.spigotConfig.currentPrimedTnt = 0; // Spigot
+ }
+
+ public <T extends Entity> void guardEntityTick(Consumer<T> tickConsumer, 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);
++ // Paper start - Prevent block entity and entity crashes
++ final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ());
++ MinecraftServer.LOGGER.error(msg, throwable);
++ getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); // Paper - ServerExceptionEvent
++ entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
++ // Paper end - Prevent block entity and entity crashes
+ }
++ }
++ // 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;
++ 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 @@
+ @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) {
++ // Paper start - Perf: Optimize capturedTileEntities lookup
++ net.minecraft.world.level.block.entity.BlockEntity blockEntity;
++ if (!this.capturedTileEntities.isEmpty() && (blockEntity = this.capturedTileEntities.get(blockposition)) != 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));
++ }
++
+ public void setBlockEntity(BlockEntity blockEntity) {
+ BlockPos blockposition = blockEntity.getBlockPos();
+
+ if (!this.isOutsideBuildHeight(blockposition)) {
++ // CraftBukkit start
++ if (this.captureBlockStates) {
++ this.capturedTileEntities.put(blockposition.immutable(), blockEntity);
++ return;
++ }
++ // CraftBukkit end
+ this.getChunkAt(blockposition).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
+
+ public static final Codec<Level.ExplosionInteraction> CODEC = StringRepresentable.fromEnum(Level.ExplosionInteraction::values);
+ private final String id;
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
new file mode 100644
index 0000000000..7c0828c3ad
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/LevelReader.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/LevelReader.java.patch
new file mode 100644
index 0000000000..4d6ea6f76d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/LevelReader.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/level/LevelReader.java
++++ b/net/minecraft/world/level/LevelReader.java
+@@ -26,6 +26,9 @@
+ @Nullable
+ ChunkAccess getChunk(int chunkX, int chunkZ, ChunkStatus leastStatus, boolean create);
+
++ @Nullable ChunkAccess getChunkIfLoadedImmediately(int x, int z); // Paper - ifLoaded api (we need this since current impl blocks if the chunk is loading)
++ @Nullable default ChunkAccess getChunkIfLoadedImmediately(BlockPos pos) { return this.getChunkIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);}
++
+ @Deprecated
+ boolean hasChunk(int chunkX, int chunkZ);
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/LevelWriter.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/LevelWriter.java.patch
new file mode 100644
index 0000000000..3008822ea1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/LevelWriter.java.patch
@@ -0,0 +1,13 @@
+--- a/net/minecraft/world/level/LevelWriter.java
++++ b/net/minecraft/world/level/LevelWriter.java
+@@ -28,4 +28,10 @@
+ default boolean addFreshEntity(Entity entity) {
+ return false;
+ }
++
++ // CraftBukkit start
++ default boolean addFreshEntity(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason reason) {
++ return false;
++ }
++ // CraftBukkit end
+ }
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
new file mode 100644
index 0000000000..28fc9a7cb8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/NaturalSpawner.java.patch
@@ -0,0 +1,212 @@
+--- 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/PathNavigationRegion.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/PathNavigationRegion.java.patch
new file mode 100644
index 0000000000..085fb1983b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/PathNavigationRegion.java.patch
@@ -0,0 +1,51 @@
+--- a/net/minecraft/world/level/PathNavigationRegion.java
++++ b/net/minecraft/world/level/PathNavigationRegion.java
+@@ -8,6 +8,7 @@
+ import net.minecraft.core.Holder;
+ import net.minecraft.core.SectionPos;
+ import net.minecraft.core.registries.Registries;
++import net.minecraft.server.level.ServerLevel;
+ import net.minecraft.world.entity.Entity;
+ import net.minecraft.world.level.biome.Biome;
+ import net.minecraft.world.level.biome.Biomes;
+@@ -66,7 +67,7 @@
+ private ChunkAccess getChunk(int chunkX, int chunkZ) {
+ int i = chunkX - this.centerX;
+ int j = chunkZ - this.centerZ;
+- if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) {
++ if (i >= 0 && i < this.chunks.length && j >= 0 && j < this.chunks[i].length) { // Paper - if this changes, update getChunkIfLoaded below
+ ChunkAccess chunkAccess = this.chunks[i][j];
+ return (ChunkAccess)(chunkAccess != null ? chunkAccess : new EmptyLevelChunk(this.level, new ChunkPos(chunkX, chunkZ), this.plains.get()));
+ } else {
+@@ -74,7 +75,31 @@
+ }
+ }
+
++ // Paper start - if loaded util
++ private @Nullable ChunkAccess getChunkIfLoaded(int x, int z) {
++ // Based on getChunk(int, int)
++ int xx = x - this.centerX;
++ int zz = z - this.centerZ;
++
++ if (xx >= 0 && xx < this.chunks.length && zz >= 0 && zz < this.chunks[xx].length) {
++ return this.chunks[xx][zz];
++ }
++ return null;
++ }
+ @Override
++ public final FluidState getFluidIfLoaded(BlockPos blockposition) {
++ ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++ return chunk == null ? null : chunk.getFluidState(blockposition);
++ }
++
++ @Override
++ public final BlockState getBlockStateIfLoaded(BlockPos blockposition) {
++ ChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4);
++ return chunk == null ? null : chunk.getBlockState(blockposition);
++ }
++ // Paper end
++
++ @Override
+ public WorldBorder getWorldBorder() {
+ return this.level.getWorldBorder();
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/ServerExplosion.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/ServerExplosion.java.patch
new file mode 100644
index 0000000000..e0f12942e5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/ServerExplosion.java.patch
@@ -0,0 +1,342 @@
+--- 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;
+ import net.minecraft.world.phys.HitResult;
+ import net.minecraft.world.phys.Vec3;
++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 final DamageSource damageSource;
+ private final ExplosionDamageCalculator damageCalculator;
+ 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;
++ 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 @@
+ 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)) {
+ break;
+@@ -149,6 +165,15 @@
+
+ if (f > 0.0F && this.damageCalculator.shouldBlockExplode(this, this.level, blockposition, iblockdata, f)) {
+ set.add(blockposition);
++ // 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 (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()));
++ }
++ }
++ // 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));
++ // 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
++ // - Damaging EntityEnderDragon does nothing
++ // - EntityEnderDragon hitbock always covers the other parts and is therefore always present
++ if (entity instanceof EnderDragonPart) {
++ continue;
++ }
++
++ entity.lastDamageCancelled = false;
++
++ if (entity instanceof EnderDragon) {
++ for (EnderDragonPart entityComplexPart : ((EnderDragon) entity).subEntities) {
++ // Calculate damage separately for each EntityComplexPart
++ if (list.contains(entityComplexPart)) {
++ entityComplexPart.hurtServer(this.level, this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, f2));
++ }
++ }
++ } else {
++ entity.hurtServer(this.level, this.damageSource, this.damageCalculator.getEntityDamageAmount(this, entity, f2));
++ }
++
++ if (entity.lastDamageCancelled) { // SPIGOT-5339, SPIGOT-6252, SPIGOT-6777: Skip entity if damage event was cancelled
++ continue;
++ }
++ // 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
+ } else {
+ d6 = d5;
+ }
+@@ -214,11 +267,19 @@
+ d3 *= d6;
+ Vec3 vec3d = 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
++ }
++ // CraftBukkit end
+ entity.push(vec3d);
+ 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);
+ }
+ }
+@@ -235,10 +296,62 @@
+ List<ServerExplosion.StackCollector> list1 = new ArrayList();
+
+ 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);
++ org.bukkit.block.Block bblock = bworld.getBlockAt(cpos.getX(), cpos.getY(), cpos.getZ());
++ if (!bblock.getType().isAir()) {
++ blockList.add(bblock);
++ }
++ }
++
++ List<org.bukkit.block.Block> bukkitBlocks;
++
++ if (this.source != null) {
++ EntityExplodeEvent event = CraftEventFactory.callEntityExplodeEvent(this.source, blockList, this.yield, this.getBlockInteraction());
++ this.wasCanceled = event.isCancelled();
++ bukkitBlocks = event.blockList();
++ this.yield = event.getYield();
++ } else {
++ org.bukkit.block.Block block = location.getBlock();
++ org.bukkit.block.BlockState blockState = (this.damageSource.getDirectBlockState() != null) ? this.damageSource.getDirectBlockState() : block.getState();
++ BlockExplodeEvent event = CraftEventFactory.callBlockExplodeEvent(block, blockState, blockList, this.yield, this.getBlockInteraction());
++ this.wasCanceled = event.isCancelled();
++ bukkitBlocks = event.blockList();
++ this.yield = event.getYield();
++ }
++
++ positions.clear();
++
++ for (org.bukkit.block.Block bblock : bukkitBlocks) {
++ BlockPos coords = new BlockPos(bblock.getX(), bblock.getY(), bblock.getZ());
++ positions.add(coords);
++ }
++
++ if (this.wasCanceled) {
++ return;
++ }
++ // CraftBukkit end
+ Iterator iterator = positions.iterator();
+
+ while (iterator.hasNext()) {
+ BlockPos blockposition = (BlockPos) iterator.next();
++ // CraftBukkit start - TNTPrimeEvent
++ BlockState iblockdata = this.level.getBlockState(blockposition);
++ 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
++ 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));
++ // 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));
++ }
++ // CraftBukkit end
+ }
+ }
+
+ }
+
+ public void explode() {
++ // CraftBukkit start
++ if (this.radius < 0.1F) {
++ return;
++ }
++ // CraftBukkit end
+ this.level.gameEvent(this.source, (Holder) GameEvent.EXPLODE, this.center);
+ List<BlockPos> list = this.calculateExplodedPositions();
+
+@@ -288,6 +410,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 @@
+
+ }
+ }
++
++ // Paper start - Optimize explosions
++ private float getBlockDensity(Vec3 vec3d, Entity entity) {
++ if (!this.level.paperConfig().environment.optimizeExplosions) {
++ return getSeenPercent(vec3d, entity);
++ }
++ CacheKey key = new CacheKey(this, entity.getBoundingBox());
++ Float blockDensity = this.level.explosionDensityCache.get(key);
++ if (blockDensity == null) {
++ blockDensity = getSeenPercent(vec3d, entity);
++ this.level.explosionDensityCache.put(key, blockDensity);
++ }
++
++ return blockDensity;
++ }
++
++ static class CacheKey {
++ private final Level world;
++ private final double posX, posY, posZ;
++ private final double minX, minY, minZ;
++ private final double maxX, maxY, maxZ;
++
++ public CacheKey(Explosion explosion, AABB aabb) {
++ this.world = explosion.level();
++ this.posX = explosion.center().x;
++ this.posY = explosion.center().y;
++ this.posZ = explosion.center().z;
++ this.minX = aabb.minX;
++ this.minY = aabb.minY;
++ this.minZ = aabb.minZ;
++ this.maxX = aabb.maxX;
++ this.maxY = aabb.maxY;
++ this.maxZ = aabb.maxZ;
++ }
++
++ @Override
++ public boolean equals(Object o) {
++ if (this == o) return true;
++ if (o == null || getClass() != o.getClass()) return false;
++
++ CacheKey cacheKey = (CacheKey) o;
++
++ if (Double.compare(cacheKey.posX, posX) != 0) return false;
++ if (Double.compare(cacheKey.posY, posY) != 0) return false;
++ if (Double.compare(cacheKey.posZ, posZ) != 0) return false;
++ if (Double.compare(cacheKey.minX, minX) != 0) return false;
++ if (Double.compare(cacheKey.minY, minY) != 0) return false;
++ if (Double.compare(cacheKey.minZ, minZ) != 0) return false;
++ if (Double.compare(cacheKey.maxX, maxX) != 0) return false;
++ if (Double.compare(cacheKey.maxY, maxY) != 0) return false;
++ if (Double.compare(cacheKey.maxZ, maxZ) != 0) return false;
++ return world.equals(cacheKey.world);
++ }
++
++ @Override
++ public int hashCode() {
++ int result;
++ long temp;
++ result = world.hashCode();
++ temp = Double.doubleToLongBits(posX);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(posY);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(posZ);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(minX);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(minY);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(minZ);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(maxX);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(maxY);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ temp = Double.doubleToLongBits(maxZ);
++ result = 31 * result + (int) (temp ^ (temp >>> 32));
++ return result;
++ }
++ }
++ // Paper end
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/ServerLevelAccessor.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/ServerLevelAccessor.java.patch
new file mode 100644
index 0000000000..2c482ebee0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/ServerLevelAccessor.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/ServerLevelAccessor.java
++++ b/net/minecraft/world/level/ServerLevelAccessor.java
+@@ -8,6 +8,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));
++ }
++
++ @Override
++ default ServerLevel getMinecraftWorld() {
++ return this.getLevel();
++ }
++ // CraftBukkit end
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/StructureManager.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/StructureManager.java.patch
new file mode 100644
index 0000000000..8a15d2f4f1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/StructureManager.java.patch
@@ -0,0 +1,38 @@
+--- a/net/minecraft/world/level/StructureManager.java
++++ b/net/minecraft/world/level/StructureManager.java
+@@ -48,7 +48,12 @@
+ }
+
+ public List<StructureStart> startsForStructure(ChunkPos pos, Predicate<Structure> predicate) {
+- Map<Structure, LongSet> map = this.level.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_REFERENCES).getAllReferences();
++ // Paper start - Fix swamp hut cat generation deadlock
++ return this.startsForStructure(pos, predicate, 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();
++ // Paper end - Fix swamp hut cat generation deadlock
+ Builder<StructureStart> builder = ImmutableList.builder();
+
+ for (Entry<Structure, LongSet> entry : map.entrySet()) {
+@@ -116,10 +121,20 @@
+ }
+
+ public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate<Holder<Structure>> predicate) {
++ // Paper start - Fix swamp hut cat generation deadlock
++ return this.getStructureWithPieceAt(pos, predicate, null);
++ }
++
++ public StructureStart getStructureWithPieceAt(BlockPos pos, TagKey<Structure> tag, @Nullable ServerLevelAccessor levelAccessor) {
++ return this.getStructureWithPieceAt(pos, structure -> structure.is(tag), levelAccessor);
++ }
++
++ public StructureStart getStructureWithPieceAt(BlockPos pos, Predicate<Holder<Structure>> predicate, @Nullable ServerLevelAccessor levelAccessor) {
++ // Paper end - Fix swamp hut cat generation deadlock
+ Registry<Structure> registry = this.registryAccess().lookupOrThrow(Registries.STRUCTURE);
+
+ for (StructureStart structureStart : this.startsForStructure(
+- new ChunkPos(pos), structure -> registry.get(registry.getId(structure)).map(predicate::test).orElse(false)
++ new ChunkPos(pos), structure -> registry.get(registry.getId(structure)).map(predicate::test).orElse(false), levelAccessor // Paper - Fix swamp hut cat generation deadlock
+ )) {
+ if (this.structureHasPieceAt(pos, structureStart)) {
+ return structureStart;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/biome/MobSpawnSettings.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/biome/MobSpawnSettings.java.patch
new file mode 100644
index 0000000000..d1fb7a4640
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/biome/MobSpawnSettings.java.patch
@@ -0,0 +1,44 @@
+--- a/net/minecraft/world/level/biome/MobSpawnSettings.java
++++ b/net/minecraft/world/level/biome/MobSpawnSettings.java
+@@ -75,8 +75,40 @@
+ }
+
+ public static class Builder {
++ // Paper start - Perf: keep track of data in a pair set to give O(1) contains calls - we have to hook removals incase plugins mess with it
++ public static class MobList extends java.util.ArrayList<MobSpawnSettings.SpawnerData> {
++ java.util.Set<MobSpawnSettings.SpawnerData> biomes = new java.util.HashSet<>();
++
++ @Override
++ public boolean contains(Object o) {
++ return biomes.contains(o);
++ }
++
++ @Override
++ public boolean add(MobSpawnSettings.SpawnerData BiomeSettingsMobs) {
++ biomes.add(BiomeSettingsMobs);
++ return super.add(BiomeSettingsMobs);
++ }
++
++ @Override
++ public MobSpawnSettings.SpawnerData remove(int index) {
++ MobSpawnSettings.SpawnerData removed = super.remove(index);
++ if (removed != null) {
++ biomes.remove(removed);
++ }
++ return removed;
++ }
++
++ @Override
++ public void clear() {
++ biomes.clear();
++ super.clear();
++ }
++ }
++ // 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(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();
+ private float creatureGenerationProbability = 0.1F;
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/AbstractCandleBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/AbstractCandleBlock.java.patch
new file mode 100644
index 0000000000..2c42f4cb51
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/AbstractCandleBlock.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/block/AbstractCandleBlock.java
++++ b/net/minecraft/world/level/block/AbstractCandleBlock.java
+@@ -47,6 +47,11 @@
+ @Override
+ protected void onProjectileHit(Level world, BlockState state, BlockHitResult hit, Projectile projectile) {
+ if (!world.isClientSide && projectile.isOnFire() && this.canBeLit(state)) {
++ // CraftBukkit start
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockIgniteEvent(world, hit.getBlockPos(), projectile).isCancelled()) {
++ return;
++ }
++ // CraftBukkit end
+ AbstractCandleBlock.setLit(world, state, hit.getBlockPos(), true);
+ }
+
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
new file mode 100644
index 0000000000..e2c8cb7f03
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/AnvilBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/AnvilBlock.java.patch
new file mode 100644
index 0000000000..81462ca698
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/AnvilBlock.java.patch
@@ -0,0 +1,13 @@
+--- a/net/minecraft/world/level/block/AnvilBlock.java
++++ b/net/minecraft/world/level/block/AnvilBlock.java
+@@ -62,8 +62,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_ANVIL);
++ } // Paper - Fix InventoryOpenEvent cancellation
+ }
+
+ return InteractionResult.SUCCESS;
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
new file mode 100644
index 0000000000..88344a70cd
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..eb12b201ec
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BambooStalkBlock.java.patch
@@ -0,0 +1,89 @@
+--- 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/BarrelBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BarrelBlock.java.patch
new file mode 100644
index 0000000000..349f801388
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BarrelBlock.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/level/block/BarrelBlock.java
++++ b/net/minecraft/world/level/block/BarrelBlock.java
+@@ -41,8 +41,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) {
+- player.openMenu(barrelBlockEntity);
++ if (world instanceof ServerLevel serverLevel && world.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/unapplied/net/minecraft/world/level/block/BaseFireBlock.java.patch
new file mode 100644
index 0000000000..aeae088ca7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BaseFireBlock.java.patch
@@ -0,0 +1,85 @@
+--- a/net/minecraft/world/level/block/BaseFireBlock.java
++++ b/net/minecraft/world/level/block/BaseFireBlock.java
+@@ -12,6 +12,7 @@
+ import net.minecraft.world.entity.Entity;
+ import net.minecraft.world.entity.player.Player;
+ 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.Level;
+ import net.minecraft.world.level.block.state.BlockBehaviour;
+@@ -127,6 +128,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.fireImmune()) {
+ if (entity.getRemainingFireTicks() < 0) {
+ entity.setRemainingFireTicks(entity.getRemainingFireTicks() + 1);
+@@ -137,7 +139,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);
++
++ if (!event.isCancelled()) {
++ entity.igniteForSeconds(event.getDuration(), false);
++ // Paper start - fix EntityCombustEvent cancellation
++ } else {
++ entity.setRemainingFireTicks(entity.getRemainingFireTicks() - 1);
++ // Paper end - fix EntityCombustEvent cancellation
++ }
++ // CraftBukkit end
+ }
+ }
+
+@@ -146,26 +159,26 @@
+ }
+
+ @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);
+
+ if (optional.isPresent()) {
+- ((PortalShape) optional.get()).createPortalBlocks(world);
++ ((PortalShape) optional.get()).createPortalBlocks(world, (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
+ }
+
+ }
+ }
+
+ 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()
+ }
+
+ @Override
+@@ -213,4 +226,12 @@
+ }
+ }
+ }
++
++ // CraftBukkit start
++ protected void fireExtinguished(net.minecraft.world.level.LevelAccessor world, BlockPos position) {
++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, position, Blocks.AIR.defaultBlockState()).isCancelled()) {
++ world.removeBlock(position, false);
++ }
++ }
++ // CraftBukkit end
+ }
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
new file mode 100644
index 0000000000..65470ccca0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BasePressurePlateBlock.java.patch
@@ -0,0 +1,56 @@
+--- 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
new file mode 100644
index 0000000000..9e6c76abd9
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/BeaconBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BeaconBlock.java.patch
new file mode 100644
index 0000000000..b23b55da23
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BeaconBlock.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/level/block/BeaconBlock.java
++++ b/net/minecraft/world/level/block/BeaconBlock.java
+@@ -46,8 +46,7 @@
+
+ @Override
+ protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
+- if (!world.isClientSide && world.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
+ 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/unapplied/net/minecraft/world/level/block/BedBlock.java.patch
new file mode 100644
index 0000000000..09946b2d9f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BedBlock.java.patch
@@ -0,0 +1,92 @@
+--- a/net/minecraft/world/level/block/BedBlock.java
++++ b/net/minecraft/world/level/block/BedBlock.java
+@@ -95,7 +95,8 @@
+ }
+ }
+
+- 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);
+ 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)) {
+ player.displayClientMessage(Component.translatable("block.minecraft.bed.occupied"), true);
+ }
+
+ return InteractionResult.SUCCESS_SERVER;
+ } else {
++ // CraftBukkit start
++ BlockState finaliblockdata = state;
++ BlockPos finalblockposition = pos;
++ // CraftBukkit end
+ player.startSleepInBed(pos).ifLeft((entityhuman_enumbedresult) -> {
++ // 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 (!event.callEvent()) {
++ return;
++ }
++ // Paper end - PlayerBedFailEnterEvent
++ // CraftBukkit start - handling bed explosion from below here
++ if (event.getWillExplode()) { // Paper - PlayerBedFailEnterEvent
++ this.explodeBed(finaliblockdata, world, finalblockposition);
++ } else
++ // CraftBukkit end
+ if (entityhuman_enumbedresult.getMessage() != null) {
+- player.displayClientMessage(entityhuman_enumbedresult.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) {
++ {
++ {
++ org.bukkit.block.BlockState blockState = org.bukkit.craftbukkit.block.CraftBlock.at(world, blockposition).getState(); // CraftBukkit - capture BlockState before remove block
++ world.removeBlock(blockposition, false);
++ BlockPos blockposition1 = blockposition.relative(((Direction) iblockdata.getValue(BedBlock.FACING)).getOpposite());
++
++ if (world.getBlockState(blockposition1).getBlock() == this) {
++ world.removeBlock(blockposition1, false);
++ }
++
++ 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
++ 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
+ }
+
+ 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);
++ // CraftBukkit start - SPIGOT-7315: Don't updated if we capture block states
++ if (world.captureBlockStates) {
++ return;
++ }
++ // CraftBukkit end
+ world.blockUpdated(pos, Blocks.AIR);
+ state.updateNeighbourShapes(world, pos, 3);
+ }
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
new file mode 100644
index 0000000000..73f015dbe6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BeehiveBlock.java.patch
@@ -0,0 +1,79 @@
+--- 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
new file mode 100644
index 0000000000..d677bbedab
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..c0eab8e717
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BigDripleafBlock.java.patch
@@ -0,0 +1,116 @@
+--- 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/BlastFurnaceBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BlastFurnaceBlock.java.patch
new file mode 100644
index 0000000000..3317c55a37
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BlastFurnaceBlock.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/level/block/BlastFurnaceBlock.java
++++ b/net/minecraft/world/level/block/BlastFurnaceBlock.java
+@@ -45,8 +45,7 @@
+ @Override
+ protected void openContainer(Level world, BlockPos pos, Player player) {
+ BlockEntity blockEntity = world.getBlockEntity(pos);
+- if (blockEntity instanceof BlastFurnaceBlockEntity) {
+- player.openMenu((MenuProvider)blockEntity);
++ if (blockEntity instanceof BlastFurnaceBlockEntity && player.openMenu((MenuProvider)blockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+ player.awardStat(Stats.INTERACT_WITH_BLAST_FURNACE);
+ }
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/Block.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/Block.java.patch
new file mode 100644
index 0000000000..c1060afc42
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/Block.java.patch
@@ -0,0 +1,154 @@
+--- a/net/minecraft/world/level/block/Block.java
++++ b/net/minecraft/world/level/block/Block.java
+@@ -88,6 +88,21 @@
+ public static final int UPDATE_LIMIT = 512;
+ protected final StateDefinition<Block, BlockState> stateDefinition;
+ private BlockState defaultBlockState;
++ // Paper start - Protect Bedrock and End Portal/Frames from being destroyed
++ public final boolean isDestroyable() {
++ return io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits ||
++ this != Blocks.BARRIER &&
++ this != Blocks.BEDROCK &&
++ this != Blocks.END_PORTAL_FRAME &&
++ this != Blocks.END_PORTAL &&
++ this != Blocks.END_GATEWAY &&
++ this != Blocks.COMMAND_BLOCK &&
++ this != Blocks.REPEATING_COMMAND_BLOCK &&
++ this != Blocks.CHAIN_COMMAND_BLOCK &&
++ this != Blocks.STRUCTURE_BLOCK &&
++ this != Blocks.JIGSAW;
++ }
++ // Paper end - Protect Bedrock and End Portal/Frames from being destroyed
+ @Nullable
+ private Item item;
+ private static final int CACHE_SIZE = 256;
+@@ -295,12 +310,38 @@
+
+ }
+
++ // Paper start - Add BlockBreakBlockEvent
++ public static boolean dropResources(BlockState state, LevelAccessor levelAccessor, BlockPos pos, @Nullable BlockEntity blockEntity, BlockPos source) {
++ if (levelAccessor instanceof ServerLevel serverLevel) {
++ List<org.bukkit.inventory.ItemStack> items = new java.util.ArrayList<>();
++ for (ItemStack drop : Block.getDrops(state, serverLevel, pos, blockEntity)) {
++ items.add(org.bukkit.craftbukkit.inventory.CraftItemStack.asBukkitCopy(drop));
++ }
++ Block block = state.getBlock(); // Paper - Properly handle xp dropping
++ io.papermc.paper.event.block.BlockBreakBlockEvent event = new io.papermc.paper.event.block.BlockBreakBlockEvent(org.bukkit.craftbukkit.block.CraftBlock.at(levelAccessor, pos), org.bukkit.craftbukkit.block.CraftBlock.at(levelAccessor, source), items);
++ event.setExpToDrop(block.getExpDrop(state, serverLevel, pos, net.minecraft.world.item.ItemStack.EMPTY, true)); // Paper - Properly handle xp dropping
++ event.callEvent();
++ for (org.bukkit.inventory.ItemStack drop : event.getDrops()) {
++ popResource(serverLevel, pos, org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(drop));
++ }
++ state.spawnAfterBreak(serverLevel, pos, ItemStack.EMPTY, false); // Paper - Properly handle xp dropping
++ block.popExperience(serverLevel, pos, event.getExpToDrop()); // Paper - Properly handle xp dropping
++ }
++ return true;
++ }
++ // Paper end - Add BlockBreakBlockEvent
++
+ public static void dropResources(BlockState state, Level world, BlockPos pos, @Nullable BlockEntity blockEntity, @Nullable Entity entity, ItemStack tool) {
++ // Paper start - Properly handle xp dropping
++ dropResources(state, world, 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) {
++ // 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
+ }
+
+ }
+@@ -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) {
++ // Paper start - add entity parameter
++ popExperience(world, pos, size, null);
++ }
++ public void popExperience(ServerLevel world, BlockPos pos, int size, 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
+ }
+
+ }
+@@ -367,10 +419,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 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
+ player.awardStat(Stats.BLOCK_MINED.get(this));
+- player.causeFoodExhaustion(0.005F);
+- Block.dropResources(state, world, 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
++ } // 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 @@
+ 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()));
+
+ if (i > 0) {
+- this.popExperience(world, pos, i);
++ // this.popExperience(worldserver, blockposition, i);
++ return i;
+ }
+
++ return 0;
+ }
+
++ public int getExpDrop(BlockState iblockdata, ServerLevel worldserver, BlockPos blockposition, ItemStack itemstack, boolean flag) {
++ return 0;
++ }
++ // CraftBukkit end
++
++ // Spigot start
++ public static float range(float min, float value, float max) {
++ if (value < min) {
++ return min;
++ }
++ if (value > max) {
++ return max;
++ }
++ return value;
++ }
++ // Spigot end
++
+ private static record ShapePairKey(VoxelShape first, VoxelShape second) {
+
+ public boolean equals(Object object) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/BrewingStandBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/BrewingStandBlock.java.patch
new file mode 100644
index 0000000000..ad19f46261
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BrewingStandBlock.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/level/block/BrewingStandBlock.java
++++ b/net/minecraft/world/level/block/BrewingStandBlock.java
+@@ -68,8 +68,7 @@
+
+ @Override
+ protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
+- if (!world.isClientSide && world.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
+ 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/unapplied/net/minecraft/world/level/block/BubbleColumnBlock.java.patch
new file mode 100644
index 0000000000..aa97c1238d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BubbleColumnBlock.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/level/block/BubbleColumnBlock.java
++++ b/net/minecraft/world/level/block/BubbleColumnBlock.java
+@@ -48,6 +48,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());
+ if (blockState.isAir()) {
+ entity.onAboveBubbleCol(state.getValue(DRAG_DOWN));
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
new file mode 100644
index 0000000000..ee6f708bf9
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..d8fb08b681
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/BushBlock.java.patch
@@ -0,0 +1,27 @@
+--- 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
new file mode 100644
index 0000000000..8311b29036
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/ButtonBlock.java.patch
@@ -0,0 +1,78 @@
+--- 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
new file mode 100644
index 0000000000..c7084f8b41
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CactusBlock.java.patch
@@ -0,0 +1,42 @@
+--- 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/CakeBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CakeBlock.java.patch
new file mode 100644
index 0000000000..346de8857c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CakeBlock.java.patch
@@ -0,0 +1,47 @@
+--- 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 @@
+ 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();
++ 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
++ }
++ // 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);
++ // 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()) {
++ player.getFoodData().eat(event.getFoodLevel() - oldFoodLevel, 0.1F);
++ }
++
++ ((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);
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
new file mode 100644
index 0000000000..133764e05b
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/CartographyTableBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CartographyTableBlock.java.patch
new file mode 100644
index 0000000000..237e4da1be
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CartographyTableBlock.java.patch
@@ -0,0 +1,13 @@
+--- a/net/minecraft/world/level/block/CartographyTableBlock.java
++++ b/net/minecraft/world/level/block/CartographyTableBlock.java
+@@ -32,8 +32,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_CARTOGRAPHY_TABLE);
++ } // Paper - Fix InventoryOpenEvent cancellation
+ }
+
+ return InteractionResult.SUCCESS;
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
new file mode 100644
index 0000000000..8b9fd76990
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CarvedPumpkinBlock.java.patch
@@ -0,0 +1,29 @@
+--- 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
new file mode 100644
index 0000000000..0d9def53a1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CauldronBlock.java.patch
@@ -0,0 +1,56 @@
+--- 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
new file mode 100644
index 0000000000..b340d9c9cb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CaveVines.java.patch
@@ -0,0 +1,42 @@
+--- 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/CaveVinesBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CaveVinesBlock.java.patch
new file mode 100644
index 0000000000..511200af88
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CaveVinesBlock.java.patch
@@ -0,0 +1,22 @@
+--- 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));
+ }
+
++ // 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
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
new file mode 100644
index 0000000000..b85ba5b3dc
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..94b24017e5
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..4b025cb2e4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/ChestBlock.java.patch
@@ -0,0 +1,118 @@
+--- 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
new file mode 100644
index 0000000000..88160b2d20
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/ChorusFlowerBlock.java.patch
@@ -0,0 +1,73 @@
+--- 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/ChorusPlantBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/ChorusPlantBlock.java.patch
new file mode 100644
index 0000000000..80c24699ea
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/ChorusPlantBlock.java.patch
@@ -0,0 +1,34 @@
+--- a/net/minecraft/world/level/block/ChorusPlantBlock.java
++++ b/net/minecraft/world/level/block/ChorusPlantBlock.java
+@@ -38,6 +38,7 @@
+
+ @Override
+ public BlockState getStateForPlacement(BlockPlaceContext ctx) {
++ 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());
+ }
+
+@@ -68,6 +69,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 @@
+
+ @Override
+ protected void tick(BlockState state, ServerLevel world, 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);
+ }
+@@ -86,6 +89,7 @@
+
+ @Override
+ protected boolean canSurvive(BlockState state, LevelReader world, 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();
+
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
new file mode 100644
index 0000000000..963cb2ef7b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CocoaBlock.java.patch
@@ -0,0 +1,33 @@
+--- 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
new file mode 100644
index 0000000000..6c6c7fbbd8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CommandBlock.java.patch
@@ -0,0 +1,37 @@
+--- 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
new file mode 100644
index 0000000000..a59662f5c7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/ComparatorBlock.java.patch
@@ -0,0 +1,39 @@
+--- 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
new file mode 100644
index 0000000000..919fb7f0cb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/ComposterBlock.java.patch
@@ -0,0 +1,184 @@
+--- 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
new file mode 100644
index 0000000000..5778abe978
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/ConcretePowderBlock.java.patch
@@ -0,0 +1,76 @@
+--- 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/CoralBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralBlock.java.patch
new file mode 100644
index 0000000000..389b3b96ec
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralBlock.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/block/CoralBlock.java
++++ b/net/minecraft/world/level/block/CoralBlock.java
+@@ -40,6 +40,11 @@
+ @Override
+ protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
+ if (!this.scanForWater(world, pos)) {
++ // CraftBukkit start
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, this.deadBlock.defaultBlockState()).isCancelled()) {
++ return;
++ }
++ // CraftBukkit end
+ world.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/unapplied/net/minecraft/world/level/block/CoralFanBlock.java.patch
new file mode 100644
index 0000000000..295f4f0049
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralFanBlock.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/block/CoralFanBlock.java
++++ b/net/minecraft/world/level/block/CoralFanBlock.java
+@@ -41,6 +41,11 @@
+ @Override
+ protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
+ if (!scanForWater(state, world, pos)) {
++ // CraftBukkit start
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, this.deadBlock.defaultBlockState().setValue(CoralFanBlock.WATERLOGGED, false)).isCancelled()) {
++ return;
++ }
++ // CraftBukkit end
+ world.setBlock(pos, (BlockState) this.deadBlock.defaultBlockState().setValue(CoralFanBlock.WATERLOGGED, false), 2);
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralPlantBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralPlantBlock.java.patch
new file mode 100644
index 0000000000..bccf3cd4a0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralPlantBlock.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/block/CoralPlantBlock.java
++++ b/net/minecraft/world/level/block/CoralPlantBlock.java
+@@ -46,6 +46,11 @@
+ @Override
+ protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
+ if (!scanForWater(state, world, pos)) {
++ // CraftBukkit start
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, this.deadBlock.defaultBlockState().setValue(CoralPlantBlock.WATERLOGGED, false)).isCancelled()) {
++ return;
++ }
++ // CraftBukkit end
+ world.setBlock(pos, (BlockState) this.deadBlock.defaultBlockState().setValue(CoralPlantBlock.WATERLOGGED, false), 2);
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralWallFanBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralWallFanBlock.java.patch
new file mode 100644
index 0000000000..172e22d8b2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CoralWallFanBlock.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/block/CoralWallFanBlock.java
++++ b/net/minecraft/world/level/block/CoralWallFanBlock.java
+@@ -41,6 +41,11 @@
+ @Override
+ protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
+ if (!scanForWater(state, world, 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()) {
++ return;
++ }
++ // CraftBukkit end
+ world.setBlock(pos, (BlockState) ((BlockState) this.deadBlock.defaultBlockState().setValue(CoralWallFanBlock.WATERLOGGED, false)).setValue(CoralWallFanBlock.FACING, (Direction) state.getValue(CoralWallFanBlock.FACING)), 2);
+ }
+
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
new file mode 100644
index 0000000000..b3f1ac312a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CrafterBlock.java.patch
@@ -0,0 +1,89 @@
+--- 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/CraftingTableBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/CraftingTableBlock.java.patch
new file mode 100644
index 0000000000..e611ea83b5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CraftingTableBlock.java.patch
@@ -0,0 +1,13 @@
+--- a/net/minecraft/world/level/block/CraftingTableBlock.java
++++ b/net/minecraft/world/level/block/CraftingTableBlock.java
+@@ -31,8 +31,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_CRAFTING_TABLE);
++ } // Paper - Fix InventoryOpenEvent cancellation
+ }
+
+ return InteractionResult.SUCCESS;
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
new file mode 100644
index 0000000000..4883d93884
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/CropBlock.java.patch
@@ -0,0 +1,59 @@
+--- 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
new file mode 100644
index 0000000000..4631339b55
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..6b148eba67
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..de7bae1c90
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/DetectorRailBlock.java.patch
@@ -0,0 +1,44 @@
+--- 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
new file mode 100644
index 0000000000..91c2eb8ada
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/DiodeBlock.java.patch
@@ -0,0 +1,29 @@
+--- 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/DirtPathBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DirtPathBlock.java.patch
new file mode 100644
index 0000000000..b394c4adfa
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/DirtPathBlock.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/block/DirtPathBlock.java
++++ b/net/minecraft/world/level/block/DirtPathBlock.java
+@@ -51,6 +51,11 @@
+
+ @Override
+ protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
++ // CraftBukkit start - do not fade if the block is valid here
++ if (state.canSurvive(world, pos)) {
++ return;
++ }
++ // CraftBukkit end
+ FarmBlock.turnToDirt((Entity) null, state, world, pos);
+ }
+
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
new file mode 100644
index 0000000000..71019ffd00
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/DispenserBlock.java.patch
@@ -0,0 +1,61 @@
+--- 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
new file mode 100644
index 0000000000..ea278f700f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/DoorBlock.java.patch
@@ -0,0 +1,37 @@
+--- 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/DoubleBlockCombiner.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DoubleBlockCombiner.java.patch
new file mode 100644
index 0000000000..657fe3fb50
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/DoubleBlockCombiner.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/level/block/DoubleBlockCombiner.java
++++ b/net/minecraft/world/level/block/DoubleBlockCombiner.java
+@@ -34,7 +34,12 @@
+ return new DoubleBlockCombiner.NeighborCombineResult.Single<>(blockEntity);
+ } else {
+ BlockPos blockPos = pos.relative(directionMapper.apply(state));
+- BlockState blockState = world.getBlockState(blockPos);
++ // Paper start - Don't load Chunks from Hoppers and other things
++ BlockState blockState = world.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
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
new file mode 100644
index 0000000000..a59d6fc4f3
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..b73b669baf
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/DragonEggBlock.java.patch
@@ -0,0 +1,29 @@
+--- 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/DropExperienceBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DropExperienceBlock.java.patch
new file mode 100644
index 0000000000..4d0a4875d4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/DropExperienceBlock.java.patch
@@ -0,0 +1,21 @@
+--- a/net/minecraft/world/level/block/DropExperienceBlock.java
++++ b/net/minecraft/world/level/block/DropExperienceBlock.java
+@@ -31,9 +31,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/DropperBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/DropperBlock.java.patch
new file mode 100644
index 0000000000..0a8d6b5e2e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/DropperBlock.java.patch
@@ -0,0 +1,75 @@
+--- 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
new file mode 100644
index 0000000000..2e2055c634
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/EndGatewayBlock.java.patch
@@ -0,0 +1,34 @@
+--- 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
new file mode 100644
index 0000000000..c075ce1cd8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/EndPortalBlock.java.patch
@@ -0,0 +1,91 @@
+--- 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
new file mode 100644
index 0000000000..0b1e52d838
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/EnderChestBlock.java.patch
@@ -0,0 +1,25 @@
+--- 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/EyeblossomBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/EyeblossomBlock.java.patch
new file mode 100644
index 0000000000..0f677874ee
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/EyeblossomBlock.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/level/block/EyeblossomBlock.java
++++ b/net/minecraft/world/level/block/EyeblossomBlock.java
+@@ -100,6 +100,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
+ && entity instanceof Bee bee
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
new file mode 100644
index 0000000000..d862f84ba3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/FarmBlock.java.patch
@@ -0,0 +1,112 @@
+--- 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
new file mode 100644
index 0000000000..78872a0616
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..5d840efc2e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/FireBlock.java.patch
@@ -0,0 +1,205 @@
+--- 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/FlowerPotBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/FlowerPotBlock.java.patch
new file mode 100644
index 0000000000..a2ee2e43ae
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/FlowerPotBlock.java.patch
@@ -0,0 +1,40 @@
+--- a/net/minecraft/world/level/block/FlowerPotBlock.java
++++ b/net/minecraft/world/level/block/FlowerPotBlock.java
+@@ -63,6 +63,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.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);
++ if (!event.callEvent()) {
++ // Update client
++ player.containerMenu.sendAllDataToRemote();
++
++ return InteractionResult.CONSUME;
++ }
++ // Paper end - Add PlayerFlowerPotManipulateEvent
+ world.setBlock(pos, blockState, 3);
+ world.gameEvent(player, GameEvent.BLOCK_CHANGE, pos);
+ player.awardStat(Stats.POT_FLOWER);
+@@ -77,6 +89,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.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);
++ if (!event.callEvent()) {
++ // Update client
++ player.containerMenu.sendAllDataToRemote();
++
++ return InteractionResult.PASS;
++ }
++ // Paper end - Add PlayerFlowerPotManipulateEvent
+ if (!player.addItem(itemStack)) {
+ player.drop(itemStack, false);
+ }
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
new file mode 100644
index 0000000000..c5fefd2f6b
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..9d1efc3454
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..6508c8df14
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/FungusBlock.java.patch
@@ -0,0 +1,16 @@
+--- 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/FurnaceBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/FurnaceBlock.java.patch
new file mode 100644
index 0000000000..ffebc4893d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/FurnaceBlock.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/level/block/FurnaceBlock.java
++++ b/net/minecraft/world/level/block/FurnaceBlock.java
+@@ -45,8 +45,7 @@
+ @Override
+ protected void openContainer(Level world, BlockPos pos, Player player) {
+ BlockEntity blockEntity = world.getBlockEntity(pos);
+- if (blockEntity instanceof FurnaceBlockEntity) {
+- player.openMenu((MenuProvider)blockEntity);
++ if (blockEntity instanceof FurnaceBlockEntity && player.openMenu((MenuProvider)blockEntity).isPresent()) { // Paper - Fix InventoryOpenEvent cancellation
+ player.awardStat(Stats.INTERACT_WITH_FURNACE);
+ }
+ }
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
new file mode 100644
index 0000000000..860a309048
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/GrindstoneBlock.java.patch
@@ -0,0 +1,13 @@
+--- 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
new file mode 100644
index 0000000000..b55ee5aef9
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/GrowingPlantHeadBlock.java.patch
@@ -0,0 +1,39 @@
+--- 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/HoneyBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/HoneyBlock.java.patch
new file mode 100644
index 0000000000..4609ae3eee
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/HoneyBlock.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/level/block/HoneyBlock.java
++++ b/net/minecraft/world/level/block/HoneyBlock.java
+@@ -60,6 +60,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 (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/unapplied/net/minecraft/world/level/block/HopperBlock.java.patch
new file mode 100644
index 0000000000..3e829e33d0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/HopperBlock.java.patch
@@ -0,0 +1,20 @@
+--- a/net/minecraft/world/level/block/HopperBlock.java
++++ b/net/minecraft/world/level/block/HopperBlock.java
+@@ -125,8 +125,7 @@
+
+ @Override
+ protected InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
+- if (!world.isClientSide && world.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
+ player.awardStat(Stats.INSPECT_HOPPER);
+ }
+
+@@ -178,6 +177,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);
+ if (blockEntity instanceof HopperBlockEntity) {
+ HopperBlockEntity.entityInside(world, pos, state, entity, (HopperBlockEntity)blockEntity);
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/HugeMushroomBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/HugeMushroomBlock.java.patch
new file mode 100644
index 0000000000..fb60ae21d7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/HugeMushroomBlock.java.patch
@@ -0,0 +1,34 @@
+--- a/net/minecraft/world/level/block/HugeMushroomBlock.java
++++ b/net/minecraft/world/level/block/HugeMushroomBlock.java
+@@ -45,6 +45,7 @@
+
+ @Override
+ public BlockState getStateForPlacement(BlockPlaceContext ctx) {
++ 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();
+ return this.defaultBlockState()
+@@ -67,6 +68,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 @@
+
+ @Override
+ protected BlockState rotate(BlockState state, Rotation rotation) {
++ 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 @@
+
+ @Override
+ protected BlockState mirror(BlockState state, Mirror mirror) {
++ 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(mirror.mirror(Direction.NORTH)), state.getValue(NORTH))
+ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.SOUTH)), state.getValue(SOUTH))
+ .setValue(PROPERTY_BY_DIRECTION.get(mirror.mirror(Direction.EAST)), state.getValue(EAST))
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
new file mode 100644
index 0000000000..003b651052
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..49d54e7dd2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/InfestedBlock.java.patch
@@ -0,0 +1,19 @@
+--- 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/LavaCauldronBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/LavaCauldronBlock.java.patch
new file mode 100644
index 0000000000..b8db542f5e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/LavaCauldronBlock.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/level/block/LavaCauldronBlock.java
++++ b/net/minecraft/world/level/block/LavaCauldronBlock.java
+@@ -32,6 +32,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 (this.isEntityInsideContent(state, pos, entity)) {
+ entity.lavaHurt();
+ }
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
new file mode 100644
index 0000000000..67743248ac
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/LayeredCauldronBlock.java.patch
@@ -0,0 +1,127 @@
+--- 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
new file mode 100644
index 0000000000..4e3cc600f3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/LeavesBlock.java.patch
@@ -0,0 +1,25 @@
+--- 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
new file mode 100644
index 0000000000..a928fea703
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/LecternBlock.java.patch
@@ -0,0 +1,69 @@
+--- 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
new file mode 100644
index 0000000000..325443e6f3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/LeverBlock.java.patch
@@ -0,0 +1,31 @@
+--- 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/LightBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/LightBlock.java.patch
new file mode 100644
index 0000000000..4e34991e01
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/LightBlock.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/world/level/block/LightBlock.java
++++ b/net/minecraft/world/level/block/LightBlock.java
+@@ -50,7 +50,15 @@
+ builder.add(LEVEL, WATERLOGGED);
+ }
+
++ // Paper start - prevent unintended light block manipulation
+ @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);
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
new file mode 100644
index 0000000000..c97ad7d96c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/LightningRodBlock.java.patch
@@ -0,0 +1,33 @@
+--- 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
new file mode 100644
index 0000000000..42df446a82
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/LiquidBlock.java.patch
@@ -0,0 +1,78 @@
+--- 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
new file mode 100644
index 0000000000..9fc3631803
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/LoomBlock.java.patch
@@ -0,0 +1,13 @@
+--- 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
new file mode 100644
index 0000000000..6febd2458e
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/MangrovePropaguleBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/MangrovePropaguleBlock.java.patch
new file mode 100644
index 0000000000..8a2e3114dc
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/MangrovePropaguleBlock.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/block/MangrovePropaguleBlock.java
++++ b/net/minecraft/world/level/block/MangrovePropaguleBlock.java
+@@ -123,7 +123,7 @@
+ @Override
+ protected void randomTick(BlockState state, ServerLevel world, 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);
+ }
+ } else {
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
new file mode 100644
index 0000000000..31dd40a615
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/MultifaceSpreader.java.patch
@@ -0,0 +1,43 @@
+--- 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
new file mode 100644
index 0000000000..7fa4b44f28
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/MushroomBlock.java.patch
@@ -0,0 +1,46 @@
+--- 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
new file mode 100644
index 0000000000..5b83537d76
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/NetherPortalBlock.java.patch
@@ -0,0 +1,163 @@
+--- 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
new file mode 100644
index 0000000000..849dc04024
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..b1d5c77400
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/NyliumBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/NyliumBlock.java.patch
new file mode 100644
index 0000000000..cc4aae4d48
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/NyliumBlock.java.patch
@@ -0,0 +1,14 @@
+--- a/net/minecraft/world/level/block/NyliumBlock.java
++++ b/net/minecraft/world/level/block/NyliumBlock.java
+@@ -41,6 +41,11 @@
+ @Override
+ protected void randomTick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) {
+ if (!NyliumBlock.canBeNylium(state, world, pos)) {
++ // CraftBukkit start
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, Blocks.NETHERRACK.defaultBlockState()).isCancelled()) {
++ return;
++ }
++ // CraftBukkit end
+ world.setBlockAndUpdate(pos, Blocks.NETHERRACK.defaultBlockState());
+ }
+
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
new file mode 100644
index 0000000000..0718f3ab29
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/ObserverBlock.java.patch
@@ -0,0 +1,30 @@
+--- 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
new file mode 100644
index 0000000000..bdd3b319d7
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..3536598553
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/PointedDripstoneBlock.java.patch
@@ -0,0 +1,96 @@
+--- 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
new file mode 100644
index 0000000000..0f012c2b02
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/PowderSnowBlock.java.patch
@@ -0,0 +1,24 @@
+--- 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
new file mode 100644
index 0000000000..e0443a21f6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/PoweredRailBlock.java.patch
@@ -0,0 +1,24 @@
+--- 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
new file mode 100644
index 0000000000..4b6ba046eb
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/PressurePlateBlock.java.patch
@@ -0,0 +1,61 @@
+--- 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/PumpkinBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/PumpkinBlock.java.patch
new file mode 100644
index 0000000000..d3d384ea46
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/PumpkinBlock.java.patch
@@ -0,0 +1,36 @@
+--- a/net/minecraft/world/level/block/PumpkinBlock.java
++++ b/net/minecraft/world/level/block/PumpkinBlock.java
+@@ -38,16 +38,24 @@
+ } else if (world.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<>());
++ 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);
++ 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,
+- 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
+ );
+ world.addFreshEntity(itemEntity);
++ } // Paper - Add PlayerShearBlockEvent
+ stack.hurtAndBreak(1, player, LivingEntity.getSlotForHand(hand));
+ world.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/unapplied/net/minecraft/world/level/block/RailState.java.patch
new file mode 100644
index 0000000000..b79bfb5efd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/RailState.java.patch
@@ -0,0 +1,52 @@
+--- a/net/minecraft/world/level/block/RailState.java
++++ b/net/minecraft/world/level/block/RailState.java
+@@ -17,6 +17,12 @@
+ private final boolean isStraight;
+ private final List<BlockPos> connections = Lists.newArrayList();
+
++ // Paper start - Fix some rails connecting improperly
++ public boolean isValid() {
++ return this.level.getBlockState(this.pos).getBlock() == this.state.getBlock();
++ }
++ // Paper end - Fix some rails connecting improperly
++
+ public RailState(Level world, BlockPos pos, BlockState state) {
+ this.level = world;
+ this.pos = pos;
+@@ -141,6 +147,11 @@
+ }
+
+ private void connectTo(RailState placementHelper) {
++ // Paper start - Fix some rails connecting improperly
++ if (!this.isValid() || !placementHelper.isValid()) {
++ return;
++ }
++ // Paper end - Fix some rails connecting improperly
+ this.connections.add(placementHelper.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) {
+ this.level.setBlock(this.pos, this.state, 3);
++ // Paper start - Fix some rails connecting improperly
++ if (!this.isValid()) {
++ return this;
++ }
++ // 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 @@
+ }
+
+ public BlockState getState() {
+- return this.state;
++ return this.level.getBlockState(this.pos); // Paper - Fix some rails connecting improperly
+ }
+ }
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
new file mode 100644
index 0000000000..29e5229732
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/RedStoneOreBlock.java.patch
@@ -0,0 +1,102 @@
+--- 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
new file mode 100644
index 0000000000..dc1b67c34c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/RedstoneLampBlock.java.patch
@@ -0,0 +1,35 @@
+--- 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/RedstoneTorchBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch
new file mode 100644
index 0000000000..69d1dcb6e7
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/RedstoneTorchBlock.java.patch
@@ -0,0 +1,88 @@
+--- 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
++
+ public class RedstoneTorchBlock extends BaseTorchBlock {
+
+ public static final MapCodec<RedstoneTorchBlock> CODEC = simpleCodec(RedstoneTorchBlock::new);
+ public static final BooleanProperty LIT = BlockStateProperties.LIT;
+- private static final Map<BlockGetter, List<RedstoneTorchBlock.Toggle>> RECENT_TOGGLES = new WeakHashMap();
++ // Paper - Faster redstone torch rapid clock removal; Move the mapped list to World
+ public static final int RECENT_TOGGLE_TIMER = 60;
+ public static final int MAX_RECENT_TOGGLES = 8;
+ public static final int RESTART_DELAY = 160;
+@@ -79,14 +81,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);
+-
+- while (list != null && !list.isEmpty() && world.getGameTime() - ((RedstoneTorchBlock.Toggle) list.get(0)).when > 60L) {
+- list.remove(0);
++ // Paper start - Faster redstone torch rapid clock removal
++ java.util.ArrayDeque<RedstoneTorchBlock.Toggle> redstoneUpdateInfos = world.redstoneUpdateInfos;
++ if (redstoneUpdateInfos != null) {
++ RedstoneTorchBlock.Toggle curr;
++ while ((curr = redstoneUpdateInfos.peek()) != null && world.getGameTime() - curr.when > 60L) {
++ redstoneUpdateInfos.poll();
++ }
+ }
++ // Paper end - Faster redstone torch rapid clock removal
+
++ // CraftBukkit start
++ org.bukkit.plugin.PluginManager manager = world.getCraftServer().getPluginManager();
++ org.bukkit.block.Block block = world.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);
++ // CraftBukkit end
+ if ((Boolean) state.getValue(RedstoneTorchBlock.LIT)) {
+ if (flag) {
++ // CraftBukkit start
++ if (oldCurrent != 0) {
++ event.setNewCurrent(0);
++ manager.callEvent(event);
++ if (event.getNewCurrent() != 0) {
++ return;
++ }
++ }
++ // 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 @@
+ }
+ }
+ } else if (!flag && !RedstoneTorchBlock.isToggledTooFrequently(world, pos, false)) {
++ // CraftBukkit start
++ if (oldCurrent != 15) {
++ event.setNewCurrent(15);
++ manager.callEvent(event);
++ if (event.getNewCurrent() != 15) {
++ return;
++ }
++ }
++ // CraftBukkit end
+ world.setBlock(pos, (BlockState) state.setValue(RedstoneTorchBlock.LIT, true), 3);
+ }
+
+@@ -134,9 +165,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();
+- });
++ // Paper start - Faster redstone torch rapid clock removal
++ java.util.ArrayDeque<RedstoneTorchBlock.Toggle> list = world.redstoneUpdateInfos;
++ if (list == null) {
++ list = world.redstoneUpdateInfos = new java.util.ArrayDeque<>();
++ }
++ // Paper end - Faster redstone torch rapid clock removal
+
+ if (addNew) {
+ list.add(new RedstoneTorchBlock.Toggle(pos.immutable(), world.getGameTime()));
diff --git a/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
new file mode 100644
index 0000000000..86e3e80aca
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/RespawnAnchorBlock.java.patch
@@ -0,0 +1,46 @@
+--- 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/RootedDirtBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/RootedDirtBlock.java.patch
new file mode 100644
index 0000000000..de8d7301e4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/RootedDirtBlock.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/block/RootedDirtBlock.java
++++ b/net/minecraft/world/level/block/RootedDirtBlock.java
+@@ -34,7 +34,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
+ }
+
+ @Override
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
new file mode 100644
index 0000000000..dbd9b130a0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SaplingBlock.java.patch
@@ -0,0 +1,117 @@
+--- 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
new file mode 100644
index 0000000000..d5960ee388
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..b7e23a6303
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkBlock.java.patch
@@ -0,0 +1,16 @@
+--- 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
new file mode 100644
index 0000000000..12314e3c0c
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..8e63b99d10
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkSensorBlock.java.patch
@@ -0,0 +1,88 @@
+--- 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
new file mode 100644
index 0000000000..8c254d2936
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkShriekerBlock.java.patch
@@ -0,0 +1,30 @@
+--- 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
new file mode 100644
index 0000000000..a334f182c5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkSpreader.java.patch
@@ -0,0 +1,98 @@
+--- 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
new file mode 100644
index 0000000000..4d009bd7c1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SculkVeinBlock.java.patch
@@ -0,0 +1,60 @@
+--- 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/ShulkerBoxBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch
new file mode 100644
index 0000000000..ef705416f0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/ShulkerBoxBlock.java.patch
@@ -0,0 +1,52 @@
+--- 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)) {
+- 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
+ player.awardStat(Stats.OPEN_SHULKER_BOX);
+ PiglinAi.angerNearbyPiglins(serverLevel, player, true);
+ }
+@@ -137,7 +137,7 @@
+ itemEntity.setDefaultPickUpDelay();
+ world.addFreshEntity(itemEntity);
+ } else {
+- shulkerBoxBlockEntity.unpackLootTable(player);
++ shulkerBoxBlockEntity.unpackLootTable(player, true); // Paper - force clear loot table so replenish data isn't persisted in the stack
+ }
+ }
+
+@@ -147,7 +147,15 @@
+ @Override
+ protected List<ItemStack> getDrops(BlockState state, LootParams.Builder builder) {
+ BlockEntity blockEntity = builder.getOptionalParameter(LootContextParams.BLOCK_ENTITY);
++ Runnable reAdd = null; // Paper
+ if (blockEntity instanceof ShulkerBoxBlockEntity shulkerBoxBlockEntity) {
++ // Paper start - clear loot table if it was already used
++ if (shulkerBoxBlockEntity.lootableData().getLastFill() != -1 || !builder.getLevel().paperConfig().lootables.retainUnlootedShulkerBoxLootTableOnNonPlayerBreak) {
++ net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> lootTableResourceKey = shulkerBoxBlockEntity.getLootTable();
++ reAdd = () -> shulkerBoxBlockEntity.setLootTable(lootTableResourceKey);
++ shulkerBoxBlockEntity.setLootTable(null);
++ }
++ // Paper end
+ builder = builder.withDynamicDrop(CONTENTS, lootConsumer -> {
+ for (int i = 0; i < shulkerBoxBlockEntity.getContainerSize(); i++) {
+ lootConsumer.accept(shulkerBoxBlockEntity.getItem(i));
+@@ -155,7 +163,13 @@
+ });
+ }
+
++ // Paper start - re-set loot table if it was cleared
++ try {
+ return super.getDrops(state, builder);
++ } finally {
++ if (reAdd != null) reAdd.run();
++ }
++ // Paper end - re-set loot table if it was cleared
+ }
+
+ @Override
diff --git a/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
new file mode 100644
index 0000000000..5dac1e3a2d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SignBlock.java.patch
@@ -0,0 +1,58 @@
+--- 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/SmithingTableBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SmithingTableBlock.java.patch
new file mode 100644
index 0000000000..e7724b2941
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SmithingTableBlock.java.patch
@@ -0,0 +1,13 @@
+--- a/net/minecraft/world/level/block/SmithingTableBlock.java
++++ b/net/minecraft/world/level/block/SmithingTableBlock.java
+@@ -38,8 +38,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_SMITHING_TABLE);
++ } // Paper - Fix InventoryOpenEvent cancellation
+ }
+
+ return InteractionResult.SUCCESS;
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/SmokerBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/SmokerBlock.java.patch
new file mode 100644
index 0000000000..1d94e2a7cc
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SmokerBlock.java.patch
@@ -0,0 +1,12 @@
+--- a/net/minecraft/world/level/block/SmokerBlock.java
++++ b/net/minecraft/world/level/block/SmokerBlock.java
+@@ -44,8 +44,7 @@
+ @Override
+ protected void openContainer(Level world, BlockPos pos, Player player) {
+ BlockEntity blockEntity = world.getBlockEntity(pos);
+- if (blockEntity instanceof SmokerBlockEntity) {
+- player.openMenu((MenuProvider)blockEntity);
++ if (blockEntity instanceof SmokerBlockEntity && player.openMenu((MenuProvider)blockEntity).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/unapplied/net/minecraft/world/level/block/SnifferEggBlock.java.patch
new file mode 100644
index 0000000000..ff63f628fe
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SnifferEggBlock.java.patch
@@ -0,0 +1,53 @@
+--- a/net/minecraft/world/level/block/SnifferEggBlock.java
++++ b/net/minecraft/world/level/block/SnifferEggBlock.java
+@@ -61,12 +61,31 @@
+ return this.getHatchLevel(state) == 2;
+ }
+
++ // Paper start - Call BlockFadeEvent
++ private void rescheduleTick(ServerLevel world, BlockPos pos) {
++ int baseDelay = hatchBoost(world, pos) ? world.paperConfig().entities.sniffer.boostedHatchTime.or(BOOSTED_HATCH_TIME_TICKS) : world.paperConfig().entities.sniffer.hatchTime.or(REGULAR_HATCH_TIME_TICKS); // Paper - Configure sniffer egg hatch time
++ world.scheduleTick(pos, this, (baseDelay / 3) + world.random.nextInt(RANDOM_HATCH_OFFSET_TICKS));
++ // reschedule to avoid being stuck here and behave like the other calls (see #onPlace)
++ }
++ // Paper end - Call BlockFadeEvent
++
+ @Override
+ public void tick(BlockState state, ServerLevel world, 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);
++ 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);
+ } else {
++ // Paper start - Call BlockFadeEvent
++ if (org.bukkit.craftbukkit.event.CraftEventFactory.callBlockFadeEvent(world, pos, state.getFluidState().createLegacyBlock()).isCancelled()) {
++ this.rescheduleTick(world, 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();
+ 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
+ }
+ }
+ }
+@@ -86,7 +105,7 @@
+ world.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));
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
new file mode 100644
index 0000000000..5db7dbeea9
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..8f4ea72cba
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SpawnerBlock.java.patch
@@ -0,0 +1,26 @@
+--- 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
new file mode 100644
index 0000000000..d0c197c400
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SpongeBlock.java.patch
@@ -0,0 +1,114 @@
+--- 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
new file mode 100644
index 0000000000..5777f59736
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..625e104922
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/StemBlock.java.patch
@@ -0,0 +1,47 @@
+--- 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/StonecutterBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/StonecutterBlock.java.patch
new file mode 100644
index 0000000000..33e97daa48
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/StonecutterBlock.java.patch
@@ -0,0 +1,13 @@
+--- a/net/minecraft/world/level/block/StonecutterBlock.java
++++ b/net/minecraft/world/level/block/StonecutterBlock.java
+@@ -48,8 +48,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_STONECUTTER);
++ } // Paper - Fix InventoryOpenEvent cancellation
+ }
+
+ return InteractionResult.SUCCESS;
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
new file mode 100644
index 0000000000..903b77b2e2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SugarCaneBlock.java.patch
@@ -0,0 +1,21 @@
+--- 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
new file mode 100644
index 0000000000..c0fadbe893
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/SweetBerryBushBlock.java.patch
@@ -0,0 +1,62 @@
+--- 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/TargetBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/TargetBlock.java.patch
new file mode 100644
index 0000000000..5478fd0968
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/TargetBlock.java.patch
@@ -0,0 +1,45 @@
+--- a/net/minecraft/world/level/block/TargetBlock.java
++++ b/net/minecraft/world/level/block/TargetBlock.java
+@@ -42,6 +42,10 @@
+ @Override
+ protected void onProjectileHit(Level world, BlockState state, BlockHitResult hit, Projectile projectile) {
+ int i = updateRedstoneOutput(world, state, hit, projectile);
++ // Paper start - Add TargetHitEvent
++ }
++ private static void awardTargetHitCriteria(Projectile projectile, BlockHitResult hit, int i) {
++ // Paper end - Add TargetHitEvent
+ 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;
++ // 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());
++ 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();
++ shouldAward = true;
++ } else {
++ return i;
++ }
++ }
++ // Paper end - Add TargetHitEvent
+ if (!world.getBlockTicks().hasScheduledTick(hitResult.getBlockPos(), state.getBlock())) {
+ setOutputPower(world, state, i, hitResult.getBlockPos(), j);
+ }
+
++ // Paper start - Award Hit Criteria after Block Update
++ if (shouldAward) {
++ awardTargetHitCriteria((Projectile) entity, hitResult, i);
++ }
++ // Paper end - Award Hit Criteria after Block Update
++
+ return i;
+ }
+
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
new file mode 100644
index 0000000000..42cf7a6881
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/TntBlock.java.patch
@@ -0,0 +1,102 @@
+--- 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
new file mode 100644
index 0000000000..0f858bdcd3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/TrapDoorBlock.java.patch
@@ -0,0 +1,51 @@
+--- 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
new file mode 100644
index 0000000000..b081063a3f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/TripWireBlock.java.patch
@@ -0,0 +1,114 @@
+--- 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
new file mode 100644
index 0000000000..85f745ec8f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/TripWireHookBlock.java.patch
@@ -0,0 +1,34 @@
+--- 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
new file mode 100644
index 0000000000..c27f68ac72
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/TurtleEggBlock.java.patch
@@ -0,0 +1,76 @@
+--- 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
new file mode 100644
index 0000000000..0403549dc8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/VineBlock.java.patch
@@ -0,0 +1,79 @@
+--- 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
new file mode 100644
index 0000000000..73be8c6267
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..279e050da1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/WaterlilyBlock.java.patch
@@ -0,0 +1,27 @@
+--- 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/WebBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/WebBlock.java.patch
new file mode 100644
index 0000000000..1204a1e128
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/WebBlock.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/level/block/WebBlock.java
++++ b/net/minecraft/world/level/block/WebBlock.java
+@@ -24,6 +24,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
+ 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/unapplied/net/minecraft/world/level/block/WeightedPressurePlateBlock.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/WeightedPressurePlateBlock.java.patch
new file mode 100644
index 0000000000..67ce37fdf0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/WeightedPressurePlateBlock.java.patch
@@ -0,0 +1,49 @@
+--- 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
new file mode 100644
index 0000000000..c456abf513
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/WitherRoseBlock.java.patch
@@ -0,0 +1,15 @@
+--- 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
new file mode 100644
index 0000000000..e34748cc0e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/WitherSkullBlock.java.patch
@@ -0,0 +1,50 @@
+--- 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
new file mode 100644
index 0000000000..8438fd826a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java.patch
@@ -0,0 +1,356 @@
+--- 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
new file mode 100644
index 0000000000..3b8ce9376c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BannerBlockEntity.java.patch
@@ -0,0 +1,81 @@
+--- 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
new file mode 100644
index 0000000000..bc89832782
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BarrelBlockEntity.java.patch
@@ -0,0 +1,52 @@
+--- 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/BaseContainerBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java.patch
new file mode 100644
index 0000000000..07ed6af225
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java.patch
@@ -0,0 +1,62 @@
+--- a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java
+@@ -73,17 +73,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
+ }
+
++ @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - Add BlockLockCheckEvent
+ public static boolean canUnlock(Player player, LockCode lock, Component containerName) {
++ // Paper start - Add BlockLockCheckEvent
++ return canUnlock(player, lock, containerName, null);
++ }
++ public static boolean canUnlock(Player player, LockCode lock, Component containerName, @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.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()))) {
++ if (event.getLockedMessage() != null) {
++ event.getPlayer().sendActionBar(event.getLockedMessage());
++ }
++ if (event.getLockedSound() != null) {
++ event.getPlayer().playSound(event.getLockedSound());
++ }
++ return false;
++ } else {
++ return true;
++ }
++ } 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
+ player.playNotifySound(SoundEvents.CHEST_LOCKED, SoundSource.BLOCKS, 1.0F, 1.0F);
+ return false;
+ } else {
+ return true;
+ }
++ } // Paper - Add BlockLockCheckEvent
+ }
+
+ protected abstract NonNullList<ItemStack> getItems();
+@@ -178,4 +205,12 @@
+ nbt.remove("lock");
+ nbt.remove("Items");
+ }
++
++ // CraftBukkit start
++ @Override
++ 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());
++ }
++ // CraftBukkit end
+ }
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
new file mode 100644
index 0000000000..736be5da6c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BeaconBlockEntity.java.patch
@@ -0,0 +1,259 @@
+--- 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
new file mode 100644
index 0000000000..8457946183
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java.patch
@@ -0,0 +1,306 @@
+--- 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/BellBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BellBlockEntity.java.patch
new file mode 100644
index 0000000000..c8f5ced60a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BellBlockEntity.java.patch
@@ -0,0 +1,65 @@
+--- a/net/minecraft/world/level/block/entity/BellBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/BellBlockEntity.java
+@@ -63,6 +63,11 @@
+
+ if (blockEntity.ticks >= 50) {
+ blockEntity.shaking = false;
++ // Paper start - Fix bell block entity memory leak
++ if (!blockEntity.resonating) {
++ blockEntity.nearbyEntities.clear();
++ }
++ // Paper end - Fix bell block entity memory leak
+ blockEntity.ticks = 0;
+ }
+
+@@ -76,6 +81,7 @@
+ ++blockEntity.resonationTicks;
+ } else {
+ bellEffect.run(world, 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
+ }
+ }
+ }
+
++ 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 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 showBellParticles(Level world, BlockPos pos, List<LivingEntity> hearingEntities) {
+@@ -178,6 +189,13 @@
+ }
+
+ 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;
++ // Paper end - Add BellRevealRaiderEvent
+ entity.addEffect(new MobEffectInstance(MobEffects.GLOWING, 60));
+ }
+
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
new file mode 100644
index 0000000000..aca157f8aa
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BlockEntity.java.patch
@@ -0,0 +1,153 @@
+--- 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
new file mode 100644
index 0000000000..c2e5be1463
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BlockEntityType.java.patch
@@ -0,0 +1,20 @@
+--- 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
new file mode 100644
index 0000000000..39dec9ecc2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java.patch
@@ -0,0 +1,232 @@
+--- 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
new file mode 100644
index 0000000000..57515e3834
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/BrushableBlockEntity.java.patch
@@ -0,0 +1,36 @@
+--- 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/CalibratedSculkSensorBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java.patch
new file mode 100644
index 0000000000..944379decd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/CalibratedSculkSensorBlockEntity.java
+@@ -20,6 +20,12 @@
+ public VibrationSystem.User createVibrationUser() {
+ return new CalibratedSculkSensorBlockEntity.VibrationUser(this.getBlockPos());
+ }
++ // Paper start - Configurable sculk sensor listener range
++ @Override
++ protected void saveRangeOverride(final net.minecraft.nbt.CompoundTag nbt) {
++ if (this.rangeOverride != null && this.rangeOverride != 16) nbt.putInt(PAPER_LISTENER_RANGE_NBT_KEY, this.rangeOverride); // only save if it's different from the default
++ }
++ // Paper end - Configurable sculk sensor listener range
+
+ protected class VibrationUser extends SculkSensorBlockEntity.VibrationUser {
+ public VibrationUser(final BlockPos pos) {
+@@ -28,6 +34,7 @@
+
+ @Override
+ public int getListenerRadius() {
++ if (CalibratedSculkSensorBlockEntity.this.rangeOverride != null) return CalibratedSculkSensorBlockEntity.this.rangeOverride; // Paper - Configurable sculk sensor listener range
+ return 16;
+ }
+
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
new file mode 100644
index 0000000000..aea8cb9be4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CampfireBlockEntity.java.patch
@@ -0,0 +1,121 @@
+--- 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
new file mode 100644
index 0000000000..537f1913b2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ChestBlockEntity.java.patch
@@ -0,0 +1,51 @@
+--- 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/ChiseledBookShelfBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java.patch
new file mode 100644
index 0000000000..af35923253
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java.patch
@@ -0,0 +1,85 @@
+--- 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;
+
++// 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<>();
++ private int maxStack = 1;
+
++ @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 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 ChiseledBookShelfBlockEntity(BlockPos pos, BlockState state) {
+ super(BlockEntityType.CHISELED_BOOKSHELF, pos, state);
+ this.items = NonNullList.withSize(6, ItemStack.EMPTY);
+@@ -100,7 +142,7 @@
+
+ this.items.set(slot, ItemStack.EMPTY);
+ 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 @@
+ public void setItem(int slot, ItemStack stack) {
+ if (stack.is(ItemTags.BOOKSHELF_BOOKS)) {
+ this.items.set(slot, stack);
+- this.updateState(slot);
++ if (this.level != null) this.updateState(slot); // CraftBukkit - SPIGOT-7381: check for null world
+ } else if (stack.isEmpty()) {
+ this.removeItem(slot, 1);
+ }
+@@ -131,7 +173,7 @@
+
+ @Override
+ public int getMaxStackSize() {
+- return 1;
++ return this.maxStack; // CraftBukkit
+ }
+
+ @Override
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
new file mode 100644
index 0000000000..49a6bff68c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CommandBlockEntity.java.patch
@@ -0,0 +1,26 @@
+--- 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
new file mode 100644
index 0000000000..6dd6c8627e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ConduitBlockEntity.java.patch
@@ -0,0 +1,108 @@
+--- 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/ContainerOpenersCounter.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch
new file mode 100644
index 0000000000..b594786d46
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java.patch
@@ -0,0 +1,76 @@
+--- a/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java
++++ b/net/minecraft/world/level/block/entity/ContainerOpenersCounter.java
+@@ -17,6 +17,7 @@
+ private static final int CHECK_TICK_DELAY = 5;
+ private int openCount;
+ private double maxInteractionRange;
++ public boolean opened; // CraftBukkit
+
+ public ContainerOpenersCounter() {}
+
+@@ -26,11 +27,36 @@
+
+ protected abstract void openerCountChanged(Level world, BlockPos pos, BlockState state, int oldViewerCount, int newViewerCount);
+
++ // CraftBukkit start
++ public void onAPIOpen(Level world, BlockPos blockposition, BlockState iblockdata) {
++ this.onOpen(world, blockposition, iblockdata);
++ }
++
++ public void onAPIClose(Level world, BlockPos blockposition, BlockState iblockdata) {
++ this.onClose(world, blockposition, iblockdata);
++ }
++
++ public void openerAPICountChanged(Level world, BlockPos blockposition, BlockState iblockdata, int i, int j) {
++ this.openerCountChanged(world, blockposition, iblockdata, i, j);
++ }
++ // CraftBukkit end
++
+ protected abstract boolean isOwnContainer(Player player);
+
+ public void incrementOpeners(Player player, Level world, 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)) {
++ int newPower = Math.max(0, Math.min(15, this.openCount));
++
++ if (oldPower != newPower) {
++ org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(world, 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 @@
+ }
+
+ public void decrementOpeners(Player player, Level world, 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)) {
++ int newPower = Math.max(0, Math.min(15, this.openCount));
++
++ if (oldPower != newPower) {
++ org.bukkit.craftbukkit.event.CraftEventFactory.callRedstoneChange(world, 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 @@
+ }
+
+ int i = list.size();
++ if (this.opened) i++; // CraftBukkit - add dummy count from API
+ int j = this.openCount;
+
+ if (j != i) {
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
new file mode 100644
index 0000000000..e71ac50ca3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/CrafterBlockEntity.java.patch
@@ -0,0 +1,68 @@
+--- 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/DecoratedPotBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/DecoratedPotBlockEntity.java.patch
new file mode 100644
index 0000000000..6c93436b46
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/DecoratedPotBlockEntity.java.patch
@@ -0,0 +1,62 @@
+--- 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;
+ 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<>();
++ private int maxStack = MAX_STACK;
++
++ @Override
++ public List<ItemStack> getContents() {
++ return Arrays.asList(this.item);
++ }
++
++ @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;
++ }
++
++ @Override
++ public Location getLocation() {
++ if (this.level == null) return null;
++ return CraftLocation.toBukkit(this.worldPosition, this.level.getWorld());
++ }
++ // CraftBukkit end
++
+ public static final String TAG_SHERDS = "sherds";
+ public static final String TAG_ITEM = "item";
+ public static final int EVENT_POT_WOBBLES = 1;
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
new file mode 100644
index 0000000000..ae1688f953
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/DispenserBlockEntity.java.patch
@@ -0,0 +1,50 @@
+--- 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
new file mode 100644
index 0000000000..58e1ea39e6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/HopperBlockEntity.java.patch
@@ -0,0 +1,322 @@
+--- 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/JigsawBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/JigsawBlockEntity.java.patch
new file mode 100644
index 0000000000..11519f80c2
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/JigsawBlockEntity.java.patch
@@ -0,0 +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) {
+ 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);
++ // Paper start - Replace getHolderOrThrow with a null check
++ Holder<StructureTemplatePool> holder = registry.get(this.pool).orElse(null);
++ if (holder == null) {
++ return;
++ }
++ // Paper end - Replace getHolderOrThrow with a null check
+ JigsawPlacement.generateJigsaw(world, holder, 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/unapplied/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java.patch
new file mode 100644
index 0000000000..fa8fa48ae9
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/JukeboxBlockEntity.java.patch
@@ -0,0 +1,92 @@
+--- 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;
+ 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>();
++ private int maxStack = MAX_STACK;
++ public boolean opened;
+
++ @Override
++ public List<ItemStack> getContents() {
++ return Collections.singletonList(this.item);
++ }
++
++ @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 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 JukeboxBlockEntity(BlockPos pos, BlockState state) {
+ super(BlockEntityType.JUKEBOX, pos, state);
+ this.item = ItemStack.EMPTY;
+@@ -137,7 +181,7 @@
+
+ @Override
+ public int getMaxStackSize() {
+- return 1;
++ return this.maxStack; // CraftBukkit
+ }
+
+ @Override
+@@ -156,12 +200,17 @@
+ }
+
+ @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
+ });
+- this.level.updateNeighborsAt(this.getBlockPos(), this.getBlockState().getBlock());
++ // CraftBukkit start - add null check for level
++ if (this.level != null) {
++ this.level.updateNeighborsAt(this.getBlockPos(), this.getBlockState().getBlock());
++ }
++ // CraftBukkit end
+ this.setChanged();
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/LecternBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/LecternBlockEntity.java.patch
new file mode 100644
index 0000000000..afaf4c6781
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/LecternBlockEntity.java.patch
@@ -0,0 +1,164 @@
+--- 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 @@
+ public static final int NUM_DATA = 1;
+ public static final int SLOT_BOOK = 0;
+ public static final int NUM_SLOTS = 1;
+- public final Container bookAccess = new Container() {
++ // CraftBukkit start - add fields and methods
++ public final Container bookAccess = new LecternInventory();
++ public class LecternInventory implements Container {
++
++ public List<HumanEntity> transaction = new ArrayList<>();
++ private int maxStack = 1;
++
+ @Override
++ public List<ItemStack> getContents() {
++ return Arrays.asList(LecternBlockEntity.this.book);
++ }
++
++ @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 void setMaxStackSize(int i) {
++ this.maxStack = i;
++ }
++
++ @Override
++ public Location getLocation() {
++ if (LecternBlockEntity.this.level == null) return null;
++ return CraftLocation.toBukkit(LecternBlockEntity.this.worldPosition, LecternBlockEntity.this.level.getWorld());
++ }
++
++ @Override
++ public InventoryHolder getOwner() {
++ return (Lectern) LecternBlockEntity.this.getOwner();
++ }
++
++ public LecternBlockEntity getLectern() {
++ return LecternBlockEntity.this;
++ }
++ // CraftBukkit end
++
++ @Override
+ public int getContainerSize() {
+ return 1;
+ }
+@@ -80,11 +138,20 @@
+ }
+
+ @Override
+- public void setItem(int slot, ItemStack stack) {}
++ // CraftBukkit start
++ public void setItem(int slot, ItemStack stack) {
++ 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
+
+ @Override
+ public int getMaxStackSize() {
+- return 1;
++ return this.maxStack; // CraftBukkit
+ }
+
+ @Override
+@@ -164,7 +231,7 @@
+ if (j != this.page) {
+ this.page = j;
+ 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;
+ }
+
++ // 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, LecternBlockEntity.this);
++ }
++
++ @Override
++ public boolean acceptsSuccess() {
++ return false;
++ }
++
++ @Override
++ public boolean acceptsFailure() {
++ return false;
++ }
++
++ @Override
++ public boolean shouldInformAdmins() {
++ return false;
++ }
++ };
++ // CraftBukkit end
++
+ private CommandSourceStack createCommandSourceStack(@Nullable Player player, ServerLevel world) {
+ String s;
+ Object object;
+@@ -203,7 +299,8 @@
+
+ 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);
+ }
+
+ @Override
+@@ -236,7 +333,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
+ }
+
+ @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch
new file mode 100644
index 0000000000..16ff81b3cd
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/RandomizableContainerBlockEntity.java.patch
@@ -0,0 +1,16 @@
+--- 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");
+ }
++
++ // Paper start - LootTable API
++ final com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData = new com.destroystokyo.paper.loottable.PaperLootableInventoryData(); // Paper
++
++ @Override
++ public com.destroystokyo.paper.loottable.PaperLootableInventoryData lootableData() {
++ return this.lootableData;
++ }
++ // Paper end - LootTable API
+ }
diff --git a/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
new file mode 100644
index 0000000000..49d4011082
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkCatalystBlockEntity.java.patch
@@ -0,0 +1,29 @@
+--- 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/SculkSensorBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java.patch
new file mode 100644
index 0000000000..112cb33190
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java.patch
@@ -0,0 +1,49 @@
+--- a/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/SculkSensorBlockEntity.java
+@@ -26,6 +26,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 @@
+ .resultOrPartial(string -> LOGGER.error("Failed to parse vibration listener for Sculk Sensor: '{}'", string))
+ .ifPresent(listener -> this.vibrationData = listener);
+ }
++ // Paper start - Configurable sculk sensor listener range
++ if (nbt.contains(PAPER_LISTENER_RANGE_NBT_KEY)) {
++ this.rangeOverride = nbt.getInt(PAPER_LISTENER_RANGE_NBT_KEY);
++ } else {
++ this.rangeOverride = null;
++ }
++ // Paper end - Configurable sculk sensor listener range
+ }
+
++ 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 @@
+ .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
+ }
++ // 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
++ }
++ // Paper end - Configurable sculk sensor listener range
+
+ @Override
+ public VibrationSystem.Data getVibrationData() {
+@@ -100,6 +115,7 @@
+
+ @Override
+ public int getListenerRadius() {
++ if (SculkSensorBlockEntity.this.rangeOverride != null) return SculkSensorBlockEntity.this.rangeOverride; // Paper - Configurable sculk sensor listener range
+ return 8;
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java.patch
new file mode 100644
index 0000000000..fb8ab267d1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java.patch
@@ -0,0 +1,25 @@
+--- a/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/SculkShriekerBlockEntity.java
+@@ -105,6 +105,13 @@
+
+ @Nullable
+ public static ServerPlayer tryGetPlayer(@Nullable Entity entity) {
++ // Paper start - check global player list where appropriate; ensure level is the same for sculk events
++ final ServerPlayer player = tryGetPlayer0(entity);
++ return player != null && player.level() == entity.level() ? player : null;
++ }
++ @Nullable
++ private static ServerPlayer tryGetPlayer0(@Nullable Entity entity) {
++ // Paper end - check global player list where appropriate
+ if (entity instanceof ServerPlayer) {
+ return (ServerPlayer)entity;
+ } else {
+@@ -190,7 +197,7 @@
+ private boolean trySummonWarden(ServerLevel world) {
+ 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
+ )
+ .isPresent();
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch
new file mode 100644
index 0000000000..384ab32533
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java.patch
@@ -0,0 +1,67 @@
+--- 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 @@
+ @Nullable
+ private final DyeColor color;
+
++ // CraftBukkit start - add fields and methods
++ public List<HumanEntity> transaction = new java.util.ArrayList<HumanEntity>();
++ private int maxStack = MAX_STACK;
++ public boolean opened;
++
++ 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;
++ }
++
++ @Override
++ public int getMaxStackSize() {
++ return this.maxStack;
++ }
++
++ public void setMaxStackSize(int size) {
++ this.maxStack = size;
++ }
++ // 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 @@
+ }
+
+ ++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 @@
+ public void stopOpen(Player player) {
+ if (!this.remove && !player.isSpectator()) {
+ --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);
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
new file mode 100644
index 0000000000..788235f327
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SignBlockEntity.java.patch
@@ -0,0 +1,269 @@
+--- 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/SkullBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SkullBlockEntity.java.patch
new file mode 100644
index 0000000000..a683a1cb1b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/SkullBlockEntity.java.patch
@@ -0,0 +1,73 @@
+--- a/net/minecraft/world/level/block/entity/SkullBlockEntity.java
++++ b/net/minecraft/world/level/block/entity/SkullBlockEntity.java
+@@ -41,7 +41,7 @@
+ @Nullable
+ private static LoadingCache<String, CompletableFuture<Optional<GameProfile>>> profileCacheByName;
+ @Nullable
+- private static LoadingCache<UUID, CompletableFuture<Optional<GameProfile>>> profileCacheById;
++ private static LoadingCache<com.mojang.datafixers.util.Pair<java.util.UUID, @org.jetbrains.annotations.Nullable GameProfile>, CompletableFuture<Optional<GameProfile>>> profileCacheById; // Paper - player profile events
+ public static final Executor CHECKED_MAIN_THREAD_EXECUTOR = runnable -> {
+ Executor executor = mainThreadExecutor;
+ if (executor != null) {
+@@ -76,9 +76,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);
+ }
+ });
+@@ -89,23 +89,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
+ : 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
+ return CompletableFuture.supplyAsync(() -> {
+ if (booleanSupplier.getAsBoolean()) {
+ return Optional.empty();
+ } else {
+- ProfileResult profileResult = apiServices.sessionService().fetchProfile(uuid, 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(), "");
++ return Optional.ofNullable(paperService.fetchProfile(profile, true)).map(ProfileResult::profile);
++ }
++ ProfileResult profileResult = apiServices.sessionService().fetchProfile(pair.getFirst(), true);
++ // Paper end - fill player profile events
+ return Optional.ofNullable(profileResult).map(ProfileResult::profile);
+ }
+- }, Util.backgroundExecutor().forName("fetchProfile"));
++ }, Util.PROFILE_EXECUTOR); // Paper - don't submit BLOCKING PROFILE LOOKUPS to the world gen thread
+ }
+
+ public static void clear() {
+@@ -210,9 +216,11 @@
+ : CompletableFuture.completedFuture(Optional.empty());
+ }
+
+- public static CompletableFuture<Optional<GameProfile>> fetchGameProfile(UUID uuid) {
+- LoadingCache<UUID, CompletableFuture<Optional<GameProfile>>> loadingCache = profileCacheById;
+- return loadingCache != null ? loadingCache.getUnchecked(uuid) : 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;
++ return loadingCache != null ? loadingCache.getUnchecked(new com.mojang.datafixers.util.Pair<>(uuid, name != null ? new com.mojang.authlib.GameProfile(uuid, name) : null)) : CompletableFuture.completedFuture(Optional.empty());
++ // Paper end - player profile events
+ }
+
+ @Override
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
new file mode 100644
index 0000000000..b5587ac5d9
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java.patch
@@ -0,0 +1,19 @@
+--- 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
new file mode 100644
index 0000000000..e58a4bb1dc
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawner.java.patch
@@ -0,0 +1,63 @@
+--- 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
new file mode 100644
index 0000000000..9f48f83f27
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java.patch
@@ -0,0 +1,32 @@
+--- 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
new file mode 100644
index 0000000000..293d3ea411
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..928e901a76
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/entity/vault/VaultBlockEntity.java.patch
@@ -0,0 +1,83 @@
+--- 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
new file mode 100644
index 0000000000..859e74b185
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/grower/TreeGrower.java.patch
@@ -0,0 +1,126 @@
+--- 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
new file mode 100644
index 0000000000..d269336ba6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/piston/PistonBaseBlock.java.patch
@@ -0,0 +1,180 @@
+--- 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/piston/PistonMovingBlockEntity.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch
new file mode 100644
index 0000000000..03d9e64403
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java.patch
@@ -0,0 +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);
+ 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);
+ } 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/unapplied/net/minecraft/world/level/block/state/BlockBehaviour.java.patch
new file mode 100644
index 0000000000..56a6b42a76
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockBehaviour.java.patch
@@ -0,0 +1,193 @@
+--- 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
+
+ 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) {
++ 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) {
++ this.onPlace(iblockdata, world, blockposition, iblockdata1, flag);
++ }
++ // CraftBukkit end
++
+ protected void onRemove(BlockState state, Level world, BlockPos pos, BlockState newState, boolean moved) {
++ org.spigotmc.AsyncCatcher.catchOp("block remove"); // Spigot
+ if (state.hasBlockEntity() && !state.is(newState.getBlock())) {
+ world.removeBlockEntity(pos);
+ }
+@@ -166,7 +178,7 @@
+ }
+
+ protected void onExplosionHit(BlockState state, ServerLevel world, BlockPos pos, Explosion explosion, BiConsumer<ItemStack, BlockPos> stackMerger) {
+- 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 (explosion.getBlockInteraction() == Explosion.BlockInteraction.DESTROY_WITH_DECAY) {
+- lootparams_a.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);
++ // CraftBukkit end
+ }
+
+ state.spawnAfterBreak(world, pos, ItemStack.EMPTY, flag);
+@@ -243,7 +257,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, Fluid fluid) {
+@@ -851,7 +865,15 @@
+ this.spawnTerrainParticles = blockbase_info.spawnTerrainParticles;
+ this.instrument = blockbase_info.instrument;
+ this.replaceable = blockbase_info.replaceable;
++ }
++ // Paper start - Perf: impl cached craft block data, lazy load to fix issue with loading at the wrong time
++ 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();
+ }
++ // 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 @@
+ }
+ }
+
++ protected boolean shapeExceedsCube = true; // Paper - moved from actual method to here
+ public void initCache() {
+ this.fluidState = ((Block) this.owner).getFluidState(this.asState());
+ this.isRandomlyTicking = ((Block) this.owner).isRandomlyTicking(this.asState());
+ if (!this.getBlock().hasDynamicShape()) {
+ this.cache = new BlockBehaviour.BlockStateBase.Cache(this.asState());
+ }
++ this.shapeExceedsCube = this.cache == null || this.cache.largeCollisionShape; // Paper - moved from actual method to here
+
+ this.legacySolid = this.calculateSolid();
+ this.occlusionShape = this.canOcclude ? ((Block) this.owner).getOcclusionShape(this.asState()) : Shapes.empty();
+@@ -925,6 +949,12 @@
+ 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 @@
+ return this.occlusionShape;
+ }
+
+- public boolean hasLargeCollisionShape() {
+- return this.cache == null || this.cache.largeCollisionShape;
++ public final boolean hasLargeCollisionShape() { // Paper
++ return this.shapeExceedsCube; // Paper - moved into shape cache init
+ }
+
+- public boolean useShapeForLightOcclusion() {
++ public final boolean useShapeForLightOcclusion() { // Paper - Perf: Final for inlining
+ return this.useShapeForLightOcclusion;
+ }
+
+- public int getLightEmission() {
++ public final int getLightEmission() { // Paper - Perf: Final for inlining
+ return this.lightEmission;
+ }
+
+- public boolean isAir() {
++ public final boolean isAir() { // Paper - Perf: Final for inlining
+ return this.isAir;
+ }
+
+@@ -1028,14 +1058,14 @@
+ }
+
+ public PushReaction getPistonPushReaction() {
+- return this.pushReaction;
++ return !this.isDestroyable() ? PushReaction.BLOCK : this.pushReaction; // Paper - Protect Bedrock and End Portal/Frames from being destroyed
+ }
+
+ public boolean isSolidRender() {
+ return this.solidRender;
+ }
+
+- public boolean canOcclude() {
++ public final boolean canOcclude() { // Paper - Perf: Final for inlining
+ return this.canOcclude;
+ }
+
+@@ -1125,7 +1155,13 @@
+ }
+
+ public void onPlace(Level world, BlockPos pos, BlockState state, boolean notify) {
+- this.getBlock().onPlace(this.asState(), world, pos, state, notify);
++ // CraftBukkit start
++ this.onPlace(world, pos, state, notify, 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);
++ // CraftBukkit end
+ }
+
+ public void onRemove(Level world, BlockPos pos, BlockState state, boolean moved) {
+@@ -1154,6 +1190,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 List<ItemStack> getDrops(LootParams.Builder builder) {
+@@ -1250,11 +1287,11 @@
+ return this.getBlock().builtInRegistryHolder().is(key);
+ }
+
+- public FluidState getFluidState() {
++ public final FluidState getFluidState() { // Paper - Perf: Final for inlining
+ return this.fluidState;
+ }
+
+- public boolean isRandomlyTicking() {
++ public final boolean isRandomlyTicking() { // Paper - Perf: Final for inlining
+ return this.isRandomlyTicking;
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockState.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockState.java.patch
new file mode 100644
index 0000000000..77d0d6d1c5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/block/state/BlockState.java.patch
@@ -0,0 +1,19 @@
+--- a/net/minecraft/world/level/block/state/BlockState.java
++++ b/net/minecraft/world/level/block/state/BlockState.java
+@@ -10,6 +10,16 @@
+ 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;
++
++ public final org.bukkit.Material getBukkitMaterial() {
++ if (this.cachedMaterial == null) {
++ this.cachedMaterial = org.bukkit.craftbukkit.block.CraftBlockType.minecraftToBukkit(this.getBlock());
++ }
++ return this.cachedMaterial;
++ }
++ // Paper end - optimise getType calls
+ public BlockState(Block block, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap, MapCodec<BlockState> codec) {
+ super(block, propertyMap, codec);
+ }
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
new file mode 100644
index 0000000000..95194b370f
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..6542f0bc6f
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..e0a6a659ba
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/border/WorldBorder.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/border/WorldBorder.java.patch
new file mode 100644
index 0000000000..c2615ad43a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/border/WorldBorder.java.patch
@@ -0,0 +1,99 @@
+--- a/net/minecraft/world/level/border/WorldBorder.java
++++ b/net/minecraft/world/level/border/WorldBorder.java
+@@ -30,6 +30,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);
++ 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());
+ }
+
++ // 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);
++ }
++ // 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);
+ }
+@@ -135,6 +148,14 @@
+ }
+
+ public void setCenter(double x, double z) {
++ // Paper start - Add worldborder events
++ if (this.world != null) {
++ io.papermc.paper.event.world.border.WorldBorderCenterChangeEvent event = new io.papermc.paper.event.world.border.WorldBorderCenterChangeEvent(world.getWorld(), world.getWorld().getWorldBorder(), new org.bukkit.Location(world.getWorld(), this.getCenterX(), 0, this.getCenterZ()), new org.bukkit.Location(world.getWorld(), x, 0, z));
++ if (!event.callEvent()) return;
++ x = event.getNewCenter().getX();
++ z = event.getNewCenter().getZ();
++ }
++ // Paper end - Add worldborder events
+ this.centerX = x;
+ this.centerZ = z;
+ this.extent.onCenterChange();
+@@ -161,6 +182,17 @@
+ }
+
+ public void setSize(double size) {
++ // Paper start - Add worldborder events
++ if (this.world != null) {
++ io.papermc.paper.event.world.border.WorldBorderBoundsChangeEvent event = new io.papermc.paper.event.world.border.WorldBorderBoundsChangeEvent(world.getWorld(), world.getWorld().getWorldBorder(), io.papermc.paper.event.world.border.WorldBorderBoundsChangeEvent.Type.INSTANT_MOVE, getSize(), size, 0);
++ if (!event.callEvent()) return;
++ if (event.getType() == io.papermc.paper.event.world.border.WorldBorderBoundsChangeEvent.Type.STARTED_MOVE && event.getDuration() > 0) { // If changed to a timed transition
++ lerpSizeBetween(event.getOldSize(), event.getNewSize(), event.getDuration());
++ return;
++ }
++ size = event.getNewSize();
++ }
++ // Paper end - Add worldborder events
+ this.extent = new WorldBorder.StaticBorderExtent(size);
+ Iterator iterator = this.getListeners().iterator();
+
+@@ -173,6 +205,20 @@
+ }
+
+ public void lerpSizeBetween(double fromSize, double toSize, 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
++ 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);
++ if (!event.callEvent()) return;
++ toSize = 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 @@
+ }
+
+ public void addListener(BorderChangeListener listener) {
++ if (this.listeners.contains(listener)) return; // CraftBukkit
+ this.listeners.add(listener);
+ }
+
+@@ -483,6 +530,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);
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkAccess.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkAccess.java.patch
new file mode 100644
index 0000000000..54c4c6342e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkAccess.java.patch
@@ -0,0 +1,88 @@
+--- a/net/minecraft/world/level/chunk/ChunkAccess.java
++++ b/net/minecraft/world/level/chunk/ChunkAccess.java
+@@ -65,7 +65,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 @@
+ 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
+ this.upgradeData = upgradeData;
+ this.levelHeightAccessor = heightLimitView;
+ this.sections = new LevelChunkSection[heightLimitView.getSectionsCount()];
+@@ -103,7 +109,11 @@
+ }
+
+ ChunkAccess.replaceMissingSections(biomeRegistry, this.sections);
++ // CraftBukkit start
++ this.biomeRegistry = biomeRegistry;
+ }
++ 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 @@
+ public boolean tryMarkSaved() {
+ if (this.unsaved) {
+ this.unsaved = false;
++ this.persistentDataContainer.dirty(false); // CraftBukkit - SPIGOT-6814: chunk was saved, pdc is no longer dirty
+ return true;
+ } else {
+ return false;
+@@ -282,7 +293,7 @@
+ }
+
+ public boolean isUnsaved() {
+- return this.unsaved;
++ return this.unsaved || this.persistentDataContainer.dirty(); // CraftBukkit - SPIGOT-6814: chunk is unsaved if pdc was mutated
+ }
+
+ public abstract ChunkStatus getPersistedStatus();
+@@ -458,10 +469,31 @@
+
+ crashreportsystemdetails.setDetail("Location", () -> {
+ return CrashReportCategory.formatLocation(this, biomeX, biomeY, biomeZ);
++ });
++ throw new ReportedException(crashreport);
++ }
++ }
++
++ // CraftBukkit start
++ public void setBiome(int i, int j, int k, Holder<Biome> biome) {
++ try {
++ int l = QuartPos.fromBlock(this.getMinY());
++ int i1 = l + QuartPos.fromBlock(this.getHeight()) - 1;
++ int j1 = Mth.clamp(j, l, i1);
++ int k1 = this.getSectionIndex(QuartPos.toBlock(j1));
++
++ this.sections[k1].setBiome(i & 3, j1 & 3, k & 3, biome);
++ } catch (Throwable throwable) {
++ CrashReport crashreport = CrashReport.forThrowable(throwable, "Setting biome");
++ CrashReportCategory crashreportsystemdetails = crashreport.addCategory("Biome being set");
++
++ crashreportsystemdetails.setDetail("Location", () -> {
++ return CrashReportCategory.formatLocation(this, i, j, k);
+ });
+ throw new ReportedException(crashreport);
+ }
+ }
++ // CraftBukkit end
+
+ public void fillBiomesFromNoise(BiomeResolver biomeSupplier, Climate.Sampler sampler) {
+ ChunkPos chunkcoordintpair = this.getPos();
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
new file mode 100644
index 0000000000..337768ffa6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkGenerator.java.patch
@@ -0,0 +1,224 @@
+--- 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/ChunkGeneratorStructureState.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java.patch
new file mode 100644
index 0000000000..f9ebf0e3b1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java.patch
@@ -0,0 +1,175 @@
+--- 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();
+ private boolean hasGeneratedPositions;
+ private final List<Holder<StructureSet>> possibleStructureSets;
++ public final 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 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
++ }
++ // 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 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);
++ this.key = key;
++ }
++ }
++ // Paper end - Add missing structure set seed configs
++
++ // Spigot start
++ private static List<Holder<StructureSet>> injectSpigot(List<Holder<StructureSet>> list, 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
++ String name = holder.unwrapKey().orElseThrow().location().getPath();
++ int seed = randomConfig.salt;
++
++ switch (name) {
++ case "desert_pyramids":
++ seed = conf.desertSeed;
++ break;
++ case "end_cities":
++ seed = conf.endCitySeed;
++ break;
++ case "nether_complexes":
++ seed = conf.netherSeed;
++ break;
++ case "igloos":
++ seed = conf.iglooSeed;
++ break;
++ case "jungle_temples":
++ seed = conf.jungleSeed;
++ break;
++ case "woodland_mansions":
++ seed = conf.mansionSeed;
++ break;
++ case "ocean_monuments":
++ seed = conf.monumentSeed;
++ break;
++ case "nether_fossils":
++ seed = conf.fossilSeed;
++ break;
++ case "ocean_ruins":
++ seed = conf.oceanSeed;
++ break;
++ case "pillager_outposts":
++ seed = conf.outpostSeed;
++ break;
++ case "ruined_portals":
++ seed = conf.portalSeed;
++ break;
++ case "shipwrecks":
++ seed = conf.shipwreckSeed;
++ break;
++ case "swamp_huts":
++ seed = conf.swampSeed;
++ break;
++ case "villages":
++ seed = conf.villageSeed;
++ break;
++ // Paper start - Add missing structure set seed configs
++ case "ancient_cities":
++ seed = conf.ancientCitySeed;
++ break;
++ case "trail_ruins":
++ seed = conf.trailRuinsSeed;
++ break;
++ case "trial_chambers":
++ seed = conf.trialChambersSeed;
++ break;
++ // Paper end - Add missing structure set seed configs
++ }
++
++ // Paper start - Add missing structure set seed configs
++ structureset = new StructureSet(structureset.structures(), new KeyedRandomSpreadStructurePlacement(holder.unwrapKey().orElseThrow(), randomConfig.locateOffset, randomConfig.frequencyReductionMethod, randomConfig.frequency, seed, randomConfig.exclusionZone, randomConfig.spacing(), randomConfig.separation(), randomConfig.spreadType()));
++ newHolder = Holder.direct(structureset); // I really wish we didn't have to do this here
++ } else {
++ newHolder = holder;
++ }
++ 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);
+ }
+
+- 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;
+ this.biomeSource = biomeSource;
+ this.concentricRingsSeed = concentricRingSeed;
+ this.possibleStructureSets = structureSets;
++ 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();
+
++ // 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);
++ } else {
++ // Paper end - Add missing structure set seed configs
+ randomsource.setSeed(this.concentricRingsSeed);
++ } // Paper - Add missing structure set seed configs
+ double d0 = randomsource.nextDouble() * Math.PI * 2.0D;
+ int l = 0;
+ int i1 = 0;
+@@ -209,7 +309,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
+ return true;
+ }
+ }
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
new file mode 100644
index 0000000000..e1be97122d
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/DataLayer.java.patch
@@ -0,0 +1,7 @@
+--- 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/EmptyLevelChunk.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/EmptyLevelChunk.java.patch
new file mode 100644
index 0000000000..1ccf6729a6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/EmptyLevelChunk.java.patch
@@ -0,0 +1,15 @@
+--- a/net/minecraft/world/level/chunk/EmptyLevelChunk.java
++++ b/net/minecraft/world/level/chunk/EmptyLevelChunk.java
+@@ -25,6 +25,12 @@
+ public BlockState getBlockState(BlockPos pos) {
+ return Blocks.VOID_AIR.defaultBlockState();
+ }
++ // Paper start
++ @Override
++ public BlockState getBlockState(final int x, final int y, final int z) {
++ return Blocks.VOID_AIR.defaultBlockState();
++ }
++ // Paper end
+
+ @Nullable
+ @Override
diff --git a/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
new file mode 100644
index 0000000000..ce7c9bcdcb
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..d33242d709
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/LevelChunk.java.patch
@@ -0,0 +1,409 @@
+--- 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/LevelChunkSection.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/LevelChunkSection.java.patch
new file mode 100644
index 0000000000..f6007d4aea
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/LevelChunkSection.java.patch
@@ -0,0 +1,51 @@
+--- a/net/minecraft/world/level/chunk/LevelChunkSection.java
++++ b/net/minecraft/world/level/chunk/LevelChunkSection.java
+@@ -19,11 +19,11 @@
+ public static final int SECTION_HEIGHT = 16;
+ public static final int SECTION_SIZE = 4096;
+ public static final int BIOME_CONTAINER_BITS = 2;
+- private short nonEmptyBlockCount;
++ short nonEmptyBlockCount; // Paper - package private
+ private short tickingBlockCount;
+ private short tickingFluidCount;
+ public final PalettedContainer<BlockState> states;
+- private PalettedContainerRO<Holder<Biome>> biomes;
++ private PalettedContainer<Holder<Biome>> biomes; // CraftBukkit - read/write
+
+ private LevelChunkSection(LevelChunkSection section) {
+ this.nonEmptyBlockCount = section.nonEmptyBlockCount;
+@@ -33,9 +33,9 @@
+ 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;
+ this.recalcBlockCounts();
+ }
+
+@@ -49,7 +49,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(); // 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);
+ }
+
++ // 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;
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
new file mode 100644
index 0000000000..aff5b81eae
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..1f9c01eb79
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/ProtoChunk.java.patch
@@ -0,0 +1,22 @@
+--- 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/UpgradeData.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/UpgradeData.java.patch
new file mode 100644
index 0000000000..902ea36be8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/UpgradeData.java.patch
@@ -0,0 +1,47 @@
+--- a/net/minecraft/world/level/chunk/UpgradeData.java
++++ b/net/minecraft/world/level/chunk/UpgradeData.java
+@@ -113,12 +113,36 @@
+ }
+ }
+
++ // Paper start - filter out relocated neighbour ticks
++ // The lists are only supposed to contain ticks for the 1 radius neighbours of the chunk
++ private static <T> void filterTickList(int chunkX, int chunkZ, List<SavedTick<T>> ticks) {
++ for (java.util.Iterator<SavedTick<T>> iterator = ticks.iterator(); iterator.hasNext();) {
++ SavedTick<T> tick = iterator.next();
++ BlockPos tickPos = tick.pos();
++ int tickCX = tickPos.getX() >> 4;
++ int tickCZ = tickPos.getZ() >> 4;
++
++ int dist = Math.max(Math.abs(chunkX - tickCX), Math.abs(chunkZ - tickCZ));
++
++ if (dist != 1) {
++ LOGGER.warn("Neighbour tick '" + tick + "' serialized in chunk (" + chunkX + "," + chunkZ + ") is too far (" + tickCX + "," + tickCZ + ")");
++ iterator.remove();
++ }
++ }
++ }
++ // Paper end - filter out relocated neighbour ticks
++
+ public void upgrade(LevelChunk chunk) {
+ this.upgradeInside(chunk);
+
+ for (Direction8 direction8 : DIRECTIONS) {
+ 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());
+ });
++ 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));
+ }
+
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch
new file mode 100644
index 0000000000..9f0303442e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/status/ChunkStatusTasks.java.patch
@@ -0,0 +1,84 @@
+--- 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());
+ }
+
+- 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));
++ // 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) -> {
++ 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)) {
++ entity.discard(null); // CraftBukkit - add Bukkit remove cause
++ needsRemoval = true;
++ }
++ checkDupeUUID(world, entity); // Paper - duplicate uuid resolving
++ return !needsRemoval;
++ }));
++ // CraftBukkit end
+ }
+
+ }
++
++ // Paper start - duplicate uuid resolving
++ // rets true if to prevent the entity from being added
++ public static boolean checkDupeUUID(ServerLevel level, net.minecraft.world.entity.Entity entity) {
++ io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode mode = level.paperConfig().entities.spawning.duplicateUuid.mode;
++ if (mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.WARN
++ && mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.DELETE
++ && mode != io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.SAFE_REGEN) {
++ return false;
++ }
++ net.minecraft.world.entity.Entity other = level.getEntity(entity.getUUID());
++
++ if (other == null || other == entity) {
++ return false;
++ }
++
++ if (mode == io.papermc.paper.configuration.WorldConfiguration.Entities.Spawning.DuplicateUUID.DuplicateUUIDMode.SAFE_REGEN && other != null && !other.isRemoved()
++ && Objects.equals(other.getEncodeId(), entity.getEncodeId())
++ && entity.getBukkitEntity().getLocation().distance(other.getBukkitEntity().getLocation()) < level.paperConfig().entities.spawning.duplicateUuid.safeRegenDeleteRange
++ ) {
++ entity.discard(null);
++ return true;
++ }
++ if (!other.isRemoved()) {
++ switch (mode) {
++ case SAFE_REGEN: {
++ entity.setUUID(java.util.UUID.randomUUID());
++ break;
++ }
++ case DELETE: {
++ entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD);
++ return true;
++ }
++ default:
++ break;
++ }
++ }
++ return false;
++ }
++ // Paper end - duplicate uuid resolving
+ }
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
new file mode 100644
index 0000000000..b82d242d2c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/ChunkStorage.java.patch
@@ -0,0 +1,158 @@
+--- 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
new file mode 100644
index 0000000000..3955a59294
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFile.java.patch
@@ -0,0 +1,127 @@
+--- 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
new file mode 100644
index 0000000000..4e4fd07bda
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFileStorage.java.patch
@@ -0,0 +1,67 @@
+--- 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/RegionFileVersion.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFileVersion.java.patch
new file mode 100644
index 0000000000..65bf070d12
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/RegionFileVersion.java.patch
@@ -0,0 +1,18 @@
+--- a/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
++++ b/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
+@@ -58,6 +58,15 @@
+ private final RegionFileVersion.StreamWrapper<InputStream> inputWrapper;
+ private final RegionFileVersion.StreamWrapper<OutputStream> outputWrapper;
+
++ // Paper start - Configurable region compression format
++ public static RegionFileVersion getCompressionFormat() {
++ return switch (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.compressionFormat) {
++ case GZIP -> VERSION_GZIP;
++ case ZLIB -> VERSION_DEFLATE;
++ case NONE -> VERSION_NONE;
++ };
++ }
++ // Paper end - Configurable region compression format
+ private RegionFileVersion(
+ int id,
+ @Nullable String name,
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
new file mode 100644
index 0000000000..15c1561f05
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/chunk/storage/SerializableChunkData.java.patch
@@ -0,0 +1,216 @@
+--- 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
new file mode 100644
index 0000000000..bc18c1f95e
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/dimension/end/DragonRespawnAnimation.java.patch
@@ -0,0 +1,57 @@
+--- 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/dimension/end/EndDragonFight.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch
new file mode 100644
index 0000000000..5514be96df
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/dimension/end/EndDragonFight.java.patch
@@ -0,0 +1,212 @@
+--- a/net/minecraft/world/level/dimension/end/EndDragonFight.java
++++ b/net/minecraft/world/level/dimension/end/EndDragonFight.java
+@@ -74,6 +74,7 @@
+ 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;
+ 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) {
++ 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.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
+ this.dragonUUID = null;
+ }
+ }
+@@ -404,9 +411,23 @@
+ 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());
++ // 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);
++ eggState.setData(Blocks.DRAGON_EGG.defaultBlockState());
++ io.papermc.paper.event.block.DragonEggFormEvent eggEvent = new io.papermc.paper.event.block.DragonEggFormEvent(org.bukkit.craftbukkit.block.CraftBlock.at(this.level, eggPosition), eggState,
++ new org.bukkit.craftbukkit.boss.CraftDragonBattle(this));
++ // Paper end - Add DragonEggFormEvent
++ if (this.level.paperConfig().entities.behavior.enderDragonsDeathAlwaysPlacesDragonEgg || !this.previouslyKilled) { // Paper - Add toggle for always placing the dragon egg
++ // Paper start - Add DragonEggFormEvent
++ // 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() {
+ this.gateways.clear();
++ }
++
++ // Paper start - More DragonBattle API
++ public boolean spawnNewGatewayIfPossible() {
++ if (!this.gateways.isEmpty()) {
++ this.spawnNewGateway();
++ return true;
++ }
++ return false;
++ }
++
++ public List<EndCrystal> getSpikeCrystals() {
++ final List<EndCrystal> endCrystals = new java.util.ArrayList<>();
++ for (final SpikeFeature.EndSpike spike : SpikeFeature.getSpikesForLevel(this.level)) {
++ 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 @@
+ }
+ }
+
++ // Paper start - Prevent "softlocked" exit portal generation
++ if (this.portalLocation.getY() <= this.level.getMinY()) {
++ 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();
++ this.resetSpikeCrystals(); // Paper - Reset ender crystals on dragon spawn
+ }
+
+ return entityenderdragon;
+@@ -480,6 +525,10 @@
+ this.ticksSinceDragonSeen = 0;
+ if (dragon.hasCustomName()) {
+ this.dragonEvent.setName(dragon.getDisplayName());
++ // Paper start - ensure reset EnderDragon boss event name
++ } else {
++ this.dragonEvent.setName(DEFAULT_BOSS_EVENT_NAME);
++ // Paper end - ensure reset EnderDragon boss event name
+ }
+ }
+
+@@ -513,7 +562,13 @@
+ return this.previouslyKilled;
+ }
+
+- public void tryRespawn() {
++ public boolean tryRespawn() { // CraftBukkit - return boolean
++ // Paper start - Perf: Do crystal-portal proximity check before entity lookup
++ return this.tryRespawn(null);
++ }
++
++ 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;
+
+@@ -531,6 +586,22 @@
+ blockposition = 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();
++ 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();
++ 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()) {
+- return;
++ return false; // CraftBukkit - return value
+ }
+
+ list.addAll(list1);
+ }
+
+ EndDragonFight.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
+ 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;
+ this.respawnTime = 0;
+ this.spawnExitPortal(false);
+- this.respawnCrystals = crystals;
++ this.respawnCrystals = list;
++ return true; // CraftBukkit - return value
+ }
+-
++ return false; // CraftBukkit - return value
+ }
+
+ public void resetSpikeCrystals() {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityAccess.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityAccess.java.patch
new file mode 100644
index 0000000000..517de585e8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityAccess.java.patch
@@ -0,0 +1,25 @@
+--- 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
+
+ public interface EntityAccess {
+
+@@ -24,6 +27,12 @@
+
+ void setRemoved(Entity.RemovalReason reason);
+
++ // CraftBukkit start - add Bukkit remove cause
++ default void setRemoved(Entity.RemovalReason entity_removalreason, EntityRemoveEvent.Cause cause) {
++ this.setRemoved(entity_removalreason);
++ }
++ // CraftBukkit end
++
+ boolean shouldBeSaved();
+
+ boolean isAlwaysTicking();
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityLookup.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityLookup.java.patch
new file mode 100644
index 0000000000..b3b9c9ccb4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/entity/EntityLookup.java.patch
@@ -0,0 +1,17 @@
+--- 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);
++ // Paper start - extra debug info
++ if (entity instanceof net.minecraft.world.entity.Entity) {
++ final T old = this.byUuid.get(entity.getUUID());
++ if (old instanceof net.minecraft.world.entity.Entity oldCast && oldCast.getId() != entity.getId() && oldCast.valid) {
++ LOGGER.error("Overwrote an existing entity {} with {}", oldCast, entity);
++ }
++ }
++ // Paper end - extra debug info
+ } else {
+ this.byUuid.put(uUID, entity);
+ this.byId.put(entity.getId(), entity);
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
new file mode 100644
index 0000000000..68a70b169a
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/entity/PersistentEntitySectionManager.java.patch
@@ -0,0 +1,276 @@
+--- 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
new file mode 100644
index 0000000000..bcaf4b1a37
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/GameEvent.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/GameEvent.java.patch
new file mode 100644
index 0000000000..508ac18f4c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/GameEvent.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/gameevent/GameEvent.java
++++ b/net/minecraft/world/level/gameevent/GameEvent.java
+@@ -85,7 +85,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
+ }
+
+ public static record Context(@Nullable Entity sourceEntity, @Nullable BlockState affectedState) {
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
new file mode 100644
index 0000000000..25f012f615
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/GameEventDispatcher.java.patch
@@ -0,0 +1,39 @@
+--- 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
new file mode 100644
index 0000000000..d96bdd56da
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/gameevent/vibrations/VibrationSystem.java.patch
@@ -0,0 +1,52 @@
+--- 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
new file mode 100644
index 0000000000..3f778b5424
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/DensityFunctions.java.patch
@@ -0,0 +1,52 @@
+--- 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
new file mode 100644
index 0000000000..da8a1b29d5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/FlatLevelSource.java.patch
@@ -0,0 +1,38 @@
+--- 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
new file mode 100644
index 0000000000..e3e53c8dde
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/NoiseBasedChunkGenerator.java.patch
@@ -0,0 +1,7 @@
+--- 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
new file mode 100644
index 0000000000..9d044b3016
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/PatrolSpawner.java.patch
@@ -0,0 +1,79 @@
+--- 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
new file mode 100644
index 0000000000..e3cf03e4f8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/PhantomSpawner.java.patch
@@ -0,0 +1,68 @@
+--- 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
new file mode 100644
index 0000000000..d2cdde6d47
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java.patch
@@ -0,0 +1,72 @@
+--- 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
new file mode 100644
index 0000000000..3db119d7ed
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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
new file mode 100644
index 0000000000..4df0e3925c
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..04607e1797
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/LegacyStructureDataHandler.java.patch
@@ -0,0 +1,32 @@
+--- 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/StructureCheck.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructureCheck.java.patch
new file mode 100644
index 0000000000..9e6cb9ad46
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructureCheck.java.patch
@@ -0,0 +1,50 @@
+--- a/net/minecraft/world/level/levelgen/structure/StructureCheck.java
++++ b/net/minecraft/world/level/levelgen/structure/StructureCheck.java
+@@ -40,7 +40,7 @@
+ private final ChunkScanAccess storageAccess;
+ private final RegistryAccess registryAccess;
+ private final StructureTemplateManager structureTemplateManager;
+- private final ResourceKey<Level> dimension;
++ private final ResourceKey<net.minecraft.world.level.dimension.LevelStem> dimension; // Paper - fix missing CB diff
+ private final ChunkGenerator chunkGenerator;
+ private final RandomState randomState;
+ private final LevelHeightAccessor heightAccessor;
+@@ -54,7 +54,7 @@
+ ChunkScanAccess chunkIoWorker,
+ RegistryAccess registryManager,
+ StructureTemplateManager structureTemplateManager,
+- ResourceKey<Level> worldKey,
++ ResourceKey<net.minecraft.world.level.dimension.LevelStem> worldKey, // Paper - fix missing CB diff
+ ChunkGenerator chunkGenerator,
+ RandomState noiseConfig,
+ LevelHeightAccessor world,
+@@ -74,6 +74,20 @@
+ this.fixerUpper = dataFixer;
+ }
+
++ // Paper start - add missing structure salt configs
++ @Nullable
++ private Integer getSaltOverride(Structure type) {
++ if (this.heightAccessor instanceof net.minecraft.server.level.ServerLevel serverLevel) {
++ if (type instanceof net.minecraft.world.level.levelgen.structure.structures.MineshaftStructure) {
++ return serverLevel.spigotConfig.mineshaftSeed;
++ } else if (type instanceof net.minecraft.world.level.levelgen.structure.structures.BuriedTreasureStructure) {
++ return serverLevel.spigotConfig.buriedTreasureSeed;
++ }
++ }
++ return null;
++ }
++ // 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);
+ 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
+ return StructureCheckResult.START_NOT_PRESENT;
+ } else {
+ boolean bl = this.featureChecks
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
new file mode 100644
index 0000000000..918d4c34ed
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructurePiece.java.patch
@@ -0,0 +1,164 @@
+--- 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
new file mode 100644
index 0000000000..83539110a1
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/StructureStart.java.patch
@@ -0,0 +1,59 @@
+--- 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
new file mode 100644
index 0000000000..8abda3baff
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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/DesertPyramidStructure.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java.patch
new file mode 100644
index 0000000000..84696b809f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/DesertPyramidStructure.java.patch
@@ -0,0 +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 @@
+
+ private static void placeSuspiciousSand(BoundingBox box, WorldGenLevel world, BlockPos pos) {
+ if (box.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);
++ 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());
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
new file mode 100644
index 0000000000..0ec21beb4f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/EndCityPieces.java.patch
@@ -0,0 +1,16 @@
+--- 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
new file mode 100644
index 0000000000..5dc0652fc4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/IglooPieces.java.patch
@@ -0,0 +1,31 @@
+--- 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
new file mode 100644
index 0000000000..32925fee48
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/MineshaftPieces.java.patch
@@ -0,0 +1,68 @@
+--- 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
new file mode 100644
index 0000000000..23051dcaf5
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/NetherFortressPieces.java.patch
@@ -0,0 +1,45 @@
+--- 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
new file mode 100644
index 0000000000..7e1e399a8b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/OceanRuinPieces.java.patch
@@ -0,0 +1,37 @@
+--- 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
new file mode 100644
index 0000000000..d667e572c3
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/ShipwreckPieces.java.patch
@@ -0,0 +1,16 @@
+--- 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
new file mode 100644
index 0000000000..90efca8745
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/structures/StrongholdPieces.java.patch
@@ -0,0 +1,55 @@
+--- 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
new file mode 100644
index 0000000000..ad3c0e1641
--- /dev/null
+++ b/paper-server/patches/unapplied/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
+@@ -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/StructurePlaceSettings.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings.java.patch
new file mode 100644
index 0000000000..60b3e8e667
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/templatesystem/StructurePlaceSettings.java.patch
@@ -0,0 +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;
+ @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 boolean knownShape;
+ private boolean finalizeEntities;
+@@ -149,6 +149,13 @@
+
+ if (i == 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.");
++ }
++ return infoLists.get(this.palette);
++ // CraftBukkit end
+ } else {
+ return (StructureTemplate.Palette) infoLists.get(this.getRandom(pos).nextInt(i));
+ }
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
new file mode 100644
index 0000000000..6bbbc67254
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplate.java.patch
@@ -0,0 +1,182 @@
+--- 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
new file mode 100644
index 0000000000..cfc052e612
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/material/FlowingFluid.java.patch
@@ -0,0 +1,157 @@
+--- 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/material/FluidState.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/material/FluidState.java.patch
new file mode 100644
index 0000000000..78b4ee0a64
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/material/FluidState.java.patch
@@ -0,0 +1,23 @@
+--- a/net/minecraft/world/level/material/FluidState.java
++++ b/net/minecraft/world/level/material/FluidState.java
+@@ -26,9 +26,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 Fluid getType() {
+@@ -44,7 +46,7 @@
+ }
+
+ public boolean isEmpty() {
+- return this.getType().isEmpty();
++ return this.isEmpty; // Paper - Perf: moved into constructor
+ }
+
+ public float getHeight(BlockGetter world, BlockPos pos) {
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/material/LavaFluid.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/material/LavaFluid.java.patch
new file mode 100644
index 0000000000..de343949f4
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/material/LavaFluid.java.patch
@@ -0,0 +1,53 @@
+--- 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)) {
++ // 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()) {
++ continue;
++ }
++ }
++ // CraftBukkit end
+ world.setBlockAndUpdate(blockposition1, BaseFireBlock.getState(world, blockposition1));
+ return;
+ }
+@@ -101,6 +108,14 @@
+ }
+
+ if (world.isEmptyBlock(blockposition2.above()) && this.isFlammable(world, blockposition2)) {
++ // 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()) {
++ continue;
++ }
++ }
++ // CraftBukkit end
+ world.setBlockAndUpdate(blockposition2.above(), BaseFireBlock.getState(world, blockposition2));
+ }
+ }
+@@ -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);
++ // CraftBukkit start
++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world.getMinecraftWorld(), pos, Blocks.STONE.defaultBlockState(), 3)) {
++ return;
++ }
++ // CraftBukkit end
+ }
+
+ this.fizz(world, pos);
+@@ -214,7 +233,7 @@
+
+ @Override
+ protected float getExplosionResistance() {
+- return 100.0F;
++ return Blocks.LAVA.getExplosionResistance(); // Paper - Get explosion resistance from actual block
+ }
+
+ @Override
diff --git a/paper-server/patches/unapplied/net/minecraft/world/level/material/WaterFluid.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/material/WaterFluid.java.patch
new file mode 100644
index 0000000000..f615848dea
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/material/WaterFluid.java.patch
@@ -0,0 +1,26 @@
+--- 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);
+ }
+
++ // Paper start - Add BlockBreakBlockEvent
+ @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 float getExplosionResistance() {
+- return 100.0F;
++ return Blocks.WATER.getExplosionResistance(); // Paper - Get explosion resistance from actual block
+ }
+
+ @Override
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
new file mode 100644
index 0000000000..94eb8f4856
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/pathfinder/Path.java.patch
@@ -0,0 +1,10 @@
+--- 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/pathfinder/WalkNodeEvaluator.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch
new file mode 100644
index 0000000000..96be982191
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java.patch
@@ -0,0 +1,16 @@
+--- a/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java
++++ b/net/minecraft/world/level/pathfinder/WalkNodeEvaluator.java
+@@ -478,7 +478,12 @@
+ }
+
+ protected static PathType getPathTypeFromState(BlockGetter world, BlockPos pos) {
+- BlockState blockState = world.getBlockState(pos);
++ // Paper start - Do not load chunks during pathfinding
++ BlockState blockState = world.getBlockStateIfLoaded(pos);
++ if (blockState == null) {
++ return PathType.BLOCKED;
++ }
++ // Paper end
+ Block block = blockState.getBlock();
+ if (blockState.isAir()) {
+ return PathType.OPEN;
diff --git a/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
new file mode 100644
index 0000000000..2bcc6db1ba
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/portal/PortalForcer.java.patch
@@ -0,0 +1,149 @@
+--- 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
new file mode 100644
index 0000000000..f0cf745dd6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/portal/PortalShape.java.patch
@@ -0,0 +1,226 @@
+--- 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
new file mode 100644
index 0000000000..1d72cf288c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/portal/TeleportTransition.java.patch
@@ -0,0 +1,71 @@
+--- 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/CollectingNeighborUpdater.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch
new file mode 100644
index 0000000000..d055cf38c0
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java.patch
@@ -0,0 +1,11 @@
+--- a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java
++++ b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java
+@@ -135,7 +135,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
+ if (this.idx < NeighborUpdater.UPDATE_ORDER.length && NeighborUpdater.UPDATE_ORDER[this.idx] == this.skipDirection) {
+ this.idx++;
+ }
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
new file mode 100644
index 0000000000..8862572ac8
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/redstone/DefaultRedstoneWireEvaluator.java.patch
@@ -0,0 +1,31 @@
+--- 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
new file mode 100644
index 0000000000..73e662622f
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/redstone/ExperimentalRedstoneWireEvaluator.java.patch
@@ -0,0 +1,31 @@
+--- 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
new file mode 100644
index 0000000000..a8973333ac
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/redstone/NeighborUpdater.java.patch
@@ -0,0 +1,50 @@
+--- 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
new file mode 100644
index 0000000000..452050da4c
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java.patch
@@ -0,0 +1,216 @@
+--- 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
new file mode 100644
index 0000000000..0a22aaf58b
--- /dev/null
+++ b/paper-server/patches/unapplied/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 +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
new file mode 100644
index 0000000000..e0a1e52b18
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/storage/LevelStorageSource.java.patch
@@ -0,0 +1,107 @@
+--- 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
new file mode 100644
index 0000000000..6ed0222e60
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/storage/PlayerDataStorage.java.patch
@@ -0,0 +1,143 @@
+--- 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
new file mode 100644
index 0000000000..131a7c275b
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/storage/PrimaryLevelData.java.patch
@@ -0,0 +1,203 @@
+--- 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
new file mode 100644
index 0000000000..c9f13ba298
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/LootDataType.java.patch
@@ -0,0 +1,22 @@
+--- 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
new file mode 100644
index 0000000000..29133b2303
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/LootTable.java.patch
@@ -0,0 +1,85 @@
+--- 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/entries/LootPoolSingletonContainer.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java.patch
new file mode 100644
index 0000000000..f97db2bfbc
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java.patch
@@ -0,0 +1,39 @@
+--- a/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java
++++ b/net/minecraft/world/level/storage/loot/entries/LootPoolSingletonContainer.java
+@@ -126,9 +126,35 @@
+ 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);
++ // Paper start - Configurable LootPool luck formula
++ // SEE: https://luckformula.emc.gs for details and data
++ if (LootPoolSingletonContainer.this.lastLuck != null && LootPoolSingletonContainer.this.lastLuck == luck) {
++ return lastWeight;
++ }
++ // This is vanilla
++ float qualityModifer = (float) LootPoolSingletonContainer.this.quality * luck;
++ double baseWeight = (LootPoolSingletonContainer.this.weight + qualityModifer);
++ if (io.papermc.paper.configuration.GlobalConfiguration.get().misc.useAlternativeLuckFormula) {
++ // Random boost to avoid losing precision in the final int cast on return
++ final int weightBoost = 100;
++ baseWeight *= weightBoost;
++ // If we have vanilla 1, bump that down to 0 so nothing is is impacted
++ // vanilla 3 = 300, 200 basis = impact 2%
++ // =($B2*(($B2-100)/100/100))
++ double impacted = baseWeight * ((baseWeight - weightBoost) / weightBoost / 100);
++ // =($B$7/100)
++ float luckModifier = Math.min(100, luck * 10) / 100;
++ // =B2 - (C2 *($B$7/100))
++ baseWeight = Math.ceil(baseWeight - (impacted * luckModifier));
++ }
++ LootPoolSingletonContainer.this.lastLuck = luck;
++ LootPoolSingletonContainer.this.lastWeight = (int) Math.max(Math.floor(baseWeight), 0);
++ return lastWeight;
+ }
+ }
++ private Float lastLuck = null;
++ private int lastWeight = 0;
++ // Paper end - Configurable LootPool luck formula
+
+ @FunctionalInterface
+ protected interface EntryConstructor {
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
new file mode 100644
index 0000000000..130a1b59c6
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/functions/ExplorationMapFunction.java.patch
@@ -0,0 +1,21 @@
+--- 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/patches/unapplied/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java.patch b/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java.patch
new file mode 100644
index 0000000000..19cd841992
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/level/storage/loot/predicates/ExplosionCondition.java.patch
@@ -0,0 +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;
++ // CraftBukkit - <= to < to allow for plugins to completely disable block drops from explosions
++ return randomsource.nextFloat() < f;
+ } else {
+ return true;
+ }
diff --git a/paper-server/patches/unapplied/net/minecraft/world/scores/ScoreboardSaveData.java.patch b/paper-server/patches/unapplied/net/minecraft/world/scores/ScoreboardSaveData.java.patch
new file mode 100644
index 0000000000..d29c6da396
--- /dev/null
+++ b/paper-server/patches/unapplied/net/minecraft/world/scores/ScoreboardSaveData.java.patch
@@ -0,0 +1,10 @@
+--- a/net/minecraft/world/scores/ScoreboardSaveData.java
++++ b/net/minecraft/world/scores/ScoreboardSaveData.java
+@@ -148,6 +148,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));