--- a/net/minecraft/world/item/ItemStack.java +++ b/net/minecraft/world/item/ItemStack.java @@ -195,12 +_,20 @@ @Override public void encode(RegistryFriendlyByteBuf buffer, ItemStack value) { - if (value.isEmpty()) { + if (value.isEmpty() || value.getItem() == null) { // CraftBukkit - NPE fix itemstack.getItem() buffer.writeVarInt(0); } else { - buffer.writeVarInt(value.getCount()); + buffer.writeVarInt(io.papermc.paper.util.sanitizer.ItemComponentSanitizer.sanitizeCount(io.papermc.paper.util.sanitizer.ItemObfuscationSession.currentSession(), value, value.getCount())); // Paper - potentially sanitize count Item.STREAM_CODEC.encode(buffer, value.getItemHolder()); + // Paper start - adventure; conditionally render translatable components + boolean prev = net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.get(); + try (final io.papermc.paper.util.SafeAutoClosable ignored = io.papermc.paper.util.sanitizer.ItemObfuscationSession.withContext(c -> c.itemStack(value))) { // pass the itemstack as context to the obfuscation session + net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(true); codec.encode(buffer, value.components.asPatch()); + } finally { + net.minecraft.network.chat.ComponentSerialization.DONT_RENDER_TRANSLATABLES.set(prev); + } + // Paper end - adventure; conditionally render translatable components } } }; @@ -365,10 +_,166 @@ return InteractionResult.PASS; } else { Item item = this.getItem(); - InteractionResult interactionResult = item.useOn(context); + // CraftBukkit start - handle all block place event logic here + DataComponentPatch previousPatch = this.components.asPatch(); + int oldCount = this.getCount(); + ServerLevel serverLevel = (ServerLevel) context.getLevel(); + + if (!(item instanceof BucketItem/* || item instanceof SolidBucketItem*/)) { // if not bucket // Paper - Fix cancelled powdered snow bucket placement + serverLevel.captureBlockStates = true; + // special case bonemeal + if (item == Items.BONE_MEAL) { + serverLevel.captureTreeGeneration = true; + } + } + InteractionResult interactionResult; + try { + interactionResult = item.useOn(context); + } finally { + serverLevel.captureBlockStates = false; + } + DataComponentPatch newPatch = this.components.asPatch(); + int newCount = this.getCount(); + this.setCount(oldCount); + this.restorePatch(previousPatch); + if (interactionResult.consumesAction() && serverLevel.captureTreeGeneration && !serverLevel.capturedBlockStates.isEmpty()) { + serverLevel.captureTreeGeneration = false; + org.bukkit.Location location = org.bukkit.craftbukkit.util.CraftLocation.toBukkit(clickedPos, serverLevel.getWorld()); + org.bukkit.TreeType treeType = net.minecraft.world.level.block.SaplingBlock.treeType; + net.minecraft.world.level.block.SaplingBlock.treeType = null; + List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); + serverLevel.capturedBlockStates.clear(); + org.bukkit.event.world.StructureGrowEvent structureEvent = null; + if (treeType != null) { + boolean isBonemeal = this.getItem() == Items.BONE_MEAL; + structureEvent = new org.bukkit.event.world.StructureGrowEvent(location, treeType, isBonemeal, (org.bukkit.entity.Player) player.getBukkitEntity(), (List) (List) blocks); + org.bukkit.Bukkit.getPluginManager().callEvent(structureEvent); + } + + org.bukkit.event.block.BlockFertilizeEvent fertilizeEvent = new org.bukkit.event.block.BlockFertilizeEvent(org.bukkit.craftbukkit.block.CraftBlock.at(serverLevel, clickedPos), (org.bukkit.entity.Player) player.getBukkitEntity(), (List) (List) blocks); + fertilizeEvent.setCancelled(structureEvent != null && structureEvent.isCancelled()); + org.bukkit.Bukkit.getPluginManager().callEvent(fertilizeEvent); + + if (!fertilizeEvent.isCancelled()) { + // Change the stack to its new contents if it hasn't been tampered with. + if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), previousPatch)) { + this.restorePatch(newPatch); + this.setCount(newCount); + } + for (org.bukkit.craftbukkit.block.CraftBlockState snapshot : blocks) { + // SPIGOT-7572 - Move fix for SPIGOT-7248 to CapturedBlockState, to allow bees in bee nest + snapshot.place(snapshot.getFlags()); + serverLevel.checkCapturedTreeStateForObserverNotify(clickedPos, snapshot); // Paper - notify observers even if grow failed + } + player.awardStat(Stats.ITEM_USED.get(item)); // SPIGOT-7236 - award stat + } + + SignItem.openSign = null; // SPIGOT-6758 - Reset on early return + return interactionResult; + } + serverLevel.captureTreeGeneration = false; if (player != null && interactionResult instanceof InteractionResult.Success success && success.wasItemInteraction()) { - player.awardStat(Stats.ITEM_USED.get(item)); + InteractionHand hand = context.getHand(); + org.bukkit.event.block.BlockPlaceEvent placeEvent = null; + List blocks = new java.util.ArrayList<>(serverLevel.capturedBlockStates.values()); + serverLevel.capturedBlockStates.clear(); + if (blocks.size() > 1) { + placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockMultiPlaceEvent(serverLevel, player, hand, blocks, clickedPos); + } else if (blocks.size() == 1 && item != Items.POWDER_SNOW_BUCKET) { // Paper - Fix cancelled powdered snow bucket placement + placeEvent = org.bukkit.craftbukkit.event.CraftEventFactory.callBlockPlaceEvent(serverLevel, player, hand, blocks.getFirst(), clickedPos); + } + + if (placeEvent != null && (placeEvent.isCancelled() || !placeEvent.canBuild())) { + interactionResult = InteractionResult.FAIL; // cancel placement + // PAIL: Remove this when MC-99075 fixed + player.containerMenu.sendAllDataToRemote(); + serverLevel.capturedTileEntities.clear(); // Paper - Allow chests to be placed with NBT data; clear out block entities as chests and such will pop loot + // revert back all captured blocks + for (org.bukkit.block.BlockState blockstate : blocks) { + ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).revertPlace(); + } + + SignItem.openSign = null; // SPIGOT-6758 - Reset on early return + } else { + // Change the stack to its new contents if it hasn't been tampered with. + if (this.getCount() == oldCount && Objects.equals(this.components.asPatch(), previousPatch)) { + this.restorePatch(newPatch); + this.setCount(newCount); + } + + for (java.util.Map.Entry e : serverLevel.capturedTileEntities.entrySet()) { + serverLevel.setBlockEntity(e.getValue()); + } + + for (org.bukkit.block.BlockState blockstate : blocks) { + int updateFlags = ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).getFlags(); + net.minecraft.world.level.block.state.BlockState oldBlock = ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).getHandle(); + BlockPos newPos = ((org.bukkit.craftbukkit.block.CraftBlockState) blockstate).getPosition(); + net.minecraft.world.level.block.state.BlockState block = serverLevel.getBlockState(newPos); + + if (!(block.getBlock() instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // Containers get placed automatically + block.onPlace(serverLevel, newPos, oldBlock, true, context); + } + + serverLevel.notifyAndUpdatePhysics(newPos, null, oldBlock, block, serverLevel.getBlockState(newPos), updateFlags, net.minecraft.world.level.block.Block.UPDATE_LIMIT); // send null chunk as chunk.k() returns false by this point + } + + if (this.item == Items.WITHER_SKELETON_SKULL) { // Special case skulls to allow wither spawns to be cancelled + BlockPos bp = clickedPos; + if (!serverLevel.getBlockState(clickedPos).canBeReplaced()) { + if (!serverLevel.getBlockState(clickedPos).isSolid()) { + bp = null; + } else { + bp = bp.relative(context.getClickedFace()); + } + } + if (bp != null) { + net.minecraft.world.level.block.entity.BlockEntity te = serverLevel.getBlockEntity(bp); + if (te instanceof net.minecraft.world.level.block.entity.SkullBlockEntity) { + net.minecraft.world.level.block.WitherSkullBlock.checkSpawn(serverLevel, bp, (net.minecraft.world.level.block.entity.SkullBlockEntity) te); + } + } + } + + // SPIGOT-4678 + if (this.item instanceof SignItem && SignItem.openSign != null) { + try { + if (serverLevel.getBlockEntity(SignItem.openSign) instanceof net.minecraft.world.level.block.entity.SignBlockEntity blockEntity) { + if (serverLevel.getBlockState(SignItem.openSign).getBlock() instanceof net.minecraft.world.level.block.SignBlock signBlock) { + signBlock.openTextEdit(player, blockEntity, true, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.PLACE); // CraftBukkit // Paper - Add PlayerOpenSignEvent + } + } + } finally { + SignItem.openSign = null; + } + } + + // SPIGOT-7315: Moved from BedBlock#setPlacedBy + if (placeEvent != null && this.item instanceof BedItem) { + BlockPos pos = ((org.bukkit.craftbukkit.block.CraftBlock) placeEvent.getBlock()).getPosition(); + net.minecraft.world.level.block.state.BlockState state = serverLevel.getBlockState(pos); + + if (state.getBlock() instanceof net.minecraft.world.level.block.BedBlock) { + serverLevel.updateNeighborsAt(pos, net.minecraft.world.level.block.Blocks.AIR); + state.updateNeighbourShapes(serverLevel, pos, 3); + } + } + + // SPIGOT-1288 - play sound stripped from BlockItem + if (this.item instanceof BlockItem && success.paperSuccessContext().placedBlockPosition() != null) { + // Paper start - Fix spigot sound playing for BlockItem ItemStacks + net.minecraft.world.level.block.state.BlockState state = serverLevel.getBlockState(success.paperSuccessContext().placedBlockPosition()); + net.minecraft.world.level.block.SoundType soundType = state.getSoundType(); + // Paper end - Fix spigot sound playing for BlockItem ItemStacks + serverLevel.playSound(player, clickedPos, soundType.getPlaceSound(), net.minecraft.sounds.SoundSource.BLOCKS, (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F); + } + + player.awardStat(Stats.ITEM_USED.get(item)); + } } + serverLevel.capturedTileEntities.clear(); + serverLevel.capturedBlockStates.clear(); + // CraftBukkit end return interactionResult; } @@ -449,31 +_,71 @@ return this.isDamageableItem() && this.getDamageValue() >= this.getMaxDamage() - 1; } - public void hurtAndBreak(int damage, ServerLevel level, @Nullable ServerPlayer player, Consumer onBreak) { - int i = this.processDurabilityChange(damage, level, player); - if (i != 0) { + public void hurtAndBreak(int damage, ServerLevel level, @Nullable LivingEntity player, Consumer onBreak) { // Paper - Add EntityDamageItemEvent + // Paper start - add force boolean overload + this.hurtAndBreak(damage, level, player, onBreak, false); + } + + public void hurtAndBreak(int damage, ServerLevel level, @Nullable LivingEntity player, Consumer onBreak, boolean force) { // Paper - Add EntityDamageItemEvent + // Paper end + final int originalDamage = damage; // Paper - Expand PlayerItemDamageEvent + int i = this.processDurabilityChange(damage, level, player, force); // Paper + // CraftBukkit start + if (i > 0 && player instanceof final ServerPlayer serverPlayer) { // Paper - Add EntityDamageItemEvent - limit to positive damage and run for player + org.bukkit.event.player.PlayerItemDamageEvent event = new org.bukkit.event.player.PlayerItemDamageEvent(serverPlayer.getBukkitEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this), i, originalDamage); // Paper - Add EntityDamageItemEvent + event.getPlayer().getServer().getPluginManager().callEvent(event); + + if (i != event.getDamage() || event.isCancelled()) { + serverPlayer.containerMenu.sendAllDataToRemote(); + } + if (event.isCancelled()) { + return; + } + + i = event.getDamage(); + // Paper start - Add EntityDamageItemEvent + } else if (i > 0 && player != null) { + io.papermc.paper.event.entity.EntityDamageItemEvent event = new io.papermc.paper.event.entity.EntityDamageItemEvent(player.getBukkitLivingEntity(), org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this), i); + if (!event.callEvent()) { + return; + } + i = event.getDamage(); + // Paper end - Add EntityDamageItemEvent + } + // CraftBukkit end + if (i != 0) { // Paper - Add EntityDamageItemEvent - diff on change for above event ifs. this.applyDamage(this.getDamageValue() + i, player, onBreak); } } - private int processDurabilityChange(int damage, ServerLevel level, @Nullable ServerPlayer player) { + private int processDurabilityChange(int damage, ServerLevel level, @Nullable LivingEntity player) { // Paper - Add EntityDamageItemEvent + // Paper start - itemstack damage api + return processDurabilityChange(damage, level, player, false); + } + private int processDurabilityChange(int damage, ServerLevel level, @Nullable LivingEntity player, boolean force) { + // Paper end - itemstack damage api if (!this.isDamageableItem()) { return 0; - } else if (player != null && player.hasInfiniteMaterials()) { + } else if (player instanceof ServerPlayer && player.hasInfiniteMaterials() && !force) { // Paper - Add EntityDamageItemEvent return 0; } else { return damage > 0 ? EnchantmentHelper.processDurabilityChange(level, this, damage) : damage; } } - private void applyDamage(int damage, @Nullable ServerPlayer player, Consumer onBreak) { - if (player != null) { - CriteriaTriggers.ITEM_DURABILITY_CHANGED.trigger(player, this, damage); + private void applyDamage(int damage, @Nullable LivingEntity player, Consumer onBreak) { // Paper - Add EntityDamageItemEvent + if (player instanceof final ServerPlayer serverPlayer) { // Paper - Add EntityDamageItemEvent + CriteriaTriggers.ITEM_DURABILITY_CHANGED.trigger(serverPlayer, this, damage); // Paper - Add EntityDamageItemEvent } this.setDamageValue(damage); if (this.isBroken()) { Item item = this.getItem(); + // CraftBukkit start - Check for item breaking + if (this.count == 1 && player instanceof final ServerPlayer serverPlayer) { // Paper - Add EntityDamageItemEvent + org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerItemBreakEvent(serverPlayer, this); // Paper - Add EntityDamageItemEvent + } + // CraftBukkit end this.shrink(1); onBreak.accept(item); } @@ -486,7 +_,26 @@ return; } - int min = Math.min(this.getDamageValue() + i, this.getMaxDamage() - 1); + int min = Math.min(this.getDamageValue() + i, this.getMaxDamage() - 1); // Paper - Expand PlayerItemDamageEvent - diff on change as min computation is copied post event. + + // Paper start - Expand PlayerItemDamageEvent + if (min - this.getDamageValue() > 0) { + org.bukkit.event.player.PlayerItemDamageEvent event = new org.bukkit.event.player.PlayerItemDamageEvent( + serverPlayer.getBukkitEntity(), + org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this), + min - this.getDamageValue(), + damage + ); + if (!event.callEvent() || event.getDamage() == 0) { + return; + } + + // Prevent breaking the item in this code path as callers may expect the item to survive + // (given the method name) + min = Math.min(this.getDamageValue() + event.getDamage(), this.getMaxDamage() - 1); + } + // Paper end - Expand PlayerItemDamageEvent + this.applyDamage(min, serverPlayer, item -> {}); } } @@ -496,9 +_,14 @@ } public void hurtAndBreak(int amount, LivingEntity entity, EquipmentSlot slot) { + // Paper start - add param to skip infinite mats check + this.hurtAndBreak(amount, entity, slot, false); + } + public void hurtAndBreak(int amount, LivingEntity entity, EquipmentSlot slot, boolean force) { + // Paper end - add param to skip infinite mats check if (entity.level() instanceof ServerLevel serverLevel) { this.hurtAndBreak( - amount, serverLevel, entity instanceof ServerPlayer serverPlayer ? serverPlayer : null, item -> entity.onEquippedItemBroken(item, slot) + amount, serverLevel, entity, item -> {if (slot != null) entity.onEquippedItemBroken(item, slot); }, force // Paper - Add EntityDamageItemEvent & itemstack damage API - do not process entity related callbacks when damaging from API ); } } @@ -712,6 +_,12 @@ return this.getItem().useOnRelease(this); } + // CraftBukkit start + public void restorePatch(DataComponentPatch datacomponentpatch) { + this.components.restorePatch(datacomponentpatch); + } + // CraftBukkit end + @Nullable public T set(DataComponentType component, @Nullable T value) { return this.components.set(component, value); @@ -759,6 +_,28 @@ this.getItem().verifyComponentsAfterLoad(this); } + // Paper start - (this is just a good no conflict location) + public org.bukkit.inventory.ItemStack asBukkitMirror() { + return org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this); + } + + public org.bukkit.inventory.ItemStack asBukkitCopy() { + return org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this.copy()); + } + + public static ItemStack fromBukkitCopy(org.bukkit.inventory.ItemStack itemstack) { + return org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(itemstack); + } + + private @Nullable org.bukkit.craftbukkit.inventory.CraftItemStack bukkitStack; + public org.bukkit.inventory.ItemStack getBukkitStack() { + if (this.bukkitStack == null || this.bukkitStack.handle != this) { + this.bukkitStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this); + } + return this.bukkitStack; + } + // Paper end + public Component getHoverName() { Component customName = this.getCustomName(); return customName != null ? customName : this.getItemName(); @@ -986,6 +_,19 @@ EnchantmentHelper.forEachModifier(this, equipmentSLot, action); } + // CraftBukkit start + @Deprecated + public void setItem(Item item) { + this.bukkitStack = null; // Paper + this.item = item; + // Paper start - change base component prototype + final DataComponentPatch patch = this.getComponentsPatch(); + this.components = new PatchedDataComponentMap(this.item.components()); + this.applyComponents(patch); + // Paper end - change base component prototype + } + // CraftBukkit end + public Component getDisplayName() { MutableComponent mutableComponent = Component.empty().append(this.getHoverName()); if (this.has(DataComponents.CUSTOM_NAME)) { @@ -1041,7 +_,7 @@ } public void consume(int amount, @Nullable LivingEntity entity) { - if (entity == null || !entity.hasInfiniteMaterials()) { + if ((entity == null || !entity.hasInfiniteMaterials()) && this != ItemStack.EMPTY) { // CraftBukkit this.shrink(amount); } }