diff --git a/src/main/java/net/minestom/server/item/ItemStack.java b/src/main/java/net/minestom/server/item/ItemStack.java index 1250bdf0c..62e3f6abb 100644 --- a/src/main/java/net/minestom/server/item/ItemStack.java +++ b/src/main/java/net/minestom/server/item/ItemStack.java @@ -67,6 +67,9 @@ public class ItemStack implements DataContainer, PublicCloneable { private StackingRule stackingRule; private Data data; + private Set canDestroy; + private Set canPlaceOn; + { if (defaultStackingRule == null) defaultStackingRule = VANILLA_STACKING_RULE; @@ -83,6 +86,9 @@ public class ItemStack implements DataContainer, PublicCloneable { this.enchantmentMap = new Object2ShortOpenHashMap<>(); this.attributes = new ArrayList<>(); + this.canDestroy = new HashSet<>(); + this.canPlaceOn = new HashSet<>(); + this.itemMeta = findMeta(); } @@ -195,6 +201,76 @@ public class ItemStack implements DataContainer, PublicCloneable { isSimilar((ItemStack) o) && ((ItemStack) o).getAmount() == getAmount(); } + /** + * Checks if this item can be placed on the block. + * This should be enforced only for adventure mode players. + * @param block the block's namespaceID + * @return true if it can be placed, false otherwise + */ + public boolean canPlaceOn(String block) { + return canPlaceOn.contains(block); + } + + /** + * Adds the block to the list of blocks that + * this item can be placed on. + * @param block the block's namespaceID + */ + public void addCanPlaceOn(String block) { + canPlaceOn.add(block); + } + + /** + * Removes the block from the set of blocks that + * this item can be placed on. + * @param block the block's namespaceID + */ + public void removeCanPlaceOn(String block) { + canPlaceOn.remove(block); + } + + /** + * Gets the blocks that this item can be placed on + * @return an unmodifiable {@link Set} of blocks + */ + public Set getCanPlaceOn() { + return Collections.unmodifiableSet(canPlaceOn); + } + + /** + * Checks if this item is allowed to break the provided block. + * This should be enforced only for adventure mode players. + * @param block the block's namespaceID + * @return true if this item can destroy it, otherwise false + */ + public boolean canDestroy(String block) { + return canDestroy.contains(block); + } + + /** + * Adds the block to the set of blocks that can be destroyed by this item. + * @param block the block's namespaceID + */ + public void addCanDestroy(String block) { + canDestroy.add(block); + } + + /** + * Removes the block from the set of blocks that can be destroyed by this item. + * @param block the block's namespaceID + */ + public void removeCanDestroy(String block) { + canDestroy.remove(block); + } + + /** + * Gets the blocks that this item can destroy + * @return an unmodifiable {@link Set} of blocks + */ + public Set getCanDestroy() { + return Collections.unmodifiableSet(canDestroy); + } + /** * Gets the item damage (durability). * @@ -555,7 +631,9 @@ public class ItemStack implements DataContainer, PublicCloneable { hideFlag != 0 || customModelData != 0 || (itemMeta != null && itemMeta.hasNbt()) || - (data != null && !data.isEmpty()); + (data != null && !data.isEmpty()) || + canDestroy.size() != 0 || + canPlaceOn.size() != 0; } /** @@ -594,6 +672,9 @@ public class ItemStack implements DataContainer, PublicCloneable { itemStack.hideFlag = hideFlag; itemStack.customModelData = customModelData; + itemStack.canPlaceOn = new HashSet<>(canPlaceOn); + itemStack.canDestroy = new HashSet<>(canDestroy); + if (itemMeta != null) itemStack.itemMeta = itemMeta.clone(); diff --git a/src/main/java/net/minestom/server/listener/BlockPlacementListener.java b/src/main/java/net/minestom/server/listener/BlockPlacementListener.java index f49dd858a..81a7776cc 100644 --- a/src/main/java/net/minestom/server/listener/BlockPlacementListener.java +++ b/src/main/java/net/minestom/server/listener/BlockPlacementListener.java @@ -19,6 +19,8 @@ import net.minestom.server.inventory.PlayerInventory; import net.minestom.server.item.ItemStack; import net.minestom.server.item.Material; import net.minestom.server.network.packet.client.play.ClientPlayerBlockPlacementPacket; +import net.minestom.server.network.packet.client.play.ClientPlayerDiggingPacket; +import net.minestom.server.network.packet.server.play.BlockChangePacket; import net.minestom.server.utils.BlockPosition; import net.minestom.server.utils.Direction; import net.minestom.server.utils.chunk.ChunkUtils; @@ -72,14 +74,19 @@ public class BlockPlacementListener { final Material useMaterial = usedItem.getMaterial(); // Verify if the player can place the block + boolean canPlaceBlock = true; { if (useMaterial == Material.AIR) { // Can't place air return; } - if (player.getGameMode().equals(GameMode.ADVENTURE)) { // Can't place in adventure mode - return; - } + //Check if the player is allowed to place blocks based on their game mode + if (player.getGameMode() == GameMode.SPECTATOR) { + canPlaceBlock = false; //Spectators can't place blocks + } else if (player.getGameMode() == GameMode.ADVENTURE) { + //Check if the block can placed on the block + canPlaceBlock = usedItem.canPlaceOn(instance.getBlock(blockPosition).getName()); + } } // Get the newly placed block position @@ -89,6 +96,16 @@ public class BlockPlacementListener { blockPosition.add(offsetX, offsetY, offsetZ); + if(!canPlaceBlock) { + //Send a block change with AIR as block to keep the client in sync, + //using refreshChunk results in the client not being in sync + //after rapid invalid block placements + BlockChangePacket blockChangePacket = new BlockChangePacket(); + blockChangePacket.blockPosition = blockPosition; + blockChangePacket.blockStateId = Block.AIR.getBlockId(); + player.getPlayerConnection().sendPacket(blockChangePacket); + return; + } final Chunk chunk = instance.getChunkAt(blockPosition); diff --git a/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java b/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java index 597a65972..25f3bd722 100644 --- a/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java +++ b/src/main/java/net/minestom/server/listener/PlayerDiggingListener.java @@ -1,5 +1,6 @@ package net.minestom.server.listener; +import net.minestom.server.entity.GameMode; import net.minestom.server.entity.Player; import net.minestom.server.event.item.ItemUpdateStateEvent; import net.minestom.server.event.player.PlayerStartDiggingEvent; @@ -10,6 +11,7 @@ import net.minestom.server.instance.block.CustomBlock; import net.minestom.server.inventory.PlayerInventory; import net.minestom.server.item.ItemStack; import net.minestom.server.item.StackingRule; +import net.minestom.server.item.attribute.ItemAttribute; import net.minestom.server.network.packet.client.play.ClientPlayerDiggingPacket; import net.minestom.server.network.packet.server.play.AcknowledgePlayerDiggingPacket; import net.minestom.server.network.packet.server.play.EntityEffectPacket; @@ -17,6 +19,7 @@ import net.minestom.server.network.packet.server.play.RemoveEntityEffectPacket; import net.minestom.server.potion.Potion; import net.minestom.server.potion.PotionEffect; import net.minestom.server.utils.BlockPosition; +import net.minestom.server.utils.NamespaceID; import org.jetbrains.annotations.NotNull; import java.util.List; @@ -36,8 +39,23 @@ public class PlayerDiggingListener { return; if (status == ClientPlayerDiggingPacket.Status.STARTED_DIGGING) { - final short blockStateId = instance.getBlockStateId(blockPosition); + + //Check if the player is allowed to break blocks based on their game mode + if (player.getGameMode() == GameMode.SPECTATOR) { + sendAcknowledgePacket(player, blockPosition, blockStateId, + ClientPlayerDiggingPacket.Status.STARTED_DIGGING, false); + return; //Spectators can't break blocks + } else if (player.getGameMode() == GameMode.ADVENTURE) { + //Check if the item can break the block with the current item + ItemStack itemInMainHand = player.getItemInMainHand(); + if (!itemInMainHand.canDestroy(instance.getBlock(blockPosition).getName())) { + sendAcknowledgePacket(player, blockPosition, blockStateId, + ClientPlayerDiggingPacket.Status.STARTED_DIGGING, false); + return; + } + } + final boolean instantBreak = player.isCreative() || player.isInstantBreak() || Block.fromStateId(blockStateId).breaksInstantaneously(); diff --git a/src/main/java/net/minestom/server/utils/NBTUtils.java b/src/main/java/net/minestom/server/utils/NBTUtils.java index afa7cee2c..aacce9ac2 100644 --- a/src/main/java/net/minestom/server/utils/NBTUtils.java +++ b/src/main/java/net/minestom/server/utils/NBTUtils.java @@ -26,10 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; // for lack of a better name public final class NBTUtils { @@ -124,6 +121,7 @@ public final class NBTUtils { return item; } + @SuppressWarnings("ConstantConditions") public static void loadDataIntoItem(@NotNull ItemStack item, @NotNull NBTCompound nbt) { if (nbt.containsKey("Damage")) item.setDamage(nbt.getInt("Damage")); if (nbt.containsKey("Unbreakable")) item.setUnbreakable(nbt.getAsByte("Unbreakable") == 1); @@ -222,6 +220,21 @@ public final class NBTUtils { } } } + + //CanPlaceOn + { + if (nbt.containsKey("CanPlaceOn")) { + NBTList canPlaceOn = nbt.getList("CanPlaceOn"); + canPlaceOn.forEach(x -> item.addCanPlaceOn(x.getValue())); + } + } + //CanDestroy + { + if (nbt.containsKey("CanDestroy")) { + NBTList canPlaceOn = nbt.getList("CanDestroy"); + canPlaceOn.forEach(x -> item.addCanDestroy(x.getValue())); + } + } } public static void loadEnchantments(NBTList enchantments, EnchantmentSetter setter) { @@ -368,6 +381,26 @@ public final class NBTUtils { } } // End ownership + + //CanDestroy + { + Set canDestroy = itemStack.getCanDestroy(); + if (canDestroy.size() > 0) { + NBTList list = new NBTList<>(NBTTypes.TAG_String); + canDestroy.forEach(x -> list.add(new NBTString(x.toString()))); + itemNBT.set("CanDestroy", list); + } + } + + //CanDestroy + { + Set canPlaceOn = itemStack.getCanPlaceOn(); + if (canPlaceOn.size() > 0) { + NBTList list = new NBTList<>(NBTTypes.TAG_String); + canPlaceOn.forEach(x -> list.add(new NBTString(x.toString()))); + itemNBT.set("CanPlaceOn", list); + } + } } /**