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 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"