diff --git a/Spigot-API-Patches/Add-an-API-for-CanPlaceOn-and-CanDestroy-NBT-values.patch b/Spigot-API-Patches/Add-an-API-for-CanPlaceOn-and-CanDestroy-NBT-values.patch
index b6059fd872..ec3278120c 100644
--- a/Spigot-API-Patches/Add-an-API-for-CanPlaceOn-and-CanDestroy-NBT-values.patch
+++ b/Spigot-API-Patches/Add-an-API-for-CanPlaceOn-and-CanDestroy-NBT-values.patch
@@ -4,8 +4,220 @@ Date: Wed, 12 Sep 2018 18:53:35 +0300
 Subject: [PATCH] Add an API for CanPlaceOn and CanDestroy NBT values
 
 
+diff --git a/src/main/java/com/destroystokyo/paper/Namespaced.java b/src/main/java/com/destroystokyo/paper/Namespaced.java
+new file mode 100644
+index 00000000..2baf58b7
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/Namespaced.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper;
++
++/**
++ * Represents a namespaced resource, see {@link org.bukkit.NamespacedKey} for single elements
++ * or {@link com.destroystokyo.paper.NamespacedTag} for a collection of elements
++ *
++ * Namespaces may only contain lowercase alphanumeric characters, periods,
++ * underscores, and hyphens.
++ * <p>
++ * Keys may only contain lowercase alphanumeric characters, periods,
++ * underscores, hyphens, and forward slashes.
++ * <p>
++ * You should not be implementing this interface yourself, use {@link org.bukkit.NamespacedKey}
++ * or {@link com.destroystokyo.paper.NamespacedTag} as needed instead.
++ */
++public interface Namespaced {
++    /**
++     * Gets the namespace this resource is a part of
++     * <p>
++     * This is contractually obligated to only contain lowercase alphanumeric characters,
++     * periods, underscores, and hyphens.
++     *
++     * @return resource namespace
++     */
++    String getNamespace();
++
++    /**
++     * Gets the key corresponding to this resource
++     * <p>
++     * This is contractually obligated to only contain lowercase alphanumeric characters,
++     * periods, underscores, hyphens, and forward slashes.
++     *
++     * @return resource key
++     */
++    String getKey();
++}
+diff --git a/src/main/java/com/destroystokyo/paper/NamespacedTag.java b/src/main/java/com/destroystokyo/paper/NamespacedTag.java
+new file mode 100644
+index 00000000..89949827
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/NamespacedTag.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper;
++
++import com.google.common.base.Preconditions;
++import java.util.Locale;
++import java.util.UUID;
++import java.util.regex.Pattern;
++import org.bukkit.plugin.Plugin;
++
++/**
++ * Represents a String based key pertaining to a tagged entry. Consists of two components - a namespace
++ * and a key.
++ * <p>
++ * Namespaces may only contain lowercase alphanumeric characters, periods,
++ * underscores, and hyphens.
++ * <p>
++ * Keys may only contain lowercase alphanumeric characters, periods,
++ * underscores, hyphens, and forward slashes.
++ *
++ */
++// Paper - entire class, based on org.bukkit.NamespacedKey
++public final class NamespacedTag implements com.destroystokyo.paper.Namespaced {
++
++    /**
++     * The namespace representing all inbuilt keys.
++     */
++    public static final String MINECRAFT = "minecraft";
++    /**
++     * The namespace representing all keys generated by Bukkit for backwards
++     * compatibility measures.
++     */
++    public static final String BUKKIT = "bukkit";
++    //
++    private static final Pattern VALID_NAMESPACE = Pattern.compile("[a-z0-9._-]+");
++    private static final Pattern VALID_KEY = Pattern.compile("[a-z0-9/._-]+");
++    //
++    private final String namespace;
++    private final String key;
++
++    /**
++     * Create a key in a specific namespace.
++     *
++     * @param namespace String representing a grouping of keys
++     * @param key Name for this specific key
++     * @deprecated should never be used by plugins, for internal use only!!
++     */
++    @Deprecated
++    public NamespacedTag(String namespace, String key) {
++        Preconditions.checkArgument(namespace != null && VALID_NAMESPACE.matcher(namespace).matches(), "Invalid namespace. Must be [a-z0-9._-]: %s", namespace);
++        Preconditions.checkArgument(key != null && VALID_KEY.matcher(key).matches(), "Invalid key. Must be [a-z0-9/._-]: %s", key);
++
++        this.namespace = namespace;
++        this.key = key;
++
++        String string = toString();
++        Preconditions.checkArgument(string.length() < 256, "NamespacedTag must be less than 256 characters", string);
++    }
++
++    /**
++     * Create a key in the plugin's namespace.
++     * <p>
++     * Namespaces may only contain lowercase alphanumeric characters, periods,
++     * underscores, and hyphens.
++     * <p>
++     * Keys may only contain lowercase alphanumeric characters, periods,
++     * underscores, hyphens, and forward slashes.
++     *
++     * @param plugin the plugin to use for the namespace
++     * @param key the key to create
++     */
++    public NamespacedTag(Plugin plugin, String key) {
++        Preconditions.checkArgument(plugin != null, "Plugin cannot be null");
++        Preconditions.checkArgument(key != null, "Key cannot be null");
++
++        this.namespace = plugin.getName().toLowerCase(Locale.ROOT);
++        this.key = key.toLowerCase().toLowerCase(Locale.ROOT);
++
++        // Check validity after normalization
++        Preconditions.checkArgument(VALID_NAMESPACE.matcher(this.namespace).matches(), "Invalid namespace. Must be [a-z0-9._-]: %s", this.namespace);
++        Preconditions.checkArgument(VALID_KEY.matcher(this.key).matches(), "Invalid key. Must be [a-z0-9/._-]: %s", this.key);
++
++        String string = toString();
++        Preconditions.checkArgument(string.length() < 256, "NamespacedTag must be less than 256 characters (%s)", string);
++    }
++
++    public String getNamespace() {
++        return namespace;
++    }
++
++    public String getKey() {
++        return key;
++    }
++
++    @Override
++    public int hashCode() {
++        int hash = 7;
++        hash = 47 * hash + this.namespace.hashCode();
++        hash = 47 * hash + this.key.hashCode();
++        return hash;
++    }
++
++    @Override
++    public boolean equals(Object obj) {
++        if (obj == null) {
++            return false;
++        }
++        if (getClass() != obj.getClass()) {
++            return false;
++        }
++        final NamespacedTag other = (NamespacedTag) obj;
++        return this.namespace.equals(other.namespace) && this.key.equals(other.key);
++    }
++
++    @Override
++    public String toString() {
++        return "#" + this.namespace + ":" + this.key;
++    }
++
++    /**
++     * Return a new random key in the {@link #BUKKIT} namespace.
++     *
++     * @return new key
++     * @deprecated should never be used by plugins, for internal use only!!
++     */
++    @Deprecated
++    public static NamespacedTag randomKey() {
++        return new NamespacedTag(BUKKIT, UUID.randomUUID().toString());
++    }
++
++    /**
++     * Get a key in the Minecraft namespace.
++     *
++     * @param key the key to use
++     * @return new key in the Minecraft namespace
++     */
++    public static NamespacedTag minecraft(String key) {
++        return new NamespacedTag(MINECRAFT, key);
++    }
++}
+diff --git a/src/main/java/org/bukkit/NamespacedKey.java b/src/main/java/org/bukkit/NamespacedKey.java
+index fe8d3468..074769c1 100644
+--- a/src/main/java/org/bukkit/NamespacedKey.java
++++ b/src/main/java/org/bukkit/NamespacedKey.java
+@@ -0,0 +0,0 @@ import org.bukkit.plugin.Plugin;
+  * underscores, hyphens, and forward slashes.
+  *
+  */
+-public final class NamespacedKey {
++public final class NamespacedKey implements com.destroystokyo.paper.Namespaced { // Paper - implement namespaced
+ 
+     /**
+      * The namespace representing all inbuilt keys.
+@@ -0,0 +0,0 @@ public final class NamespacedKey {
+         Preconditions.checkArgument(string.length() < 256, "NamespacedKey must be less than 256 characters (%s)", string);
+     }
+ 
++    @Override // Paper
+     public String getNamespace() {
+         return namespace;
+     }
+ 
++    @Override // Paper
+     public String getKey() {
+         return key;
+     }
 diff --git a/src/main/java/org/bukkit/inventory/meta/ItemMeta.java b/src/main/java/org/bukkit/inventory/meta/ItemMeta.java
-index 7ac07ac07ac0..7ac07ac07ac0 100644
+index 2278d470..13a153c8 100644
 --- a/src/main/java/org/bukkit/inventory/meta/ItemMeta.java
 +++ b/src/main/java/org/bukkit/inventory/meta/ItemMeta.java
 @@ -0,0 +0,0 @@ public interface ItemMeta extends Cloneable, ConfigurationSerializable {
@@ -17,29 +229,79 @@ index 7ac07ac07ac0..7ac07ac07ac0 100644
 +     * Gets set of materials what given item can destroy in {@link org.bukkit.GameMode#ADVENTURE}
 +     *
 +     * @return Set of materials
++     * @deprecated Minecraft does not limit this to the material enum, Use {@link #getDestroyableKeys()} as a replacement
 +     */
++    @Deprecated
 +    Set<org.bukkit.Material> getCanDestroy();
 +
 +    /**
 +     * Sets set of materials what given item can destroy in {@link org.bukkit.GameMode#ADVENTURE}
 +     *
 +     * @param canDestroy Set of materials
++     * @deprecated Minecraft does not limit this to the material enum, Use {@link #setDestroyableKeys(Collection)} as a replacement
 +     */
++    @Deprecated
 +    void setCanDestroy(Set<org.bukkit.Material> canDestroy);
 +
 +    /**
 +     * Gets set of materials where given item can be placed on in {@link org.bukkit.GameMode#ADVENTURE}
 +     *
 +     * @return Set of materials
++     * @deprecated Minecraft does not limit this to the material enum, Use {@link #getPlaceableKeys()} as a replacement
 +     */
++    @Deprecated
 +    Set<org.bukkit.Material> getCanPlaceOn();
 +
 +    /**
 +     * Sets set of materials where given item can be placed on in {@link org.bukkit.GameMode#ADVENTURE}
 +     *
 +     * @param canPlaceOn Set of materials
++     * @deprecated Minecraft does not limit this to the material enum, Use {@link #setPlaceableKeys(Collection)} as a replacement
 +     */
++    @Deprecated
 +    void setCanPlaceOn(Set<org.bukkit.Material> canPlaceOn);
++
++    /**
++     * Gets the collection of namespaced keys that the item can destroy in {@link org.bukkit.GameMode#ADVENTURE}
++     *
++     * @return Set of {@link com.destroystokyo.paper.Namespaced}
++     */
++    Set<com.destroystokyo.paper.Namespaced> getDestroyableKeys();
++
++    /**
++     * Sets the collection of namespaced keys that the item can destroy in {@link org.bukkit.GameMode#ADVENTURE}
++     *
++     * @param canDestroy Set of {@link com.destroystokyo.paper.Namespaced}
++     */
++    void setDestroyableKeys(Collection<com.destroystokyo.paper.Namespaced> canDestroy);
++
++    /**
++     * Gets the collection of namespaced keys that the item can be placed on in {@link org.bukkit.GameMode#ADVENTURE}
++     *
++     * @return Set of {@link com.destroystokyo.paper.Namespaced}
++     */
++    Set<com.destroystokyo.paper.Namespaced> getPlaceableKeys();
++
++    /**
++     * Sets the set of namespaced keys that the item can be placed on in {@link org.bukkit.GameMode#ADVENTURE}
++     *
++     * @param canPlaceOn Set of {@link Collection<com.destroystokyo.paper.Namespaced>}
++     */
++    void setPlaceableKeys(Collection<com.destroystokyo.paper.Namespaced> canPlaceOn);
++
++    /**
++     * Checks for the existence of any keys that the item can be placed on
++     *
++     * @return true if this item has placeable keys
++     */
++    boolean hasPlaceableKeys();
++
++    /**
++     * Checks for the existence of any keys that the item can destroy
++     *
++     * @return true if this item has destroyable keys
++     */
++    boolean hasDestroyableKeys();
 +    // Paper end
  }
 --
\ No newline at end of file
diff --git a/Spigot-Server-Patches/Add-ArmorStand-Item-Meta.patch b/Spigot-Server-Patches/Add-ArmorStand-Item-Meta.patch
index e7d7cb917a..dd270fd69a 100644
--- a/Spigot-Server-Patches/Add-ArmorStand-Item-Meta.patch
+++ b/Spigot-Server-Patches/Add-ArmorStand-Item-Meta.patch
@@ -354,7 +354,7 @@ index 000000000..0e8acf12e
 +    }
 +}
 diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
-index 081904dad..6a95f5fa3 100644
+index 081904dad..dacca4bc4 100644
 --- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
 +++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
 @@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable {
@@ -370,8 +370,8 @@ index 081904dad..6a95f5fa3 100644
                          CraftMetaBlockState.BLOCK_ENTITY_TAG.NBT,
                          CraftMetaKnowledgeBook.BOOK_RECIPES.NBT,
 -                        CraftMetaTropicalFishBucket.VARIANT.NBT
-+                        // Paper start
 +                        CraftMetaTropicalFishBucket.VARIANT.NBT,
++                        // Paper start
 +                        CraftMetaArmorStand.ENTITY_TAG.NBT,
 +                        CraftMetaArmorStand.INVISIBLE.NBT,
 +                        CraftMetaArmorStand.NO_BASE_PLATE.NBT,
diff --git a/Spigot-Server-Patches/Implement-an-API-for-CanPlaceOn-and-CanDestroy-NBT-v.patch b/Spigot-Server-Patches/Implement-an-API-for-CanPlaceOn-and-CanDestroy-NBT-v.patch
index dd7773a73d..8d26c9c5a1 100644
--- a/Spigot-Server-Patches/Implement-an-API-for-CanPlaceOn-and-CanDestroy-NBT-v.patch
+++ b/Spigot-Server-Patches/Implement-an-API-for-CanPlaceOn-and-CanDestroy-NBT-v.patch
@@ -4,18 +4,124 @@ 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/net/minecraft/server/ArgumentBlock.java b/src/main/java/net/minecraft/server/ArgumentBlock.java
+index 35c436d19..fcfb17e4e 100644
+--- a/src/main/java/net/minecraft/server/ArgumentBlock.java
++++ b/src/main/java/net/minecraft/server/ArgumentBlock.java
+@@ -0,0 +0,0 @@ public class ArgumentBlock {
+     private final boolean j;
+     private final Map<IBlockState<?>, Comparable<?>> k = Maps.newHashMap();
+     private final Map<String, String> l = Maps.newHashMap();
+-    private MinecraftKey m = new MinecraftKey("");
++    private MinecraftKey m = new MinecraftKey(""); public MinecraftKey getBlockKey() { return this.m; } // Paper - OBFHELPER
+     private BlockStateList<Block, IBlockData> n;
+     private IBlockData o;
+     @Nullable
+@@ -0,0 +0,0 @@ public class ArgumentBlock {
+         return this.p;
+     }
+ 
++    public @Nullable MinecraftKey getTagKey() { return d(); } // Paper - OBFHELPER
+     @Nullable
+     public MinecraftKey d() {
+         return this.q;
+     }
+ 
++    public ArgumentBlock parse(boolean parseTile) throws CommandSyntaxException { return this.a(parseTile); } // Paper - OBFHELPER
+     public ArgumentBlock a(boolean flag) throws CommandSyntaxException {
+         this.s = this::l;
+         if (this.i.canRead() && this.i.peek() == '#') {
+@@ -0,0 +0,0 @@ public class ArgumentBlock {
+         if (this.q != null && !this.q.getKey().isEmpty()) {
+             Tag tag = TagsBlock.a().a(this.q);
+             if (tag != null) {
+-                for(Block block : tag.a()) {
++                for(Block block : (java.util.Collection<Block>) tag.a()) { // Paper - decompiler fix
+                     for(IBlockState iblockstate : block.getStates().d()) {
+                         if (!this.l.containsKey(iblockstate.a()) && iblockstate.a().startsWith(sx)) {
+                             suggestionsbuilder.suggest(iblockstate.a() + '=');
+@@ -0,0 +0,0 @@ public class ArgumentBlock {
+             if (this.q != null) {
+                 Tag tag = TagsBlock.a().a(this.q);
+                 if (tag != null) {
+-                    for(Block block : tag.a()) {
++                    for(Block block : (java.util.Collection<Block>) tag.a()) { // Paper - decompiler fix
+                         if (block.isTileEntity()) {
+                             return true;
+                         }
+@@ -0,0 +0,0 @@ public class ArgumentBlock {
+     private static <T extends Comparable<T>> SuggestionsBuilder a(SuggestionsBuilder suggestionsbuilder, IBlockState<T> iblockstate) {
+         for(Comparable comparable : iblockstate.d()) {
+             if (comparable instanceof Integer) {
+-                suggestionsbuilder.suggest(comparable);
++                suggestionsbuilder.suggest((Integer) comparable); // Paper - decompiler fix
+             } else {
+-                suggestionsbuilder.suggest(iblockstate.a(comparable));
++                suggestionsbuilder.suggest(iblockstate.a((T) comparable)); // Paper - decompiler fix
+             }
+         }
+ 
+@@ -0,0 +0,0 @@ public class ArgumentBlock {
+             Tag tag = TagsBlock.a().a(this.q);
+             if (tag != null) {
+                 label40:
+-                for(Block block : tag.a()) {
++                for(Block block : (java.util.Collection<Block>) tag.a()) { // Paper - decompiler fix
+                     IBlockState iblockstate = block.getStates().a(sx);
+                     if (iblockstate != null) {
+                         a(suggestionsbuilder, iblockstate);
+@@ -0,0 +0,0 @@ public class ArgumentBlock {
+                 boolean flag = false;
+                 boolean flag1 = false;
+ 
+-                for(Block block : tag.a()) {
++                for(Block block : (java.util.Collection<Block>) tag.a()) { // Paper - decompiler fix
+                     flag |= !block.getStates().d().isEmpty();
+                     flag1 |= block.isTileEntity();
+                     if (flag && flag1) {
+@@ -0,0 +0,0 @@ public class ArgumentBlock {
+     private <T extends Comparable<T>> void a(IBlockState<T> iblockstate, String sx, int ix) throws CommandSyntaxException {
+         Optional optional = iblockstate.b(sx);
+         if (optional.isPresent()) {
+-            this.o = (IBlockData)this.o.set(iblockstate, (Comparable)optional.get());
+-            this.k.put(iblockstate, optional.get());
++            this.o = (IBlockData)this.o.set(iblockstate, (T)optional.get()); // Paper - decompiler fix
++            this.k.put(iblockstate, (Comparable<?>) optional.get()); // Paper - decompiler fix
+         } else {
+             this.i.setCursor(ix);
+             throw e.createWithContext(this.i, this.m.toString(), iblockstate.a(), sx);
+@@ -0,0 +0,0 @@ public class ArgumentBlock {
+     private static <T extends Comparable<T>> void a(StringBuilder stringbuilder, IBlockState<T> iblockstate, Comparable<?> comparable) {
+         stringbuilder.append(iblockstate.a());
+         stringbuilder.append('=');
+-        stringbuilder.append(iblockstate.a(comparable));
++        stringbuilder.append(iblockstate.a((T) comparable)); // Paper - decompile fix
+     }
+ 
+     public CompletableFuture<Suggestions> a(SuggestionsBuilder suggestionsbuilder) {
 diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
-index 7ac07ac07ac0..7ac07ac07ac0 100644
+index dacca4bc4..0b040527f 100644
 --- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
 +++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java
+@@ -0,0 +0,0 @@ import javax.annotation.Nullable;
+ 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:
+  *
 @@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable {
      static final ItemMetaKey UNBREAKABLE = new ItemMetaKey("Unbreakable");
      @Specific(Specific.To.NBT)
      static final ItemMetaKey DAMAGE = new ItemMetaKey("Damage");
 +    // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
-+    @Specific(Specific.To.NBT)
 +    static final ItemMetaKey CAN_DESTROY = new ItemMetaKey("CanDestroy");
-+    @Specific(Specific.To.NBT)
 +    static final ItemMetaKey CAN_PLACE_ON = new ItemMetaKey("CanPlaceOn");
 +    // Paper end
  
@@ -26,8 +132,8 @@ index 7ac07ac07ac0..7ac07ac07ac0 100644
      private boolean unbreakable;
      private int damage;
 +    // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
-+    private Set<Material> canPlaceOn = Sets.newHashSet();
-+    private Set<Material> canDestroy = Sets.newHashSet();
++    private Set<Namespaced> placeableKeys;
++    private Set<Namespaced> destroyableKeys;
 +    // Paper end
  
      private static final Set<String> HANDLED_TAGS = Sets.newHashSet();
@@ -37,8 +143,13 @@ index 7ac07ac07ac0..7ac07ac07ac0 100644
          this.unbreakable = meta.unbreakable;
          this.damage = meta.damage;
 +        // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
-+        this.canDestroy = new java.util.HashSet<>(meta.canDestroy);
-+        this.canPlaceOn = new java.util.HashSet<>(meta.canPlaceOn);
++        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);
  
@@ -49,50 +160,87 @@ index 7ac07ac07ac0..7ac07ac07ac0 100644
          }
 +        // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
 +        if (tag.hasKey(CAN_DESTROY.NBT)) {
++            this.destroyableKeys = Sets.newHashSet();
 +            NBTTagList list = tag.getList(CAN_DESTROY.NBT, CraftMagicNumbers.NBT.TAG_STRING);
 +            for (int i = 0; i < list.size(); i++) {
-+                Material material = Material.matchMaterial(list.getString(i), false);
-+                if (material == null) {
++                Namespaced namespaced = this.deserializeNamespaced(list.getString(i));
++                if (namespaced == null) {
 +                    continue;
 +                }
 +
-+                this.canDestroy.add(material);
++                this.destroyableKeys.add(namespaced);
 +            }
 +        }
 +
 +        if (tag.hasKey(CAN_PLACE_ON.NBT)) {
++            this.placeableKeys = Sets.newHashSet();
 +            NBTTagList list = tag.getList(CAN_PLACE_ON.NBT, CraftMagicNumbers.NBT.TAG_STRING);
 +            for (int i = 0; i < list.size(); i++) {
-+                Material material = Material.matchMaterial(list.getString(i), false);
-+                if (material == null) {
++                Namespaced namespaced = this.deserializeNamespaced(list.getString(i));
++                if (namespaced == null) {
 +                    continue;
 +                }
 +
-+                this.canPlaceOn.add(material);
++                this.placeableKeys.add(namespaced);
 +            }
 +        }
 +        // Paper end
  
          Set<String> keys = tag.getKeys();
          for (String key : keys) {
+@@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable {
+             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) {
++            this.placeableKeys = Sets.newHashSet();
++            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) {
++            this.destroyableKeys = Sets.newHashSet();
++            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.decodeBase64(internal));
 @@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable {
          if (hasDamage()) {
              itemTag.setInt(DAMAGE.NBT, damage);
          }
 +        // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
-+        if (!this.canPlaceOn.isEmpty()) {
-+            List<String> items = this.canPlaceOn.stream()
-+                .map(Material::getKey)
-+                .map(org.bukkit.NamespacedKey::toString)
++        if (hasPlaceableKeys()) {
++            List<String> items = this.placeableKeys.stream()
++                .map(this::serializeNamespaced)
 +                .collect(java.util.stream.Collectors.toList());
 +
 +            itemTag.set(CAN_PLACE_ON.NBT, createStringList(items));
 +        }
 +
-+        if (!this.canDestroy.isEmpty()) {
-+            List<String> items = this.canDestroy.stream()
-+                .map(Material::getKey)
-+                .map(org.bukkit.NamespacedKey::toString)
++        if (hasDestroyableKeys()) {
++            List<String> items = this.destroyableKeys.stream()
++                .map(this::serializeNamespaced)
 +                .collect(java.util.stream.Collectors.toList());
 +
 +            itemTag.set(CAN_DESTROY.NBT, createStringList(items));
@@ -102,6 +250,81 @@ index 7ac07ac07ac0..7ac07ac07ac0 100644
          for (Map.Entry<String, NBTBase> e : unhandledTags.entrySet()) {
              itemTag.set(e.getKey(), e.getValue());
 @@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable {
+ 
+     @Overridden
+     boolean isEmpty() {
+-        return !(hasDisplayName() || hasLocalizedName() || hasEnchants() || hasLore() || hasRepairCost() || !unhandledTags.isEmpty() || hideFlag != 0 || isUnbreakable() || hasDamage() || hasAttributeModifiers());
++        return !(hasDisplayName() || hasLocalizedName() || hasEnchants() || hasLore() || hasRepairCost() || !unhandledTags.isEmpty() || hideFlag != 0 || isUnbreakable() || hasDamage() || hasAttributeModifiers()
++            || hasPlaceableKeys() || hasDestroyableKeys()); // Paper - Implement an API for CanPlaceOn and CanDestroy NBT values
+     }
+ 
+     public String getDisplayName() {
+@@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable {
+                 && (this.unhandledTags.equals(that.unhandledTags))
+                 && (this.hideFlag == that.hideFlag)
+                 && (this.isUnbreakable() == that.isUnbreakable())
+-                && (this.hasDamage() ? that.hasDamage() && this.damage == that.damage : !that.hasDamage());
++                && (this.hasDamage() ? that.hasDamage() && this.damage == that.damage : !that.hasDamage())
++                // 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
+     }
+ 
+     /**
+@@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable {
+         hash = 61 * hash + (isUnbreakable() ? 1231 : 1237);
+         hash = 61 * hash + (hasDamage() ? this.damage : 0);
+         hash = 61 * hash + (hasAttributeModifiers() ? this.attributeModifiers.hashCode() : 0);
++        // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
++        hash = 61 * hash + (hasPlaceableKeys() ? this.placeableKeys.hashCode() : 0);
++        hash = 61 * hash + (hasDestroyableKeys() ? this.destroyableKeys.hashCode() : 0);
++        // Paper end
+         return hash;
+     }
+ 
+@@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable {
+             clone.hideFlag = this.hideFlag;
+             clone.unbreakable = this.unbreakable;
+             clone.damage = this.damage;
++            // 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);
+@@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable {
+             builder.put(DAMAGE.BUKKIT, damage);
+         }
+ 
++        // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
++        if (hasPlaceableKeys()) {
++            List<String> cerealPlaceable = this.placeableKeys.stream()
++                .map(this::serializeNamespaced)
++                .collect(java.util.stream.Collectors.toList());
++
++            builder.put(CAN_PLACE_ON.BUKKIT, cerealPlaceable);
++        }
++
++        if (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, NBTBase> internalTags = new HashMap<String, NBTBase>(unhandledTags);
+         serializeInternal(internalTags);
+         if (!internalTags.isEmpty()) {
+@@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable {
                          CraftMetaArmorStand.NO_BASE_PLATE.NBT,
                          CraftMetaArmorStand.SHOW_ARMS.NBT,
                          CraftMetaArmorStand.SMALL.NBT,
@@ -118,33 +341,145 @@ index 7ac07ac07ac0..7ac07ac07ac0 100644
      // Spigot end
 +    // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values
 +    @Override
++    @SuppressWarnings("deprecation")
 +    public Set<Material> getCanDestroy() {
-+        return new java.util.HashSet<>(canDestroy);
++        return this.destroyableKeys == null ? Collections.emptySet() : legacyGetMatsFromKeys(this.destroyableKeys);
 +    }
 +
 +    @Override
 +    @SuppressWarnings("deprecation")
 +    public void setCanDestroy(Set<Material> canDestroy) {
-+        if (canDestroy.stream().anyMatch(Material::isLegacy)) {
-+            throw new IllegalArgumentException("canDestroy set must not contain any legacy materials!");
-+        }
-+        this.canDestroy.clear();
-+        this.canDestroy.addAll(canDestroy);
++        Validate.notNull(canDestroy, "Cannot replace with null set!");
++        legacyClearAndReplaceKeys(this.destroyableKeys, canDestroy);
 +    }
 +
 +    @Override
++    @SuppressWarnings("deprecation")
 +    public Set<Material> getCanPlaceOn() {
-+        return new java.util.HashSet<>(canPlaceOn);
++        return this.placeableKeys == null ? Collections.emptySet() : legacyGetMatsFromKeys(this.placeableKeys);
 +    }
 +
 +    @Override
 +    @SuppressWarnings("deprecation")
 +    public void setCanPlaceOn(Set<Material> canPlaceOn) {
-+        if (canPlaceOn.stream().anyMatch(Material::isLegacy)) {
-+            throw new IllegalArgumentException("canPlaceOn set must not contain any legacy materials!");
++        Validate.notNull(canPlaceOn, "Cannot replace with null set!");
++        legacyClearAndReplaceKeys(this.placeableKeys, canPlaceOn);
++    }
++
++    @Override
++    public Set<Namespaced> getDestroyableKeys() {
++        return this.destroyableKeys == null ? 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 this.placeableKeys == null ? 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!");
 +        }
-+        this.canPlaceOn.clear();
-+        this.canPlaceOn.addAll(canPlaceOn);
++
++        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.codePointAt(0) == '#';
++        net.minecraft.server.ArgumentBlock blockParser = new net.minecraft.server.ArgumentBlock(new com.mojang.brigadier.StringReader(raw), true);
++        try {
++            blockParser = blockParser.parse(false);
++        } catch (com.mojang.brigadier.exceptions.CommandSyntaxException e) {
++            e.printStackTrace();
++            return null;
++        }
++
++        net.minecraft.server.MinecraftKey key;
++        if (isTag) {
++            key = blockParser.getTagKey();
++        } else {
++            key = blockParser.getBlockKey();
++        }
++
++        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.b(), key.getKey());
++            } 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) {
++        boolean valid = true;
++        for (Namespaced resource : namespacedResources) {
++            if (valid && !(resource instanceof org.bukkit.NamespacedKey || resource instanceof com.destroystokyo.paper.NamespacedTag)) {
++                valid = false;
++            }
++        }
++
++        return valid;
 +    }
 +    // Paper end
  }