diff --git a/.gitignore b/.gitignore
index f0380db4..e9f18c1c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,8 @@
.classpath
.project
.settings/**
+
+/.idea/
+/*.iml
+/*.ipr
+/*.iws
diff --git a/pom.xml b/pom.xml
index e7647401..b53c2d66 100644
--- a/pom.xml
+++ b/pom.xml
@@ -72,24 +72,6 @@
-
- org.apache.maven.plugins
- maven-checkstyle-plugin
- 2.17
-
- https://build.devotedmc.com/job/Style-guide-master/lastSuccessfulBuild/artifact/src/main/resources/devoted_checks.xml
- true
-
-
-
- checkstyle
-
- checkstyle
-
- prepare-package
-
-
-
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java b/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java
index 220a9383..1dd06c13 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/CivModCorePlugin.java
@@ -1,5 +1,8 @@
package vg.civcraft.mc.civmodcore;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.TestItemSolvingCommand;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.TestMatchingCommand;
+
/**
* The sole purpose of this class is to make Spigot recognize this library as a plugin and automatically load the
* classes onto the classpath for us.
@@ -15,6 +18,9 @@ public void onEnable() {
super.onEnable();
// needed for some of the apis
instance = this;
+
+ getCommand("testitemsolving").setExecutor(new TestItemSolvingCommand());
+ getCommand("testitemmatching").setExecutor(new TestMatchingCommand());
}
public static CivModCorePlugin getInstance() {
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/ItemMap.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/ItemMap.java
index 310d55d2..56993405 100644
--- a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/ItemMap.java
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/ItemMap.java
@@ -1,13 +1,11 @@
package vg.civcraft.mc.civmodcore.itemHandling;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.Map.Entry;
-import java.util.Set;
+import java.util.function.Predicate;
import java.util.logging.Logger;
+
+import com.google.common.collect.Lists;
import net.minecraft.server.v1_13_R2.NBTTagCompound;
import net.minecraft.server.v1_13_R2.NBTTagList;
import org.bukkit.Bukkit;
@@ -18,6 +16,7 @@
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemExpression;
/**
* Allows the storage and comparison of itemstacks while ignoring their maximum possible stack sizes. This offers
@@ -562,6 +561,48 @@ public boolean removeSafelyFrom(Inventory i) {
return true;
}
+ /**
+ * Matches a number of ItemExpression on this ItemMap, trying to match each ItemExpression to a ItemStack 1:1.
+ *
+ * This works similar to, but not exactly like ItemMap.getEntries().steam().allMatch().
+ *
+ * For example, if this is an ItemMap of a diamond sword and a stack of dirt, passing this function a
+ * ItemExpression that matches any sword will return true, but passing it a collection of
+ * ItemExpressions [Matches diamond sword, Matches any sword] will return false, because it can't match each
+ * ItemExpression to an ItemStack.
+ * @param itemExpressions The collection of ItemExpression that will match over this ItemMap.
+ * @return true if all if all of the ItemExpressions matched an item in this ItemMap in a 1:1 fashion, otherwise false.
+ */
+ public boolean itemExpressionsMatchItems(Collection itemExpressions) {
+ // clone so we can remove elements as needed.
+ ArrayList itemExpressions1 = new ArrayList<>(itemExpressions);
+ ItemMap clone = clone();
+
+ // The list is reversed so we can remove the current ItemExpression without shifting over the list.
+ for (ItemExpression e : Lists.reverse(itemExpressions1)) {
+ Predicate> iePredicate = e.getMatchesItemMapPredicate(clone);
+ boolean matched = false;
+
+ for (Entry entry : clone.getEntrySet()) {
+ if (iePredicate.test(entry)) {
+ // Make sure that each ItemExpression can match one ItemStack,
+ // and that each ItemStack can only be matched by one ItemExpression
+ clone.removeItemStack(entry.getKey());
+ itemExpressions1.remove(e);
+ matched = true;
+ break;
+ }
+ }
+
+ if (!matched) {
+ // slight optimization to return early instead of needing to check every single ItemExpression
+ return false;
+ }
+ }
+
+ return itemExpressions1.isEmpty();
+ }
+
@Override
public boolean equals(Object o) {
if (o instanceof ItemMap) {
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/ItemExpression.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/ItemExpression.java
new file mode 100644
index 00000000..72a97335
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/ItemExpression.java
@@ -0,0 +1,1151 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression;
+
+import org.bukkit.*;
+import org.bukkit.attribute.Attribute;
+import org.bukkit.attribute.AttributeModifier;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.MemoryConfiguration;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.entity.EntityType;
+import org.bukkit.entity.TropicalFish;
+import org.bukkit.inventory.*;
+import org.bukkit.inventory.meta.BookMeta;
+import org.bukkit.map.MapView;
+import org.bukkit.potion.PotionEffectType;
+import org.bukkit.potion.PotionType;
+import vg.civcraft.mc.civmodcore.itemHandling.ItemMap;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.*;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.book.*;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.color.ColorMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.color.ExactlyColor;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.color.ListColor;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enchantment.*;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher.*;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.firework.*;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.lore.ExactlyLore;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.lore.ItemLoreMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.lore.LoreMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.lore.RegexLore;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map.*;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc.*;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.mobspawner.*;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name.*;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.potion.*;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.tropicalbucket.ItemTropicFishBBodyColorMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.tropicalbucket.ItemTropicFishBPatternColorMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.tropicalbucket.ItemTropicFishBPatternMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.uuid.*;
+
+import java.util.*;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import static vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enchantment.EnchantmentsSource.HELD;
+import static vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enchantment.EnchantmentsSource.ITEM;
+import static vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc.ListMatchingMode.*;
+
+/**
+ * A unified syntax for matching any ItemStack for things like the material, amount, lore contents, and more.
+ *
+ * While mostly designed to be used in a .yaml config file, this can also be used from java.
+ *
+ * @author Ameliorate
+ */
+public class ItemExpression implements Matcher {
+ /**
+ * Creates the default ItemExpression.
+ *
+ * By default, it will match any ItemStack.
+ */
+ public ItemExpression() {}
+
+ /**
+ * Creates an ItemExpression from a section of bukkit configuration format.
+ * @param configurationSection The subsection of config that should be parsed.
+ */
+ public ItemExpression(ConfigurationSection configurationSection) {
+ parseConfig(configurationSection);
+ }
+
+ /**
+ * Creates an ItemExpression that matches exactly the passed ItemStack, and no other item.
+ *
+ * This constructor uses ItemStack.equals() directly, so this supports all aspects of an item, even those that are
+ * not supported by ItemExpression.
+ *
+ * @param item The ItemStack that this ItemExpression would exactly match.
+ */
+ public ItemExpression(ItemStack item) {
+ this(item, false);
+ }
+
+ /**
+ * Creates an ItemExpression that matches exactly the passed ItemStack, or acts equilivent to ItemStack.isSimilar().
+ *
+ * See also ItemExpression(ItemStack).
+ * @param item The item that this ItemExpression would match.
+ * @param acceptSimilar If this ItemExpression should use ItemStack.isSimilar() instead of .equals().
+ */
+ public ItemExpression(ItemStack item, boolean acceptSimilar) {
+ addMatcher(new ItemExactlyStackMatcher(item, acceptSimilar));
+ }
+
+ /**
+ * Mutate this ItemExpression, overriding the existing options set for this with the options given in the
+ * ConfigurationSection.
+ * @param config The config that options will be taken from.
+ */
+ public void parseConfig(ConfigurationSection config) {
+ // material
+ addMatcher(ItemMaterialMatcher.construct(parseEnumMatcher(config, "material", Material.class)));
+
+ // amount
+ addMatcher(ItemAmountMatcher.construct(parseAmount(config, "amount")));
+
+ // damage
+ addMatcher(ItemDamageMatcher.construct(parseAmount(config, "damage")));
+
+ // lore
+ addMatcher(ItemLoreMatcher.construct(parseLore(config, "lore")));
+
+ // display name
+ addMatcher(ItemNameMatcher.construct(parseName(config, "name")));
+
+ // enchantments (example: eff5 diamond pickaxe)
+ addMatcher(parseEnchantment(config, "enchantments.any", ANY, ITEM));
+ addMatcher(parseEnchantment(config, "enchantments.all", ALL, ITEM));
+ addMatcher(parseEnchantment(config, "enchantments.none", NONE, ITEM));
+ addMatcher(parseEnchangmentCount(config, "enchantments.count", ITEM));
+
+ // held enchantments (example: enchanted book)
+ addMatcher(parseEnchantment(config, "enchantments.held.any", ANY, HELD));
+ addMatcher(parseEnchantment(config, "enchantments.held.all", ALL, HELD));
+ addMatcher(parseEnchantment(config, "enchantments.held.none", NONE, HELD));
+ addMatcher(parseEnchangmentCount(config, "enchantments.held.count", HELD));
+
+ // skull
+ addMatcher(ItemSkullMatcher.construct(parseSkull(config, "skull")));
+
+ // item flags (https://hub.spigotmc.org/javadocs/spigot/org/bukkit/inventory/ItemFlag.html)
+ addMatcher(parseFlags(config, "flags"));
+
+ // unbreakable
+ addMatcher(parseUnbreakable(config, "unbreakable"));
+
+ // held inventory (example: shulker box)
+ addMatcher(parseInventory(config, "inventory"));
+
+ // shulker box color
+ addMatcher(ItemShulkerBoxColorMatcher.construct(parseEnumMatcher(config, "shulkerbox.color", DyeColor.class)));
+
+ // written book
+ addMatcher(parseBook(config, "book"));
+
+ // exact item stack
+ addMatcher(parseExactly(config, "exactly"));
+
+ // knowlege book (creative item that holds recipe unlocks)
+ addMatcher(ItemKnowledgeBookMatcher.construct(parseName(config, "knowlegebook.recipes.any"), false));
+ addMatcher(ItemKnowledgeBookMatcher.construct(parseName(config, "knowlegebook.recipes.all"), true));
+
+ // potion
+ addMatcher(parsePotion(config, "potion"));
+
+ // attributes
+ addMatcher(parseAllAttributes(config, "attributes"));
+
+ // tropical fish bucket (added in 1.13)
+ addMatcher(parseTropicFishBucket(config, "tropicalFishBucket"));
+
+ // leather armor color
+ addMatcher(ItemLeatherArmorColorMatcher.construct(parseColor(config, "leatherArmorColor")));
+
+ // map
+ addMatcher(parseMap(config, "map"));
+
+ // mob spawner
+ addMatcher(parseMobSpawner(config, "spawner"));
+
+ // firework holder (example: firework star)
+ addMatcher(ItemFireworkEffectHolderMatcher.construct(parseFireworkEffect(config, "fireworkEffectHolder")));
+
+ // firework
+ addMatcher(parseFirework(config, "firework"));
+ }
+
+ /**
+ * Gets a ItemExpression from the given path in the config
+ * @param configurationSection The config to get the ItemExpression from
+ * @param path The path to the ItemExpression
+ * @return The ItemExpression in the config that path points to, or empty if there was not an ItemExpression at path.
+ */
+ public static Optional getItemExpression(ConfigurationSection configurationSection, String path) {
+ if (configurationSection == null)
+ return Optional.empty();
+ if (!configurationSection.contains(path))
+ return Optional.empty();
+ return Optional.of(new ItemExpression(configurationSection.getConfigurationSection(path)));
+ }
+
+ /**
+ * Parses out a list of ItemExpressions from a config.
+ * @param config The config to parse
+ * @param path The path to the list of ItemExpressions
+ * @return A list of ItemExpressions parsed from the config.
+ */
+ public static List getItemExpressionList(ConfigurationSection config, String path) {
+ if (!config.contains(path))
+ return Collections.emptyList();
+
+ List itemExpressionsConfig = getConfigList(config, path);
+ List itemExpressions = new ArrayList<>();
+
+ for (ConfigurationSection itemExConfig : itemExpressionsConfig) {
+ itemExpressions.add(new ItemExpression(itemExConfig));
+ }
+
+ return itemExpressions;
+ }
+
+ @SuppressWarnings("unchecked") // fix your warnings, java
+ private static List getConfigList(ConfigurationSection config, String path)
+ {
+ if (!config.isList(path))
+ return Collections.emptyList();
+
+ List list = new ArrayList<>();
+
+ for (Object object : config.getList(path)) {
+ if (object instanceof Map) {
+ MemoryConfiguration mc = new MemoryConfiguration();
+
+ mc.addDefaults((Map) object);
+
+ list.add(mc);
+ }
+ }
+
+ return list;
+ }
+
+ public static Map getItemExpressionMap(ConfigurationSection config, String path) {
+ if (!config.isConfigurationSection(path))
+ return Collections.emptyMap();
+
+ HashMap result = new HashMap<>();
+ ConfigurationSection ieConfig = config.getConfigurationSection(path);
+ for (String section : ieConfig.getKeys(false)) {
+ result.put(section, getItemExpression(ieConfig, section).orElseThrow(AssertionError::new));
+ }
+
+ return result;
+ }
+
+ private Optional parseAmount(ConfigurationSection config, String path) {
+ if (config.contains(path + ".range"))
+ return Optional.of((new RangeAmount(
+ config.getDouble(path + ".range.low", Double.NEGATIVE_INFINITY),
+ config.getDouble(path + ".range.high", Double.POSITIVE_INFINITY),
+ config.getBoolean(path + ".range.inclusiveLow", true),
+ config.getBoolean(path + ".range.inclusiveHigh", true))));
+ else if ("any".equals(config.getString(path)))
+ return Optional.of(new AnyAmount());
+ else if (config.contains(path))
+ return Optional.of(new ExactlyAmount(config.getDouble(path)));
+ return Optional.empty();
+ }
+
+ private Optional parseLore(ConfigurationSection config, String path) {
+ if (config.contains(path + ".regex")) {
+ String patternStr = config.getString(path + ".regex");
+ boolean multiline = config.getBoolean(path + ".regexMultiline", true);
+ Pattern pattern = Pattern.compile(patternStr, multiline ? Pattern.MULTILINE : 0);
+
+ return Optional.of(new RegexLore(pattern));
+ } else if (config.contains(path))
+ return Optional.of(new ExactlyLore(config.getStringList(path)));
+ return Optional.empty();
+ }
+
+ private Optional parseName(ConfigurationSection config, String path, boolean caseSensitive) {
+ if (config.contains(path + ".regex"))
+ return Optional.of(new RegexName(Pattern.compile(config.getString(path + ".regex"),
+ caseSensitive ? 0 : Pattern.CASE_INSENSITIVE)));
+ else if ("vanilla".equals(config.getString(path)))
+ return Optional.of(new VanillaName());
+ else if (config.contains(path))
+ return Optional.of(new ExactlyName(config.getString(path), caseSensitive));
+ return Optional.empty();
+ }
+
+ private Optional parseName(ConfigurationSection config, String path) {
+ return parseName(config, path, true);
+ }
+
+ private Optional parseEnchantment(ConfigurationSection config, String path,
+ ListMatchingMode mode,
+ EnchantmentsSource source) {
+ ConfigurationSection enchantments = config.getConfigurationSection(path);
+ if (enchantments == null)
+ return Optional.empty();
+
+ ArrayList enchantmentMatcher = new ArrayList<>();
+ for (String enchantName : enchantments.getKeys(false)) {
+ EnchantmentMatcher matcher;
+ AmountMatcher amountMatcher = parseAmount(config, path + "." + enchantName)
+ .orElseThrow(AssertionError::new);
+ if (enchantName.equals("any")) {
+ matcher = new AnyEnchantment(amountMatcher);
+ } else {
+ matcher = new ExactlyEnchantment(Enchantment.getByKey(NamespacedKey.minecraft(enchantName)),
+ amountMatcher);
+ }
+
+ enchantmentMatcher.add(matcher);
+ }
+
+ return Optional.of(new ItemEnchantmentsMatcher(enchantmentMatcher, mode, source));
+ }
+
+ private Optional parseEnchangmentCount(ConfigurationSection config, String path,
+ EnchantmentsSource source) {
+ if (!config.contains(path))
+ return Optional.empty();
+
+ return Optional.of(new ItemEnchantmentCountMatcher(parseAmount(config, path)
+ .orElseThrow(ItemExpressionConfigParsingError::new), source));
+ }
+
+ private List parseSkull(ConfigurationSection config, String path) {
+ List matchers = new ArrayList<>();
+ ConfigurationSection skull = config.getConfigurationSection(path);
+ if (skull == null)
+ return Collections.emptyList();
+
+ for (String name : skull.getStringList("names")) {
+ matchers.add(new PlayerNameUUID(name));
+ }
+
+ for (String uuid : skull.getStringList("uuids")) {
+ matchers.add(new ExactlyUUID(UUID.fromString(uuid)));
+ }
+
+ if (skull.contains("name"))
+ matchers.add(new PlayerNameUUID(skull.getString("name")));
+ if (skull.contains("uuid"))
+ matchers.add(new ExactlyUUID(UUID.fromString(skull.getString("name"))));
+
+ if (skull.contains("regex"))
+ matchers.add(new PlayerNameRegexUUID(Pattern.compile(skull.getString("regex"))));
+
+ return matchers;
+ }
+
+ private List parseFlags(ConfigurationSection config, String path) {
+ List matchers = new ArrayList<>();
+
+ ConfigurationSection flags = config.getConfigurationSection(path);
+ if (flags == null)
+ return Collections.emptyList();
+
+ for (String flagKey : flags.getKeys(false)) {
+ ItemFlag flag = ItemFlag.valueOf(flagKey.toUpperCase());
+ boolean setting = flags.getBoolean(flagKey);
+
+ matchers.add(new ItemFlagMatcher(flag, setting));
+ }
+
+ return matchers;
+ }
+
+ private Optional parseUnbreakable(ConfigurationSection config, String path) {
+ if (!config.contains(path))
+ return Optional.empty();
+ boolean unbreakable = config.getBoolean(path);
+ return Optional.of(new ItemUnbreakableMatcher(unbreakable));
+ }
+
+ private Optional parseInventory(ConfigurationSection config, String path) {
+ List itemExpressions = getItemExpressionList(config, path);
+ if (itemExpressions.isEmpty())
+ return Optional.empty();
+
+ return Optional.of(new ItemExactlyInventoryMatcher(itemExpressions));
+ }
+
+ private List parseBook(ConfigurationSection config, String path) {
+ if (!config.contains(path))
+ return Collections.emptyList();
+
+ ConfigurationSection book = config.getConfigurationSection(path);
+ ArrayList matchers = new ArrayList<>();
+
+ // author
+ if (book.contains("author")) {
+ matchers.add(new ItemBookAuthorMatcher(parseName(book, "author")
+ .orElseThrow(ItemExpressionConfigParsingError::new)));
+ }
+
+ // generation
+ matchers.add(new ItemBookGenerationMatcher(
+ parseEnumMatcher(config, "generation", BookMeta.Generation.class)
+ .orElseThrow(ItemExpressionConfigParsingError::new)));
+
+ // title
+ if (book.contains("title")) {
+ matchers.add(new ItemBookTitleMatcher(parseName(book, "title")
+ .orElseThrow(ItemExpressionConfigParsingError::new)));
+ }
+
+ // pages
+ if (book.contains("pages.regex")) {
+ boolean isMultiline = book.getBoolean("pages.regexMultiline", true);
+ Pattern pattern = Pattern.compile(book.getString("pages.regex"),isMultiline ? Pattern.MULTILINE : 0);
+
+ matchers.add(new ItemBookPagesMatcher(new RegexBookPages(pattern)));
+ } else if (book.isList("pages")) {
+ List pages = book.getStringList("pages");
+ matchers.add(new ItemBookPagesMatcher(new ExactlyBookPages(pages)));
+ }
+
+ // page count
+ if (book.contains("pageCount")) {
+ matchers.add(new ItemBookPageCountMatcher(parseAmount(book, "pageCount")
+ .orElseThrow(ItemExpressionConfigParsingError::new)));
+ }
+
+ return matchers;
+ }
+
+ private Optional parseExactly(ConfigurationSection config, String path) {
+ if (!config.contains(path))
+ return Optional.empty();
+
+ boolean acceptBoolean = config.getBoolean(path + ".acceptSimilar");
+
+ return Optional.of(new ItemExactlyStackMatcher(config.getItemStack(path), acceptBoolean));
+ }
+
+ private List parsePotion(ConfigurationSection config, String path) {
+ if (!config.contains(path))
+ return Collections.emptyList();
+
+ ArrayList matchers = new ArrayList<>();
+
+ ConfigurationSection potion = config.getConfigurationSection(path);
+
+ matchers.add(parsePotionEffects(potion, "customEffects.any", ANY).orElse(null));
+ matchers.add(parsePotionEffects(potion, "customEffects.all", ALL).orElse(null));
+ matchers.add(parsePotionEffects(potion, "customEffects.none", NONE).orElse(null));
+
+ if (potion.isConfigurationSection("base")) {
+ ConfigurationSection base = potion.getConfigurationSection("base");
+
+ Boolean isExtended = base.contains("extended") ? base.getBoolean("extended") : null;
+ Boolean isUpgraded = base.contains("upgraded") ? base.getBoolean("upgraded") : null;
+ EnumMatcher type;
+
+ if (base.contains("type")) {
+ type = parseEnumMatcher(base, "type", PotionType.class)
+ .orElseThrow(ItemExpressionConfigParsingError::new);
+ } else {
+ type = new EnumFromListMatcher<>(Arrays.asList(PotionType.values()));
+ }
+
+ matchers.add(new ItemPotionBaseEffectMatcher(type,
+ Optional.ofNullable(isExtended), Optional.ofNullable(isUpgraded)));
+ }
+
+ return matchers;
+ }
+
+ private Optional parsePotionEffects(ConfigurationSection config, String path, ListMatchingMode mode) {
+ if (!config.isList(path))
+ return Optional.empty();
+
+ ArrayList matchers = new ArrayList<>();
+
+ for (ConfigurationSection effect : getConfigList(config, path)) {
+ String type = effect.getString("type");
+ AmountMatcher level = parseAmount(effect, "level").orElse(new AnyAmount());
+ AmountMatcher duration = parseAmount(effect, "durationTicks").orElse(new AnyAmount());
+
+ PotionEffectMatcher matcher = type.equals("any") ?
+ new AnyPotionEffect(level, duration) :
+ new ExactlyPotionEffect(PotionEffectType.getByName(type), level, duration);
+
+ matchers.add(matcher);
+ }
+
+ return Optional.of(new ItemPotionEffectsMatcher(matchers, mode));
+ }
+
+ private List parseAllAttributes(ConfigurationSection config, String path) {
+ ArrayList matchers = new ArrayList<>();
+
+ for (ListMatchingMode mode : ListMatchingMode.values()) {
+ for (EquipmentSlot slot : EquipmentSlot.values()) {
+ String modeString = mode.getLowerCamelCase();
+
+ matchers.add(parseAttributes(config, path + "." + slot + "." + modeString, slot, mode)
+ .orElse(null));
+ }
+ }
+
+ for (ListMatchingMode mode : ListMatchingMode.values()) {
+ matchers.add(parseAttributes(config, path + ".any." + mode.getLowerCamelCase(), null, mode)
+ .orElse(null));
+ }
+
+ return matchers;
+ }
+
+ private Optional parseAttributes(ConfigurationSection config, String path, EquipmentSlot slot,
+ ListMatchingMode mode) {
+ if (!(config.isList(path)))
+ return Optional.empty();
+
+ List attributeMatchers = new ArrayList<>();
+
+ for (ConfigurationSection attribute : getConfigList(config, path)) {
+ EnumMatcher attributeM = parseEnumMatcher(attribute, "attribute", Attribute.class)
+ .orElse(new AnyEnum<>());
+ EnumMatcher operation = parseEnumMatcher(attribute, "operation", AttributeModifier.Operation.class)
+ .orElse(new AnyEnum<>());
+ NameMatcher name = parseName(attribute, "name").orElse(new AnyName());
+ UUIDMatcher uuid = attribute.isString("uuid") ?
+ new ExactlyUUID(UUID.fromString(attribute.getString("uuid"))) : new AnyUUID();
+ AmountMatcher amount = parseAmount(attribute, "amount").orElse(new AnyAmount());
+
+ ItemAttributeMatcher.AttributeMatcher attributeMatcher =
+ new ItemAttributeMatcher.AttributeMatcher(attributeM, name, operation, uuid, amount);
+
+ attributeMatchers.add(attributeMatcher);
+ }
+
+ return Optional.of(new ItemAttributeMatcher(attributeMatchers, slot, mode));
+ }
+
+ private List parseTropicFishBucket(ConfigurationSection config, String path) {
+ if (!config.contains(path))
+ return Collections.emptyList();
+
+ ArrayList matchers = new ArrayList<>();
+
+ ConfigurationSection bucket = config.getConfigurationSection(path);
+
+ matchers.add(ItemTropicFishBBodyColorMatcher.construct(parseEnumMatcher(bucket, "bodyColor", DyeColor.class)));
+ matchers.add(ItemTropicFishBPatternColorMatcher.construct(parseEnumMatcher(bucket, "patternColor", DyeColor.class)));
+ matchers.add(ItemTropicFishBPatternMatcher.construct(parseEnumMatcher(bucket, "pattern", TropicalFish.Pattern.class)));
+
+ return matchers;
+ }
+
+ private > Optional> parseEnumMatcher(ConfigurationSection config, String path,
+ Class enumClass) {
+ if (!config.contains(path))
+ return Optional.empty();
+
+ if (config.isList(path)) {
+ List enumStrings = config.getStringList(path);
+ boolean notInList = false;
+
+ if (enumStrings.get(0).equals("noneOf")) {
+ notInList = true;
+ enumStrings.remove(0);
+ }
+
+ List properties = enumStrings.stream()
+ .map((name) -> E.valueOf(enumClass, name.toUpperCase()))
+ .collect(Collectors.toList());
+
+ return Optional.of(new EnumFromListMatcher<>(properties, notInList));
+ } if (config.isInt(path + ".index")) {
+ return Optional.of(new EnumIndexMatcher<>(config.getInt(path + ".index"), enumClass));
+ } if (config.isString(path)) {
+ E en = Arrays.stream(enumClass.getEnumConstants())
+ .filter((e) -> e.name().equals(config.getString(path)))
+ .findFirst()
+ .orElseThrow(() -> new Error("could not find enum constant " + config.getString(path)));
+ return Optional.of(new ExactlyEnumMatcher<>(en));
+ } else {
+ return parseName(config, path, false)
+ .map(nameMatcher -> new NameEnumMatcher(nameMatcher, enumClass));
+ }
+ }
+
+ private Optional parseColor(ConfigurationSection config, String path) {
+ if (!config.contains(path))
+ return Optional.empty();
+
+ return parseColor(config.get(path));
+ }
+
+ private Optional parseColor(Object config) {
+ if (config == null)
+ return Optional.empty();
+
+ if (config instanceof String) {
+ // vanilla dye name
+ return Optional.of(new ExactlyColor(ExactlyColor.getColorByVanillaName((String) config)));
+
+ } else if (config instanceof Integer) {
+ // rgb color
+ return Optional.of(new ExactlyColor(Color.fromRGB((Integer) config)));
+
+ } else if (config instanceof List) {
+ // any of matchers in list
+ return parseListColor((List>) config).map(lc -> lc); // identity map to fix type inferency errors
+ // By default, java can't cast Option to Option.
+ // However, it can cast ListColor to ColorMatcher, of course. By having an identity map, we give java the
+ // chance to make that type inference.
+
+ } else if (config instanceof Map) {
+ if (((Map) config).containsKey("rgb")) {
+ // rgb int or [r, g, b]
+ Object rgb = ((Map) config).get("rgb");
+
+ if (rgb instanceof Integer) {
+ // rgb int
+ return Optional.of(new ExactlyColor(Color.fromRGB((Integer) rgb)));
+ } else if (rgb instanceof List) {
+ // [r, g, b]
+ int red = (int) ((List) rgb).get(0);
+ int green = (int) ((List) rgb).get(1);
+ int blue = (int) ((List) rgb).get(2);
+ return Optional.of(new ExactlyColor(Color.fromRGB(red, green, blue)));
+ }
+
+ } else if (((Map) config).containsKey("html")) {
+ // html color name
+ String htmlColorName = (String) ((Map) config).get("html");
+ return Optional.of(new ExactlyColor(ExactlyColor.getColorByHTMLName(htmlColorName)));
+
+ } else if (((Map) config).containsKey("firework")) {
+ // firework vanilla dye name
+ String fireworkDyeColorName = (String) ((Map) config).get("firework");
+ return Optional.of(new ExactlyColor(ExactlyColor.getColorByVanillaName(fireworkDyeColorName, true)));
+
+ } else if (((Map) config).containsKey("anyOf")) {
+ // any of matchers in list
+ return parseListColor((List>) ((Map) config).get("anyOf")).map(lc -> lc);
+
+ } else if (((Map) config).containsKey("noneOf")) {
+ // none of matchers in list
+ return parseListColor((List>) ((Map) config).get("noneOf")).map(lc -> lc);
+ }
+ }
+
+ return Optional.empty();
+ }
+
+ private Optional parseListColor(List> config) {
+ if (config == null)
+ return Optional.empty();
+
+ return Optional.of(new ListColor(((List>) config).stream()
+ .map(this::parseColor)
+ .map((option) -> option.orElseThrow(ItemExpressionConfigParsingError::new))
+ .collect(Collectors.toList()), false));
+ }
+
+ private List parseMap(ConfigurationSection config, String path) {
+ if (!config.isConfigurationSection(path))
+ return Collections.emptyList();
+
+ List matchers = new ArrayList<>();
+
+ ConfigurationSection map = config.getConfigurationSection(path);
+
+ if (map.contains("center.x")) {
+ matchers.add(new ItemMapViewMatcher(new CenterMapView(parseAmount(map, "center.x")
+ .orElseThrow(AssertionError::new), CenterMapView.CenterCoordinate.X)));
+ }
+
+ if (map.contains("center.z")) {
+ matchers.add(new ItemMapViewMatcher(new CenterMapView(parseAmount(map, "center.z")
+ .orElseThrow(AssertionError::new), CenterMapView.CenterCoordinate.Z)));
+ }
+
+ if (map.contains("id")) {
+ matchers.add(new ItemMapViewMatcher(new IDMapView(parseAmount(map, "id")
+ .orElseThrow(AssertionError::new))));
+ }
+
+ if (map.isBoolean("isUnlimitedTracking")) {
+ matchers.add(new ItemMapViewMatcher(new IsUnlimitedTrackingMapView(map.getBoolean("isUnlimitedTracking"))));
+ }
+
+ if (map.isBoolean("isVirtual")) {
+ matchers.add(new ItemMapViewMatcher(new IsVirtualMapView(map.getBoolean("isVirtual"))));
+ }
+
+ if (map.contains("scale")) {
+ matchers.add(new ItemMapViewMatcher(new ScaleMapView(
+ parseEnumMatcher(map, "scale", MapView.Scale.class).orElseThrow(AssertionError::new))));
+ }
+
+ if (map.contains("world")) {
+ matchers.add(new ItemMapViewMatcher(new WorldMapView(parseName(map, "world").orElseThrow(AssertionError::new))));
+ }
+
+ if (map.contains("color")) {
+ matchers.add(new ItemMapColorMatcher(parseColor(map, "color")
+ .orElseThrow(ItemExpressionConfigParsingError::new)));
+ }
+
+ if (map.isBoolean("isScaling")) {
+ matchers.add(new ItemMapIsScalingMatcher(map.getBoolean("isScaling")));
+ }
+
+ if (map.contains("location")) {
+ matchers.add(new ItemMapLocationMatcher(parseName(map, "location").orElseThrow(AssertionError::new)));
+ }
+
+ return matchers;
+ }
+
+ private List parseMobSpawner(ConfigurationSection config, String path) {
+ if (!config.isConfigurationSection(path))
+ return Collections.emptyList();
+
+ ArrayList matchers = new ArrayList<>();
+ ConfigurationSection spawner = config.getConfigurationSection(path);
+
+ if (spawner.contains("delay.current")) {
+ // Caution: This is the time until the spawner will spawn its next mob. See minDelay and maxDelay for
+ // what might be expected.
+ matchers.add(new ItemMobSpawnerDelayMatcher(parseAmount(spawner, "delay.current")
+ .orElseThrow(AssertionError::new)));
+ }
+
+ if (spawner.contains("maxNearbyEntities")) {
+ matchers.add(new ItemMobSpawnerMaxNearbyEntitiesMatcher(parseAmount(spawner, "maxNearbyEntities")
+ .orElseThrow(AssertionError::new)));
+ }
+
+ if (spawner.contains("requiredPlayerRange")) {
+ matchers.add(new ItemMobSpawnerRequiredPlayerRangeMatcher(parseAmount(spawner, "requiredPlayerRange")
+ .orElseThrow(AssertionError::new)));
+ }
+
+ if (spawner.contains("spawnCount")) {
+ matchers.add(new ItemMobSpawnerSpawnCountMatcher(parseAmount(spawner, "spawnCount")
+ .orElseThrow(AssertionError::new)));
+ }
+
+ if (spawner.contains("delay.max")) {
+ matchers.add(new ItemMobSpawnerSpawnDelayMatcher(parseAmount(spawner, "delay.max")
+ .orElseThrow(AssertionError::new), ItemMobSpawnerSpawnDelayMatcher.MinMax.MAX));
+ }
+
+ if (spawner.contains("delay.min")) {
+ matchers.add(new ItemMobSpawnerSpawnDelayMatcher(parseAmount(spawner, "delay.min")
+ .orElseThrow(AssertionError::new), ItemMobSpawnerSpawnDelayMatcher.MinMax.MIN));
+ }
+
+ if (spawner.contains("mob")) {
+ matchers.add(new ItemMobSpawnerSpawnedMobMatcher(parseEnumMatcher(spawner, "mob", EntityType.class)
+ .orElseThrow(AssertionError::new)));
+ } else if (spawner.contains("entity")) {
+ // duplicate of "mob", for completeness
+ matchers.add(new ItemMobSpawnerSpawnedMobMatcher(parseEnumMatcher(spawner, "entity", EntityType.class)
+ .orElseThrow(AssertionError::new)));
+ }
+
+ if (spawner.contains("radius")) {
+ matchers.add(new ItemMobSpawnerSpawnRadiusMatcher(parseAmount(spawner, "radius")
+ .orElseThrow(AssertionError::new)));
+ }
+
+ return matchers;
+ }
+
+ private Optional parseFireworkEffect(ConfigurationSection config, String path) {
+ if (!config.isConfigurationSection(path))
+ return Optional.empty();
+
+ ConfigurationSection fireworkEffect = config.getConfigurationSection(path);
+
+ // effect base color
+ List colors = new ArrayList<>();
+ ListMatchingMode colorsMode = ListMatchingMode.valueOf(fireworkEffect.getString("colorsMode", "ANY").toUpperCase());
+
+ if (fireworkEffect.isList("colors")) {
+ for (Object color : fireworkEffect.getList("colors")) {
+ colors.add(parseColor(color).orElseThrow(ItemExpressionConfigParsingError::new));
+ }
+ }
+
+ parseColor(fireworkEffect, "color").map(colors::add);
+
+ List fadeColors = new ArrayList<>();
+ ListMatchingMode fadeColorsMode = ListMatchingMode.valueOf(fireworkEffect.getString("fadeColorsMode", "ANY").toUpperCase());
+
+ if (fireworkEffect.isList("fadeColors")) {
+ for (Object color : fireworkEffect.getList("fadeColors")) {
+ fadeColors.add(parseColor(color).orElseThrow(ItemExpressionConfigParsingError::new));
+ }
+ }
+
+ parseColor(fireworkEffect, "fadeColor").map(fadeColors::add);
+
+ EnumMatcher type = parseEnumMatcher(fireworkEffect, "type", FireworkEffect.Type.class)
+ .orElse(new AnyEnum<>());
+
+ Optional hasFlicker = Optional.empty();
+ if (fireworkEffect.isBoolean("hasFlicker")) {
+ hasFlicker = Optional.of(fireworkEffect.getBoolean("hasFlicker"));
+ }
+
+ Optional hasTrail = Optional.empty();
+ if (fireworkEffect.isBoolean("hasTrail")) {
+ hasTrail = Optional.of(fireworkEffect.getBoolean("hasTrail"));
+ }
+
+ return Optional.of(new ExactlyFireworkEffect(type, colors, colorsMode, fadeColors, fadeColorsMode, hasFlicker, hasTrail));
+ }
+
+ private List parseFirework(ConfigurationSection config, String path) {
+ if (!config.isConfigurationSection(path))
+ return Collections.emptyList();
+
+ ConfigurationSection firework = config.getConfigurationSection(path);
+ ArrayList matchers = new ArrayList<>();
+
+ for (ListMatchingMode mode : ListMatchingMode.values()) {
+ ArrayList effectMatchers = new ArrayList<>();
+
+ for (ConfigurationSection effect : getConfigList(firework, "effects" + mode.getUpperCamelCase())) {
+ FireworkEffectMatcher matcher = parseFireworkEffect(effect, "")
+ .orElseThrow(ItemExpressionConfigParsingError::new);
+ effectMatchers.add(matcher);
+ }
+
+ if (!effectMatchers.isEmpty())
+ matchers.add(new ItemFireworkEffectsMatcher(effectMatchers, mode));
+ }
+
+ Optional power = parseAmount(firework, "power");
+ power.ifPresent(aPower -> matchers.add(new ItemFireworkPowerMatcher(aPower)));
+
+ Optional effectsCount = parseAmount(firework, "effectsCount");
+ effectsCount.ifPresent(aEffectsCount -> matchers.add(new ItemFireworkEffectsCountMatcher(aEffectsCount)));
+
+ return matchers;
+ }
+
+ /**
+ * Runs this ItemExpression on a given ItemStack.
+ *
+ * This will not mutate the ItemStack nor this ItemExpression.
+ * @param item The ItemStack to be matched upon.
+ * @return If the given item matches.
+ */
+ public boolean matches(ItemStack item) {
+ //System.out.println("These match:");
+ //matchers.stream().filter((matcher) -> matcher.matches(item)).forEach(System.out::println);
+ //System.out.println("These do not match:");
+ //matchers.stream().filter((matcher) -> !matcher.matches(item)).forEach(System.out::println);
+ return matchers.stream().allMatch((matcher) -> matcher.matches(item));
+ }
+
+ /**
+ * Solves this ItemExpression for an ItemStack that matches, taking base attributes from the passed ItemStack.
+ * @param inheritFrom The itemstack whose attributes and nbt data will be inherited from if this ItemExpression
+ * doesn't mutate them while solving.
+ * @return An ItemStack that satisfies the conditions of this piticular ItemStack such that matches(item) == true.
+ * @throws NotSolvableException If this ItemExpression uses elements such as Regular Expression matching that can
+ * not be solved in reasonable time.
+ */
+ @Override
+ public ItemStack solve(ItemStack inheritFrom) throws NotSolvableException {
+ inheritFrom = inheritFrom.clone();
+
+ for (ItemMatcher matcher : matchers) {
+ if (!inheritFrom.hasItemMeta()) {
+ inheritFrom.setItemMeta(Bukkit.getItemFactory().getItemMeta(inheritFrom.getType()));
+ // so many matchers require meta that setting it here fixes a lot of bugs
+ // although it also creates a lot of dead code
+ }
+
+ inheritFrom = matcher.solve(inheritFrom);
+ }
+
+ if (!matches(inheritFrom)) {
+ throw new NotSolvableException("not solvable: generated item " + inheritFrom + " does not match");
+ }
+
+ return inheritFrom;
+ }
+
+ /**
+ * Solves this ItemExpression for an ItemStack item where matches(item) == true.
+ * @return An ItemStack that satisfies the conditions of this piticular ItemStack such that matches(item) == true.
+ * @throws NotSolvableException If this ItemExpression uses elements such as Regular Expression matching that can
+ * not be solved in reasonable time.
+ */
+ public ItemStack solve() throws NotSolvableException {
+ return solve(new ItemStack(Material.STONE, 1));
+ }
+
+ /**
+ * Returns a lambda with the ItemMap bound into its environment. This is an instance of currying in java.
+ *
+ * The lambda returned uses the Entry it is passed to match on the ItemStack key. If the matcher implements
+ * ItemMapMatcher, it'll use that interface instead of the normal ItemMatcher. The reason it used an Entry instead
+ * of directly the ItemStack contained within is that it's designed to be used to be used as
+ * ItemMap.getEntries().stream().anyMatch(getMatchesItemMapPredicate(ItemMap)).
+ *
+ * If you wanted to call this function directly you'd say `getMatchesItemMapPredicate(itemMap)(entry)`.
+ *
+ * This function is implemented in this way in order to be able to reuse the predicate inside multiple functions
+ * while still being able to have the ItemMap usable inside the lambda.
+ *
+ * This function is mostly used to implement ItemMap advanced matching. It is not recommended to be used.
+ * @param itemMap The curried ItemMap value
+ * @return The curried function.
+ */
+ public Predicate> getMatchesItemMapPredicate(ItemMap itemMap) {
+ // currying in java 2019
+ return (kv) -> {
+ ItemStack item = kv.getKey();
+ //Integer amount = kv.get();
+ return matchers.stream().allMatch((matcher) -> {
+ if (matcher instanceof ItemMapMatcher) {
+ return ((ItemMapMatcher) matcher).matches(itemMap, item);
+ } else {
+ return matcher.matches(item);
+ }
+ });
+ };
+ }
+
+ /**
+ * Runs this ItemExpression on a given ItemMap, and returns true if the ItemExpression matched any one of the
+ * ItemStacks contained within the ItemMap.
+ * @param itemMap The ItemMap this ItemExpression will match over.
+ * @return If this ItemExpression matched at least one of the ItemStacks within the ItemMap.
+ */
+ public boolean matchesAnyItemMap(ItemMap itemMap) {
+ return itemMap.getEntrySet().stream().anyMatch(getMatchesItemMapPredicate(itemMap));
+ }
+
+ /**
+ * Runs this ItemExpression on a given ItemMap, and returns true if the ItemExpression matched all of the
+ * ItemStacks contained within the ItemMap.
+ * @param itemMap The ItemMap this ItemExpression will match over.
+ * @return If this ItemExpression matched every one of the ItemStacks within the ItemMap.
+ */
+ public boolean matchesAllItemMap(ItemMap itemMap) {
+ return itemMap.getEntrySet().stream().allMatch(getMatchesItemMapPredicate(itemMap));
+ }
+
+ /**
+ * Removes amount items that match this ItemExpression from tne inventory.
+ *
+ * This function correctly handles situations where the inventory has two or more ItemStacks that do not satisfy
+ * .isSimilar() but do match this ItemExpression.
+ * @param inventory The inventory items will be removed from.
+ * @param amount The number of items to remove. If this is -1, all items that match will be removed.
+ * @return If there were enough items to remove. If this is false, no items have been removed from the inventory.
+ */
+ public boolean removeFromInventory(Inventory inventory, int amount) {
+ ItemStack[] contents = inventory.getStorageContents();
+
+ ItemStack[] result = removeFromItemArray(contents, amount);
+ if (result == null)
+ return false;
+
+ inventory.setStorageContents(result);
+ return true;
+ }
+
+ /**
+ * Removes amount items that match this ItemExpression from contents, returning the modified version of contents.
+ * @param contents The list of items to be matched and possibly removed. This may contain nulls.
+ * The array and the ItemStacks inside will not be mutated.
+ * @param amount The number of items to remove.
+ * @return The new list of items will amount removed. If there were not enough items to remove, null will be returned.
+ */
+ private ItemStack[] removeFromItemArray(ItemStack[] contents, int amount) {
+ // store the amount matchers, because it'll mess with things later
+ // for exacple, what happens when amount=1 was passed into this function but amount: 64 is in the config?
+ List amountMatchers = matchers.stream().filter((m) -> m instanceof ItemAmountMatcher).collect(Collectors.toList());
+ for (ItemMatcher m : amountMatchers) {
+ matchers.remove(m);
+ }
+
+ try {
+ int runningAmount = amount;
+ boolean infinite = false;
+ if (runningAmount == -1) {
+ runningAmount = Integer.MAX_VALUE;
+ infinite = true;
+ }
+
+ contents = Arrays.stream(contents).map(item -> item != null ? item.clone() : null).toArray(ItemStack[]::new);
+ for (ItemStack item : contents) {
+ if (item == null)
+ continue;
+ if (item.getType() == Material.AIR)
+ continue;
+
+ if (matches(item)) {
+ if (item.getAmount() >= runningAmount) {
+ int itemOldAmount = item.getAmount();
+ item.setAmount(item.getAmount() - runningAmount);
+ runningAmount -= itemOldAmount - item.getAmount();
+ break;
+ } else if (item.getAmount() < runningAmount) {
+ runningAmount -= item.getAmount();
+ item.setAmount(0);
+ }
+ }
+ }
+
+ if (runningAmount == 0 || infinite) {
+ return contents;
+ } else if (runningAmount < 0) {
+ // big trouble, this isn't supposed to happen
+ throw new AssertionError("runningAmount is less than 0, there's a bug somewhere. runningAmount: " + runningAmount);
+ } else {
+ // items remaining
+ return null;
+ }
+ } finally {
+ // restore the amount matchers now we're done
+ amountMatchers.forEach(this::addMatcher);
+ }
+ }
+
+ /**
+ * Removes the items that match this ItemExpression. The amount to remove is infered from the amount of this
+ * ItemExpression.
+ *
+ * If amount is `any`, all items that match this ItemExpression will be removed.
+ * If amount is a range and random is true, a random number of items within the range will be removed.
+ * If amount is a range and random is false, the lower bound of the range will be used.
+ * @param inventory The inventory items will be removed from.
+ * @param random To select a random number within amount. This only applies if amount is a range.
+ * @return If there were enough items to remove. If this is false, no items have been removed from the inventory.
+ */
+ public boolean removeFromInventory(Inventory inventory, boolean random) {
+ return removeFromInventory(inventory, getAmount(random));
+ }
+
+ /**
+ * @param random To select a random number within amount. This only applies if amount is a range.
+ * @return The amount field of the config format. This is extracted from the structure of this ItemStack, not the config.
+ */
+ public int getAmount(boolean random) {
+ List amountMatchers = matchers.stream()
+ .filter((m) -> m instanceof ItemAmountMatcher)
+ .map((m) -> (ItemAmountMatcher) m)
+ .collect(Collectors.toList());
+
+ AmountMatcher amountMatcher;
+ if (amountMatchers.size() > 1)
+ throw new IllegalStateException("Can't infer the amount from an ItemExpression with multiple " +
+ "ItemAmountMatchers.");
+ else if (amountMatchers.size() == 1)
+ amountMatcher = amountMatchers.get(0).matcher;
+ else {
+ amountMatcher = new AnyAmount();
+ }
+
+ if (amountMatcher instanceof ExactlyAmount) {
+ return (int) ((ExactlyAmount) amountMatcher).amount;
+ } else if (amountMatcher instanceof AnyAmount) {
+ return -1;
+ } else if (amountMatcher instanceof RangeAmount && !random) {
+ RangeAmount rangeAmount = (RangeAmount) amountMatcher;
+ return (int) (rangeAmount.getLow() + (rangeAmount.lowInclusive ? 0 : 1));
+ } else if (amountMatcher instanceof RangeAmount && random) {
+ RangeAmount rangeAmount = (RangeAmount) amountMatcher;
+ return ThreadLocalRandom.current()
+ .nextInt((int) rangeAmount.getLow() + (rangeAmount.lowInclusive ? 0 : -1),
+ (int) rangeAmount.getHigh() + (rangeAmount.highInclusive ? 1 : 0));
+ } else {
+ throw new IllegalArgumentException("removeFromInventory(Inventory, boolean) does not work with custom AmountMatchers");
+ }
+ }
+
+ /**
+ * Removes amount items that match this ItemExpression from the main hand or the off hand, prefeing the main hand.
+ * @param inventory The player's inventory to remove the items from.
+ * @param amount The number of items to remove. All ItemStacks that match will be removed if this is -1.
+ * @return If there were enough items to remove. If this is false, no items were removed.
+ */
+ public boolean removeFromMainHandOrOffHand(PlayerInventory inventory, int amount) {
+ ItemStack[] items = new ItemStack[2];
+ items[0] = inventory.getItemInMainHand();
+ items[1] = inventory.getItemInOffHand();
+
+ ItemStack[] result = removeFromItemArray(items, amount);
+ if (result == null)
+ return false;
+
+ inventory.setItemInMainHand(result[0]);
+ inventory.setItemInOffHand(result[1]);
+ return true;
+ }
+
+ /**
+ * Removes the items that match this ItemExpression from either the main hand or the oof hand of the player.
+ * The amount to remove is infered from the amount of this ItemExpression.
+ *
+ * If amount is `any`, all items that match this ItemExpression will be removed.
+ * If amount is a range and random is true, a random number of items within the range will be removed.
+ * If amount is a range and random is false, the lower bound of the range will be used.
+ * @param inventory The player inventory to remove the items from.
+ * @param random To select a random number within amount. This only applies if amount is a range.
+ * @return If there were enough items to remove. If this is false, no items have been removed from the inventory.
+ */
+ public boolean removeFromMainHandOrOffHand(PlayerInventory inventory, boolean random) {
+ return removeFromMainHandOrOffHand(inventory, getAmount(random));
+ }
+
+ /**
+ * Add a property of the item to be checked, using an ItemMatcher.
+ * @param matcher The ItemMatcher to be added to the list that will be checked aganst each item inside
+ * ItemExpression.matches(ItemStack). If this is null, this function will do nothing.
+ */
+ public void addMatcher(ItemMatcher matcher) {
+ if (matcher != null)
+ matchers.add(matcher);
+ }
+
+ /**
+ * Add a number of properties if the item to be checked, using a number of ItemMatchers.
+ * @param matchers The list of ItemMatchers that will be added to the list of ItemMatchers to check aganst a given
+ * item. If this list contains any null elements, those null elements will be ignored.
+ */
+ public void addMatcher(Collection matchers) {
+ if (matchers == null)
+ return;
+
+ matchers.forEach(this::addMatcher);
+ }
+
+ /**
+ * Adds the matcher if Optional is not none.
+ * @param matcher The optional that may contain an ItemMatcher.
+ * @param The type of ItemMatcher being added.
+ */
+ public void addMatcher(Optional matcher) {
+ if (!matcher.isPresent())
+ return;
+
+ matcher.ifPresent(this::addMatcher);
+ }
+
+ /**
+ * All of the matchers in this set must return true in order for this ItemExpression to match a given item.
+ *
+ * This is the only data structure holding ItemMatchers in this ItemExpression, so it is fine to mutate this field.
+ */
+ public ArrayList matchers = new ArrayList<>();
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/ItemMapMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/ItemMapMatcher.java
new file mode 100644
index 00000000..c754cf8f
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/ItemMapMatcher.java
@@ -0,0 +1,11 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression;
+
+import org.bukkit.inventory.ItemStack;
+import vg.civcraft.mc.civmodcore.itemHandling.ItemMap;
+
+/**
+ * @author Ameliorate
+ */
+public interface ItemMapMatcher extends ItemMatcher {
+ boolean matches(ItemMap itemMap, ItemStack item);
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/ItemMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/ItemMatcher.java
new file mode 100644
index 00000000..6ab6b5e2
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/ItemMatcher.java
@@ -0,0 +1,13 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression;
+
+import org.bukkit.inventory.ItemStack;
+
+/**
+ * Represents a single property of an item that should be checked.
+ *
+ * If any one of these reject an item by returning false, ItemExpression.matches(ItemStack) will return false.
+ *
+ * @author Ameliorate
+ */
+public interface ItemMatcher extends Matcher {
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/Matcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/Matcher.java
new file mode 100644
index 00000000..0f8ea994
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/Matcher.java
@@ -0,0 +1,52 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression;
+
+/**
+ * Represents any interface for matching over a class.
+ *
+ * This is used mainly in the implementation of ListMatcherMode.matches()
+ * in order to be able to accept any list of matchers and list of things.
+ *
+ * However, this is used by all the *Matcher's in order to define the boolean matches(TheThing) function.
+ *
+ * @param The thing that is matched over by this matcher.
+ *
+ * @author Ameliorate
+ */
+public interface Matcher {
+ /**
+ * Determines if this Matcher matches the thing passed in.
+ *
+ * This should not mutate matched in any way.
+ *
+ * @param matched The thing that this matcher is matching over.
+ * @return If this matcher matched the thing.
+ */
+ boolean matches(T matched);
+
+ /**
+ * Mutates the state of defaultValue such that matches(defaultValue) == true.
+ *
+ * If a value for startingValue where matches(defaultValue) == true, then NotSolvableException should be thrown.
+ * @param defaultValue Used as a "base" value for feilds that don't need changed for matches(defaultValue) to be true.
+ * This may or may not be mutated.
+ * @return The value that matches this matcher.
+ * @throws NotSolvableException If a state for defaultValue could not be found that matches.
+ */
+ T solve(T defaultValue) throws NotSolvableException;
+
+ /**
+ * Thrown if a certain matcher can not be solved.
+ *
+ * This might be thrown if the matcher is based on Regular Expressions, as most regular expressions can not be
+ * solved in reasonable time.
+ */
+ class NotSolvableException extends Exception {
+ public NotSolvableException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public NotSolvableException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/TestItemSolvingCommand.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/TestItemSolvingCommand.java
new file mode 100644
index 00000000..4e81cae9
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/TestItemSolvingCommand.java
@@ -0,0 +1,42 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression;
+
+import org.bukkit.ChatColor;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import vg.civcraft.mc.civmodcore.CivModCorePlugin;
+
+import java.util.Map;
+
+public class TestItemSolvingCommand implements CommandExecutor {
+ @Override
+ public boolean onCommand(CommandSender commandSender, Command command, String name, String[] args) {
+ if (args.length != 1)
+ return false;
+
+ if (!(commandSender instanceof Player)) {
+ commandSender.sendMessage(ChatColor.RED + "This command can only be used by players.");
+ return true;
+ }
+
+ CivModCorePlugin.getInstance().reloadConfig();
+ Map itemExpressions = ItemExpression.getItemExpressionMap(
+ CivModCorePlugin.getInstance().getConfig(), "itemExpressions");
+
+ Player player = (Player) commandSender;
+
+ String ieName = args[0];
+ ItemExpression expression = itemExpressions.get(ieName);
+
+ try {
+ player.getInventory().addItem(expression.solve());
+ } catch (Matcher.NotSolvableException e) {
+ e.printStackTrace();
+ commandSender.sendMessage(ChatColor.RED + "Could not solve item expression: " + e.getLocalizedMessage() +
+ ". Please check the server log for more details.");
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/TestMatchingCommand.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/TestMatchingCommand.java
new file mode 100644
index 00000000..121f51ea
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/TestMatchingCommand.java
@@ -0,0 +1,42 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression;
+
+import org.bukkit.ChatColor;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import vg.civcraft.mc.civmodcore.CivModCorePlugin;
+
+import java.util.Map;
+
+public class TestMatchingCommand implements CommandExecutor {
+ @Override
+ public boolean onCommand(CommandSender commandSender, Command command, String name, String[] args) {
+ if (args.length != 1)
+ return false;
+
+ if (!(commandSender instanceof Player)) {
+ commandSender.sendMessage(ChatColor.RED + "This command can only be used by players.");
+ return true;
+ }
+
+ CivModCorePlugin.getInstance().reloadConfig();
+ Map itemExpressions = ItemExpression.getItemExpressionMap(
+ CivModCorePlugin.getInstance().getConfig(), "itemExpressions");
+
+ Player player = (Player) commandSender;
+
+ String ieName = args[0];
+ ItemExpression expression = itemExpressions.get(ieName);
+ ItemStack item = player.getInventory().getItemInMainHand();
+
+ if (expression.matches(item)) {
+ commandSender.sendMessage("Matches!");
+ } else {
+ commandSender.sendMessage("Does not match.");
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/AmountMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/AmountMatcher.java
new file mode 100644
index 00000000..11569c60
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/AmountMatcher.java
@@ -0,0 +1,19 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount;
+
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+
+/**
+ * @author Ameliorate
+ *
+ * In addition to the amount of items in an ItemStack, these are also used for the durability of an item, and
+ * the level of an enchantment.
+ */
+public interface AmountMatcher extends Matcher {
+ default boolean matches(int amount) {
+ return matches((double) amount);
+ }
+
+ default int solve(int defaultAmount) throws NotSolvableException {
+ return (int) (double) solve((double) defaultAmount);
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/AnyAmount.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/AnyAmount.java
new file mode 100644
index 00000000..07c87adb
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/AnyAmount.java
@@ -0,0 +1,18 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount;
+
+/**
+ * Accepts any amount.
+ *
+ * @author Ameliorate
+ */
+public class AnyAmount implements AmountMatcher {
+ @Override
+ public boolean matches(Double amount) {
+ return true;
+ }
+
+ @Override
+ public Double solve(Double defaultValue) throws NotSolvableException {
+ return defaultValue;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/ExactlyAmount.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/ExactlyAmount.java
new file mode 100644
index 00000000..5c80a5e7
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/ExactlyAmount.java
@@ -0,0 +1,24 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount;
+
+/**
+ * Accepts an amount exactly equal to the amount passed in.
+ *
+ * @author Ameliorate
+ */
+public class ExactlyAmount implements AmountMatcher {
+ public ExactlyAmount(double amount) {
+ this.amount = amount;
+ }
+
+ public double amount;
+
+ @Override
+ public boolean matches(Double amount) {
+ return this.amount == amount;
+ }
+
+ @Override
+ public Double solve(Double defaultValue) throws NotSolvableException {
+ return amount;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/ItemAmountMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/ItemAmountMatcher.java
new file mode 100644
index 00000000..5a7d11f6
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/ItemAmountMatcher.java
@@ -0,0 +1,39 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount;
+
+import org.bukkit.inventory.ItemStack;
+import vg.civcraft.mc.civmodcore.itemHandling.ItemMap;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMapMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+import java.util.Optional;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemAmountMatcher implements ItemMatcher, ItemMapMatcher {
+ public ItemAmountMatcher(AmountMatcher matcher) {
+ this.matcher = matcher;
+ }
+
+ public static ItemAmountMatcher construct(Optional matcher) {
+ return matcher.map(ItemAmountMatcher::new).orElse(null);
+ }
+
+ public AmountMatcher matcher;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ return matcher.matches(item.getAmount());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ item.setAmount(matcher.solve(1));
+ return item;
+ }
+
+ @Override
+ public boolean matches(ItemMap itemMap, ItemStack item) {
+ return matcher.matches(itemMap.getAmount(item));
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/ItemDamageMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/ItemDamageMatcher.java
new file mode 100644
index 00000000..b33f7361
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/ItemDamageMatcher.java
@@ -0,0 +1,44 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount;
+
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.Damageable;
+import org.bukkit.inventory.meta.ItemMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+import java.util.Optional;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemDamageMatcher implements ItemMatcher {
+ public ItemDamageMatcher(AmountMatcher matcher) {
+ this.matcher = matcher;
+ }
+
+ public static ItemDamageMatcher construct(Optional matcher) {
+ return matcher.map(ItemDamageMatcher::new).orElse(null);
+ }
+
+ public AmountMatcher matcher;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof Damageable))
+ return false;
+
+ return matcher.matches(((Damageable) item.getItemMeta()).getDamage());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ ItemMeta meta = item.getItemMeta();
+
+ if (!(meta instanceof Damageable))
+ throw new NotSolvableException("item does not have durability");
+
+ ((Damageable) meta).setDamage(matcher.solve(1));
+ item.setItemMeta(meta);
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/RangeAmount.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/RangeAmount.java
new file mode 100644
index 00000000..31622414
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/amount/RangeAmount.java
@@ -0,0 +1,76 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount;
+
+/**
+ * Accepts an amount between high and low. Also allows selecting if the range should include high and low.
+ *
+ * @author Ameliorate
+ */
+public class RangeAmount implements AmountMatcher {
+ /**
+ * @param low The lowest number that this range should accept.
+ * @param high The highest number that this range should accpet.
+ * @param lowInclusive If this range should accept values equal to low.
+ * @param highInclusive If this range should accept values equal to high.
+ */
+ public RangeAmount(double low, double high, boolean lowInclusive, boolean highInclusive) {
+ set(low, high);
+ this.highInclusive = highInclusive;
+ this.lowInclusive = lowInclusive;
+ }
+
+ private double low;
+ private double high;
+ public boolean highInclusive;
+ public boolean lowInclusive;
+
+ public void set(double low, double high) {
+ if (low <= high) {
+ // expected situation, do as normal
+ this.low = low;
+ this.high = high;
+ } else {
+ // accidentally reversed, fix it silently
+ this.low = high;
+ this.high = low;
+ }
+ }
+
+ public double getLow() {
+ return low;
+ }
+
+ public double getHigh() {
+ return high;
+ }
+
+ @Override
+ public boolean matches(Double amount) {
+ if (lowInclusive) {
+ if (amount < low)
+ return false;
+ } else {
+ if (amount <= low)
+ return false;
+ }
+ if (highInclusive) {
+ if (amount > high)
+ return false;
+ } else {
+ if (amount >= high)
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public Double solve(Double defaultValue) throws NotSolvableException {
+ if (matches(defaultValue))
+ return defaultValue;
+
+ if (low == high && !highInclusive && !lowInclusive)
+ throw new NotSolvableException("range has equal low and high and is exclusive");
+
+ return low + (lowInclusive ? 0 : 1);
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/BookPageMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/BookPageMatcher.java
new file mode 100644
index 00000000..42491adf
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/BookPageMatcher.java
@@ -0,0 +1,11 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.book;
+
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+
+import java.util.List;
+
+/**
+ * @author Ameliorate
+ */
+public interface BookPageMatcher extends Matcher> {
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ExactlyBookPages.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ExactlyBookPages.java
new file mode 100644
index 00000000..06e25687
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ExactlyBookPages.java
@@ -0,0 +1,24 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.book;
+
+import java.util.List;
+
+/**
+ * @author Ameliorate
+ */
+public class ExactlyBookPages implements BookPageMatcher {
+ public ExactlyBookPages(List pages) {
+ this.pages = pages;
+ }
+
+ public List pages;
+
+ @Override
+ public boolean matches(List pages) {
+ return this.pages.equals(pages);
+ }
+
+ @Override
+ public List solve(List defaultValue) throws NotSolvableException {
+ return pages;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookAuthorMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookAuthorMatcher.java
new file mode 100644
index 00000000..b8e8a810
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookAuthorMatcher.java
@@ -0,0 +1,43 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.book;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BookMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name.NameMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemBookAuthorMatcher implements ItemMatcher {
+ public ItemBookAuthorMatcher(NameMatcher author) {
+ this.author = author;
+ }
+
+ public NameMatcher author;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ String author = "";
+ if (item.hasItemMeta() && item.getItemMeta() instanceof BookMeta && ((BookMeta) item.getItemMeta()).hasAuthor()) {
+ author = ((BookMeta) item.getItemMeta()).getAuthor();
+ }
+
+ return this.author.matches(author);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof BookMeta) ||
+ !((BookMeta) item.getItemMeta()).hasAuthor()) {
+ item.setType(Material.WRITTEN_BOOK);
+ }
+
+ assert item.getItemMeta() instanceof BookMeta; // mostly to get intellij autocomplete for BookMeta
+
+ BookMeta meta = (BookMeta) item.getItemMeta();
+ meta.setAuthor(author.solve("Nobody"));
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookGenerationMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookGenerationMatcher.java
new file mode 100644
index 00000000..1efd5247
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookGenerationMatcher.java
@@ -0,0 +1,42 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.book;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BookMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher.EnumMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemBookGenerationMatcher implements ItemMatcher {
+ public ItemBookGenerationMatcher(EnumMatcher generations) {
+ this.generations = generations;
+ }
+
+ public EnumMatcher generations;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof BookMeta) || !((BookMeta) item.getItemMeta()).hasGeneration()) {
+ return false;
+ }
+
+ return generations.matches(((BookMeta) item.getItemMeta()).getGeneration());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof BookMeta) ||
+ !((BookMeta) item.getItemMeta()).hasGeneration()) {
+ item.setType(Material.WRITTEN_BOOK);
+ }
+
+ assert item.getItemMeta() instanceof BookMeta; // mostly to get intellij autocomplete for BookMeta
+
+ BookMeta meta = (BookMeta) item.getItemMeta();
+ meta.setGeneration(generations.solve(BookMeta.Generation.ORIGINAL));
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookPageCountMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookPageCountMatcher.java
new file mode 100644
index 00000000..b196af6f
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookPageCountMatcher.java
@@ -0,0 +1,46 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.book;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BookMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemBookPageCountMatcher implements ItemMatcher {
+ public ItemBookPageCountMatcher(AmountMatcher pageCount) {
+ this.pageCount = pageCount;
+ }
+
+ public AmountMatcher pageCount;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof BookMeta))
+ return false;
+
+ return pageCount.matches(((BookMeta) item.getItemMeta()).getPageCount());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof BookMeta)) {
+ item.setType(Material.WRITABLE_BOOK);
+ }
+
+ assert item.getItemMeta() instanceof BookMeta;
+
+ int count = pageCount.solve(1);
+ List pages = Collections.nCopies(count, "");
+
+ BookMeta meta = (BookMeta) item.getItemMeta();
+ meta.setPages(pages);
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookPagesMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookPagesMatcher.java
new file mode 100644
index 00000000..e2ed9e96
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookPagesMatcher.java
@@ -0,0 +1,49 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.book;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BookMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemBookPagesMatcher implements ItemMatcher {
+ public ItemBookPagesMatcher(BookPageMatcher matcher) {
+ this.matcher = matcher;
+ }
+
+ public BookPageMatcher matcher;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ List pages = Collections.emptyList();
+
+ if (item.hasItemMeta() && item.getItemMeta() instanceof BookMeta && ((BookMeta) item.getItemMeta()).hasPages()) {
+ pages = ((BookMeta) item.getItemMeta()).getPages();
+ }
+
+ return matcher.matches(pages);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof BookMeta)) {
+ item.setType(Material.WRITABLE_BOOK);
+ }
+
+ assert item.getItemMeta() instanceof BookMeta;
+
+ List pages = matcher.solve(new ArrayList<>());
+ // new arraylist instead of Collections.emptyList() because solve() might mutate the list.
+
+ BookMeta meta = (BookMeta) item.getItemMeta();
+ meta.setPages(pages);
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookTitleMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookTitleMatcher.java
new file mode 100644
index 00000000..693c6870
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/ItemBookTitleMatcher.java
@@ -0,0 +1,49 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.book;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BookMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name.NameMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemBookTitleMatcher implements ItemMatcher {
+ public ItemBookTitleMatcher(NameMatcher title) {
+ this.title = title;
+ }
+
+ public NameMatcher title;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ String title = "";
+
+ if (item.hasItemMeta()) {
+ if (item.getItemMeta() instanceof BookMeta && ((BookMeta) item.getItemMeta()).hasTitle()) {
+ title = ((BookMeta) item.getItemMeta()).getTitle();
+ } else {
+ title = item.getItemMeta().getDisplayName(); // is this a good?
+ }
+ }
+
+ return this.title.matches(title);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof BookMeta) ||
+ !((BookMeta) item.getItemMeta()).hasTitle()) {
+ item.setType(Material.WRITTEN_BOOK);
+ }
+
+ assert item.getItemMeta() instanceof BookMeta; // mostly to get intellij autocomplete for BookMeta
+
+ String title = this.title.solve("Unnamed Book");
+ BookMeta meta = (BookMeta) item.getItemMeta();
+ meta.setTitle(title);
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/RegexBookPages.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/RegexBookPages.java
new file mode 100644
index 00000000..c3baf723
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/book/RegexBookPages.java
@@ -0,0 +1,33 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.book;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * @author Ameliorate
+ */
+public class RegexBookPages implements BookPageMatcher {
+ public RegexBookPages(Pattern regex) {
+ this.regex = regex;
+ }
+
+ public Pattern regex;
+
+ @Override
+ public boolean matches(List pages) {
+ StringBuilder pageBuilder = new StringBuilder();
+ for (String page : pages) {
+ pageBuilder.append("\ueB0F");
+ pageBuilder.append(page);
+ pageBuilder.append('\ueE0F');
+ }
+
+ String formattedPages = pageBuilder.toString();
+ return regex.matcher(formattedPages).find();
+ }
+
+ @Override
+ public List solve(List defaultValue) throws NotSolvableException {
+ throw new NotSolvableException("can't solve regexes");
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/color/ColorMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/color/ColorMatcher.java
new file mode 100644
index 00000000..51b5fba0
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/color/ColorMatcher.java
@@ -0,0 +1,10 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.color;
+
+import org.bukkit.Color;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+
+/**
+ * @author Ameliorate
+ */
+public interface ColorMatcher extends Matcher {
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/color/ExactlyColor.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/color/ExactlyColor.java
new file mode 100644
index 00000000..8a9a53ce
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/color/ExactlyColor.java
@@ -0,0 +1,77 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.color;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Color;
+import org.bukkit.DyeColor;
+
+/**
+ * @author Ameliorate
+ */
+public class ExactlyColor implements ColorMatcher {
+ public ExactlyColor(Color colour) {
+ this.color = colour;
+ }
+
+ public Color color;
+
+ @Override
+ public boolean matches(Color color) {
+ return this.color.equals(color);
+ }
+
+ @Override
+ public Color solve(Color defaultValue) throws NotSolvableException {
+ return color;
+ }
+
+ /**
+ * Maps between HTML colors and org.bukkit.Color. See https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Color.html
+ * for the full list of colors.
+ */
+ public static Color getColorByHTMLName(String name) {
+ switch (name.toUpperCase()) {
+ case "AQUA": return Color.AQUA;
+ case "BLACK": return Color.BLACK;
+ case "BLUE": return Color.BLUE;
+ case "FUCHSIA": return Color.FUCHSIA;
+ case "GRAY": return Color.GRAY;
+ case "GREEN": return Color.GREEN;
+ case "LIME": return Color.LIME;
+ case "MAROON": return Color.MAROON;
+ case "NAVY": return Color.NAVY;
+ case "OLIVE": return Color.ORANGE;
+ case "PURPLE": return Color.PURPLE;
+ case "RED": return Color.RED;
+ case "SILVER": return Color.SILVER;
+ case "TEAL": return Color.TEAL;
+ case "WHITE": return Color.WHITE;
+ case "YELLOW": return Color.YELLOW;
+ default:
+ throw new IllegalArgumentException(name + " is not a html color name in org.bukkit.Color.");
+ }
+ }
+
+ /**
+ * Maps between the vanilla dye colors and org.bukkit.Color. See https://hub.spigotmc.org/javadocs/spigot/org/bukkit/DyeColor.html
+ * for the full list of colors.
+ *
+ * @param useFireworkColor Use the colors used for firework crafting instead of the vanilla default colors used for
+ * wool and other items.
+ */
+ public static Color getColorByVanillaName(String name, boolean useFireworkColor) {
+ if ("defaultLeather".equals(name)) {
+ return Bukkit.getServer().getItemFactory().getDefaultLeatherColor();
+ }
+
+ DyeColor color = DyeColor.valueOf(name.toUpperCase());
+ return useFireworkColor ? color.getFireworkColor() : color.getColor();
+ }
+
+ /**
+ * Maps between the vanilla dye colors and org.bukkit.Color. See https://hub.spigotmc.org/javadocs/spigot/org/bukkit/DyeColor.html
+ * for the full list of colors.
+ */
+ public static Color getColorByVanillaName(String name) {
+ return getColorByVanillaName(name, false);
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/color/ListColor.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/color/ListColor.java
new file mode 100644
index 00000000..9e8e74ec
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/color/ListColor.java
@@ -0,0 +1,86 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.color;
+
+import org.bukkit.Color;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+/**
+ * @author Ameliorate
+ */
+public class ListColor implements ColorMatcher {
+ public ListColor(List matchers, boolean noneInList) {
+ this.matchers = matchers;
+ this.noneInList = noneInList;
+ }
+
+ public List matchers;
+ public boolean noneInList;
+
+ @Override
+ public boolean matches(Color color) {
+ Stream stream = matchers.stream();
+ Predicate predicate = (matcher) -> matcher.matches(color);
+
+ if (noneInList) {
+ return stream.noneMatch(predicate);
+ } else {
+ return stream.anyMatch(predicate);
+ }
+ }
+
+ @Override
+ public Color solve(Color defaultValue) throws NotSolvableException {
+ if (!noneInList) {
+ List causes = new ArrayList<>();
+
+ for (ColorMatcher matcher : matchers) {
+ try {
+ return matcher.solve(defaultValue);
+ } catch (NotSolvableException e) {
+ causes.add(e);
+ }
+ }
+
+ NotSolvableException e = new NotSolvableException("couldn't solve list of color matchers");
+ causes.forEach(e::addSuppressed);
+ throw e;
+ } else {
+ // just brute force it, starting at defaultValue.
+
+ // A brute force search is okay because you can only match over exactly color or in list color.
+ // If you were to list every single color, ypu'd exaust memory on nearly every machine.
+
+ Color color = defaultValue;
+ while (!matches(color)) {
+ int red = color.getRed();
+ int green = color.getGreen();
+ int blue = color.getBlue();
+
+ if (++red > 255) {
+ red = 0;
+ green++;
+ }
+ if (green > 255) {
+ green = 0;
+ blue++;
+ }
+ if (blue > 255) {
+ blue = 0;
+ }
+
+ // we manually implement overflowing because java doesn't have a unsigned 24 bit type, and unsigned
+ // bytes aren't easy either.
+
+ color = Color.fromRGB(red, green, blue);
+
+ if (color == defaultValue)
+ throw new NotSolvableException("exausted entire rgb space searching for matching color");
+ }
+
+ return color;
+ }
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/AnyEnchantment.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/AnyEnchantment.java
new file mode 100644
index 00000000..56bc98d8
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/AnyEnchantment.java
@@ -0,0 +1,31 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enchantment;
+
+import org.bukkit.enchantments.Enchantment;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+import java.util.AbstractMap;
+import java.util.Map;
+
+/**
+ * Matches any enchantment type, as long as the level matches the level matcher.
+ *
+ * @author Ameliorate
+ */
+public class AnyEnchantment implements EnchantmentMatcher {
+ public AnyEnchantment(AmountMatcher level) {
+ this.level = level;
+ }
+
+ public AmountMatcher level;
+
+ @Override
+ public boolean matches(Enchantment enchantment, int level) {
+ return this.level.matches(level);
+ }
+
+ @Override
+ public Map.Entry solve(Map.Entry entry) throws NotSolvableException {
+ int level = this.level.solve(1);
+ return new AbstractMap.SimpleEntry<>(entry.getKey(), level);
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/EnchantmentMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/EnchantmentMatcher.java
new file mode 100644
index 00000000..790ad448
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/EnchantmentMatcher.java
@@ -0,0 +1,18 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enchantment;
+
+import org.bukkit.enchantments.Enchantment;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+
+import java.util.Map;
+
+/**
+ * @author Ameliorate
+ */
+public interface EnchantmentMatcher extends Matcher> {
+ boolean matches(Enchantment enchantment, int level);
+
+ @Override
+ default boolean matches(Map.Entry matched) {
+ return matches(matched.getKey(), matched.getValue());
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/EnchantmentsSource.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/EnchantmentsSource.java
new file mode 100644
index 00000000..a2b10e18
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/EnchantmentsSource.java
@@ -0,0 +1,99 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enchantment;
+
+import org.bukkit.Material;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.EnchantmentStorageMeta;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Represents the different holders of enchantments an item can have.
+ *
+ * @author Ameliorate
+ */
+public enum EnchantmentsSource {
+ /**
+ * The normal enchantments on the item that take effect. For example a diamond sword with Sharpness 5.
+ */
+ ITEM,
+
+ /**
+ * For example from the enchantments held inside an enchanted book
+ */
+ HELD;
+
+ /**
+ * Gets the enchantments from an item based on the value of this.
+ * @param item The item to get the enchantments from.
+ * @return The stored enchantments if this == HELD, or the regular enchantments if this == ITEM.
+ */
+ public Map get(ItemStack item) {
+ if (!item.hasItemMeta())
+ return Collections.emptyMap();
+
+ if (this == ITEM)
+ return item.getEnchantments();
+
+ if (this == HELD) {
+ if (!(item.getItemMeta() instanceof EnchantmentStorageMeta))
+ return Collections.emptyMap();
+ return ((EnchantmentStorageMeta) item.getItemMeta()).getStoredEnchants();
+ }
+
+ throw new AssertionError("not reachable");
+ }
+
+ public void set(ItemStack item, Map enchantments, boolean unsafe) {
+ if (this == ITEM) {
+ item.getEnchantments().keySet().forEach(item::removeEnchantment);
+ if (unsafe)
+ item.addUnsafeEnchantments(enchantments);
+ else
+ item.addEnchantments(enchantments);
+ }
+
+ if (this == HELD) {
+ if (!(item.getItemMeta() instanceof EnchantmentStorageMeta))
+ throw new IllegalArgumentException("item does not store enchantments");
+
+ EnchantmentStorageMeta meta = (EnchantmentStorageMeta) item.getItemMeta();
+
+ meta.getStoredEnchants().keySet().forEach(meta::removeStoredEnchant);
+ enchantments.forEach((enchantment, level) ->
+ meta.addStoredEnchant(enchantment, level, unsafe));
+ item.setItemMeta(meta);
+ }
+ }
+
+ public void add(ItemStack item, Enchantment enchantment, int level, boolean unsafe) {
+ if (this == ITEM) {
+ if (unsafe)
+ item.addUnsafeEnchantment(enchantment, level);
+ else
+ item.addEnchantment(enchantment, level);
+ }
+
+ if (this == HELD) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof EnchantmentStorageMeta))
+ throw new IllegalArgumentException("item does not store enchantments");
+
+ EnchantmentStorageMeta meta = (EnchantmentStorageMeta) item.getItemMeta();
+ meta.addStoredEnchant(enchantment, level, unsafe);
+ item.setItemMeta(meta);
+ }
+ }
+
+ /**
+ * @return A material that can reasonably hold enchantments in the slot according to this enum.
+ */
+ public Material getReasonableType() {
+ if (this == ITEM)
+ return Material.WOODEN_SWORD;
+ if (this == HELD)
+ return Material.ENCHANTED_BOOK;
+
+ throw new AssertionError("not reachable");
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/ExactlyEnchantment.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/ExactlyEnchantment.java
new file mode 100644
index 00000000..04153ce5
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/ExactlyEnchantment.java
@@ -0,0 +1,33 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enchantment;
+
+import org.bukkit.enchantments.Enchantment;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+import java.util.AbstractMap;
+import java.util.Map;
+
+/**
+ * @author Ameliorate
+ */
+public class ExactlyEnchantment implements EnchantmentMatcher {
+ public ExactlyEnchantment(Enchantment enchantment, AmountMatcher level) {
+ this.enchantment = enchantment;
+ this.level = level;
+ }
+
+ public Enchantment enchantment;
+ public AmountMatcher level;
+
+ @Override
+ public boolean matches(Enchantment enchantment, int level) {
+ if (!this.enchantment.equals(enchantment))
+ return false;
+ return this.level.matches(level);
+ }
+
+ @Override
+ public Map.Entry solve(Map.Entry defaultValue) throws NotSolvableException {
+ int level = this.level.solve(defaultValue.getValue());
+ return new AbstractMap.SimpleEntry<>(enchantment, level);
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/ItemEnchantmentCountMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/ItemEnchantmentCountMatcher.java
new file mode 100644
index 00000000..ed466c4e
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/ItemEnchantmentCountMatcher.java
@@ -0,0 +1,95 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enchantment;
+
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.EnchantmentStorageMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enchantment.EnchantmentsSource.HELD;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemEnchantmentCountMatcher implements ItemMatcher {
+ public ItemEnchantmentCountMatcher(AmountMatcher enchantmentCount, EnchantmentsSource source) {
+ this.enchantmentCount = enchantmentCount;
+ this.source = source;
+ }
+
+ public ItemEnchantmentCountMatcher(AmountMatcher enchantmentCount) {
+ this(enchantmentCount, EnchantmentsSource.ITEM);
+ }
+
+ public AmountMatcher enchantmentCount;
+ public EnchantmentsSource source;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta())
+ return false;
+ if (source == HELD && !(item.getItemMeta() instanceof EnchantmentStorageMeta))
+ return false;
+
+ int count = 0;
+ switch (source) {
+ case HELD:
+ count = ((EnchantmentStorageMeta) item.getItemMeta()).getStoredEnchants().size();
+ break;
+ case ITEM:
+ count = item.getItemMeta().getEnchants().size();
+ break;
+ }
+
+ return enchantmentCount.matches(count);
+ }
+
+ private static List allEnchantments = new ArrayList<>();
+
+ static {
+ Class enchantmentClass = Enchantment.class;
+ List staticFields = Arrays.stream(enchantmentClass.getFields())
+ .filter((f) -> Modifier.isStatic(f.getModifiers())) // is static
+ .filter((f) -> f.getType().isAssignableFrom(Enchantment.class)) // is of type Enchantment
+ .filter((f) -> Modifier.isPublic(f.getModifiers())) // is public
+ .collect(Collectors.toList()); // in other words, get all the enchantments declared in Enchangments.
+
+ staticFields.forEach((f) -> {
+ try {
+ allEnchantments.add((Enchantment) f.get(Enchantment.PROTECTION_FALL));
+ } catch (IllegalAccessException e) {
+ throw new AssertionError("expected f to be a public member", e);
+ }
+ });
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ int i = 0;
+ boolean unsafe = false;
+
+ while (!enchantmentCount.matches(source.get(item).size())) {
+ if (unsafe || source == HELD || allEnchantments.get(i).canEnchantItem(item))
+ source.add(item, allEnchantments.get(i), 1, unsafe);
+ i++;
+
+ if (i >= allEnchantments.size() && unsafe)
+ throw new NotSolvableException("not enough enchantments exist to solve for enchantment count on item");
+
+ if (i >= allEnchantments.size()) {
+ // if can't get enough enchantments with safe enchantments, add some unsafe enchantments.
+ unsafe = true;
+ i = 0;
+ }
+ }
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/ItemEnchantmentsMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/ItemEnchantmentsMatcher.java
new file mode 100644
index 00000000..6fbcdf6f
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enchantment/ItemEnchantmentsMatcher.java
@@ -0,0 +1,71 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enchantment;
+
+import org.bukkit.Material;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.EnchantmentStorageMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc.ListMatchingMode;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enchantment.EnchantmentsSource.HELD;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemEnchantmentsMatcher implements ItemMatcher {
+ public ItemEnchantmentsMatcher(List enchantmentMatchers, ListMatchingMode mode, EnchantmentsSource source) {
+ if (enchantmentMatchers.isEmpty())
+ throw new IllegalArgumentException("enchanmentMatchers can not be empty. If an empty enchantmentMatchers " +
+ "was allowed, it would cause many subtle logic errors.");
+ this.enchantmentMatchers = enchantmentMatchers;
+ this.mode = mode;
+ this.source = source;
+ }
+
+ public List enchantmentMatchers;
+ public ListMatchingMode mode;
+ public EnchantmentsSource source;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ switch (source) {
+ case ITEM:
+ return matches(item.getEnchantments());
+ case HELD:
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof EnchantmentStorageMeta))
+ return false;
+ return matches(((EnchantmentStorageMeta) item.getItemMeta()).getStoredEnchants());
+ }
+ throw new AssertionError("not reachable");
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (source == HELD && !(item.getItemMeta() instanceof EnchantmentStorageMeta))
+ item.setType(Material.ENCHANTED_BOOK);
+
+ Map defaultEnchantments = item.getEnchantments();
+ if (defaultEnchantments.isEmpty()) {
+ defaultEnchantments = new HashMap<>(defaultEnchantments); // spigot returns a immutable hashmap
+ defaultEnchantments.put(Enchantment.DAMAGE_ALL, 1);
+ }
+
+ List> enchantments =
+ mode.solve(enchantmentMatchers,
+ new ListMatchingMode.LazyFromListEntrySupplier<>(defaultEnchantments));
+
+ Map enchantmentMap =
+ enchantments.stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+ source.set(item, enchantmentMap, true);
+ return item;
+ }
+
+ public boolean matches(Map enchantments) {
+ return mode.matches(enchantmentMatchers, enchantments.entrySet());
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/AnyEnum.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/AnyEnum.java
new file mode 100644
index 00000000..d9c600d5
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/AnyEnum.java
@@ -0,0 +1,30 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher;
+
+/**
+ * Accepts any enum. This is intended to be used as a default value in some places, and is not exposed in the config.
+ *
+ * @author Ameliorate
+ */
+public class AnyEnum> implements EnumMatcher {
+ public AnyEnum() {
+ }
+
+ public AnyEnum(Class enumClass) {
+ this.enumClass = enumClass;
+ }
+
+ public Class enumClass = null;
+
+ @Override
+ public boolean matches(E matched) {
+ return true;
+ }
+
+ @Override
+ public E solve(E defaultValue) throws NotSolvableException {
+ if (enumClass == null)
+ throw new NotSolvableException("not able to solve an AnyEnum without a enumClass set");
+
+ return enumClass.getEnumConstants()[0];
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/EnumFromListMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/EnumFromListMatcher.java
new file mode 100644
index 00000000..493b2752
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/EnumFromListMatcher.java
@@ -0,0 +1,38 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher;
+
+import java.util.List;
+
+/**
+ * @author Ameliorate
+ */
+public class EnumFromListMatcher> implements EnumMatcher {
+ public EnumFromListMatcher(List enums, boolean notInList) {
+ this.enums = enums;
+ this.notInList = notInList;
+ }
+
+ public EnumFromListMatcher(List enums) {
+ this(enums, false);
+ }
+
+ public List enums;
+
+ /**
+ * If this should do "not in the list of enums" instead of the default of "is in the list of enums".
+ */
+ public boolean notInList;
+
+ @Override
+ public boolean matches(E enumm) {
+ if (notInList) {
+ return !enums.contains(enumm);
+ } else {
+ return enums.contains(enumm);
+ }
+ }
+
+ @Override
+ public E solve(E defaultValue) throws NotSolvableException {
+ return enums.get(0);
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/EnumIndexMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/EnumIndexMatcher.java
new file mode 100644
index 00000000..b344cf71
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/EnumIndexMatcher.java
@@ -0,0 +1,31 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class EnumIndexMatcher> implements EnumMatcher {
+ public EnumIndexMatcher(int index) {
+ this.index = index;
+ }
+
+ public EnumIndexMatcher(int index, Class enumClass) {
+ this(index);
+ this.enumClass = enumClass;
+ }
+
+ public int index;
+ public Class enumClass = null;
+
+ @Override
+ public boolean matches(E enumm) {
+ return enumm.ordinal() == index;
+ }
+
+ @Override
+ public E solve(E defaultValue) throws NotSolvableException {
+ if (enumClass == null)
+ throw new NotSolvableException("can't solve an EnumIndex without enumClass set");
+
+ return enumClass.getEnumConstants()[index];
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/EnumMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/EnumMatcher.java
new file mode 100644
index 00000000..e5d8ec10
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/EnumMatcher.java
@@ -0,0 +1,13 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher;
+
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+
+/**
+ * Matches over an Enum in an enum-generic way.
+ *
+ * @param The enum being matched over.
+ *
+ * @author Ameliorate
+ */
+public interface EnumMatcher> extends Matcher {
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/ExactlyEnumMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/ExactlyEnumMatcher.java
new file mode 100644
index 00000000..1392366e
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/ExactlyEnumMatcher.java
@@ -0,0 +1,22 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ExactlyEnumMatcher> implements EnumMatcher {
+ public ExactlyEnumMatcher(E exactly) {
+ this.exactly = exactly;
+ }
+
+ public E exactly;
+
+ @Override
+ public boolean matches(E enumm) {
+ return exactly.equals(enumm);
+ }
+
+ @Override
+ public E solve(E defaultValue) throws NotSolvableException {
+ return exactly;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/NameEnumMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/NameEnumMatcher.java
new file mode 100644
index 00000000..4f828544
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/enummatcher/NameEnumMatcher.java
@@ -0,0 +1,37 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher;
+
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name.NameMatcher;
+
+import java.util.Arrays;
+
+/**
+ * @author Ameliorate
+ */
+public class NameEnumMatcher> implements EnumMatcher {
+ public NameEnumMatcher(NameMatcher nameMatcher) {
+ this.nameMatcher = nameMatcher;
+ }
+
+ public NameEnumMatcher(NameMatcher nameMatcher, Class enumClass) {
+ this(nameMatcher);
+ this.enumClass = enumClass;
+ }
+
+ public NameMatcher nameMatcher;
+ public Class enumClass = null;
+
+ @Override
+ public boolean matches(E enumm) {
+ return nameMatcher.matches(enumm.name());
+ }
+
+ @Override
+ public E solve(E defaultValue) throws NotSolvableException {
+ String name = nameMatcher.solve(defaultValue.name());
+ return Arrays.stream(enumClass.getEnumConstants())
+ .filter((e) -> name.equals(e.name()))
+ .findFirst()
+ .orElseThrow(() -> new NotSolvableException(
+ "name of enum " + name + " does not match any variants of enum "+ enumClass.getName()));
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ExactlyFireworkEffect.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ExactlyFireworkEffect.java
new file mode 100644
index 00000000..b8d5d757
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ExactlyFireworkEffect.java
@@ -0,0 +1,80 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.firework;
+
+import org.bukkit.Color;
+import org.bukkit.FireworkEffect;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.color.ColorMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher.EnumMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc.ListMatchingMode;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Amleiorate
+ */
+public class ExactlyFireworkEffect implements FireworkEffectMatcher {
+ public ExactlyFireworkEffect(EnumMatcher type,
+ List colors, ListMatchingMode colorsMode,
+ List fadeColors, ListMatchingMode fadeColorsMode,
+ Optional hasFlicker, Optional hasTrail) {
+ this.type = type;
+ this.colors = colors;
+ this.colorsMode = colorsMode;
+ this.fadeColors = fadeColors;
+ this.fadeColorsMode = fadeColorsMode;
+ this.hasFlicker = hasFlicker;
+ this.hasTrail = hasTrail;
+ }
+
+ public EnumMatcher type;
+
+ public List colors;
+ public ListMatchingMode colorsMode;
+
+ public List fadeColors;
+ public ListMatchingMode fadeColorsMode;
+
+ public Optional hasFlicker = Optional.empty();
+ public Optional hasTrail = Optional.empty();
+
+ @Override
+ public boolean matches(FireworkEffect effect) {
+ if (hasFlicker.isPresent()) {
+ if (hasFlicker.get() != effect.hasFlicker())
+ return false;
+ }
+
+ if (hasTrail.isPresent()) {
+ if (hasTrail.get() != effect.hasTrail())
+ return false;
+ }
+
+ if (type != null && !type.matches(effect.getType()))
+ return false;
+
+ if (!colorsMode.matches(colors, effect.getColors()))
+ return false;
+
+ if (!fadeColorsMode.matches(fadeColors, effect.getFadeColors()))
+ return false;
+
+ return true;
+ }
+
+ @Override
+ public FireworkEffect solve(FireworkEffect effect) throws NotSolvableException {
+ FireworkEffect.Type type = this.type.solve(effect.getType());
+ List colors = colorsMode.solve(this.colors, () -> Color.WHITE);
+ List fadeColors = fadeColorsMode.solve(this.fadeColors, () -> Color.WHITE);
+ boolean hasFlicker = this.hasFlicker.orElse(effect.hasFlicker());
+ boolean hasTrail = this.hasTrail.orElse(effect.hasTrail());
+
+ return FireworkEffect.builder()
+ .with(type)
+ .withColor(colors)
+ .withFade(fadeColors)
+ .flicker(hasFlicker)
+ .trail(hasTrail)
+ .build();
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/FireworkEffectMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/FireworkEffectMatcher.java
new file mode 100644
index 00000000..a57b860a
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/FireworkEffectMatcher.java
@@ -0,0 +1,10 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.firework;
+
+import org.bukkit.FireworkEffect;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+
+/**
+ * @author Ameliorate
+ */
+public interface FireworkEffectMatcher extends Matcher {
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkEffectHolderMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkEffectHolderMatcher.java
new file mode 100644
index 00000000..024a55c5
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkEffectHolderMatcher.java
@@ -0,0 +1,43 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.firework;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.FireworkEffectMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+import java.util.Optional;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemFireworkEffectHolderMatcher implements ItemMatcher {
+ public ItemFireworkEffectHolderMatcher(FireworkEffectMatcher effect) {
+ this.effect = effect;
+ }
+
+ public static ItemFireworkEffectHolderMatcher construct(Optional effect) {
+ return effect.map(ItemFireworkEffectHolderMatcher::new).orElse(null);
+ }
+
+ public FireworkEffectMatcher effect;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof FireworkEffectMeta) ||
+ !((FireworkEffectMeta) item.getItemMeta()).hasEffect())
+ return false;
+
+ return effect.matches(((FireworkEffectMeta) item.getItemMeta()).getEffect());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof FireworkEffectMeta))
+ item.setType(Material.FIREWORK_STAR);
+
+ FireworkEffectMeta meta = (FireworkEffectMeta) item.getItemMeta();
+ meta.setEffect(effect.solve(meta.getEffect()));
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkEffectsCountMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkEffectsCountMatcher.java
new file mode 100644
index 00000000..2a5d1c20
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkEffectsCountMatcher.java
@@ -0,0 +1,96 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.firework;
+
+import com.google.common.hash.Hashing;
+import org.bukkit.Color;
+import org.bukkit.FireworkEffect;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.FireworkMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+import java.util.ArrayList;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemFireworkEffectsCountMatcher implements ItemMatcher {
+ public ItemFireworkEffectsCountMatcher(AmountMatcher count) {
+ this.count = count;
+ }
+
+ public AmountMatcher count;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof FireworkMeta))
+ return false;
+
+ return count.matches(((FireworkMeta) item.getItemMeta()).getEffectsSize());
+ }
+
+ public static final int TRAIL_MASK = 0b00000000000000000000000000000001;
+ public static final int FLICK_MASK = 0b00000000000000000000000000000010; // shift 1 to get as an int
+ public static final int TYPE_MASK = 0b00000000000000000000000000011100; // shift 2
+ public static final int CRED_MASK = 0b00000000000000000000001111100000; // shift 5 or 2 for 8 bit MSB
+ public static final int CGREE_MASK = 0b00000000000000000111110000000000; // shift 10 or 7
+ public static final int CBLUE_MASK = 0b00000000000001111000000000000000; // shift 15 or 11
+ public static final int FRED_MASK = 0b00000000111110000000000000000000; // shift 19 or 16
+ public static final int FGREE_MASK = 0b00001111000000000000000000000000; // shift 24 or 20
+ public static final int FBLUE_MASK = 0b11110000000000000000000000000000; // shift 28 or 24
+
+ @SuppressWarnings("UnstableApiUsage")
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ int count = this.count.solve(0);
+
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof FireworkMeta))
+ item.setType(Material.FIREWORK_ROCKET);
+
+ assert item.getItemMeta() instanceof FireworkMeta;
+
+ FireworkMeta meta = (FireworkMeta) item.getItemMeta();
+
+ ArrayList effects = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ effects.add(getFireworkEffectWithIndex(i));
+ }
+
+ meta.addEffects(effects);
+ item.setItemMeta(meta);
+ return item;
+ }
+
+ @SuppressWarnings("UnstableApiUsage")
+ public static FireworkEffect getFireworkEffectWithIndex(int i) {
+ int b = Hashing.crc32().hashInt(i).asInt();
+ boolean trail = (b & TRAIL_MASK) == 1;
+ boolean flicker = (b & FLICK_MASK) == 2;
+ int typeIndex = b & TYPE_MASK >>> 2;
+ int typeIndexOver = 0;
+ if (typeIndex <= 5) {
+ typeIndexOver = typeIndex - 4;
+ typeIndex -= 5;
+ }
+ FireworkEffect.Type type = FireworkEffect.Type.values()[typeIndex];
+
+ byte colorRed = (byte) ((b & CRED_MASK) >>> 2);
+ byte colorGreen = (byte) ((b & CGREE_MASK) >>> 7);
+ byte colorBlue = (byte) ((b & CBLUE_MASK) >>> 11);
+ colorBlue += typeIndexOver << 2; // recover the entropy lost from clipping typeIndex.
+ Color color = Color.fromRGB(colorRed, colorGreen, colorBlue);
+
+ byte fadeRed = (byte) ((b & FRED_MASK) >>> 16);
+ byte fadeGreen = (byte) ((b & FGREE_MASK) >>> 20);
+ byte fadeBlue = (byte) ((b & FBLUE_MASK) >>> 24);
+ Color fadeColor = Color.fromRGB(fadeRed, fadeGreen, fadeBlue);
+
+ return FireworkEffect.builder()
+ .flicker(flicker)
+ .trail(trail)
+ .withColor(color)
+ .withFade(fadeColor)
+ .with(type)
+ .build();
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkEffectsMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkEffectsMatcher.java
new file mode 100644
index 00000000..964e7072
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkEffectsMatcher.java
@@ -0,0 +1,60 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.firework;
+
+import org.bukkit.FireworkEffect;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.FireworkMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc.ListMatchingMode;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemFireworkEffectsMatcher implements ItemMatcher {
+ public ItemFireworkEffectsMatcher(List effects, ListMatchingMode mode) {
+ this.effects = effects;
+ this.mode = mode;
+ }
+
+ public List effects;
+ public ListMatchingMode mode;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof FireworkMeta) ||
+ !((FireworkMeta) item.getItemMeta()).hasEffects())
+ return false;
+
+ List effects = ((FireworkMeta) item.getItemMeta()).getEffects();
+ return mode.matches(this.effects, effects);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof FireworkMeta))
+ item.setType(Material.FIREWORK_ROCKET);
+
+ FireworkMeta meta = (FireworkMeta) item.getItemMeta();
+ if (mode == ListMatchingMode.NONE) {
+ meta.clearEffects();
+ // we clear effects because there may be in there an effectmatcher that matches an effect in effects
+ // except, this is pointless because ListMatchingMode can't solve a NONE.
+ }
+
+ List effects = mode.solve(this.effects, new Supplier() {
+ public int index = 0;
+
+ @Override
+ public FireworkEffect get() {
+ return ItemFireworkEffectsCountMatcher.getFireworkEffectWithIndex(index++);
+ }
+ }); // we use the old way of lambdas so we can have the index thing there.
+
+ meta.addEffects(effects);
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkPowerMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkPowerMatcher.java
new file mode 100644
index 00000000..f6ff6185
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/firework/ItemFireworkPowerMatcher.java
@@ -0,0 +1,40 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.firework;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.FireworkMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemFireworkPowerMatcher implements ItemMatcher {
+ public ItemFireworkPowerMatcher(AmountMatcher power) {
+ this.power = power;
+ }
+
+ public AmountMatcher power;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof FireworkMeta))
+ return false;
+
+ return power.matches(((FireworkMeta) item.getItemMeta()).getPower());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof FireworkMeta)) {
+ item.setType(Material.FIREWORK_ROCKET);
+ }
+
+ assert item.getItemMeta() instanceof FireworkMeta;
+
+ FireworkMeta meta = (FireworkMeta) item.getItemMeta();
+ meta.setPower(power.solve(1));
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/ExactlyLore.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/ExactlyLore.java
new file mode 100644
index 00000000..08d44078
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/ExactlyLore.java
@@ -0,0 +1,27 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.lore;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Ameliorate
+ */
+public class ExactlyLore implements LoreMatcher {
+ public ExactlyLore(List lore) {
+ if (lore == null)
+ lore = Collections.emptyList();
+ this.lore = lore;
+ }
+
+ public List lore;
+
+ @Override
+ public boolean matches(List lore) {
+ return this.lore.equals(lore);
+ }
+
+ @Override
+ public List solve(List defaultValue) throws NotSolvableException {
+ return lore;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/ItemLoreMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/ItemLoreMatcher.java
new file mode 100644
index 00000000..01e9338e
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/ItemLoreMatcher.java
@@ -0,0 +1,40 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.lore;
+
+import org.bukkit.Bukkit;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+import java.util.Optional;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemLoreMatcher implements ItemMatcher {
+ public ItemLoreMatcher(LoreMatcher matcher) {
+ this.matcher = matcher;
+ }
+
+ public static ItemLoreMatcher construct(Optional matcher) {
+ return matcher.map(ItemLoreMatcher::new).orElse(null);
+ }
+
+ public LoreMatcher matcher;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !item.getItemMeta().hasLore())
+ return false;
+
+ return matcher.matches(item.getItemMeta().getLore());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ ItemMeta meta = item.hasItemMeta() ? item.getItemMeta() : Bukkit.getItemFactory().getItemMeta(item.getType());
+
+ meta.setLore(matcher.solve(meta.getLore()));
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/LoreMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/LoreMatcher.java
new file mode 100644
index 00000000..34dcb600
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/LoreMatcher.java
@@ -0,0 +1,11 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.lore;
+
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+
+import java.util.List;
+
+/**
+ * @author Ameliorate
+ */
+public interface LoreMatcher extends Matcher> {
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/RegexLore.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/RegexLore.java
new file mode 100644
index 00000000..d132de56
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/lore/RegexLore.java
@@ -0,0 +1,25 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.lore;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * @author Ameliorate
+ */
+public class RegexLore implements LoreMatcher {
+ public RegexLore(Pattern pattern) {
+ this.pattern = pattern;
+ }
+
+ public Pattern pattern;
+
+ @Override
+ public boolean matches(List lore) {
+ return pattern.matcher(String.join("\n", lore)).find();
+ }
+
+ @Override
+ public List solve(List defaultValue) throws NotSolvableException {
+ throw new NotSolvableException("can't solve a regex");
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/CenterMapView.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/CenterMapView.java
new file mode 100644
index 00000000..9b5f6966
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/CenterMapView.java
@@ -0,0 +1,43 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map;
+
+import org.bukkit.map.MapView;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class CenterMapView implements MapViewMatcher {
+ public CenterMapView(AmountMatcher mapLocation, CenterCoordinate centerCoordinate) {
+ this.mapLocation = mapLocation;
+ this.centerCoordinate = centerCoordinate;
+ }
+
+ public AmountMatcher mapLocation;
+ public CenterCoordinate centerCoordinate;
+
+ @Override
+ public boolean matches(MapView map) {
+ int coordinate = centerCoordinate == CenterCoordinate.X ? map.getCenterX() : map.getCenterZ();
+
+ return mapLocation.matches(coordinate);
+ }
+
+ @Override
+ public MapView solve(MapView view) throws NotSolvableException {
+ switch (centerCoordinate) {
+ case X:
+ view.setCenterX(mapLocation.solve(0));
+ break;
+ case Z:
+ view.setCenterZ(mapLocation.solve(0));
+ break;
+ }
+
+ return view;
+ }
+
+ public enum CenterCoordinate {
+ X,
+ Z,
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/IDMapView.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/IDMapView.java
new file mode 100644
index 00000000..1152b822
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/IDMapView.java
@@ -0,0 +1,28 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map;
+
+import org.bukkit.Bukkit;
+import org.bukkit.map.MapView;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class IDMapView implements MapViewMatcher {
+ public IDMapView(AmountMatcher id) {
+ this.id = id;
+ }
+
+ public AmountMatcher id;
+
+ @Override
+ public boolean matches(MapView map) {
+ int id = map.getId();
+ return this.id.matches(id);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public MapView solve(MapView defaultValue) throws NotSolvableException {
+ return Bukkit.getServer().getMap(id.solve(0));
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/IsUnlimitedTrackingMapView.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/IsUnlimitedTrackingMapView.java
new file mode 100644
index 00000000..370149ed
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/IsUnlimitedTrackingMapView.java
@@ -0,0 +1,26 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map;
+
+import org.bukkit.map.MapView;
+
+/**
+ * @author Ameliorate
+ */
+public class IsUnlimitedTrackingMapView implements MapViewMatcher {
+ public IsUnlimitedTrackingMapView(boolean isUmlimitedTracking) {
+ this.isUmlimitedTracking = isUmlimitedTracking;
+ }
+
+ public boolean isUmlimitedTracking;
+
+ @Override
+ public boolean matches(MapView map) {
+ return map.isUnlimitedTracking() == isUmlimitedTracking;
+ }
+
+ @Override
+ public MapView solve(MapView map) throws NotSolvableException {
+ map.setUnlimitedTracking(isUmlimitedTracking);
+
+ return map;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/IsVirtualMapView.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/IsVirtualMapView.java
new file mode 100644
index 00000000..dbf13fd8
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/IsVirtualMapView.java
@@ -0,0 +1,38 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map;
+
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.map.MapCanvas;
+import org.bukkit.map.MapRenderer;
+import org.bukkit.map.MapView;
+
+/**
+ * @author Ameliorate
+ */
+public class IsVirtualMapView implements MapViewMatcher {
+ public IsVirtualMapView(boolean isVirtual) {
+ this.isVirtual = isVirtual;
+ }
+
+ public boolean isVirtual;
+
+ @Override
+ public boolean matches(MapView map) {
+ return map.isVirtual() == isVirtual;
+ }
+
+ @Override
+ public MapView solve(MapView map) throws NotSolvableException {
+ MapView view = Bukkit.createMap(Bukkit.getWorld("world"));
+ view.addRenderer(new MapRenderer() {
+ @Override
+ public void render(MapView mapView, MapCanvas mapCanvas, Player player) {
+ // do nothing
+ }
+ });
+
+ assert map.isVirtual();
+
+ return view;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapColorMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapColorMatcher.java
new file mode 100644
index 00000000..93c411a0
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapColorMatcher.java
@@ -0,0 +1,41 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map;
+
+import org.bukkit.DyeColor;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.MapMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.color.ColorMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemMapColorMatcher implements ItemMatcher {
+ public ItemMapColorMatcher(ColorMatcher color) {
+ this.color = color;
+ }
+
+ public ColorMatcher color;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof MapMeta) ||
+ !((MapMeta) item.getItemMeta()).hasColor())
+ return false;
+
+ return color.matches(((MapMeta) item.getItemMeta()).getColor());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof MapMeta))
+ item.setType(Material.MAP);
+
+ MapMeta meta = (MapMeta) item.getItemMeta();
+ meta.setColor(color.solve(DyeColor.WHITE.getColor()));
+
+ item.setItemMeta(meta);
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapIsScalingMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapIsScalingMatcher.java
new file mode 100644
index 00000000..9b1a6235
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapIsScalingMatcher.java
@@ -0,0 +1,38 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.MapMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemMapIsScalingMatcher implements ItemMatcher {
+ public ItemMapIsScalingMatcher(boolean isScaling) {
+ this.isScaling = isScaling;
+ }
+
+ public boolean isScaling;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || (item.getItemMeta() instanceof MapMeta))
+ return false;
+
+ return ((MapMeta) item.getItemMeta()).isScaling() == isScaling;
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof MapMeta))
+ item.setType(Material.MAP);
+
+ MapMeta meta = (MapMeta) item.getItemMeta();
+ meta.setScaling(isScaling);
+
+ item.setItemMeta(meta);
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapLocationMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapLocationMatcher.java
new file mode 100644
index 00000000..3499ccf0
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapLocationMatcher.java
@@ -0,0 +1,41 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.MapMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name.NameMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemMapLocationMatcher implements ItemMatcher {
+ public ItemMapLocationMatcher(NameMatcher locationName) {
+ this.locationName = locationName;
+ }
+
+ public NameMatcher locationName;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof MapMeta) ||
+ !((MapMeta) item.getItemMeta()).hasLocationName())
+ return false;
+
+ String location = ((MapMeta) item.getItemMeta()).getLocationName();
+ return locationName.matches(location);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof MapMeta))
+ item.setType(Material.MAP);
+
+ MapMeta meta = (MapMeta) item.getItemMeta();
+ meta.setLocationName(locationName.solve(""));
+
+ item.setItemMeta(meta);
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapViewMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapViewMatcher.java
new file mode 100644
index 00000000..6ff79c90
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ItemMapViewMatcher.java
@@ -0,0 +1,43 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.MapMeta;
+import org.bukkit.map.MapView;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemMapViewMatcher implements ItemMatcher {
+ public ItemMapViewMatcher(MapViewMatcher matcher) {
+ this.matcher = matcher;
+ }
+
+ public MapViewMatcher matcher;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof MapMeta) ||
+ !((MapMeta) item.getItemMeta()).hasMapView())
+ return false;
+
+ return matcher.matches(((MapMeta) item.getItemMeta()).getMapView());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof MapMeta))
+ item.setType(Material.MAP);
+
+ MapView view = Bukkit.createMap(Bukkit.getWorld("world"));
+ view = matcher.solve(view);
+
+ MapMeta meta = (MapMeta) item.getItemMeta();
+ meta.setMapView(view);
+ item.setItemMeta(meta);
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/MapViewMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/MapViewMatcher.java
new file mode 100644
index 00000000..d897a9d2
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/MapViewMatcher.java
@@ -0,0 +1,10 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map;
+
+import org.bukkit.map.MapView;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+
+/**
+ * @author Ameliorate
+ */
+public interface MapViewMatcher extends Matcher {
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ScaleMapView.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ScaleMapView.java
new file mode 100644
index 00000000..3893d921
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/ScaleMapView.java
@@ -0,0 +1,27 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map;
+
+import org.bukkit.map.MapView;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher.EnumMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ScaleMapView implements MapViewMatcher {
+ public ScaleMapView(EnumMatcher scale) {
+ this.scale = scale;
+ }
+
+ public EnumMatcher scale;
+
+ @Override
+ public boolean matches(MapView map) {
+ return scale.matches(map.getScale());
+ }
+
+ @Override
+ public MapView solve(MapView view) throws NotSolvableException {
+ view.setScale(scale.solve(MapView.Scale.NORMAL));
+
+ return view;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/WorldMapView.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/WorldMapView.java
new file mode 100644
index 00000000..a0cc72f5
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/map/WorldMapView.java
@@ -0,0 +1,37 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.map;
+
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.map.MapView;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name.NameMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class WorldMapView implements MapViewMatcher {
+ public WorldMapView(NameMatcher world) {
+ this.world = world;
+ }
+
+ public NameMatcher world;
+
+ @Override
+ public boolean matches(MapView map) {
+ World world = map.getWorld();
+
+ return this.world.matches(world.getName());
+ }
+
+ @Override
+ public MapView solve(MapView view) throws NotSolvableException {
+ String worldStr = world.solve("world");
+ World world = Bukkit.getWorld(worldStr);
+
+ if (world == null) {
+ throw new NotSolvableException("world matcher does not solve to a valid world name");
+ }
+
+ view.setWorld(world);
+ return view;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemAttributeMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemAttributeMatcher.java
new file mode 100644
index 00000000..3e0c4b74
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemAttributeMatcher.java
@@ -0,0 +1,124 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+import org.bukkit.Bukkit;
+import org.bukkit.attribute.Attribute;
+import org.bukkit.attribute.AttributeModifier;
+import org.bukkit.inventory.EquipmentSlot;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher.EnumMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name.NameMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.uuid.UUIDMatcher;
+
+import java.util.AbstractMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * Matches over the attributes of an item that apply for a given slot.
+ *
+ * @author Ameliorate
+ */
+public class ItemAttributeMatcher implements ItemMatcher {
+ /**
+ * @param slot May be null, for an item that applies no matter what slot it is in.
+ */
+ public ItemAttributeMatcher(List matchers, EquipmentSlot slot, ListMatchingMode mode) {
+ this.matchers = matchers;
+ this.slot = slot;
+ this.mode = mode;
+
+ matchers.forEach((m) -> m.slot = slot);
+ }
+
+ public List matchers;
+ public EquipmentSlot slot;
+ public ListMatchingMode mode;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !item.getItemMeta().hasAttributeModifiers())
+ return false;
+
+ return mode.matches(matchers, item.getItemMeta().getAttributeModifiers(slot).entries());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ ItemMeta meta = item.hasItemMeta() ? item.getItemMeta() : Bukkit.getItemFactory().getItemMeta(item.getType());
+
+ List> attributes = mode.solve(matchers,
+ () -> new AbstractMap.SimpleEntry<>(Attribute.GENERIC_ARMOR,
+ new AttributeModifier("a", 1, AttributeModifier.Operation.ADD_NUMBER)));
+
+ Multimap attributesMap = HashMultimap.create();
+
+ for (Map.Entry entry : attributes) {
+ attributesMap.put(entry.getKey(), entry.getValue());
+ }
+
+ meta.setAttributeModifiers(attributesMap);
+ item.setItemMeta(meta);
+ return item;
+ }
+
+ public static class AttributeMatcher implements Matcher> {
+ public AttributeMatcher(EnumMatcher attribute,
+ NameMatcher name, EnumMatcher operation,
+ UUIDMatcher uuid, AmountMatcher amount) {
+ this.attribute = attribute;
+ this.name = name;
+ this.operation = operation;
+ this.uuid = uuid;
+ this.amount = amount;
+ }
+
+ public EnumMatcher attribute;
+ public EnumMatcher operation;
+ public NameMatcher name;
+ public UUIDMatcher uuid;
+ public AmountMatcher amount;
+
+ private EquipmentSlot slot;
+
+ public boolean matches(Attribute attribute, AttributeModifier modifier) {
+ if (this.attribute != null && !this.attribute.matches(attribute))
+ return false;
+ else if (name != null && !name.matches(modifier.getName()))
+ return false;
+ else if (operation != null && !operation.matches(modifier.getOperation()))
+ return false;
+ else if (uuid != null && !uuid.matches(modifier.getUniqueId()))
+ return false;
+ else if (amount != null && !amount.matches(modifier.getAmount()))
+ return false;
+ else
+ return true;
+ }
+
+ @Override
+ public boolean matches(Map.Entry matched) {
+ return matches(matched.getKey(), matched.getValue());
+ }
+
+ @Override
+ public Map.Entry solve(Map.Entry entry) throws NotSolvableException {
+ Attribute attribute = this.attribute.solve(entry.getKey());
+
+ AttributeModifier defaultModifier = entry.getValue();
+
+ AttributeModifier.Operation operation = this.operation.solve(defaultModifier.getOperation());
+ String name = this.name.solve(defaultModifier.getName());
+ UUID uuid = this.uuid.solve(defaultModifier.getUniqueId());
+ double amount = this.amount.solve(defaultModifier.getAmount());
+
+ return new AbstractMap.SimpleEntry<>(attribute, new AttributeModifier(uuid, name, amount, operation, slot));
+ }
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemExactlyInventoryMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemExactlyInventoryMatcher.java
new file mode 100644
index 00000000..17ffea26
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemExactlyInventoryMatcher.java
@@ -0,0 +1,72 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+import org.bukkit.Material;
+import org.bukkit.block.Container;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BlockStateMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.ItemMap;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemExpression;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+import java.util.List;
+
+/**
+ * Matches an item if all of the ItemExpressions match the items within the inventory in a 1:1 fashion.
+ *
+ * See also ItemMap.itemExpressionsMatchItems().
+ *
+ * @author Ameliorate
+ */
+public class ItemExactlyInventoryMatcher implements ItemMatcher {
+ public ItemExactlyInventoryMatcher(List itemExpressions) {
+ this.itemExpressions = itemExpressions;
+ }
+
+ public List itemExpressions;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ return getItemHeldInventory(item).itemExpressionsMatchItems(itemExpressions);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof BlockStateMeta) ||
+ !((BlockStateMeta) item.getItemMeta()).hasBlockState() ||
+ !(((BlockStateMeta) item.getItemMeta()).getBlockState() instanceof Container))
+ item.setType(Material.SHULKER_BOX);
+
+ ItemMap map = new ItemMap();
+ for (ItemExpression itemExpression : itemExpressions) {
+ map.addItemAmount(itemExpression.solve(), itemExpression.getAmount(false));
+ }
+
+ BlockStateMeta meta = (BlockStateMeta) item.getItemMeta();
+ Container container = (Container) meta.getBlockState();
+ Inventory inventory = container.getInventory();
+
+ if (!map.fitsIn(inventory))
+ throw new NotSolvableException("doesn't fit inside inventory");
+ map.getItemStackRepresentation().forEach(inventory::addItem);
+
+ meta.setBlockState(container);
+ item.setItemMeta(meta);
+
+ return item;
+ }
+
+ /**
+ * @param item The item to get the inventory of.
+ * @return An ItemMap of the item's inventory.
+ * If the item does not have an inventory or has an empty inventory, returns an empty ItemMap.
+ */
+ public static ItemMap getItemHeldInventory(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof BlockStateMeta) ||
+ !((BlockStateMeta) item.getItemMeta()).hasBlockState() ||
+ !(((BlockStateMeta) item.getItemMeta()).getBlockState() instanceof Container))
+ return new ItemMap();
+ else
+ return new ItemMap(((Container) ((BlockStateMeta) item.getItemMeta()).getBlockState()).getInventory());
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemExactlyStackMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemExactlyStackMatcher.java
new file mode 100644
index 00000000..0263022f
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemExactlyStackMatcher.java
@@ -0,0 +1,31 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+import org.bukkit.inventory.ItemStack;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+public class ItemExactlyStackMatcher implements ItemMatcher {
+ public ItemExactlyStackMatcher(ItemStack itemStack) {
+ this(itemStack, false);
+ }
+
+ public ItemExactlyStackMatcher(ItemStack itemStack, boolean acceptSimilar) {
+ this.itemStack = itemStack;
+ this.acceptSimilar = acceptSimilar;
+ }
+
+ public ItemStack itemStack;
+ public boolean acceptSimilar;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!acceptSimilar)
+ return itemStack.equals(item);
+ else
+ return itemStack.isSimilar(item);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack defaultValue) throws NotSolvableException {
+ return itemStack.clone();
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemExpressionConfigParsingError.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemExpressionConfigParsingError.java
new file mode 100644
index 00000000..b0e5ef34
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemExpressionConfigParsingError.java
@@ -0,0 +1,9 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+/**
+ * When there is some error when parsing an ItemExpression out of the config.
+ *
+ * @author Ameliorate
+ */
+public class ItemExpressionConfigParsingError extends RuntimeException {
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemFlagMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemFlagMatcher.java
new file mode 100644
index 00000000..d3cb28f4
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemFlagMatcher.java
@@ -0,0 +1,43 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+import org.bukkit.Bukkit;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+public class ItemFlagMatcher implements ItemMatcher {
+ public ItemFlagMatcher(ItemFlag flag, boolean setting) {
+ this.flag = flag;
+ this.setting = setting;
+ }
+
+ public ItemFlag flag;
+ public boolean setting;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ boolean setting;
+ if (item.hasItemMeta()) {
+ setting = item.getItemMeta().hasItemFlag(flag);
+ } else {
+ setting = false;
+ // this is okay because all the flags default to false.
+ }
+
+ return this.setting == setting;
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ ItemMeta meta = item.hasItemMeta() ? item.getItemMeta() : Bukkit.getItemFactory().getItemMeta(item.getType());
+
+ if (setting == true)
+ meta.addItemFlags(flag);
+ else
+ meta.removeItemFlags(flag);
+
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemKnowledgeBookMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemKnowledgeBookMatcher.java
new file mode 100644
index 00000000..36ba003e
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemKnowledgeBookMatcher.java
@@ -0,0 +1,76 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.KnowledgeBookMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name.NameMatcher;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemKnowledgeBookMatcher implements ItemMatcher {
+ public ItemKnowledgeBookMatcher(NameMatcher recipeMatcher, boolean requireAllMatch) {
+ this.recipeMatcher = recipeMatcher;
+ this.requireAllMatch = requireAllMatch;
+ }
+
+ public ItemKnowledgeBookMatcher(NameMatcher recipeMatcher) {
+ this(recipeMatcher, false);
+ }
+
+ public static ItemKnowledgeBookMatcher construct(Optional recipeMatcher, boolean requireAllMatch) {
+ return recipeMatcher.map((aRecipeMatcher) -> new ItemKnowledgeBookMatcher(aRecipeMatcher, requireAllMatch))
+ .orElse(null);
+ }
+
+ public NameMatcher recipeMatcher;
+ public boolean requireAllMatch;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof KnowledgeBookMeta) ||
+ !((KnowledgeBookMeta) item.getItemMeta()).hasRecipes())
+ return false;
+
+ List recipes = ((KnowledgeBookMeta) item.getItemMeta()).getRecipes().stream()
+ .map(NamespacedKey::toString).collect(Collectors.toList());
+
+ Stream recipesStream = recipes.stream();
+
+ if (requireAllMatch)
+ return recipesStream.allMatch(recipeMatcher::matches);
+ else
+ return recipesStream.anyMatch(recipeMatcher::matches);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof KnowledgeBookMeta))
+ item.setType(Material.KNOWLEDGE_BOOK);
+
+ KnowledgeBookMeta meta = (KnowledgeBookMeta) item.getItemMeta();
+
+ List recipes = meta.getRecipes();
+ if (requireAllMatch)
+ recipes.clear();
+
+ String defaultValue = recipes.isEmpty() ? "" : recipes.get(0).toString();
+ String[] solved = recipeMatcher.solve(defaultValue).split(":", 2);
+ String namespace = solved[0];
+ String str = solved[1];
+
+ recipes.add(new NamespacedKey(namespace, str));
+
+ meta.setRecipes(recipes);
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemLeatherArmorColorMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemLeatherArmorColorMatcher.java
new file mode 100644
index 00000000..25214e0b
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemLeatherArmorColorMatcher.java
@@ -0,0 +1,48 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+import org.bukkit.Color;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.LeatherArmorMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.color.ColorMatcher;
+
+import java.util.Optional;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemLeatherArmorColorMatcher implements ItemMatcher {
+ public ItemLeatherArmorColorMatcher(ColorMatcher color) {
+ this.color = color;
+ }
+
+ public static ItemLeatherArmorColorMatcher construct(Optional color) {
+ return color.map(ItemLeatherArmorColorMatcher::new).orElse(null);
+ }
+
+ public ColorMatcher color;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof LeatherArmorMeta))
+ return false;
+
+ Color leatherColor = ((LeatherArmorMeta) item.getItemMeta()).getColor();
+ return color.matches(leatherColor);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof LeatherArmorMeta))
+ item.setType(Material.LEATHER_CHESTPLATE);
+
+ assert item.getItemMeta() instanceof LeatherArmorMeta;
+
+ LeatherArmorMeta meta = (LeatherArmorMeta) item.getItemMeta();
+
+ meta.setColor(color.solve(meta.getColor()));
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemMaterialMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemMaterialMatcher.java
new file mode 100644
index 00000000..5f4b1ad4
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemMaterialMatcher.java
@@ -0,0 +1,34 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher.EnumMatcher;
+
+import java.util.Optional;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemMaterialMatcher implements ItemMatcher {
+ public ItemMaterialMatcher(EnumMatcher matcher) {
+ this.matcher = matcher;
+ }
+
+ public static ItemMaterialMatcher construct(Optional> matcher) {
+ return matcher.map(ItemMaterialMatcher::new).orElse(null);
+ }
+
+ public EnumMatcher matcher;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ return matcher.matches(item.getType());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ item.setType(matcher.solve(item.getType()));
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemShulkerBoxColorMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemShulkerBoxColorMatcher.java
new file mode 100644
index 00000000..f922a513
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemShulkerBoxColorMatcher.java
@@ -0,0 +1,65 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import org.bukkit.DyeColor;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher.EnumMatcher;
+
+import java.util.Optional;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemShulkerBoxColorMatcher implements ItemMatcher {
+ public ItemShulkerBoxColorMatcher(EnumMatcher color) {
+ this.color = color;
+ }
+
+ public static ItemShulkerBoxColorMatcher construct(Optional> color) {
+ return color.map(ItemShulkerBoxColorMatcher::new).orElse(null);
+ }
+
+ public EnumMatcher color;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!colorsShulkerBox.containsKey(item.getType()))
+ return false;
+
+ return this.color.matches(colorsShulkerBox.get(item.getType()));
+ }
+
+ private static BiMap shulkerBoxColors = HashBiMap.create();
+ private static BiMap colorsShulkerBox;
+
+ static {
+ shulkerBoxColors.put(DyeColor.BLACK, Material.BLACK_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.BLUE, Material.BLUE_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.BROWN, Material.BROWN_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.CYAN, Material.CYAN_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.GRAY, Material.GRAY_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.GREEN, Material.GREEN_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.LIGHT_BLUE, Material.LIGHT_BLUE_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.LIGHT_GRAY, Material.LIGHT_GRAY_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.LIME, Material.LIME_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.MAGENTA, Material.MAGENTA_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.ORANGE, Material.ORANGE_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.PINK, Material.PINK_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.PURPLE, Material.PURPLE_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.RED, Material.RED_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.WHITE, Material.WHITE_SHULKER_BOX);
+ shulkerBoxColors.put(DyeColor.YELLOW, Material.YELLOW_SHULKER_BOX);
+
+ colorsShulkerBox = shulkerBoxColors.inverse();
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ DyeColor color = this.color.solve(DyeColor.PURPLE);
+ item.setType(shulkerBoxColors.get(color));
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemSkullMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemSkullMatcher.java
new file mode 100644
index 00000000..d3a262ff
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemSkullMatcher.java
@@ -0,0 +1,58 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.SkullMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.uuid.UUIDMatcher;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemSkullMatcher implements ItemMatcher {
+ public ItemSkullMatcher(List ownerMatcher) {
+ this.ownerMatcher = ownerMatcher;
+ }
+
+ public static ItemSkullMatcher construct(List ownerMatcher) {
+ if (ownerMatcher == null || ownerMatcher.isEmpty())
+ return null;
+
+ return new ItemSkullMatcher(ownerMatcher);
+ }
+
+ public List ownerMatcher;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof SkullMeta))
+ return false;
+ UUID owner;
+ SkullMeta meta = (SkullMeta) item.getItemMeta();
+ if (!meta.hasOwner())
+ owner = new UUID(0, 0);
+ else
+ owner = meta.getOwningPlayer().getUniqueId();
+ return ownerMatcher.stream().anyMatch((matcher) -> matcher.matches(owner));
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ UUID uuid = ListMatchingMode.ANY.solve(ownerMatcher,
+ () -> new UUID(0, 0))
+ .get(0);
+
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof SkullMeta))
+ item.setType(Material.PLAYER_HEAD);
+ assert item.getItemMeta() instanceof SkullMeta;
+
+ SkullMeta meta = (SkullMeta) item.getItemMeta();
+ meta.setOwningPlayer(Bukkit.getOfflinePlayer(uuid));
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemUnbreakableMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemUnbreakableMatcher.java
new file mode 100644
index 00000000..1d7361e2
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ItemUnbreakableMatcher.java
@@ -0,0 +1,36 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+import org.bukkit.Bukkit;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemUnbreakableMatcher implements ItemMatcher {
+ public ItemUnbreakableMatcher(boolean unbreakable) {
+ this.unbreakable = unbreakable;
+ }
+
+ public boolean unbreakable;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ boolean isUnbreakable = false;
+ if (item.hasItemMeta())
+ // an item without metadata can not be unbreakable
+ isUnbreakable = item.getItemMeta().isUnbreakable();
+ return isUnbreakable == unbreakable;
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ ItemMeta meta = item.hasItemMeta() ? item.getItemMeta() : Bukkit.getItemFactory().getItemMeta(item.getType());
+
+ meta.setUnbreakable(unbreakable);
+
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ListMatchingMode.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ListMatchingMode.java
new file mode 100644
index 00000000..f676e35e
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/misc/ListMatchingMode.java
@@ -0,0 +1,230 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc;
+
+import com.google.common.collect.Lists;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+
+import java.util.*;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+/**
+ * Represents the different ways of interpreting a list of things when comparing it to another list.
+ *
+ * @author Ameliorate
+ */
+public enum ListMatchingMode {
+ /**
+ * At least one element in the list must match an element in the other list.
+ */
+ ANY("Any", "any"),
+
+ /**
+ * Every one of the elements in the list must match an element in the other list.
+ */
+ ALL("All", "all"),
+
+ /**
+ * No element in the list may match an element in the other list.
+ */
+ NONE("None", "none"),
+
+ /**
+ * Try to match each element to another element in the other list 1:1, where no element in either list has a
+ * counterpart in the other list. Returns true when every item in the first list matched an item in the second
+ * list.
+ *
+ * This is like ALL, but in ALL an item in the first list can match multiple items in the second list. In this mode,
+ * an item in the first list can only match one item in the second list.
+ */
+ ONE_TO_ONE("OneToOne", "oneToOne");
+
+ ListMatchingMode(String upperCamelCase, String lowerCamelCase) {
+ this.upperCamelCase = upperCamelCase;
+ this.lowerCamelCase = lowerCamelCase;
+ }
+
+ /**
+ * Returns the name of this mode, in CamelCase starting with an upper case letter.
+ *
+ * For example: ONE_TO_ONE becomes OneToOne.
+ *
+ * @return The name of the mode, in UpperCammelCase.
+ */
+ public String getUpperCamelCase() {
+ return upperCamelCase;
+ }
+
+ /**
+ * Returns the name of this mode, in CamelCase starting with a lower case letter.
+ *
+ * For example: ONE_TO_ONE becomes oneToOne.
+ *
+ * @return The name of the mode, in lowerCamelCase.
+ */
+ public String getLowerCamelCase() {
+ return lowerCamelCase;
+ }
+
+ private String upperCamelCase;
+ private String lowerCamelCase;
+
+ /**
+ * Generic function to exxert this ListMatchingMode in matching a list of things using a list of matchers.
+ *
+ * @param matchers The list of matchers that will match over elements of the list of things.
+ * @param matched The list of things that the list of matchers will match over.
+ * @param The type of the list of things.
+ * @param The type of each matcher.
+ * @return If the list of matchers matched over the list of things, in the order that was defined in this ListMatchingMode.
+ */
+ public > boolean matches(Collection matchers, Collection matched) {
+ Stream matcherStream = matchers.stream();
+ Predicate matchedPredicate = (matcher) -> matched.stream().anyMatch(matcher::matches);
+
+ switch (this) {
+ case ANY:
+ return matcherStream.anyMatch(matchedPredicate);
+ case ALL:
+ return matcherStream.allMatch(matchedPredicate);
+ case NONE:
+ return matcherStream.noneMatch(matchedPredicate);
+ case ONE_TO_ONE:
+ List matchersClone = new ArrayList<>(matchers);
+ List matchedClone = new ArrayList<>(matched);
+
+ for (M matcher : Lists.reverse(matchersClone)) {
+ boolean hasMatched = false;
+
+ for (T matchedElement : matchedClone) {
+ if (matcher.matches(matchedElement)) {
+ matchedClone.remove(matchedElement);
+ matchersClone.remove(matcher);
+ hasMatched = true;
+ break;
+ }
+ }
+
+ if (!hasMatched)
+ return false;
+ }
+
+ return matchersClone.isEmpty();
+ }
+
+ throw new AssertionError("not reachable");
+ }
+
+ public > List solve(Collection matchers, Supplier defaultValue)
+ throws Matcher.NotSolvableException {
+ ArrayList result = new ArrayList<>();
+
+ switch (this) {
+ case ONE_TO_ONE:
+ case ALL:
+ for (M matcher : matchers) {
+ result.add(matcher.solve(defaultValue.get()));
+ }
+
+ break;
+
+ case ANY:
+ List causes = new ArrayList<>();
+ boolean hasSolved = false;
+ for (Matcher matcher : matchers) {
+ try {
+ result.add(matcher.solve(defaultValue.get()));
+ hasSolved = true;
+ break;
+ } catch (Matcher.NotSolvableException e) {
+ causes.add(e);
+ }
+ }
+
+ if (!hasSolved) {
+ Matcher.NotSolvableException e = new Matcher.NotSolvableException("while solving for any matching");
+ causes.forEach(e::addSuppressed);
+ throw e;
+ }
+
+ break;
+
+ case NONE:
+ throw new Matcher.NotSolvableException("can't yet do negative solving");
+ }
+
+ return result;
+ }
+
+ // TODO: how to make this class for non-entry:
+ // use object reflection to optain all fields of defaultValue. use reflection to obtain all fields of returnedValue.
+ // use Object.== operator to compare reference equality. Consiter an entry taken if any of the feilds between the
+ // two are equal.
+ public static class LazyFromListEntrySupplier implements Supplier> {
+ public LazyFromListEntrySupplier(Supplier>> entriesSupplier) {
+ this.entriesSupplier = entriesSupplier;
+ }
+
+ public LazyFromListEntrySupplier(Collection> collection) {
+ this(() -> new ArrayList<>(collection));
+ if (collection.isEmpty())
+ throw new AssertionError("collection can not be empty");
+ }
+
+ public LazyFromListEntrySupplier(Map map) {
+ this(new HashMap<>(map).entrySet());
+ if (map.isEmpty())
+ throw new AssertionError("map can not be empty");
+ }
+
+ private void regen() {
+ if (entries == null || entries.isEmpty())
+ entries = entriesSupplier.get();
+ if (entries.isEmpty())
+ throw new AssertionError("entries can not be empty");
+ }
+
+ public Collection> entries;
+ public Supplier>> entriesSupplier;
+
+ @Override
+ public Map.Entry get() {
+ return new Map.Entry() {
+ private K e;
+ private V l;
+ private boolean taken = false;
+
+ private void evaluate() {
+ if (!taken) {
+ regen();
+ Map.Entry entry = entries.stream().limit(1).findFirst()
+ .orElseThrow(NullPointerException::new);
+ entries.remove(entry);
+
+ e = entry.getKey();
+ l = entry.getValue();
+
+ taken = true;
+ }
+ }
+
+ @Override
+ public K getKey() {
+ evaluate();
+ return e;
+ }
+
+ @Override
+ public V getValue() {
+ evaluate();
+ return l;
+ }
+
+ @Override
+ public V setValue(V value) {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerDelayMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerDelayMatcher.java
new file mode 100644
index 00000000..7847afbd
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerDelayMatcher.java
@@ -0,0 +1,40 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.mobspawner;
+
+import org.bukkit.block.CreatureSpawner;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BlockStateMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+/**
+ * @author Amelorate
+ */
+public class ItemMobSpawnerDelayMatcher implements ItemMatcher {
+ public ItemMobSpawnerDelayMatcher(AmountMatcher delay) {
+ this.delay = delay;
+ }
+
+ public AmountMatcher delay;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!MobSpawnerUtil.isMobSpawner(item))
+ return false;
+
+ return delay.matches(MobSpawnerUtil.getMobSpawnerState(item).getDelay());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ MobSpawnerUtil.setToMobSpawner(item);
+ CreatureSpawner spawner = MobSpawnerUtil.getMobSpawnerState(item);
+
+ spawner.setDelay(delay.solve(0));
+
+ BlockStateMeta meta = (BlockStateMeta) item.getItemMeta();
+ meta.setBlockState(spawner);
+ item.setItemMeta(meta);
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerMaxNearbyEntitiesMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerMaxNearbyEntitiesMatcher.java
new file mode 100644
index 00000000..0dfe7599
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerMaxNearbyEntitiesMatcher.java
@@ -0,0 +1,42 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.mobspawner;
+
+import org.bukkit.block.CreatureSpawner;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BlockStateMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemMobSpawnerMaxNearbyEntitiesMatcher implements ItemMatcher {
+ public ItemMobSpawnerMaxNearbyEntitiesMatcher(AmountMatcher maxNearbyEntities) {
+ this.maxNearbyEntities = maxNearbyEntities;
+ }
+
+ public AmountMatcher maxNearbyEntities;
+
+ private final int DEFAULT_MAX_NEARBY_ENTITIES = 6;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!MobSpawnerUtil.isMobSpawner(item))
+ return false;
+
+ return maxNearbyEntities.matches(MobSpawnerUtil.getMobSpawnerState(item).getMaxNearbyEntities());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ MobSpawnerUtil.setToMobSpawner(item);
+ CreatureSpawner spawner = MobSpawnerUtil.getMobSpawnerState(item);
+
+ spawner.setMaxNearbyEntities(maxNearbyEntities.solve(DEFAULT_MAX_NEARBY_ENTITIES));
+
+ BlockStateMeta meta = (BlockStateMeta) item.getItemMeta();
+ meta.setBlockState(spawner);
+ item.setItemMeta(meta);
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerRequiredPlayerRangeMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerRequiredPlayerRangeMatcher.java
new file mode 100644
index 00000000..200e6aa4
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerRequiredPlayerRangeMatcher.java
@@ -0,0 +1,42 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.mobspawner;
+
+import org.bukkit.block.CreatureSpawner;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BlockStateMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemMobSpawnerRequiredPlayerRangeMatcher implements ItemMatcher {
+ public ItemMobSpawnerRequiredPlayerRangeMatcher(AmountMatcher range) {
+ this.range = range;
+ }
+
+ public AmountMatcher range;
+
+ private final int DEFAULT_REQUIRED_PLAYER_RANGE = 16;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!MobSpawnerUtil.isMobSpawner(item))
+ return false;
+
+ return range.matches(MobSpawnerUtil.getMobSpawnerState(item).getRequiredPlayerRange());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ MobSpawnerUtil.setToMobSpawner(item);
+ CreatureSpawner spawner = MobSpawnerUtil.getMobSpawnerState(item);
+
+ spawner.setRequiredPlayerRange(range.solve(DEFAULT_REQUIRED_PLAYER_RANGE));
+
+ BlockStateMeta meta = (BlockStateMeta) item.getItemMeta();
+ meta.setBlockState(spawner);
+ item.setItemMeta(meta);
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnCountMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnCountMatcher.java
new file mode 100644
index 00000000..8432be16
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnCountMatcher.java
@@ -0,0 +1,42 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.mobspawner;
+
+import org.bukkit.block.CreatureSpawner;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BlockStateMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemMobSpawnerSpawnCountMatcher implements ItemMatcher {
+ public ItemMobSpawnerSpawnCountMatcher(AmountMatcher spawnCount) {
+ this.spawnCount = spawnCount;
+ }
+
+ public AmountMatcher spawnCount;
+
+ private final int DEFAULT_SPAWN_COUNT = 4;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!MobSpawnerUtil.isMobSpawner(item))
+ return false;
+
+ return spawnCount.matches(MobSpawnerUtil.getMobSpawnerState(item).getSpawnCount());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ MobSpawnerUtil.setToMobSpawner(item);
+ CreatureSpawner spawner = MobSpawnerUtil.getMobSpawnerState(item);
+
+ spawner.setSpawnCount(spawnCount.solve(DEFAULT_SPAWN_COUNT));
+
+ BlockStateMeta meta = (BlockStateMeta) item.getItemMeta();
+ meta.setBlockState(spawner);
+ item.setItemMeta(meta);
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnDelayMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnDelayMatcher.java
new file mode 100644
index 00000000..59d56ac6
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnDelayMatcher.java
@@ -0,0 +1,60 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.mobspawner;
+
+import org.bukkit.block.CreatureSpawner;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BlockStateMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemMobSpawnerSpawnDelayMatcher implements ItemMatcher {
+ public ItemMobSpawnerSpawnDelayMatcher(AmountMatcher spawnDelay, MinMax source) {
+ this.spawnDelay = spawnDelay;
+ this.source = source;
+ }
+
+ public AmountMatcher spawnDelay;
+ public MinMax source;
+
+ private final int DEFAULT_MIN_SPAWN_DELAY = 200; // ticks, 10 seconds
+ private final int DEFAULT_MAX_SPAWN_DELAY = 799; // ticks, 39.95 seconds
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!MobSpawnerUtil.isMobSpawner(item))
+ return false;
+
+ CreatureSpawner spawner = MobSpawnerUtil.getMobSpawnerState(item);
+ int spawnDelay = source == MinMax.MAX ? spawner.getMaxSpawnDelay() : spawner.getMinSpawnDelay();
+
+ return this.spawnDelay.matches(spawnDelay);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ MobSpawnerUtil.setToMobSpawner(item);
+ CreatureSpawner spawner = MobSpawnerUtil.getMobSpawnerState(item);
+
+ switch (source) {
+ case MIN:
+ spawner.setMinSpawnDelay(spawnDelay.solve(DEFAULT_MIN_SPAWN_DELAY));
+ break;
+ case MAX:
+ spawner.setMaxSpawnDelay(spawnDelay.solve(DEFAULT_MAX_SPAWN_DELAY));
+ break;
+ }
+
+ BlockStateMeta meta = (BlockStateMeta) item.getItemMeta();
+ meta.setBlockState(spawner);
+ item.setItemMeta(meta);
+
+ return item;
+ }
+
+ public enum MinMax {
+ MIN,
+ MAX,
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnRadiusMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnRadiusMatcher.java
new file mode 100644
index 00000000..cec40a8d
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnRadiusMatcher.java
@@ -0,0 +1,42 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.mobspawner;
+
+import org.bukkit.block.CreatureSpawner;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BlockStateMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemMobSpawnerSpawnRadiusMatcher implements ItemMatcher {
+ public ItemMobSpawnerSpawnRadiusMatcher(AmountMatcher spawnRadius) {
+ this.spawnRadius = spawnRadius;
+ }
+
+ public AmountMatcher spawnRadius;
+
+ private final int DEFAULT_SPAWN_RADIUS = 3;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!MobSpawnerUtil.isMobSpawner(item))
+ return false;
+
+ return spawnRadius.matches(MobSpawnerUtil.getMobSpawnerState(item).getSpawnRange());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ MobSpawnerUtil.setToMobSpawner(item);
+ CreatureSpawner spawner = MobSpawnerUtil.getMobSpawnerState(item);
+
+ spawner.setSpawnRange(spawnRadius.solve(DEFAULT_SPAWN_RADIUS));
+
+ BlockStateMeta meta = (BlockStateMeta) item.getItemMeta();
+ meta.setBlockState(spawner);
+ item.setItemMeta(meta);
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnedMobMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnedMobMatcher.java
new file mode 100644
index 00000000..3a1c7fe8
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/ItemMobSpawnerSpawnedMobMatcher.java
@@ -0,0 +1,41 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.mobspawner;
+
+import org.bukkit.block.CreatureSpawner;
+import org.bukkit.entity.EntityType;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BlockStateMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher.EnumMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemMobSpawnerSpawnedMobMatcher implements ItemMatcher {
+ public ItemMobSpawnerSpawnedMobMatcher(EnumMatcher spawned) {
+ this.spawned = spawned;
+ }
+
+ public EnumMatcher spawned;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!MobSpawnerUtil.isMobSpawner(item))
+ return false;
+
+ return spawned.matches(MobSpawnerUtil.getMobSpawnerState(item).getSpawnedType());
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ MobSpawnerUtil.setToMobSpawner(item);
+ CreatureSpawner spawner = MobSpawnerUtil.getMobSpawnerState(item);
+
+ spawner.setSpawnedType(spawned.solve(EntityType.PIG));
+
+ BlockStateMeta meta = (BlockStateMeta) item.getItemMeta();
+ meta.setBlockState(spawner);
+ item.setItemMeta(meta);
+
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/MobSpawnerUtil.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/MobSpawnerUtil.java
new file mode 100644
index 00000000..92782b0e
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/mobspawner/MobSpawnerUtil.java
@@ -0,0 +1,58 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.mobspawner;
+
+import org.bukkit.Material;
+import org.bukkit.block.CreatureSpawner;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.BlockStateMeta;
+
+/**
+ * Utility class for dealing with Creature Spawners.
+ *
+ * This is mostly ment for ItemExpressions matching over mob spawners.
+ *
+ * @author Ameliorate
+ */
+public class MobSpawnerUtil {
+ /**
+ * Checks if a given item holds a BlockState, and if it does if that BlockState holds a CreatureSpawner.
+ *
+ * @param item The item that may or may not hold a CreatureSpawner
+ * @return true if the item holds a CreatureSpawner, false otherwise
+ */
+ public static boolean isMobSpawner(ItemStack item) {
+ if (!item.hasItemMeta())
+ return false;
+
+ if (!(item.getItemMeta() instanceof BlockStateMeta))
+ return false;
+
+ if (!((BlockStateMeta) item.getItemMeta()).hasBlockState())
+ return false;
+
+ return ((BlockStateMeta) item.getItemMeta()).getBlockState() instanceof CreatureSpawner;
+ }
+
+ /**
+ * Gets a CreatureSpawner from an ItemStack, by getting a stored BlockState from the item's meta, and then
+ * casting that BlockState to a CreatureSpawner.
+ *
+ * @param item The item to get a CreatureSpawner from.
+ * @return The CreatureSpawner the ItemStack was holding.
+ * @throws IllegalArgumentException If the item does not hold a CreatureSpawner
+ */
+ public static CreatureSpawner getMobSpawnerState(ItemStack item) {
+ if (!isMobSpawner(item))
+ throw new IllegalArgumentException("item is not a mob spawner");
+
+ BlockStateMeta meta = (BlockStateMeta) item.getItemMeta();
+
+ return (CreatureSpawner) meta.getBlockState();
+ }
+
+ public static void setToMobSpawner(ItemStack item) {
+ if (isMobSpawner(item))
+ return;
+
+ item.setType(Material.SPAWNER);
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/AnyName.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/AnyName.java
new file mode 100644
index 00000000..7f774df5
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/AnyName.java
@@ -0,0 +1,16 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name;
+
+/**
+ * @author Ameliorate
+ */
+public class AnyName implements NameMatcher {
+ @Override
+ public boolean matches(String matched) {
+ return true;
+ }
+
+ @Override
+ public String solve(String defaultValue) throws NotSolvableException {
+ return defaultValue;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/ExactlyName.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/ExactlyName.java
new file mode 100644
index 00000000..843ffeb7
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/ExactlyName.java
@@ -0,0 +1,31 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name;
+
+/**
+ * @author Ameliorate
+ */
+public class ExactlyName implements NameMatcher {
+ public ExactlyName(String name) {
+ this.name = name;
+ }
+
+ public ExactlyName(String name, boolean caseSensitive) {
+ this.name = name;
+ this.caseSensitive = caseSensitive;
+ }
+
+ public String name;
+ public boolean caseSensitive = true;
+
+ @Override
+ public boolean matches(String name) {
+ if (caseSensitive)
+ return this.name.equals(name);
+ else
+ return this.name.equals(name.toLowerCase());
+ }
+
+ @Override
+ public String solve(String defaultValue) throws NotSolvableException {
+ return name;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/ItemNameMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/ItemNameMatcher.java
new file mode 100644
index 00000000..4dfb7f5e
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/ItemNameMatcher.java
@@ -0,0 +1,46 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name;
+
+import org.bukkit.Bukkit;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+
+import java.util.Optional;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemNameMatcher implements ItemMatcher {
+ public ItemNameMatcher(NameMatcher matcher) {
+ this.matcher = matcher;
+ }
+
+ public static ItemNameMatcher construct(Optional matcher) {
+ return matcher.map(ItemNameMatcher::new).orElse(null);
+ }
+
+ public NameMatcher matcher;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ String name;
+ if (!item.hasItemMeta() || !item.getItemMeta().hasDisplayName())
+ name = "";
+ else
+ name = item.getItemMeta().getDisplayName();
+ return matcher.matches(name);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ ItemMeta meta;
+ if (!item.hasItemMeta())
+ meta = Bukkit.getItemFactory().getItemMeta(item.getType());
+ else
+ meta = item.getItemMeta();
+
+ meta.setDisplayName(matcher.solve(meta.getDisplayName()));
+ item.setItemMeta(meta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/NameMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/NameMatcher.java
new file mode 100644
index 00000000..87935352
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/NameMatcher.java
@@ -0,0 +1,9 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name;
+
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+
+/**
+ * @author Ameliorate
+ */
+public interface NameMatcher extends Matcher {
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/RegexName.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/RegexName.java
new file mode 100644
index 00000000..b0484cef
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/RegexName.java
@@ -0,0 +1,24 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name;
+
+import java.util.regex.Pattern;
+
+/**
+ * @author Ameliorate
+ */
+public class RegexName implements NameMatcher {
+ public RegexName(Pattern regex) {
+ this.regex = regex;
+ }
+
+ public Pattern regex;
+
+ @Override
+ public boolean matches(String name) {
+ return regex.matcher(name).matches();
+ }
+
+ @Override
+ public String solve(String defaultValue) throws NotSolvableException {
+ throw new NotSolvableException("can't solve a regex");
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/VanillaName.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/VanillaName.java
new file mode 100644
index 00000000..af6abbc1
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/name/VanillaName.java
@@ -0,0 +1,20 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.name;
+
+/**
+ * Makes sure the name is what shows up when you have not renamed the item.
+ *
+ * Actually just checks for empty string.
+ *
+ * @author Ameliorate
+ */
+public class VanillaName implements NameMatcher {
+ @Override
+ public boolean matches(String name) {
+ return name.isEmpty();
+ }
+
+ @Override
+ public String solve(String defaultValue) throws NotSolvableException {
+ return "";
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/AnyPotionEffect.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/AnyPotionEffect.java
new file mode 100644
index 00000000..bbde69d0
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/AnyPotionEffect.java
@@ -0,0 +1,30 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.potion;
+
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+/**
+ * Matches any potion type, as long as the level matches the level matcher.
+ *
+ * @author Ameliorate
+ */
+public class AnyPotionEffect implements PotionEffectMatcher {
+ public AnyPotionEffect(AmountMatcher level, AmountMatcher duration) {
+ this.level = level;
+ this.duration = duration;
+ }
+
+ public AmountMatcher level;
+ public AmountMatcher duration;
+
+ @Override
+ public boolean matches(PotionEffect effect) {
+ return level.matches(effect.getAmplifier()) && duration.matches(effect.getDuration());
+ }
+
+ @Override
+ public PotionEffect solve(PotionEffect defaultValue) throws NotSolvableException {
+ return new PotionEffect(PotionEffectType.HEAL, 0, 0);
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/ExactlyPotionEffect.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/ExactlyPotionEffect.java
new file mode 100644
index 00000000..ee7f0b38
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/ExactlyPotionEffect.java
@@ -0,0 +1,34 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.potion;
+
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.amount.AmountMatcher;
+
+/**
+ * @author Ameliorate
+ */
+public class ExactlyPotionEffect implements PotionEffectMatcher {
+ public ExactlyPotionEffect(PotionEffectType type, AmountMatcher level, AmountMatcher duration) {
+ this.type = type;
+ this.level = level;
+ this.duration = duration;
+ }
+
+ public PotionEffectType type;
+ public AmountMatcher level;
+ public AmountMatcher duration;
+
+ @Override
+ public boolean matches(PotionEffect effect) {
+ return type.equals(effect.getType()) &&
+ level.matches(effect.getAmplifier()) &&
+ duration.matches(effect.getDuration());
+ }
+
+ @Override
+ public PotionEffect solve(PotionEffect defaultValue) throws NotSolvableException {
+ return new PotionEffect(type,
+ (int) (double) level.solve(0.0),
+ (int) (double) duration.solve(0.0));
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/ItemPotionBaseEffectMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/ItemPotionBaseEffectMatcher.java
new file mode 100644
index 00000000..78376fe1
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/ItemPotionBaseEffectMatcher.java
@@ -0,0 +1,76 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.potion;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.PotionMeta;
+import org.bukkit.potion.PotionData;
+import org.bukkit.potion.PotionType;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher.EnumMatcher;
+
+import java.util.Optional;
+
+/**
+ * Matches the "base" effect of a potion.
+ *
+ * A potion obtainable in vanilla survival singleplayer is represented by this class, storing only the type,
+ * and a boolean of if it's upgraded and if it's extended.
+ *
+ * Notably, this class as well as vanilla does not expose a duration. Instead the duration is calculated from the
+ * type and extended boolean.
+ *
+ * @author Ameliorate
+ */
+public class ItemPotionBaseEffectMatcher implements ItemMatcher {
+ public ItemPotionBaseEffectMatcher(EnumMatcher type, Optional isExtended, Optional isUpgraded) {
+ this.type = type;
+ this.isExtended = isExtended;
+ this.isUpgraded = isUpgraded;
+ }
+
+ public EnumMatcher type;
+ public Optional isExtended;
+ public Optional isUpgraded;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof PotionMeta))
+ return false;
+
+ PotionData data = ((PotionMeta) item.getItemMeta()).getBasePotionData();
+
+ PotionType type = data.getType();
+ boolean extended = data.isExtended();
+ boolean upgraded = data.isUpgraded();
+
+ if (isExtended.isPresent()) {
+ if (extended != isExtended.get())
+ return false;
+ }
+
+ if (isUpgraded.isPresent()) {
+ if (upgraded != isUpgraded.get())
+ return false;
+ }
+
+ return this.type.matches(type);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (item.getType() != Material.POTION &&
+ item.getType() != Material.SPLASH_POTION &&
+ item.getType() != Material.LINGERING_POTION)
+ item.setType(Material.POTION);
+
+ PotionMeta potionMeta = (PotionMeta) item.getItemMeta();
+
+ PotionType type = this.type.solve(PotionType.AWKWARD);
+ boolean isExtended = this.isExtended.orElse(false);
+ boolean isUpgraded = this.isUpgraded.orElse(false);
+
+ potionMeta.setBasePotionData(new PotionData(type, isExtended, isUpgraded));
+ item.setItemMeta(potionMeta);
+ return item;
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/ItemPotionEffectsMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/ItemPotionEffectsMatcher.java
new file mode 100644
index 00000000..f139c5ef
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/ItemPotionEffectsMatcher.java
@@ -0,0 +1,62 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.potion;
+
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.PotionMeta;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.misc.ListMatchingMode;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemPotionEffectsMatcher implements ItemMatcher {
+ public ItemPotionEffectsMatcher(List potionMatchers, ListMatchingMode mode) {
+ if (potionMatchers.isEmpty())
+ throw new IllegalArgumentException("potionMatchers can not be empty. If an empty potionMatchers " +
+ "was allowed, it would cause many subtle logic errors.");
+ this.potionMatchers = potionMatchers;
+ this.mode = mode;
+ }
+
+ public List potionMatchers;
+ public ListMatchingMode mode;
+
+ @Override
+ public boolean matches(ItemStack item) {
+ if (!item.hasItemMeta() || !(item.getItemMeta() instanceof PotionMeta))
+ return false;
+
+ PotionMeta potion = (PotionMeta) item.getItemMeta();
+
+ List effects = new ArrayList<>();
+ if (potion.hasCustomEffects())
+ effects.addAll(potion.getCustomEffects());
+
+ return matches(effects);
+ }
+
+ @Override
+ public ItemStack solve(ItemStack item) throws NotSolvableException {
+ if (item.getType() != Material.POTION &&
+ item.getType() != Material.SPLASH_POTION &&
+ item.getType() != Material.LINGERING_POTION)
+ item.setType(Material.POTION);
+
+ List effects = mode.solve(potionMatchers,
+ () -> new PotionEffect(PotionEffectType.HEAL, 0, 0));
+
+ PotionMeta meta = (PotionMeta) item.getItemMeta();
+ effects.forEach(potionEffect -> meta.addCustomEffect(potionEffect, true));
+ item.setItemMeta(meta);
+ return item;
+ }
+
+ public boolean matches(List effects) {
+ return mode.matches(potionMatchers, effects);
+ }
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/PotionEffectMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/PotionEffectMatcher.java
new file mode 100644
index 00000000..46aab89e
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/potion/PotionEffectMatcher.java
@@ -0,0 +1,10 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.potion;
+
+import org.bukkit.potion.PotionEffect;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher;
+
+/**
+ * @author Ameliorate
+ */
+public interface PotionEffectMatcher extends Matcher {
+}
diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/tropicalbucket/ItemTropicFishBBodyColorMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/tropicalbucket/ItemTropicFishBBodyColorMatcher.java
new file mode 100644
index 00000000..a9d89c37
--- /dev/null
+++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/tropicalbucket/ItemTropicFishBBodyColorMatcher.java
@@ -0,0 +1,46 @@
+package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.tropicalbucket;
+
+import org.bukkit.DyeColor;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.TropicalFishBucketMeta;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.ItemMatcher;
+import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.enummatcher.EnumMatcher;
+
+import java.util.Optional;
+
+/**
+ * @author Ameliorate
+ */
+public class ItemTropicFishBBodyColorMatcher implements ItemMatcher {
+ public ItemTropicFishBBodyColorMatcher(EnumMatcher