diff --git a/src/main/java/ch/njol/skript/conditions/CondHasAdventureRestrictions.java b/src/main/java/ch/njol/skript/conditions/CondHasAdventureRestrictions.java
new file mode 100644
index 00000000000..a330c28a8f2
--- /dev/null
+++ b/src/main/java/ch/njol/skript/conditions/CondHasAdventureRestrictions.java
@@ -0,0 +1,150 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.conditions;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.aliases.ItemData;
+import ch.njol.skript.aliases.ItemType;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Condition;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Set;
+
+@Name("Has Adventure Restrictions")
+@Description("Check if an item has any adventure restrictions.")
+@Examples({
+ "player's tool has any build restrictions",
+ "{_item} doesn't have a break restriction",
+ "{_item} is able to break (stone and dirt) in adventure mode",
+ "{_item} can not be placed on diamond ore in adventure"
+})
+@Since("INSERT VERSION")
+@RequiredPlugins("Paper")
+public class CondHasAdventureRestrictions extends Condition {
+
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ private static Method DESTROY_HAS, PLACE_HAS, DESTROY_GET, PLACE_GET;
+
+ static {
+
+ if (Skript.methodExists(ItemMeta.class, "hasDestroyableKeys")) {
+ Skript.registerCondition(CondHasAdventureRestrictions.class,
+ "%itemtypes% (has|have) [a|any|:no] (break|place:(build|place)) restriction[s]",
+ "%itemtypes% (doesn't|does not|do not|don't) have [a|any] (break|place:(build|place)) restriction[s]",
+ "%itemtypes% (can|is able to) (break|destroy|mine|place:be placed on) %itemtypes% in adventure [mode]",
+ "%itemtypes% (can't|can[ ]not|is unable to) (break|destroy|mine|place:be placed on) %itemtypes% in adventure [mode]"
+ );
+
+ try {
+ Class> META_CLASS = Class.forName("org.bukkit.inventory.meta.ItemMeta");
+
+ DESTROY_HAS = META_CLASS.getDeclaredMethod("hasDestroyableKeys");
+ PLACE_HAS = META_CLASS.getDeclaredMethod("hasPlaceableKeys");
+ DESTROY_GET = META_CLASS.getDeclaredMethod("getDestroyableKeys");
+ PLACE_GET = META_CLASS.getDeclaredMethod("getPlaceableKeys");
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ assert false: e.getMessage();
+ }
+ }
+
+ }
+
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ private Expression items;
+ @Nullable
+ private Expression keysToCheck;
+ private boolean place;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ this.items = (Expression) exprs[0];
+ if (matchedPattern == 2 || matchedPattern == 3)
+ this.keysToCheck = (Expression) exprs[1];
+ this.place = parseResult.hasTag("place");
+ setNegated(matchedPattern == 1 || matchedPattern == 3 || parseResult.hasTag("no"));
+ return true;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean check(Event event) {
+ if (keysToCheck == null) {
+ return items.check(event, item -> {
+ try {
+ return place ? ((boolean) PLACE_HAS.invoke(item.getItemMeta())) : ((boolean) DESTROY_HAS.invoke(item.getItemMeta()));
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ return false;
+ }
+ }, isNegated());
+ }
+
+ return items.check(event, (itemType) -> {
+ Set itemKeys = Collections.EMPTY_SET;
+ try {
+ itemKeys = (Set) (place ? PLACE_GET.invoke(itemType.getItemMeta()) : DESTROY_GET.invoke(itemType.getItemMeta()));
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ assert false: e.getMessage();
+ }
+
+ // short circuit if no keys
+ if (itemKeys.isEmpty())
+ return false;
+
+ // for each key itemtype, if it isAll, we need to ensure every Material is represented in the item's keys
+ // if it isn't isAll, we need to ensure at least one of the Materials is represented in the item's keys.
+ Set finalItemKeys = itemKeys;
+ return keysToCheck.check(event, (keyItemType) -> {
+ boolean isAll = keyItemType.isAll();
+ for (ItemData keyData : keyItemType.getTypes()) {
+ if (!finalItemKeys.contains(keyData.getType().getKey())) {
+ // if we're isAll, we can exit out early. Not all keys matched.
+ if (isAll)
+ return false;
+ } else if (!isAll) {
+ // if we're not isAll, we can exit out early, since one matched.
+ return true;
+ }
+ }
+ return isAll;
+ });
+ }, isNegated());
+ }
+
+ @SuppressWarnings("ConstantConditions")
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ if (keysToCheck == null)
+ return items.toString(event, debug) + " has" + (isNegated() ? " no" : "") + (place ? " placeable" : " destroyable") + " keys";
+ return "the" + (place ? " placeable" : " destroyable") + " keys of" + items.toString(event, debug) + (isNegated() ? " do not" : "") + " contain" + keysToCheck.toString(event, debug);
+ }
+}
diff --git a/src/main/java/ch/njol/skript/effects/EffAdventureRestrictions.java b/src/main/java/ch/njol/skript/effects/EffAdventureRestrictions.java
new file mode 100644
index 00000000000..57e106b69a6
--- /dev/null
+++ b/src/main/java/ch/njol/skript/effects/EffAdventureRestrictions.java
@@ -0,0 +1,135 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.effects;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.aliases.ItemType;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Effect;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+@Name("Apply Adventure Restrictions")
+@Description("Allow or prevent an item to destroy or be placed on certain types of blocks while in /gamemode adventure.")
+@Examples({
+ "allow player's tool to destroy (stone, oak wood planks) in adventure mode",
+ "prevent {_item} from being placed on (diamond ore, diamond block) in adventure"
+})
+@Since("INSERT VERSION")
+@RequiredPlugins("Paper")
+public class EffAdventureRestrictions extends Effect {
+
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ private static Method DESTROY_SET, PLACE_SET, DESTROY_GET, PLACE_GET;
+
+ static {
+ if (Skript.methodExists(ItemMeta.class, "setDestroyableKeys", Collection.class)) {
+ Skript.registerEffect(EffAdventureRestrictions.class,
+ "allow %~itemtypes% to (destroy|break|mine|place:be placed on) %itemtypes% in adventure [mode]",
+ "(disallow|prevent) %~itemtypes% from (destroying|breaking|mining|place:being placed on) %itemtypes% in adventure [mode]");
+
+ try {
+ Class> META_CLASS = Class.forName("org.bukkit.inventory.meta.ItemMeta");
+
+ DESTROY_GET = META_CLASS.getDeclaredMethod("getDestroyableKeys");
+ PLACE_GET = META_CLASS.getDeclaredMethod("getPlaceableKeys");
+ DESTROY_SET = META_CLASS.getDeclaredMethod("setDestroyableKeys", Collection.class);
+ PLACE_SET = META_CLASS.getDeclaredMethod("setPlaceableKeys", Collection.class);
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ assert false: e.getMessage();
+ }
+ }
+ }
+
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ private Expression items;
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ private Expression deltaKeys;
+ private boolean allow;
+ private boolean destroy;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ this.items = (Expression) exprs[0];
+ this.deltaKeys = (Expression) exprs[1];
+ this.allow = matchedPattern == 0;
+ this.destroy = !parseResult.hasTag("place");
+ return true;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void execute(Event event) {
+ ItemType[] items = this.items.getArray(event);
+ if (items.length == 0)
+ return;
+
+ Set keys = new HashSet<>();
+
+ for (ItemType itemType : deltaKeys.getArray(event)) {
+ Iterator iter = itemType.containerIterator();
+ while (iter.hasNext()) {
+ ItemStack stack = iter.next();
+ keys.add(stack.getType().getKey());
+ }
+ }
+
+ if (!keys.isEmpty()) {
+ for (ItemType item : items) {
+ try {
+ ItemMeta meta = item.getItemMeta();
+ Set existingKeys = new HashSet<>((Set) (destroy ? DESTROY_GET.invoke(meta) : PLACE_GET.invoke(meta)));
+ if (allow) {
+ existingKeys.addAll(keys);
+ } else {
+ existingKeys.removeAll(keys);
+ }
+ if (destroy) { DESTROY_SET.invoke(meta, existingKeys); } else { PLACE_SET.invoke(meta, existingKeys); }
+ item.setItemMeta(meta);
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ assert false: e.getMessage();
+ }
+ }
+ }
+ }
+
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ if (allow)
+ return "allow " + items.toString(event, debug) + " to " + (destroy ? "destroy " : "be placed on ") + deltaKeys.toString(event, debug);
+ return "prevent " + items.toString(event, debug) + " from " + (destroy ? "destroying " : "being placed on ") + deltaKeys.toString(event, debug);
+ }
+}
diff --git a/src/main/java/ch/njol/skript/expressions/ExprAdventureRestrictions.java b/src/main/java/ch/njol/skript/expressions/ExprAdventureRestrictions.java
new file mode 100644
index 00000000000..7bd14d3435e
--- /dev/null
+++ b/src/main/java/ch/njol/skript/expressions/ExprAdventureRestrictions.java
@@ -0,0 +1,204 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.expressions;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.aliases.ItemType;
+import ch.njol.skript.bukkitutil.BukkitUnsafe;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.PropertyExpression;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.ExpressionType;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.Material;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+@Name("Adventure Restrictions Of Item")
+@Description("Get or modify the adventure restrictions of an item.")
+@Examples({
+ "add dirt to destroyable restrictions of player's tool",
+ "add (stone, diamond ore) to breakable blocks of {_item}",
+ "clear break restrictions of {_item}",
+ "remove sand from destroyable blocks of {_item} in adventure mode"
+})
+@Since("INSERT VERSION")
+@RequiredPlugins("Paper")
+public class ExprAdventureRestrictions extends PropertyExpression {
+
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ private static Method DESTROY_HAS, PLACE_HAS, DESTROY_GET, PLACE_GET, DESTROY_SET, PLACE_SET;
+
+ static {
+ if (Skript.methodExists(ItemMeta.class, "getDestroyableKeys")) {
+ Skript.registerExpression(ExprAdventureRestrictions.class, ItemType.class, ExpressionType.PROPERTY,
+ "[the] (:break|:destroy|:place|:build)able blocks of %itemtypes% [in adventure [mode]]",
+ "[the] (:break|:destroy|:place|:build)[able] restrictions of %itemtypes%",
+ "%itemtypes%'[s] (:break|:destroy|:place|:build)able blocks [in adventure [mode]]",
+ "%itemtypes%'[s] (:break|:destroy|:place|:build)[able] restrictions");
+
+ try {
+ Class> META_CLASS = Class.forName("org.bukkit.inventory.meta.ItemMeta");
+
+ DESTROY_HAS = META_CLASS.getDeclaredMethod("hasDestroyableKeys");
+ PLACE_HAS = META_CLASS.getDeclaredMethod("hasPlaceableKeys");
+ DESTROY_GET = META_CLASS.getDeclaredMethod("getDestroyableKeys");
+ PLACE_GET = META_CLASS.getDeclaredMethod("getPlaceableKeys");
+ DESTROY_SET = META_CLASS.getDeclaredMethod("setDestroyableKeys", Collection.class);
+ PLACE_SET = META_CLASS.getDeclaredMethod("setPlaceableKeys", Collection.class);
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ assert false: e.getMessage();
+ }
+ }
+ }
+
+ private boolean destroy;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ setExpr((Expression) exprs[0]);
+ this.destroy = parseResult.hasTag("break") || parseResult.hasTag("destroy");
+ return true;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected ItemType @NotNull [] get(Event event, ItemType [] source) {
+ Set existingKeys = new HashSet<>();
+ for (ItemType item : source) {
+ ItemMeta meta = item.getItemMeta();
+ try {
+ if (destroy ? (boolean) DESTROY_HAS.invoke(meta) : (boolean) PLACE_HAS.invoke(meta)) {
+ for (Object key : (Set) (destroy ? DESTROY_GET.invoke(meta) : PLACE_GET.invoke(meta))) {
+ Material material = BukkitUnsafe.getMaterialFromMinecraftId(key.toString());
+ if (material != null)
+ existingKeys.add(new ItemType(material));
+ }
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ assert false: e.getMessage();
+ }
+ }
+ return existingKeys.toArray(new ItemType[0]);
+ }
+
+ @SuppressWarnings("NullableProblems")
+ @Override
+ public Class> @Nullable [] acceptChange(ChangeMode mode) {
+ if (mode == ChangeMode.SET || mode == ChangeMode.ADD || mode == ChangeMode.REMOVE) {
+ return CollectionUtils.array(ItemType[].class);
+ } else if (mode == ChangeMode.RESET || mode == ChangeMode.DELETE) {
+ return CollectionUtils.array();
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) {
+ ItemType[] source = getExpr().getArray(event);
+
+ Set deltaKeys = new HashSet<>();
+ if (mode == ChangeMode.SET || mode == ChangeMode.ADD || mode == ChangeMode.REMOVE) {
+ for (Object o : delta) {
+ if (o instanceof ItemType) {
+ for (ItemStack stack : ((ItemType) o).getAll())
+ deltaKeys.add(stack.getType().getKey());
+ }
+ }
+ }
+
+ try {
+ for (ItemType item : source) {
+
+ ItemMeta meta = item.getItemMeta();
+ Collection newKeys = new ArrayList<>();
+
+ switch (mode) {
+ case RESET:
+ case DELETE:
+ break;
+ case SET:
+ newKeys = deltaKeys;
+ break;
+ case ADD:
+ case REMOVE:
+ newKeys = new HashSet<>((Set) (destroy ? DESTROY_GET.invoke(meta) : PLACE_GET.invoke(meta)));
+ if (mode == ChangeMode.ADD) {
+ newKeys.addAll(deltaKeys);
+ } else {
+ newKeys.removeAll(deltaKeys);
+ }
+ break;
+ case REMOVE_ALL:
+ assert false;
+ }
+
+ if (destroy) {
+ DESTROY_SET.invoke(meta, newKeys);
+ } else {
+ PLACE_SET.invoke(meta, newKeys);
+ }
+
+ item.setItemMeta(meta);
+ }
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ assert false: e.getMessage();
+ }
+
+ }
+
+ @Override
+ public boolean isSingle() {
+ return false;
+ }
+
+ @Override
+ public @NotNull Class extends ItemType> getReturnType() {
+ return ItemType.class;
+ }
+
+ @Override
+ public @NotNull String toString(@Nullable Event event, boolean debug) {
+ if (destroy) {
+ return "destroyable restrictions of " + getExpr().toString(event, debug);
+ } else {
+ return "placeable restrictions of " + getExpr().toString(event, debug);
+ }
+ }
+
+}
diff --git a/src/test/skript/tests/syntaxes/conditions/CondHasAdventureRestrictions.sk b/src/test/skript/tests/syntaxes/conditions/CondHasAdventureRestrictions.sk
new file mode 100644
index 00000000000..064e56f65b8
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/conditions/CondHasAdventureRestrictions.sk
@@ -0,0 +1,81 @@
+test "has adventure restrictions" when running below minecraft "1.20.5":
+
+ set {_tool} to stone shovel
+ assert {_tool} has no break restrictions with "plain shovel has unexpected break restrictions"
+ assert {_tool} has no place restrictions with "plain shovel has unexpected place restrictions"
+
+ # no restrictions: should be able to break nothing
+ assert {_tool} cannot break dirt in adventure mode with "plain shovel can unexpectedly break dirt in adventure"
+ assert {_tool} cannot break any log in adventure mode with "plain shovel can unexpectedly break logs in adventure"
+
+ # add basic restrictions
+ allow {_tool} to break dirt in adventure mode
+ assert {_tool} can break dirt in adventure mode with "shovel should be able to break dirt in adventure"
+ add oak log to break restrictions of {_tool}
+ assert {_tool} can break oak log in adventure mode with "shovel should be able to break oak logs in adventure"
+ assert {_tool} can break any logs in adventure mode with "shovel should be able to break any log in adventure"
+ assert {_tool} cannot break all logs in adventure mode with "shovel should not be able to break all logs in adventure"
+
+ # removal
+ prevent {_tool} from breaking dirt in adventure mode
+ assert {_tool} cannot break dirt in adventure mode with "shovel should not be able to break dirt in adventure"
+
+ remove all logs from break restrictions of {_tool}
+ assert {_tool} cannot break oak log in adventure mode with "shovel should not be able to break oak log in adventure"
+
+ # any log
+ allow {_tool} to break any log in adventure mode
+ assert {_tool} can break oak log in adventure mode with "shovel should be able to break oak logs in adventure"
+ assert {_tool} can break spruce log in adventure mode with "shovel should be able to break spruce logs in adventure"
+ assert {_tool} can break birch log in adventure mode with "shovel should be able to break birch logs in adventure"
+ assert {_tool} can break any log in adventure mode with "shovel should be able to break any log in adventure"
+ assert {_tool} can break all logs in adventure mode with "shovel should be able to break all logs in adventure"
+
+ # multiple inputs
+ set {_a} to stone shovel
+ set {_b} to iron pickaxe
+ set {_c} to dirt block
+
+ assert {_a}, {_b}, and {_c} cannot break dirt in adventure mode with "AND tools can unexpectedly break dirt in adventure"
+ assert {_a}, {_b}, or {_c} cannot break dirt in adventure mode with "OR tools can unexpectedly break dirt in adventure"
+
+ allow {_a} and {_b} to break dirt in adventure mode
+ assert {_a}, {_b}, and {_c} can break dirt in adventure mode to fail with "AND tools can unexpectedly break dirt in adventure 2"
+ assert {_a}, {_b}, or {_c} can break dirt in adventure mode with "OR tools should be able to break dirt in adventure"
+
+ allow {_c} to break dirt in adventure mode
+ assert {_a}, {_b}, and {_c} can break dirt in adventure mode with "AND tools should be able to break dirt in adventure"
+ assert {_a}, {_b}, or {_c} can break dirt in adventure mode with "OR tools should be able to break dirt in adventure 2"
+
+
+ # multiple inputs and any keys
+ set {_a} to stone shovel
+ set {_b} to iron pickaxe
+ set {_c} to dirt block
+
+ allow {_a} to break oak logs in adventure mode
+ allow {_b} to break spruce logs in adventure mode
+ allow {_c} to break birch logs in adventure mode
+ assert {_a}, {_b}, and {_c} can break any logs in adventure mode with "AND tools should be able to break any logs in adventure"
+ assert {_a}, {_b}, or {_c} can break any logs in adventure mode with "OR tools should be able to break any logs in adventure"
+ assert {_a}, {_b}, and {_c} cannot break all logs in adventure mode with "AND tools should not be able to break all logs in adventure"
+ assert {_a}, {_b}, or {_c} cannot break all logs in adventure mode with "OR tools should not be able to break all logs in adventure"
+
+ # multiple inputs multiple keys
+ set {_a} to stone shovel
+ set {_b} to iron pickaxe
+ set {_c} to dirt block
+
+ allow {_a} to break oak logs in adventure mode
+ allow {_b} to break spruce logs in adventure mode
+ allow {_c} to break birch logs in adventure mode
+ assert ({_a}, {_b}, and {_c}) can break (oak logs, spruce logs, and birch logs) in adventure mode to fail with "AND tools, AND keys should fail"
+ assert ({_a}, {_b}, and {_c}) can break (oak logs, spruce logs, or birch logs) in adventure mode with "AND tools, OR keys should pass"
+ assert ({_a}, {_b}, or {_c}) can break (oak logs, spruce logs, and birch logs) in adventure mode to fail with "OR tools, AND keys should fail"
+ assert ({_a}, {_b}, or {_c}) can break (oak logs, spruce logs, or birch logs) in adventure mode with "OR tools, OR keys should pass"
+
+
+ assert ({_a}, {_b}, and {_c}) cannot break (oak logs, spruce logs, and birch logs) in adventure mode with "negated AND tools, AND keys should pass"
+ assert ({_a}, {_b}, and {_c}) cannot break (oak logs, spruce logs, or birch logs) in adventure mode to fail with "negated AND tools, OR keys should fail"
+ assert ({_a}, {_b}, or {_c}) cannot break (oak logs, spruce logs, and birch logs) in adventure mode with "negated OR tools, AND keys should pass"
+ assert ({_a}, {_b}, or {_c}) cannot break (oak logs, spruce logs, or birch logs) in adventure mode to fail with "negated OR tools, OR keys should fail"