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 color) { + this.color = color; + } + + public static ItemTropicFishBBodyColorMatcher construct(Optional> color) { + return color.map(ItemTropicFishBBodyColorMatcher::new).orElse(null); + } + + public EnumMatcher color; + + @Override + public boolean matches(ItemStack item) { + if (!item.hasItemMeta() || !(item.getItemMeta() instanceof TropicalFishBucketMeta) || + !((TropicalFishBucketMeta) item.getItemMeta()).hasVariant()) + return false; + + return color.matches(((TropicalFishBucketMeta) item.getItemMeta()).getBodyColor()); + } + + @Override + public ItemStack solve(ItemStack item) throws NotSolvableException { + if (!item.hasItemMeta() || !(item.getItemMeta() instanceof TropicalFishBucketMeta)) + item.setType(Material.TROPICAL_FISH_BUCKET); + + TropicalFishBucketMeta meta = (TropicalFishBucketMeta) item.getItemMeta(); + + meta.setBodyColor(color.solve(meta.getBodyColor())); + item.setItemMeta(meta); + return item; + } +} diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/tropicalbucket/ItemTropicFishBPatternColorMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/tropicalbucket/ItemTropicFishBPatternColorMatcher.java new file mode 100644 index 00000000..e016b5d7 --- /dev/null +++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/tropicalbucket/ItemTropicFishBPatternColorMatcher.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 ItemTropicFishBPatternColorMatcher implements ItemMatcher { + public ItemTropicFishBPatternColorMatcher(EnumMatcher color) { + this.color = color; + } + + public static ItemTropicFishBPatternColorMatcher construct(Optional> color) { + return color.map(ItemTropicFishBPatternColorMatcher::new).orElse(null); + } + + public EnumMatcher color; + + @Override + public boolean matches(ItemStack item) { + if (!item.hasItemMeta() || !(item.getItemMeta() instanceof TropicalFishBucketMeta) || + !((TropicalFishBucketMeta) item.getItemMeta()).hasVariant()) + return false; + + return color.matches(((TropicalFishBucketMeta) item.getItemMeta()).getPatternColor()); + } + + @Override + public ItemStack solve(ItemStack item) throws NotSolvableException { + if (!item.hasItemMeta() || !(item.getItemMeta() instanceof TropicalFishBucketMeta)) + item.setType(Material.TROPICAL_FISH_BUCKET); + + TropicalFishBucketMeta meta = (TropicalFishBucketMeta) item.getItemMeta(); + + meta.setPatternColor(color.solve(meta.getPatternColor())); + item.setItemMeta(meta); + return item; + } +} diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/tropicalbucket/ItemTropicFishBPatternMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/tropicalbucket/ItemTropicFishBPatternMatcher.java new file mode 100644 index 00000000..7a8105be --- /dev/null +++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/tropicalbucket/ItemTropicFishBPatternMatcher.java @@ -0,0 +1,46 @@ +package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.tropicalbucket; + +import org.bukkit.Material; +import org.bukkit.entity.TropicalFish; +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 ItemTropicFishBPatternMatcher implements ItemMatcher { + public ItemTropicFishBPatternMatcher(EnumMatcher pattern) { + this.pattern = pattern; + } + + public static ItemTropicFishBPatternMatcher construct(Optional> color) { + return color.map(ItemTropicFishBPatternMatcher::new).orElse(null); + } + + public EnumMatcher pattern; + + @Override + public boolean matches(ItemStack item) { + if (!item.hasItemMeta() || !(item.getItemMeta() instanceof TropicalFishBucketMeta) || + !((TropicalFishBucketMeta) item.getItemMeta()).hasVariant()) + return false; + + return pattern.matches(((TropicalFishBucketMeta) item.getItemMeta()).getPattern()); + } + + @Override + public ItemStack solve(ItemStack item) throws NotSolvableException { + if (!item.hasItemMeta() || !(item.getItemMeta() instanceof TropicalFishBucketMeta)) + item.setType(Material.TROPICAL_FISH_BUCKET); + + TropicalFishBucketMeta meta = (TropicalFishBucketMeta) item.getItemMeta(); + + meta.setPattern(pattern.solve(meta.getPattern())); + item.setItemMeta(meta); + return item; + } +} diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/AnyUUID.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/AnyUUID.java new file mode 100644 index 00000000..2c6ea755 --- /dev/null +++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/AnyUUID.java @@ -0,0 +1,18 @@ +package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.uuid; + +import java.util.UUID; + +/** + * @author Ameliorate + */ +public class AnyUUID implements UUIDMatcher { + @Override + public boolean matches(UUID matched) { + return true; + } + + @Override + public UUID solve(UUID defaultValue) throws NotSolvableException { + return defaultValue; + } +} diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/ExactlyUUID.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/ExactlyUUID.java new file mode 100644 index 00000000..b5bb6f5e --- /dev/null +++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/ExactlyUUID.java @@ -0,0 +1,24 @@ +package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.uuid; + +import java.util.UUID; + +/** + * @author Ameliorate + */ +public class ExactlyUUID implements UUIDMatcher { + public ExactlyUUID(UUID uuid) { + this.uuid = uuid; + } + + public UUID uuid; + + @Override + public boolean matches(UUID uuid) { + return this.uuid.equals(uuid); + } + + @Override + public UUID solve(UUID defaultValue) throws NotSolvableException { + return uuid; + } +} diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/PlayerNameRegexUUID.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/PlayerNameRegexUUID.java new file mode 100644 index 00000000..c697fd2f --- /dev/null +++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/PlayerNameRegexUUID.java @@ -0,0 +1,33 @@ +package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.uuid; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; + +import java.util.UUID; +import java.util.regex.Pattern; + +/** + * @author Ameliorate + */ +public class PlayerNameRegexUUID implements UUIDMatcher { + public PlayerNameRegexUUID(Pattern pattern) { + this.pattern = pattern; + } + + public Pattern pattern; + + @Override + public boolean matches(UUID uuid) { + OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); + if (player.getName() != null) { + String name = player.getName(); + return pattern.matcher(name).matches(); + } else + return false; + } + + @Override + public UUID solve(UUID startingValue) throws NotSolvableException { + throw new NotSolvableException("can't solve a regex"); + } +} diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/PlayerNameUUID.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/PlayerNameUUID.java new file mode 100644 index 00000000..9184191c --- /dev/null +++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/PlayerNameUUID.java @@ -0,0 +1,37 @@ +package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.uuid; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; + +import java.util.UUID; + +/** + * @author Ameliorate + */ +public class PlayerNameUUID implements UUIDMatcher { + public PlayerNameUUID(String name) { + this.name = name; + } + + public String name; + + @Override + public boolean matches(UUID uuid) { + OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); + if (player.getName() != null) + return player.getName().equals(name); + else + return false; + } + + @SuppressWarnings("deprecation") + @Override + public UUID solve(UUID startingValue) throws NotSolvableException { + OfflinePlayer player = Bukkit.getOfflinePlayer(name); + + if (player == null) + throw new NotSolvableException("can't find player with name " + name); + + return player.getUniqueId(); + } +} diff --git a/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/UUIDMatcher.java b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/UUIDMatcher.java new file mode 100644 index 00000000..42cef605 --- /dev/null +++ b/src/main/java/vg/civcraft/mc/civmodcore/itemHandling/itemExpression/uuid/UUIDMatcher.java @@ -0,0 +1,13 @@ +package vg.civcraft.mc.civmodcore.itemHandling.itemExpression.uuid; + +import vg.civcraft.mc.civmodcore.itemHandling.itemExpression.Matcher; + +import java.util.UUID; + +/** + * @author Ameliorate + * + * These classes are generally used for matching player heads. + */ +public interface UUIDMatcher extends Matcher { +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index b9774e4d..d8149df5 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -4,3 +4,19 @@ authors: [valadian, rourke750, ProgrammerDan, Maxopoly, DeadBeef] main: ${groupId}.${name}Plugin api-version: 1.13 dev-url: null + +commands: + testitemsolving: + description: Test item solving using an ItemExpression in the CivModCore config. This will give you an item that matches the ItemExpression. + usage: /testitemsolving [name] + permission: civmodcore.test + testitemmatching: + description: Test item matching using an ItemExpression in the CivModCore config. This will tell you if the item in your hand matches an ItemExpression. + usage: /testitemmatches [name] + permission: civmodcore.test + +permissions: + civmodcore.test: + description: Allows testing internal APIs of CivModCore. Note that this permission unlocks commands that can be used to give/dupe items. + default: op +