From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Mark Vainomaa <mikroskeem@mikroskeem.eu>
Date: Wed, 12 Sep 2018 18:53:55 +0300
Subject: [PATCH] Implement an API for CanPlaceOn and CanDestroy NBT values


diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
index 01ceb8de8411193fa407bf19bbd25a4bf44765d3..faa0d91713fb19820ca744f8c9449f79f44a55cb 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
@@ -83,6 +83,12 @@ import org.bukkit.persistence.PersistentDataContainer;
 import static org.spigotmc.ValidateUtils.*;
 // Spigot end
 
+// Paper start
+import com.destroystokyo.paper.Namespaced;
+import com.destroystokyo.paper.NamespacedTag;
+import java.util.Collections;
+// Paper end
+
 /**
  * Children must include the following:
  *
@@ -268,6 +274,10 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
     @Specific(Specific.To.NBT)
     static final ItemMetaKey BLOCK_DATA = new ItemMetaKey("BlockStateTag");
     static final ItemMetaKey BUKKIT_CUSTOM_TAG = new ItemMetaKey("PublicBukkitValues");
+    // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
+    static final ItemMetaKey CAN_DESTROY = new ItemMetaKey("CanDestroy");
+    static final ItemMetaKey CAN_PLACE_ON = new ItemMetaKey("CanPlaceOn");
+    // Paper end
 
     // We store the raw original JSON representation of all text data. See SPIGOT-5063, SPIGOT-5656, SPIGOT-5304
     private String displayName;
@@ -281,6 +291,10 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
     private int hideFlag;
     private boolean unbreakable;
     private int damage;
+    // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
+    private Set<Namespaced> placeableKeys = Sets.newHashSet();
+    private Set<Namespaced> destroyableKeys = Sets.newHashSet();
+    // Paper end
 
     private static final Set<String> HANDLED_TAGS = Sets.newHashSet();
     private static final CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new CraftPersistentDataTypeRegistry();
@@ -318,6 +332,15 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
         this.hideFlag = meta.hideFlag;
         this.unbreakable = meta.unbreakable;
         this.damage = meta.damage;
+        // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
+        if (meta.hasPlaceableKeys()) {
+            this.placeableKeys = new java.util.HashSet<>(meta.placeableKeys);
+        }
+
+        if (meta.hasDestroyableKeys()) {
+            this.destroyableKeys = new java.util.HashSet<>(meta.destroyableKeys);
+        }
+        // Paper end
         this.unhandledTags.putAll(meta.unhandledTags);
         this.persistentDataContainer.putAll(meta.persistentDataContainer.getRaw());
 
@@ -381,6 +404,31 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
                 this.persistentDataContainer.put(key, compound.get(key).copy());
             }
         }
+        // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
+        if (tag.contains(CAN_DESTROY.NBT)) {
+            ListTag list = tag.getList(CAN_DESTROY.NBT, CraftMagicNumbers.NBT.TAG_STRING);
+            for (int i = 0; i < list.size(); i++) {
+                Namespaced namespaced = this.deserializeNamespaced(list.getString(i));
+                if (namespaced == null) {
+                    continue;
+                }
+
+                this.destroyableKeys.add(namespaced);
+            }
+        }
+
+        if (tag.contains(CAN_PLACE_ON.NBT)) {
+            ListTag list = tag.getList(CAN_PLACE_ON.NBT, CraftMagicNumbers.NBT.TAG_STRING);
+            for (int i = 0; i < list.size(); i++) {
+                Namespaced namespaced = this.deserializeNamespaced(list.getString(i));
+                if (namespaced == null) {
+                    continue;
+                }
+
+                this.placeableKeys.add(namespaced);
+            }
+        }
+        // Paper end
 
         Set<String> keys = tag.getAllKeys();
         for (String key : keys) {
@@ -519,6 +567,34 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
             this.setDamage(damage);
         }
 
+        // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
+        Iterable<?> canPlaceOnSerialized = SerializableMeta.getObject(Iterable.class, map, CAN_PLACE_ON.BUKKIT, true);
+        if (canPlaceOnSerialized != null) {
+            for (Object canPlaceOnElement : canPlaceOnSerialized) {
+                String canPlaceOnRaw = (String) canPlaceOnElement;
+                Namespaced value = this.deserializeNamespaced(canPlaceOnRaw);
+                if (value == null) {
+                    continue;
+                }
+
+                this.placeableKeys.add(value);
+            }
+        }
+
+        Iterable<?> canDestroySerialized = SerializableMeta.getObject(Iterable.class, map, CAN_DESTROY.BUKKIT, true);
+        if (canDestroySerialized != null) {
+            for (Object canDestroyElement : canDestroySerialized) {
+                String canDestroyRaw = (String) canDestroyElement;
+                Namespaced value = this.deserializeNamespaced(canDestroyRaw);
+                if (value == null) {
+                    continue;
+                }
+
+                this.destroyableKeys.add(value);
+            }
+        }
+        // Paper end
+
         String internal = SerializableMeta.getString(map, "internal", true);
         if (internal != null) {
             ByteArrayInputStream buf = new ByteArrayInputStream(Base64.getDecoder().decode(internal));
@@ -647,6 +723,23 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
         if (this.hasDamage()) {
             itemTag.putInt(DAMAGE.NBT, damage);
         }
+        // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
+        if (hasPlaceableKeys()) {
+            List<String> items = this.placeableKeys.stream()
+                .map(this::serializeNamespaced)
+                .collect(java.util.stream.Collectors.toList());
+
+            itemTag.put(CAN_PLACE_ON.NBT, createNonComponentStringList(items));
+        }
+
+        if (hasDestroyableKeys()) {
+            List<String> items = this.destroyableKeys.stream()
+                .map(this::serializeNamespaced)
+                .collect(java.util.stream.Collectors.toList());
+
+            itemTag.put(CAN_DESTROY.NBT, createNonComponentStringList(items));
+        }
+        // Paper end
 
         for (Map.Entry<String, Tag> e : this.unhandledTags.entrySet()) {
             itemTag.put(e.getKey(), e.getValue());
@@ -663,6 +756,21 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
         }
     }
 
+    // Paper start
+    static ListTag createNonComponentStringList(List<String> list) {
+        if (list == null || list.isEmpty()) {
+            return null;
+        }
+
+        ListTag tagList = new ListTag();
+        for (String value : list) {
+            tagList.add(StringTag.valueOf(value)); // Paper - NBTTagString.of(String str)
+        }
+
+        return tagList;
+    }
+    // Paper end
+
     ListTag createStringList(List<String> list) {
         if (list == null) {
             return null;
@@ -746,7 +854,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
 
     @Overridden
     boolean isEmpty() {
-        return !(this.hasDisplayName() || this.hasLocalizedName() || this.hasEnchants() || (this.lore != null) || this.hasCustomModelData() || this.hasBlockData() || this.hasRepairCost() || !this.unhandledTags.isEmpty() || !this.persistentDataContainer.isEmpty() || this.hideFlag != 0 || this.isUnbreakable() || this.hasDamage() || this.hasAttributeModifiers());
+        return !(this.hasDisplayName() || this.hasLocalizedName() || this.hasEnchants() || (this.lore != null) || this.hasCustomModelData() || this.hasBlockData() || this.hasRepairCost() || !this.unhandledTags.isEmpty() || !this.persistentDataContainer.isEmpty() || this.hideFlag != 0 || this.isUnbreakable() || this.hasDamage() || this.hasAttributeModifiers() || this.hasPlaceableKeys() || this.hasDestroyableKeys()); // Paper - Implement an API for CanPlaceOn and CanDestroy NBT values
     }
 
     // Paper start
@@ -1177,7 +1285,11 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
                 && (this.hideFlag == that.hideFlag)
                 && (this.isUnbreakable() == that.isUnbreakable())
                 && (this.hasDamage() ? that.hasDamage() && this.damage == that.damage : !that.hasDamage())
-                && (this.version == that.version);
+                && (this.version == that.version)
+                // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
+                && (this.hasPlaceableKeys() ? that.hasPlaceableKeys() && this.placeableKeys.equals(that.placeableKeys) : !that.hasPlaceableKeys())
+                && (this.hasDestroyableKeys() ? that.hasDestroyableKeys() && this.destroyableKeys.equals(that.destroyableKeys) : !that.hasDestroyableKeys());
+                // Paper end
     }
 
     /**
@@ -1212,6 +1324,10 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
         hash = 61 * hash + (this.hasDamage() ? this.damage : 0);
         hash = 61 * hash + (this.hasAttributeModifiers() ? this.attributeModifiers.hashCode() : 0);
         hash = 61 * hash + this.version;
+        // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
+        hash = 61 * hash + (this.hasPlaceableKeys() ? this.placeableKeys.hashCode() : 0);
+        hash = 61 * hash + (this.hasDestroyableKeys() ? this.destroyableKeys.hashCode() : 0);
+        // Paper end
         return hash;
     }
 
@@ -1236,6 +1352,14 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
             clone.unbreakable = this.unbreakable;
             clone.damage = this.damage;
             clone.version = this.version;
+            // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
+            if (this.placeableKeys != null) {
+                clone.placeableKeys = Sets.newHashSet(this.placeableKeys);
+            }
+            if (this.destroyableKeys != null) {
+                clone.destroyableKeys = Sets.newHashSet(this.destroyableKeys);
+            }
+            // Paper end
             return clone;
         } catch (CloneNotSupportedException e) {
             throw new Error(e);
@@ -1293,6 +1417,23 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
             builder.put(DAMAGE.BUKKIT, damage);
         }
 
+        // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
+        if (this.hasPlaceableKeys()) {
+            List<String> cerealPlaceable = this.placeableKeys.stream()
+                .map(this::serializeNamespaced)
+                .collect(java.util.stream.Collectors.toList());
+
+            builder.put(CAN_PLACE_ON.BUKKIT, cerealPlaceable);
+        }
+
+        if (this.hasDestroyableKeys()) {
+            List<String> cerealDestroyable = this.destroyableKeys.stream()
+                .map(this::serializeNamespaced)
+                .collect(java.util.stream.Collectors.toList());
+
+            builder.put(CAN_DESTROY.BUKKIT, cerealDestroyable);
+        }
+        // Paper end
         final Map<String, Tag> internalTags = new HashMap<String, Tag>(this.unhandledTags);
         this.serializeInternal(internalTags);
         if (!internalTags.isEmpty()) {
@@ -1459,6 +1600,8 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
                         CraftMetaArmorStand.SHOW_ARMS.NBT,
                         CraftMetaArmorStand.SMALL.NBT,
                         CraftMetaArmorStand.MARKER.NBT,
+                        CAN_DESTROY.NBT,
+                        CAN_PLACE_ON.NBT,
                         // Paper end
                         CraftMetaCompass.LODESTONE_DIMENSION.NBT,
                         CraftMetaCompass.LODESTONE_POS.NBT,
@@ -1487,4 +1630,146 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
     }
     // Paper end
 
+    // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
+    @Override
+    @SuppressWarnings("deprecation")
+    public Set<Material> getCanDestroy() {
+        return !hasDestroyableKeys() ? Collections.emptySet() : legacyGetMatsFromKeys(this.destroyableKeys);
+    }
+
+    @Override
+    @SuppressWarnings("deprecation")
+    public void setCanDestroy(Set<Material> canDestroy) {
+        Validate.notNull(canDestroy, "Cannot replace with null set!");
+        legacyClearAndReplaceKeys(this.destroyableKeys, canDestroy);
+    }
+
+    @Override
+    @SuppressWarnings("deprecation")
+    public Set<Material> getCanPlaceOn() {
+        return !hasPlaceableKeys() ? Collections.emptySet() : legacyGetMatsFromKeys(this.placeableKeys);
+    }
+
+    @Override
+    @SuppressWarnings("deprecation")
+    public void setCanPlaceOn(Set<Material> canPlaceOn) {
+        Validate.notNull(canPlaceOn, "Cannot replace with null set!");
+        legacyClearAndReplaceKeys(this.placeableKeys, canPlaceOn);
+    }
+
+    @Override
+    public Set<Namespaced> getDestroyableKeys() {
+        return !hasDestroyableKeys() ? Collections.emptySet() : Sets.newHashSet(this.destroyableKeys);
+    }
+
+    @Override
+    public void setDestroyableKeys(Collection<Namespaced> canDestroy) {
+        Validate.notNull(canDestroy, "Cannot replace with null collection!");
+        Validate.isTrue(ofAcceptableType(canDestroy), "Can only use NamespacedKey or NamespacedTag objects!");
+        this.destroyableKeys.clear();
+        this.destroyableKeys.addAll(canDestroy);
+    }
+
+    @Override
+    public Set<Namespaced> getPlaceableKeys() {
+        return !hasPlaceableKeys() ? Collections.emptySet() : Sets.newHashSet(this.placeableKeys);
+    }
+
+    @Override
+    public void setPlaceableKeys(Collection<Namespaced> canPlaceOn) {
+        Validate.notNull(canPlaceOn, "Cannot replace with null collection!");
+        Validate.isTrue(ofAcceptableType(canPlaceOn), "Can only use NamespacedKey or NamespacedTag objects!");
+        this.placeableKeys.clear();
+        this.placeableKeys.addAll(canPlaceOn);
+    }
+
+    @Override
+    public boolean hasPlaceableKeys() {
+        return this.placeableKeys != null && !this.placeableKeys.isEmpty();
+    }
+
+    @Override
+    public boolean hasDestroyableKeys() {
+        return this.destroyableKeys != null && !this.destroyableKeys.isEmpty();
+    }
+
+    @Deprecated
+    private void legacyClearAndReplaceKeys(Collection<Namespaced> toUpdate, Collection<Material> beingSet) {
+        if (beingSet.stream().anyMatch(Material::isLegacy)) {
+            throw new IllegalArgumentException("Set must not contain any legacy materials!");
+        }
+
+        toUpdate.clear();
+        toUpdate.addAll(beingSet.stream().map(Material::getKey).collect(java.util.stream.Collectors.toSet()));
+    }
+
+    @Deprecated
+    private Set<Material> legacyGetMatsFromKeys(Collection<Namespaced> names) {
+        Set<Material> mats = Sets.newHashSet();
+        for (Namespaced key : names) {
+            if (!(key instanceof org.bukkit.NamespacedKey)) {
+                continue;
+            }
+
+            Material material = Material.matchMaterial(key.toString(), false);
+            if (material != null) {
+                mats.add(material);
+            }
+        }
+
+        return mats;
+    }
+
+    private @Nullable Namespaced deserializeNamespaced(String raw) {
+        boolean isTag = raw.length() > 0 && raw.codePointAt(0) == '#';
+        com.mojang.datafixers.util.Either<net.minecraft.commands.arguments.blocks.BlockStateParser.BlockResult, net.minecraft.commands.arguments.blocks.BlockStateParser.TagResult> result;
+        try {
+            result = net.minecraft.commands.arguments.blocks.BlockStateParser.parseForTesting(net.minecraft.core.registries.BuiltInRegistries.BLOCK.asLookup(), raw, false);
+        } catch (com.mojang.brigadier.exceptions.CommandSyntaxException e) {
+            return null;
+        }
+
+        net.minecraft.resources.ResourceLocation key = null;
+        if (isTag && result.right().isPresent() && result.right().get().tag() instanceof net.minecraft.core.HolderSet.Named<net.minecraft.world.level.block.Block> namedSet) {
+            key = namedSet.key().location();
+        } else if (result.left().isPresent()) {
+            key = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getKey(result.left().get().blockState().getBlock());
+        }
+
+        if (key == null) {
+            return null;
+        }
+
+        // don't DC the player if something slips through somehow
+        Namespaced resource = null;
+        try {
+            if (isTag) {
+                resource = new NamespacedTag(key.getNamespace(), key.getPath());
+            } else {
+                resource = CraftNamespacedKey.fromMinecraft(key);
+            }
+        } catch (IllegalArgumentException ex) {
+            org.bukkit.Bukkit.getLogger().warning("Namespaced resource does not validate: " + key.toString());
+            ex.printStackTrace();
+        }
+
+        return resource;
+    }
+
+    private @Nonnull String serializeNamespaced(Namespaced resource) {
+        return resource.toString();
+    }
+
+    // not a fan of this
+    private boolean ofAcceptableType(Collection<Namespaced> namespacedResources) {
+        
+        for (Namespaced resource : namespacedResources) {
+            if (!(resource instanceof org.bukkit.NamespacedKey || resource instanceof com.destroystokyo.paper.NamespacedTag)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+    // Paper end
 }
