diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index ebdb93120cd..4394ef7997c 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -65,6 +65,7 @@ import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; import org.junit.runner.notification.Failure; import org.skriptlang.skript.bukkit.SkriptMetrics; +import org.skriptlang.skript.bukkit.tags.TagModule; import org.skriptlang.skript.bukkit.breeding.BreedingModule; import org.skriptlang.skript.bukkit.displays.DisplayModule; import org.skriptlang.skript.bukkit.furnace.FurnaceModule; @@ -533,6 +534,7 @@ public void onEnable() { BreedingModule.load(); DisplayModule.load(); InputModule.load(); + TagModule.load(); FurnaceModule.load(); LootTableModule.load(); } catch (final Exception e) { diff --git a/src/main/java/ch/njol/skript/aliases/ItemType.java b/src/main/java/ch/njol/skript/aliases/ItemType.java index 4cbea58df59..11d8f2cae5a 100644 --- a/src/main/java/ch/njol/skript/aliases/ItemType.java +++ b/src/main/java/ch/njol/skript/aliases/ItemType.java @@ -47,6 +47,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -1419,6 +1420,17 @@ public Material getMaterial() { } /** + * @return All Materials this ItemType represents. + */ + public Material[] getMaterials() { + Set materials = new HashSet<>(); + for (ItemData data : types) { + materials.add(data.getType()); + } + return materials.toArray(new Material[0]); + } + + /** * @return A random block material this ItemType represents. * @throws IllegalStateException If {@link #hasBlock()} is false. */ diff --git a/src/main/java/ch/njol/util/Pair.java b/src/main/java/ch/njol/util/Pair.java index a103491e0e6..25bb2cc0b83 100644 --- a/src/main/java/ch/njol/util/Pair.java +++ b/src/main/java/ch/njol/util/Pair.java @@ -1,52 +1,49 @@ package ch.njol.util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; + +import java.io.Serial; import java.io.Serializable; import java.util.Map.Entry; import java.util.Objects; -import org.jetbrains.annotations.Nullable; - -/** - * @author Peter Güttinger - */ public class Pair implements Entry, Cloneable, Serializable { + @Serial private static final long serialVersionUID = 8296563685697678334L; - - @Nullable - protected T1 first; - @Nullable - protected T2 second; + + protected @UnknownNullability T1 first; + protected @UnknownNullability T2 second; public Pair() { first = null; second = null; } - public Pair(final @Nullable T1 first, final @Nullable T2 second) { + public Pair(@Nullable T1 first, @Nullable T2 second) { this.first = first; this.second = second; } - public Pair(final Entry e) { - this.first = e.getKey(); - this.second = e.getValue(); + public Pair(@NotNull Entry entry) { + this.first = entry.getKey(); + this.second = entry.getValue(); } - - @Nullable - public T1 getFirst() { + + public @UnknownNullability T1 getFirst() { return first; } - public void setFirst(final @Nullable T1 first) { + public void setFirst(@Nullable T1 first) { this.first = first; } - - @Nullable - public T2 getSecond() { + + public @UnknownNullability T2 getSecond() { return second; } - public void setSecond(final @Nullable T2 second) { + public void setSecond(@Nullable T2 second) { this.second = second; } @@ -55,23 +52,22 @@ public void setSecond(final @Nullable T2 second) { */ @Override public String toString() { - return "" + first + "," + second; + return first + "," + second; } /** * Checks for equality with Entries to match {@link #hashCode()} */ @Override - public final boolean equals(final @Nullable Object obj) { + public final boolean equals(@Nullable Object obj) { if (obj == this) return true; - if (!(obj instanceof Entry)) + if (!(obj instanceof Entry entry)) return false; - final Entry other = (Entry) obj; - final T1 first = this.first; - final T2 second = this.second; - return (first == null ? other.getKey() == null : first.equals(other.getKey())) && - (second == null ? other.getValue() == null : second.equals(other.getValue())); + T1 first = this.first; + T2 second = this.second; + return (first == null ? entry.getKey() == null : first.equals(entry.getKey())) && + (second == null ? entry.getValue() == null : second.equals(entry.getValue())); } /** @@ -83,21 +79,18 @@ public final int hashCode() { } @Override - @Nullable - public T1 getKey() { + public @UnknownNullability T1 getKey() { return first; } @Override - @Nullable - public T2 getValue() { + public @UnknownNullability T2 getValue() { return second; } @Override - @Nullable - public T2 setValue(final @Nullable T2 value) { - final T2 old = second; + public @UnknownNullability T2 setValue(@Nullable T2 value) { + T2 old = second; second = value; return old; } diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/SkriptTag.java b/src/main/java/org/skriptlang/skript/bukkit/tags/SkriptTag.java new file mode 100644 index 00000000000..e565d0a3304 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/SkriptTag.java @@ -0,0 +1,44 @@ +package org.skriptlang.skript.bukkit.tags; + +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Represents a custom tag created by the Skript user. + * Implementation of {@link Tag} with a custom set of contents. + * @param The type of the contents. + */ +public class SkriptTag implements Tag { + + private final Set contents; + private final NamespacedKey key; + + public SkriptTag(NamespacedKey key, Collection contents) { + this.key = key; + this.contents = new HashSet<>(contents); + } + + @Override + public boolean isTagged(@NotNull T item) { + return contents.contains(item); + } + + @Override + public @NotNull @UnmodifiableView Set getValues() { + return Collections.unmodifiableSet(contents); + } + + @Override + public @NotNull NamespacedKey getKey() { + return key; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/TagModule.java b/src/main/java/org/skriptlang/skript/bukkit/tags/TagModule.java new file mode 100644 index 00000000000..71987b74857 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/TagModule.java @@ -0,0 +1,109 @@ +package org.skriptlang.skript.bukkit.tags; + +import ch.njol.skript.Skript; +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.bukkitutil.EntityUtils; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.classes.Parser; +import ch.njol.skript.entity.EntityData; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.slot.Slot; +import org.bukkit.Keyed; +import org.bukkit.Tag; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Entity; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.comparator.Comparators; +import org.skriptlang.skript.lang.comparator.Relation; + +import java.io.IOException; + +public class TagModule { + + // paper tags + public static final boolean PAPER_TAGS_EXIST = Skript.classExists("com.destroystokyo.paper.MaterialTags"); + + // tag object + public static TagRegistry tagRegistry; + + public static void load() throws IOException { + // abort if no class exists + if (!Skript.classExists("org.bukkit.Tag")) + return; + + // Classes + Classes.registerClass(new ClassInfo<>(Tag.class, "minecrafttag") + .user("minecraft ?tags?") + .name("Minecraft Tag") + .description("A tag that classifies a material, or entity.") + .since("INSERT VERSION") + .parser(new Parser>() { + @Override + public boolean canParse(ParseContext context) { + return false; + } + + @Override + public String toString(Tag tag, int flags) { + return "tag " + tag.getKey(); + } + + @Override + public String toVariableNameString(Tag tag) { + return toString(tag, 0); + } + })); + + // load classes (todo: replace with registering methods after registration api + Skript.getAddonInstance().loadClasses("org.skriptlang.skript.bukkit", "tags"); + + // compare tags by keys, not by object instance. + Comparators.registerComparator(Tag.class, Tag.class, (a, b) -> Relation.get(a.getKey().equals(b.getKey()))); + + // init tags + tagRegistry = new TagRegistry(); + } + + /** + * Retrieves a Keyed array based on the type of the provided input object. + * + * @param input the input object to determine the keyed value, can be of type Entity, + * EntityData, ItemType, ItemStack, Slot, Block, or BlockData. + * @return a Keyed array corresponding to the input's type, or null if the input is null + * or if no corresponding Keyed value can be determined. ItemTypes may return multiple values, + * though everything else will return a single element array. + */ + @Contract(value = "null -> null", pure = true) + public static @Nullable Keyed[] getKeyed(Object input) { + Keyed value = null; + Keyed[] values = null; + if (input == null) + return null; + if (input instanceof Entity entity) { + value = entity.getType(); + } if (input instanceof EntityData data) { + value = EntityUtils.toBukkitEntityType(data); + } else if (input instanceof ItemType itemType) { + values = itemType.getMaterials(); + } else if (input instanceof ItemStack itemStack) { + value = itemStack.getType(); + } else if (input instanceof Slot slot) { + ItemStack stack = slot.getItem(); + if (stack == null) + return null; + value = stack.getType(); + } else if (input instanceof Block block) { + value = block.getType(); + } else if (input instanceof BlockData data) { + value = data.getMaterial(); + } + if (value == null) + return values; + return new Keyed[]{value}; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/TagRegistry.java b/src/main/java/org/skriptlang/skript/bukkit/tags/TagRegistry.java new file mode 100644 index 00000000000..7bbd2eb2d5c --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/TagRegistry.java @@ -0,0 +1,218 @@ +package org.skriptlang.skript.bukkit.tags; + +import ch.njol.util.coll.iterator.CheckedIterator; +import com.destroystokyo.paper.MaterialTags; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Iterators; +import io.papermc.paper.tag.EntityTags; +import org.bukkit.Keyed; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.bukkit.entity.EntityType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.tags.sources.BukkitTagSource; +import org.skriptlang.skript.bukkit.tags.sources.PaperTagSource; +import org.skriptlang.skript.bukkit.tags.sources.SkriptTagSource; +import org.skriptlang.skript.bukkit.tags.sources.TagOrigin; +import org.skriptlang.skript.bukkit.tags.sources.TagSource; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; + +/** + * A class in charge of storing and handling all the tags Skript can access. + */ +public class TagRegistry { + + private final TagSourceMap tagSourceMap = new TagSourceMap(); + + /** + * Each new instance will create a new set of tag sources, in an effort to be reload safe. + */ + TagRegistry() { + tagSourceMap.put(TagType.ITEMS, new BukkitTagSource<>("items", TagType.ITEMS)); + tagSourceMap.put(TagType.BLOCKS, new BukkitTagSource<>("blocks", TagType.BLOCKS)); + tagSourceMap.put(TagType.ENTITIES, new BukkitTagSource<>("entity_types", TagType.ENTITIES)); + + if (TagModule.PAPER_TAGS_EXIST) { + try { + List> itemTags = new ArrayList<>(); + List> blockTags = new ArrayList<>(); + List> blockAndItemTag = new ArrayList<>(); + for (Field field : MaterialTags.class.getDeclaredFields()) { + if (field.canAccess(null)) { + //noinspection unchecked + Tag tag = (Tag) field.get(null); + boolean hasItem = false; + boolean hasBlock = false; + for (Material material : tag.getValues()) { + if (!hasBlock && material.isBlock()) { + blockTags.add(tag); + hasBlock = true; + } + if (!hasItem && material.isItem()) { + itemTags.add(tag); + hasItem = true; + } + if (hasItem && hasBlock) { + blockAndItemTag.add(tag); + break; + } + } + } + } + PaperTagSource paperMaterialTags = new PaperTagSource<>(blockAndItemTag, TagType.BLOCKS, TagType.ITEMS); + PaperTagSource paperItemTags = new PaperTagSource<>(itemTags, TagType.ITEMS); + PaperTagSource paperBlockTags = new PaperTagSource<>(blockTags, TagType.BLOCKS); + tagSourceMap.put(TagType.BLOCKS, paperMaterialTags); + tagSourceMap.put(TagType.ITEMS, paperMaterialTags); + + tagSourceMap.put(TagType.BLOCKS, paperBlockTags); + tagSourceMap.put(TagType.ITEMS, paperItemTags); + + List> entityTags = new ArrayList<>(); + for (Field field : EntityTags.class.getDeclaredFields()) { + if (field.canAccess(null)) + //noinspection unchecked + entityTags.add((Tag) field.get(null)); + } + PaperTagSource paperEntityTags = new PaperTagSource<>(entityTags, TagType.ENTITIES); + tagSourceMap.put(TagType.ENTITIES, paperEntityTags); + } catch (IllegalAccessException ignored) {} + } + + SkriptTagSource.makeDefaultSources(); + tagSourceMap.put(TagType.ITEMS, SkriptTagSource.ITEMS()); + tagSourceMap.put(TagType.BLOCKS, SkriptTagSource.BLOCKS()); + tagSourceMap.put(TagType.ENTITIES, SkriptTagSource.ENTITIES()); + } + + /** + * Gets all the tags of a specific origin that are applicable to a given class. + * @param origin The origin to filter by. + * @param typeClass The class the tags should be applicable to. + * @param types Tag types to check with. Leaving this empty will check all tag types. + * @return TagRegistry from the given origin and types that apply to the given class. + * @param see typeClass. + */ + public Iterable> getTags(TagOrigin origin, Class typeClass, TagType... types) { + List>> tagIterators = new ArrayList<>(); + if (types == null) + types = tagSourceMap.map.keys().toArray(new TagType[0]); + for (TagType type : types) { + if (typeClass.isAssignableFrom(type.type())) { + //noinspection unchecked + Iterator> iterator = getTags(origin, (TagType) type).iterator(); + if (iterator.hasNext()) + tagIterators.add(iterator); + } + } + return new Iterable<>() { + @Override + public @NotNull Iterator> iterator() { + return Iterators.concat(tagIterators.iterator()); + } + }; + } + + /** + * Gets all the tags of a specific origin that are of a specific type. + * @param origin The origin to filter by. + * @param type The type of tags to get. + * @return TagRegistry from the given origin that are of the given type. + * @param The class these tags apply to. + */ + public Iterable> getTags(TagOrigin origin, TagType type) { + if (!tagSourceMap.containsKey(type)) + return List.of(); + Iterator> tagSources = tagSourceMap.get(origin, type).iterator(); + if (!tagSources.hasNext()) + return List.of(); + return new Iterable<>() { + @Override + public @NotNull Iterator> iterator() { + return Iterators.concat(new Iterator>>() { + @Override + public boolean hasNext() { + return tagSources.hasNext(); + } + + @Override + public Iterator> next() { + return tagSources.next().getAllTags().iterator(); + } + }); + } + }; + } + + /** + * Gets all the tags of a specific origin that are of a specific type. Filters the resulting tags using the given + * predicate. + * @param origin The origin to filter by. + * @param type The type of tags to get. + * @param predicate A predicate to filter the tags with. + * @return TagRegistry from the given origin that are of the given type and that pass the filter. + * @param The class these tags apply to. + */ + public Iterable> getMatchingTags(TagOrigin origin, TagType type, Predicate> predicate) { + Iterator> tagIterator = getTags(origin, type).iterator(); + return new Iterable<>() { + @Override + public @NotNull Iterator> iterator() { + return new CheckedIterator<>(tagIterator, predicate::test); + } + }; + } + + /** + * Gets a specific tag of a specific origin that is of a specific type. + * @param origin The origin to filter by. + * @param type The type of tags to get. + * @param key The key of the tag to get. + * @return The tag that matched the above values. Null if no tag is found. + * @param The class these tags apply to. + */ + public @Nullable Tag getTag(TagOrigin origin, TagType type, NamespacedKey key) { + Tag tag; + for (TagSource source : tagSourceMap.get(origin, type)) { + tag = source.getTag(key); + if (tag != null) + return tag; + } + return null; + } + + /** + * A MultiMap that maps TagTypes to multiple TagSources, matching generics. + */ + private static class TagSourceMap { + + private final ArrayListMultimap, TagSource> map = ArrayListMultimap.create(); + + public void put(TagType key, TagSource value) { + map.put(key, value); + } + + public @NotNull List> get(TagOrigin origin, TagType key) { + List> sources = new ArrayList<>(); + for (TagSource source : map.get(key)) { + if (source.getOrigin().matches(origin)) + //noinspection unchecked + sources.add((TagSource) source); + } + return sources; + } + + public boolean containsKey(TagType type) { + return map.containsKey(type); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/TagType.java b/src/main/java/org/skriptlang/skript/bukkit/tags/TagType.java new file mode 100644 index 00000000000..e0dc3aecfdb --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/TagType.java @@ -0,0 +1,141 @@ +package org.skriptlang.skript.bukkit.tags; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.util.Kleenean; +import org.bukkit.Keyed; +import org.bukkit.Material; +import org.bukkit.entity.EntityType; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * The type of a tag. Represents a category or context that the tags apply in. + * For example, {@link #ITEMS} tags apply to {@link Material}s, like {@link #BLOCKS}, but in item form rather than as + * placed blocks. + *
+ * This class also contains a static registry of all tag types. + * + * @param see type. + */ +public class TagType { + + private static final List> REGISTERED_TAG_TYPES = Collections.synchronizedList(new ArrayList<>()); + + public static final TagType ITEMS = new TagType<>("item", Material.class); + public static final TagType BLOCKS = new TagType<>("block", Material.class); + public static final TagType ENTITIES = new TagType<>("entity [type]", "entity type", EntityType.class); + + static { + TagType.addType(ITEMS, BLOCKS, ENTITIES); + } + + private final String pattern; + private final String toString; + private final Class type; + + /** + * @param pattern The pattern to use when constructing the selection Skript pattern. + * @param type The class this tag type applies to. + */ + public TagType(String pattern, Class type) { + this(pattern, pattern, type); + } + + /** + * @param pattern The pattern to use when constructing the selection Skript pattern. + * @param toString The string to use when printing a toString. + * @param type The class this tag type applies to. + */ + public TagType(String pattern, String toString, Class type) { + this.pattern = pattern; + this.type = type; + this.toString = toString; + } + + public String pattern() { + return pattern; + } + + public Class type() { + return type; + } + + @Override + public String toString() { + return toString; + } + + /** + * Adds types to the registered tag types. + * @param type The types to add. + */ + public static void addType(TagType... type) { + REGISTERED_TAG_TYPES.addAll(List.of(type)); + } + + /** + * @return An unmodifiable list of all the registered types. + */ + @Contract(pure = true) + public static @NotNull @UnmodifiableView List> getTypes() { + return Collections.unmodifiableList(REGISTERED_TAG_TYPES); + } + + /** + * Gets tag types by index. If a negative value is used, gets all the tag types. + * @param i The index of the type to get. + * @return The type at that index, or all tags if index < 0. + */ + public static TagType @NotNull [] getType(int i) { + if (i < 0) + return REGISTERED_TAG_TYPES.toArray(new TagType[0]); + return new TagType[]{REGISTERED_TAG_TYPES.get(i)}; + } + + /** + * Gets tag types by parser mark. Equivalent to {@code getType(i - 1)}. + * @param i The index of the type to get. + * @return The type at that index, or all tags if index < 0. + * @see #getType(int) + * @see #getFullPattern() + */ + public static TagType @NotNull [] fromParseMark(int i) { + return getType(i - 1); + } + + /** + * @return Returns an optional choice pattern for use in Skript patterns. Contains parse marks. + * Pass the parse mark to {@link #fromParseMark(int)} to get the + * selected tag type in + * {@link ch.njol.skript.lang.SyntaxElement#init(Expression[], int, Kleenean, SkriptParser.ParseResult)}. + */ + public static @NotNull String getFullPattern() { + return getFullPattern(false); + } + + /** + * @param required whether the choice should be optional or required. + * @return Returns a choice pattern for use in Skript patterns. Contains parse marks. + * Pass the parse mark to {@link #fromParseMark(int)} to get the + * selected tag type in + * {@link ch.njol.skript.lang.SyntaxElement#init(Expression[], int, Kleenean, SkriptParser.ParseResult)}. + */ + public static @NotNull String getFullPattern(boolean required) { + StringBuilder fullPattern = new StringBuilder(required ? "(" : "["); + int numRegistries = REGISTERED_TAG_TYPES.size(); + for (int i = 0; i < numRegistries; i++) { + fullPattern.append(i + 1).append(":").append(REGISTERED_TAG_TYPES.get(i).pattern()); + if (i + 1 != numRegistries) + fullPattern.append("|"); + } + fullPattern.append(required ? ")" : "]"); + return fullPattern.toString(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/elements/CondIsTagged.java b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/CondIsTagged.java new file mode 100644 index 00000000000..53bac73628e --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/CondIsTagged.java @@ -0,0 +1,103 @@ +package org.skriptlang.skript.bukkit.tags.elements; + +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.conditions.base.PropertyCondition; +import ch.njol.skript.conditions.base.PropertyCondition.PropertyType; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Keywords; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Condition; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import org.bukkit.Keyed; +import org.bukkit.Tag; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.tags.TagModule; + +@Name("Is Tagged") +@Description({ + "Checks whether an item, block, entity, or entitydata is tagged with the given tag." +}) +@Examples({ + "if player's tool is tagged with minecraft tag \"enchantable/sharp_weapon\":", + "\tenchant player's tool with sharpness 1", + "", + "if all logs are tagged with tag \"minecraft:logs\"" +}) +@Since("INSERT VERSION") +@Keywords({"blocks", "minecraft tag", "type", "category"}) +public class CondIsTagged extends Condition { + + static { + PropertyCondition.register(CondIsTagged.class, PropertyCondition.PropertyType.BE, + "tagged (as|with) %minecrafttags%", + "itemtypes/entities/entitydatas"); + } + + private Expression> tags; + private Expression elements; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + this.elements = expressions[0]; + //noinspection unchecked + this.tags = (Expression>) expressions[1]; + setNegated(matchedPattern == 1); + return true; + } + + @Override + public boolean check(Event event) { + Tag[] tags = this.tags.getArray(event); + if (tags.length == 0) + return isNegated(); + boolean and = this.tags.getAnd(); + return elements.check(event, element -> { + boolean isAny = (element instanceof ItemType itemType && !itemType.isAll()); + Keyed[] values = TagModule.getKeyed(element); + if (values == null) + return false; + + for (Tag tag : tags) { + if (isTagged(tag, values, !isAny)) { + if (!and) + return true; + } else if (and) { + return false; + } + } + return and; + }, isNegated()); + } + + /** + * Helper method for checking if a series of values have a tag. + * @param tag The tag to check for. + * @param values The values to check against. + * @param allTagged Whether all values need to have the tag (true), or just one (false). + * @return Whether the values are tagged with the tag. + */ + private boolean isTagged(Tag tag, Keyed @NotNull [] values, boolean allTagged) { + for (Keyed value : values) { + if (tag.isTagged(value)) { + if (!allTagged) + return true; + } else if (allTagged) { + return false; + } + } + return allTagged; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return PropertyCondition.toString(this, PropertyType.BE, event, debug, elements, + " tagged as " + tags.toString(event, debug)); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/elements/EffRegisterTag.java b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/EffRegisterTag.java new file mode 100644 index 00000000000..c9a723f5df6 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/EffRegisterTag.java @@ -0,0 +1,167 @@ +package org.skriptlang.skript.bukkit.tags.elements; + +import ch.njol.skript.Skript; +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Keywords; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; +import ch.njol.util.Kleenean; +import org.bukkit.Keyed; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.bukkit.entity.EntityType; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.tags.SkriptTag; +import org.skriptlang.skript.bukkit.tags.TagModule; +import org.skriptlang.skript.bukkit.tags.TagType; +import org.skriptlang.skript.bukkit.tags.sources.SkriptTagSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.regex.Pattern; + +@Name("Register Tag") +@Description({ + "Registers a new tag containing either items or entity datas. Note that items will NOT keep any information other " + + "than their type, so adding `diamond sword named \"test\"` to a tag is the same as adding `diamond sword`", + "Item tags should be used for contexts where the item is not placed down, while block tags should be used " + + "for contexts where the item is placed. For example, and item tag could be \"skript:edible\", " + + "while a block tag would be \"skript:needs_water_above\".", + "All custom tags will be given the namespace \"skript\", followed by the name you give it. The name must only " + + "include the characters A to Z, 0 to 9, and '/', '.', '_', and '-'. Otherwise, the tag will not register.", + "", + "Please note that two tags can share a name if they are of different types. Registering a new tag of the same " + + "name and type will overwrite the existing tag. Tags will reset on server shutdown." +}) +@Examples({ + "register a new custom entity tag named \"fish\" using cod, salmon, tropical fish, and pufferfish", + "register an item tag named \"skript:wasp_weapons/swords\" containing diamond sword and netherite sword", + "register block tag named \"pokey\" containing sweet berry bush and bamboo sapling", + "", + "on player move:", + "\tblock at player is tagged as tag \"skript:pokey\"", + "\tdamage the player by 1 heart" +}) +@Since("INSERT VERSION") +@Keywords({"blocks", "minecraft tag", "type", "category"}) +public class EffRegisterTag extends Effect { + + private static final Pattern KEY_PATTERN = Pattern.compile("[a-zA-Z0-9/._-]+"); + + static { + Skript.registerEffect(EffRegisterTag.class, + "register [a[n]] [custom] " + TagType.getFullPattern(true) + + " tag named %string% (containing|using) %entitydatas/itemtypes%"); + } + + private Expression name; + private Expression contents; + private TagType type; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + //noinspection unchecked + name = (Expression) expressions[0]; + if (name instanceof Literal literal) { + String key = removeSkriptNamespace(literal.getSingle()); + if (!KEY_PATTERN.matcher(key).matches()) { + Skript.error("Tag names can only contain the following characters: letters, numbers, and some symbols: " + + "'/', '.', '_', and '-'"); + return false; + } + } + + contents = expressions[1]; + type = TagType.getType(parseResult.mark - 1)[0]; + return true; + } + + @Override + protected void execute(Event event) { + String name = this.name.getSingle(event); + if (name == null) + return; + + name = removeSkriptNamespace(name); + + if (!KEY_PATTERN.matcher(name).matches()) + return; + + NamespacedKey key = new NamespacedKey(Skript.getInstance(), name); + + Object[] contents = this.contents.getArray(event); + if (contents.length == 0) + return; + + + if (this.type.type() == Material.class) { + Tag tag = getMaterialTag(key, contents); + if (this.type == TagType.ITEMS) { + SkriptTagSource.ITEMS().addTag(tag); + } else if (this.type == TagType.BLOCKS) { + SkriptTagSource.BLOCKS().addTag(tag); + } + + } else if (this.type.type() == EntityType.class) { + Tag tag = getEntityTag(key, contents); + SkriptTagSource.ENTITIES().addTag(tag); + } + } + + private static @NotNull String removeSkriptNamespace(@NotNull String key) { + if (key.startsWith("skript:")) + key = key.substring(7); + return key; + } + + @Contract("_, _ -> new") + private @NotNull Tag getMaterialTag(NamespacedKey key, Object @NotNull [] contents) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + List tagContents = new ArrayList<>(); + for (Object object : contents) { + Keyed[] values = TagModule.getKeyed(object); + if (object instanceof ItemType itemType && !itemType.isAll()) { + // add random + tagContents.add((Material) values[random.nextInt(0, values.length)]); + } else { + for (Keyed value : values) { + if (value instanceof Material material) + tagContents.add(material); + } + } + } + return new SkriptTag<>(key, tagContents); + } + + @Contract("_, _ -> new") + private @NotNull Tag getEntityTag(NamespacedKey key, Object @NotNull [] contents) { + List tagContents = new ArrayList<>(); + for (Object object : contents) { + for (Keyed value : TagModule.getKeyed(object)) { + if (value instanceof EntityType entityType) + tagContents.add(entityType); + } + } + return new SkriptTag<>(key, tagContents); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return new SyntaxStringBuilder(event, debug) + .append("register a new", type.toString(), "tag named", name, "containing", contents) + .toString(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTag.java b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTag.java new file mode 100644 index 00000000000..2de27c8e2d4 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTag.java @@ -0,0 +1,137 @@ +package org.skriptlang.skript.bukkit.tags.elements; + +import ch.njol.skript.Skript; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Keywords; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.RequiredPlugins; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.lang.util.SimpleLiteral; +import ch.njol.util.Kleenean; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.tags.TagModule; +import org.skriptlang.skript.bukkit.tags.TagType; +import org.skriptlang.skript.bukkit.tags.sources.TagOrigin; + +import java.util.ArrayList; +import java.util.List; + +@Name("Tag") +@Description({ + "Represents a tag which can be used to classify items, blocks, or entities.", + "Tags are composed of a value and an optional namespace: \"minecraft:oak_logs\".", + "If you omit the namespace, one will be provided for you, depending on what kind of tag you're using. " + + "For example, `tag \"doors\"` will be the tag \"minecraft:doors\", " + + "while `paper tag \"doors\"` will be \"paper:doors\".", + "`minecraft tag` will search through the vanilla tags, `datapack tag` will search for datapack-provided tags " + + "(a namespace is required here!), `paper tag` will search for Paper's custom tags if you are running Paper, " + + "and `custom tag` will look in the \"skript\" namespace for custom tags you've registered.", + "You can also filter by tag types using \"item\", \"block\", or \"entity\"." +}) +@Examples({ + "minecraft tag \"dirt\" # minecraft:dirt", + "paper tag \"doors\" # paper:doors", + "tag \"skript:custom_dirt\" # skript:custom_dirt", + "custom tag \"dirt\" # skript:dirt", + "datapack block tag \"dirt\" # minecraft:dirt", + "datapack tag \"my_pack:custom_dirt\" # my_pack:custom_dirt", + "tag \"minecraft:mineable/pickaxe\" # minecraft:mineable/pickaxe", + "custom item tag \"blood_magic_sk/can_sacrifice_with\" # skript:blood_magic_sk/can_sacrifice_with" +}) +@Since("INSERT VERSION") +@RequiredPlugins("Paper (paper tags)") +@Keywords({"blocks", "minecraft tag", "type", "category"}) +public class ExprTag extends SimpleExpression { + + static { + Skript.registerExpression(ExprTag.class, Tag.class, ExpressionType.COMBINED, + TagOrigin.getFullPattern() + " " + TagType.getFullPattern() + " tag %strings%"); + } + + private Expression names; + TagType[] types; + private TagOrigin origin; + private boolean datapackOnly; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + //noinspection unchecked + names = (Expression) expressions[0]; + types = TagType.fromParseMark(parseResult.mark); + origin = TagOrigin.fromParseTags(parseResult.tags); + datapackOnly = origin == TagOrigin.BUKKIT && parseResult.hasTag("datapack"); + return true; + } + + @Override + protected Tag @Nullable [] get(Event event) { + String[] names = this.names.getArray(event); + List> tags = new ArrayList<>(); + + String namespace = switch (origin) { + case ANY, BUKKIT -> "minecraft"; + case PAPER -> "paper"; + case SKRIPT -> "skript"; + }; + + nextName: for (String name : names) { + // get key + NamespacedKey key; + if (name.contains(":")) { + key = NamespacedKey.fromString(name); + } else { + // populate namespace if not provided + key = new NamespacedKey(namespace, name); + } + if (key == null) + continue; + + Tag tag; + for (TagType type : types) { + tag = TagModule.tagRegistry.getTag(origin, type, key); + if (tag != null + // ensures that only datapack/minecraft tags are sent when specifically requested + && (origin != TagOrigin.BUKKIT || (datapackOnly ^ tag.getKey().getNamespace().equals("minecraft"))) + ) { + tags.add(tag); + continue nextName; // ensure 1:1 + } + } + } + return tags.toArray(new Tag[0]); + } + + @Override + public boolean isSingle() { + return names.isSingle(); + } + + @Override + @SuppressWarnings("rawtypes") + public Class getReturnType() { + return Tag.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + String registry = types.length > 1 ? "" : " " + types[0].toString(); + return origin.toString(datapackOnly) + registry + " tag " + names.toString(event, debug); + } + + @Override + public Expression simplify() { + if (names instanceof Literal) + return new SimpleLiteral<>(getArray(null), Tag.class, true); + return this; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagContents.java b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagContents.java new file mode 100644 index 00000000000..334abbf3463 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagContents.java @@ -0,0 +1,104 @@ +package org.skriptlang.skript.bukkit.tags.elements; + +import ch.njol.skript.Skript; +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.bukkitutil.EntityUtils; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Keywords; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.util.Utils; +import ch.njol.util.Kleenean; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.entity.EntityType; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.tags.TagType; + +import java.util.Objects; + +@Name("Tags Contents") +@Description({ + "Returns all the values that a tag contains.", + "For item and block tags, this will return items. For entity tags, " + + "it will return entity datas (a creeper, a zombie)." +}) +@Examples({ + "broadcast tag values of minecraft tag \"dirt\"", + "broadcast (first element of player's tool's block tags)'s tag contents" +}) +@Since("INSERT VERSION") +@Keywords({"blocks", "minecraft tag", "type", "category"}) +public class ExprTagContents extends SimpleExpression { + + static { + Skript.registerExpression(ExprTagContents.class, Object.class, ExpressionType.PROPERTY, + "[the] tag (contents|values) of %minecrafttag%", + "%minecrafttag%'[s] tag (contents|values)"); + } + + private Expression> tag; + private TagType @Nullable [] tagTypes; + + @Override + public boolean init(Expression @NotNull [] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + //noinspection unchecked + tag = (Expression>) expressions[0]; + if (expressions[0] instanceof ExprTag exprTag) { + tagTypes = exprTag.types; + } else if (expressions[0] instanceof ExprTagsOf exprTagsOf) { + tagTypes = exprTagsOf.types; + } else if (expressions[0] instanceof ExprTagsOfType exprTagsOfType) { + tagTypes = exprTagsOfType.types; + } + return true; + } + + @Override + protected Object @Nullable [] get(Event event) { + Tag tag = this.tag.getSingle(event); + if (tag == null) + return null; + return tag.getValues().stream() + .map(value -> { + if (value instanceof Material material) { + return new ItemType(material); + } else if (value instanceof EntityType entityType) { + return EntityUtils.toSkriptEntityData(entityType); + } + return null; + }) + .filter(Objects::nonNull) + .toArray(); + } + + @Override + public boolean isSingle() { + return false; + } + + @Override + public Class getReturnType() { + if (tagTypes != null) { + Class[] possibleTypes = new Class[tagTypes.length]; + for (int i = 0; i < tagTypes.length; i++) { + possibleTypes[i] = tagTypes[i].type(); + } + return Utils.getSuperType(possibleTypes); + } + return Object.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "the tag contents of " + tag.toString(event, debug); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagKey.java b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagKey.java new file mode 100644 index 00000000000..1fda044bf25 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagKey.java @@ -0,0 +1,45 @@ +package org.skriptlang.skript.bukkit.tags.elements; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Keywords; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import org.bukkit.Tag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +// TODO: adapt to generic expression after Any X is merged + +@Name("Tag Namespaced Key") +@Description("The namespaced key of a minecraft tag. This takes the form of \"namespace:key\", e.g. \"minecraft:dirt\".") +@Examples({ + "broadcast namespaced keys of the tags of player's tool", + "if the key of {_my-tag} is \"minecraft:stone\":", + "\treturn true" +}) +@Since("INSERT VERSION") +@Keywords({"minecraft tag", "type", "key", "namespace"}) +public class ExprTagKey extends SimplePropertyExpression, String> { + + static { + register(ExprTagKey.class, String.class, "[namespace[d]] key[s]", "minecrafttags"); + } + + @Override + public @Nullable String convert(@NotNull Tag from) { + return from.getKey().toString(); + } + + @Override + protected String getPropertyName() { + return "namespaced key"; + } + + @Override + public Class getReturnType() { + return String.class; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagsOf.java b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagsOf.java new file mode 100644 index 00000000000..4c6a3e7dbd8 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagsOf.java @@ -0,0 +1,129 @@ +package org.skriptlang.skript.bukkit.tags.elements; + +import ch.njol.skript.Skript; +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Keywords; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.RequiredPlugins; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.PropertyExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import org.bukkit.Keyed; +import org.bukkit.Tag; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.tags.TagModule; +import org.skriptlang.skript.bukkit.tags.TagType; +import org.skriptlang.skript.bukkit.tags.sources.TagOrigin; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ThreadLocalRandom; + +@Name("Tags of X") +@Description({ + "Returns all the tags of an item, block, or entity.", + "`minecraft tag` will return only the vanilla tags, `datapack tag` will return only datapack-provided tags, " + + "`paper tag` will return only Paper's custom tags (if you are running Paper), " + + "and `custom tag` will look in the \"skript\" namespace for custom tags you've registered.", + "You can also filter by tag types using \"item\", \"block\", or \"entity\"." +}) +@Examples({ + "broadcast minecraft tags of dirt", + "send true if paper item tags of target block contains paper tag \"doors\"", + "broadcast the block tags of player's tool" +}) +@Since("INSERT VERSION") +@RequiredPlugins("Paper (paper tags)") +@Keywords({"blocks", "minecraft tag", "type", "category"}) +public class ExprTagsOf extends PropertyExpression { + + static { + Skript.registerExpression(ExprTagsOf.class, Tag.class, ExpressionType.PROPERTY, + "[all [[of] the]|the] " + TagOrigin.getFullPattern() + " " + TagType.getFullPattern() + " tags of %itemtype/entity/entitydata%", + "%itemtype/entity/entitydata%'[s] " + TagOrigin.getFullPattern() + " " + TagType.getFullPattern() + " tags"); + } + + TagType[] types; + TagOrigin origin; + boolean datapackOnly; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + this.setExpr(expressions[0]); + types = TagType.fromParseMark(parseResult.mark); + origin = TagOrigin.fromParseTags(parseResult.tags); + datapackOnly = origin == TagOrigin.BUKKIT && parseResult.hasTag("datapack"); + return true; + } + + @Override + protected Tag @Nullable [] get(Event event, Object @NotNull [] source) { + if (source.length == 0) + return null; + boolean isAny = (source[0] instanceof ItemType itemType && !itemType.isAll()); + Keyed[] values = TagModule.getKeyed(source[0]); + if (values == null) + return null; + // choose single material if it's something like `any log` + if (isAny) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + values = new Keyed[]{values[random.nextInt(0, values.length)]}; + } + + Set> tags = new TreeSet<>(Comparator.comparing(Keyed::key)); + for (Keyed value : values) { + tags.addAll(getTags(value)); + } + + return tags.stream() + .filter(tag -> + // ensures that only datapack/minecraft tags are sent when specifically requested + (origin != TagOrigin.BUKKIT || (datapackOnly ^ tag.getKey().getNamespace().equals("minecraft")))) + .toArray(Tag[]::new); + } + + /** + * Helper method for getting the tags of a value. + * @param value The value to get the tags of. + * @return The tags the value is a part of. + * @param The type of the value. + */ + public Collection> getTags(@NotNull T value) { + List> tags = new ArrayList<>(); + //noinspection unchecked + Class clazz = (Class) value.getClass(); + for (Tag tag : TagModule.tagRegistry.getTags(origin, clazz, types)) { + if (tag.isTagged(value)) + tags.add(tag); + } + return tags; + } + + @Override + public boolean isSingle() { + return false; + } + + @Override + public Class getReturnType() { + return Tag.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + String registry = types.length > 1 ? "" : " " + types[0].toString(); + return origin.toString(datapackOnly) + registry + " tags of " + getExpr().toString(event, debug); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagsOfType.java b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagsOfType.java new file mode 100644 index 00000000000..d13874ebd47 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagsOfType.java @@ -0,0 +1,92 @@ +package org.skriptlang.skript.bukkit.tags.elements; + +import ch.njol.skript.Skript; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Keywords; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.RequiredPlugins; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.tags.TagModule; +import org.skriptlang.skript.bukkit.tags.TagType; +import org.skriptlang.skript.bukkit.tags.sources.TagOrigin; + +import java.util.Comparator; +import java.util.Set; +import java.util.TreeSet; + +@Name("All Tags of a Type") +@Description({ + "Returns all the tags.", + "`minecraft tag` will return only the vanilla tags, `datapack tag` will return only datapack-provided tags, " + + "`paper tag` will return only Paper's custom tags (if you are running Paper), " + + "and `custom tag` will look in the \"skript\" namespace for custom tags you've registered.", + "You can also filter by tag types using \"item\", \"block\", or \"entity\"." +}) +@Examples({ + "broadcast minecraft tags", + "send paper entity tags", + "broadcast all block tags" +}) +@Since("INSERT VERSION") +@RequiredPlugins("Paper (paper tags)") +@Keywords({"blocks", "minecraft tag", "type", "category"}) +public class ExprTagsOfType extends SimpleExpression { + + static { + Skript.registerExpression(ExprTagsOfType.class, Tag.class, ExpressionType.SIMPLE, + "[all [[of] the]|the] " + TagOrigin.getFullPattern() + " " + TagType.getFullPattern() + " tags"); + } + + TagType[] types; + private TagOrigin origin; + private boolean datapackOnly; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + types = TagType.fromParseMark(parseResult.mark); + origin = TagOrigin.fromParseTags(parseResult.tags); + datapackOnly = origin == TagOrigin.BUKKIT && parseResult.hasTag("datapack"); + return true; + } + + @Override + protected Tag @Nullable [] get(Event event) { + Set> tags = new TreeSet<>(Comparator.comparing(Keyed::key)); + for (TagType type : types) { + for (Tag tag : TagModule.tagRegistry.getMatchingTags(origin, type, + tag -> (origin != TagOrigin.BUKKIT || (datapackOnly ^ tag.getKey().getNamespace().equals(NamespacedKey.MINECRAFT)))) + ) { + tags.add(tag); + } + } + return tags.toArray(new Tag[0]); + } + + @Override + public boolean isSingle() { + return false; + } + + @Override + public Class getReturnType() { + return Tag.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + String registry = types.length > 1 ? "" : " " + types[0].toString(); + return "all of the " + origin.toString(datapackOnly) + registry + " tags"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/sources/BukkitTagSource.java b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/BukkitTagSource.java new file mode 100644 index 00000000000..ccf74ce6bd0 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/BukkitTagSource.java @@ -0,0 +1,38 @@ +package org.skriptlang.skript.bukkit.tags.sources; + +import org.bukkit.Bukkit; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.tags.TagType; + +/** + * A set of tags provided by Bukkit. + * @param The class of the tags of this source. + */ +public class BukkitTagSource extends TagSource { + + private final String registry; + + /** + * @param registry The name of the registry to use. For example, {@link Tag#REGISTRY_ITEMS}. + * @param type The type of tag this represents. To continue the example, {@link TagType#ITEMS}. + */ + public BukkitTagSource(String registry, TagType type) { + super(TagOrigin.BUKKIT, type); + this.registry = registry; + } + + @Override + public @NotNull Iterable> getAllTags() { + return Bukkit.getTags(registry, getTypes()[0].type()); + } + + @Override + public @Nullable Tag getTag(NamespacedKey key) { + return Bukkit.getTag(registry, key, getTypes()[0].type()); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/sources/CustomTagSource.java b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/CustomTagSource.java new file mode 100644 index 00000000000..364621734ef --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/CustomTagSource.java @@ -0,0 +1,46 @@ +package org.skriptlang.skript.bukkit.tags.sources; + +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.tags.TagType; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A custom source of tags that stores its own tags. + * @param The class of the tags provided by this source. + */ +public sealed class CustomTagSource extends TagSource permits PaperTagSource, SkriptTagSource { + + final Map> tags; + + /** + * @param origin The origin of this source. + * @param tags The tags this source will own. + * @param types The tag types this source will represent. + */ + @SafeVarargs + CustomTagSource(TagOrigin origin, @NotNull Iterable> tags, TagType... types) { + super(origin, types); + this.tags = new ConcurrentHashMap<>(); + for (Tag tag : tags) { + this.tags.put(tag.getKey(), tag); + } + } + + @Override + public Iterable> getAllTags() { + return Collections.unmodifiableCollection(tags.values()); + } + + @Override + public @Nullable Tag getTag(NamespacedKey key) { + return tags.get(key); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/sources/PaperTagSource.java b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/PaperTagSource.java new file mode 100644 index 00000000000..1a7bbb58624 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/PaperTagSource.java @@ -0,0 +1,73 @@ +package org.skriptlang.skript.bukkit.tags.sources; + +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.jetbrains.annotations.NotNull; +import org.skriptlang.skript.bukkit.tags.TagType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +/** + * A set of tags provided by Paper. + * @param The class of tag this source provides. + */ +public final class PaperTagSource extends CustomTagSource { + + /** + * Creates {@link PaperTag}s from the raw tags. Removes _settag. + * @param tags The raw tags provided by Paper. + * @return The modified tags with _settag removed from their keys. + * @param The class of the tags. + */ + private static @NotNull Iterable> getPaperTags(@NotNull Iterable> tags) { + List> modifiedTags = new ArrayList<>(); + for (Tag tag : tags) { + modifiedTags.add(new PaperTag<>(tag)); + } + return modifiedTags; + } + + /** + * @param tags The raw tags from Paper. + * @param types The tag types this source represents. + */ + @SafeVarargs + public PaperTagSource(Iterable> tags, TagType... types) { + super(TagOrigin.PAPER, getPaperTags(tags), types); + } + + /** + * Wrapper for Paper tags to remove "_settag" from their key. + * @param The class of the tag. + */ + private static class PaperTag implements Tag { + + private final Tag paperTag; + private final NamespacedKey key; + + public PaperTag(@NotNull Tag paperTag) { + this.paperTag = paperTag; + this.key = NamespacedKey.fromString(paperTag.getKey().toString().replace("_settag", "")); + } + + @Override + public boolean isTagged(@NotNull T1 item) { + return paperTag.isTagged(item); + } + + @Override + public @NotNull Set getValues() { + return paperTag.getValues(); + } + + @Override + public @NotNull NamespacedKey getKey() { + return key; + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/sources/SkriptTagSource.java b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/SkriptTagSource.java new file mode 100644 index 00000000000..b197878f989 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/SkriptTagSource.java @@ -0,0 +1,55 @@ +package org.skriptlang.skript.bukkit.tags.sources; + +import ch.njol.util.coll.iterator.EmptyIterable; +import org.bukkit.Keyed; +import org.bukkit.Material; +import org.bukkit.Tag; +import org.bukkit.entity.EntityType; +import org.skriptlang.skript.bukkit.tags.TagType; + +public final class SkriptTagSource extends CustomTagSource { + + private static SkriptTagSource ITEMS; + private static SkriptTagSource BLOCKS; + private static SkriptTagSource ENTITIES; + + public static void makeDefaultSources() { + ITEMS = new SkriptTagSource<>(TagType.ITEMS); + BLOCKS = new SkriptTagSource<>(TagType.BLOCKS); + ENTITIES = new SkriptTagSource<>(TagType.ENTITIES); + } + + /** + * @param types The tag types this source will represent. + */ + @SafeVarargs + private SkriptTagSource(TagType... types) { + super(TagOrigin.SKRIPT, new EmptyIterable<>(), types); + } + + public void addTag(Tag tag) { + tags.put(tag.getKey(), tag); + } + + /** + * @return Skript tag source for item contexts + */ + public static SkriptTagSource ITEMS() { + return ITEMS; + } + + /** + * @return Skript tag source for block contexts + */ + public static SkriptTagSource BLOCKS() { + return BLOCKS; + } + + /** + * @return Skript tag source for entities + */ + public static SkriptTagSource ENTITIES() { + return ENTITIES; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/sources/TagOrigin.java b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/TagOrigin.java new file mode 100644 index 00000000000..82ff9c2f089 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/TagOrigin.java @@ -0,0 +1,94 @@ +package org.skriptlang.skript.bukkit.tags.sources; + +import org.bukkit.event.Event; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.skriptlang.skript.bukkit.tags.TagModule; + +import java.util.Collection; + +/** + * The origin of a tag, eg. from Bukkit, from Paper, from a custom Skript tag, or from anywhere. + * Used for classification and filtering tags. + */ +public enum TagOrigin { + /** + * Bukkit supplies both native minecraft tags and datapack tags. + */ + BUKKIT, + + /** + * Paper supplies a set of custom tags they curate. + */ + PAPER, + + /** + * Custom tags registered via Skript. + */ + SKRIPT, + + /** + * Used when asking for tags, matches all origins. + */ + ANY; + + /** + * Returns an optional choice of all the origins (minecraft, datapack, paper, and custom). + * Will not include paper on non-paper servers. + * Contains parse tags. + * @see #fromParseTags(Collection) + */ + @Contract(pure = true) + public static @NotNull String getFullPattern() { + if (TagModule.PAPER_TAGS_EXIST) + return "[:minecraft|:datapack|:paper|:custom]"; + return "[:minecraft|:datapack|:custom]"; + } + + /** + * Determines the origin of tags based on the parse tags provided. + * + * @param tags the list of tags to parse for determining the origin. + * @return the determined {@code TagOrigin}. + * Returns {@code TagOrigin.ANY} if no specific origin is found. + * @see #getFullPattern() + */ + @Contract(value = "_ -> new", pure = true) + public static TagOrigin fromParseTags(@NotNull Collection tags) { + TagOrigin origin = TagOrigin.ANY; + if (tags.contains("minecraft") || tags.contains("datapack")) { + origin = TagOrigin.BUKKIT; + } else if (tags.contains("paper")) { + origin = TagOrigin.PAPER; + } else if (tags.contains("custom")) { + origin = TagOrigin.SKRIPT; + } + return origin; + } + + /** + * Checks if the current TagOrigin matches another TagOrigin, considering ANY as a wildcard. + * + * @param other The other TagOrigin to be matched against. + * @return {@code true} if the TagOrigins match (i.e., they are the same, or either is {@link #ANY}). + */ + public boolean matches(TagOrigin other) { + return this == other || this == ANY || other == ANY; + } + + /** + * Returns a string for use in {@link ch.njol.skript.lang.Debuggable#toString(Event, boolean)} methods. + * @param datapackOnly Whether to output "datapack " or "minecraft " for {@link #BUKKIT}. + * @return a string representing the origin, with a trailing space. + */ + @Contract(pure = true) + public @NotNull String toString(boolean datapackOnly) { + return switch (this) { + case BUKKIT -> datapackOnly ? "datapack" : "minecraft"; + case PAPER -> "paper"; + case SKRIPT -> "custom"; + case ANY -> ""; + }; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/sources/TagSource.java b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/TagSource.java new file mode 100644 index 00000000000..ec933de34d6 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/sources/TagSource.java @@ -0,0 +1,76 @@ +package org.skriptlang.skript.bukkit.tags.sources; + +import ch.njol.util.coll.iterator.CheckedIterator; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.Tag; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.tags.TagType; + +import java.util.Iterator; +import java.util.function.Predicate; + +/** + * A source for {@link org.bukkit.Tag}s, be it Bukkit's tag registries, Paper's handmade tags, or + * custom tags made by the user. + * @param The type of tags this source will return. + * For example, the Bukkit "items" tag registry would return {@link org.bukkit.Material}s. + */ +public abstract class TagSource { + + private final TagType[] types; + private final TagOrigin origin; + + /** + * @param origin The origin of this source. + * @param types The tag types this source represents. + */ + @SafeVarargs + protected TagSource(TagOrigin origin, TagType... types) { + this.types = types; + this.origin = origin; + } + + /** + * @return All the tags associated with this source. + */ + public abstract Iterable> getAllTags(); + + /** + * For use in getting specific subsets of tags. + * @param predicate A Predicate used to filter tags. + * @return All the tags from this source, filtered based on the predicate. + */ + public Iterable> getAllTagsMatching(Predicate> predicate) { + Iterator> tagIterator = getAllTags().iterator(); + return new Iterable<>() { + @Override + public @NotNull Iterator> iterator() { + return new CheckedIterator<>(tagIterator, predicate::test); + } + }; + } + + /** + * Gets a specific tag by the key. + * @param key The key to use to find the tag. + * @return The tag associated with the key. Null if no such tag exists. + */ + public abstract @Nullable Tag getTag(NamespacedKey key); + + /** + * @return All the tag types that are represented by this source. + */ + public TagType[] getTypes() { + return types; + } + + /** + * @return The origin of this source. + */ + public TagOrigin getOrigin() { + return origin; + } + +} diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index 1a1587770fc..efbabaec12d 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2629,6 +2629,7 @@ types: billboard: billboard¦s @a textalignment: text alignment¦s @a itemdisplaytransform: item display transform¦s @an + minecrafttag: minecraft tag¦s @a experiencecooldownchangereason: experience cooldown change reason¦s @a inputkey: input key¦s @an villagertype: villager type¦s @a diff --git a/src/test/skript/tests/syntaxes/conditions/CondIsTagged.sk b/src/test/skript/tests/syntaxes/conditions/CondIsTagged.sk new file mode 100644 index 00000000000..7b6ec7b5638 --- /dev/null +++ b/src/test/skript/tests/syntaxes/conditions/CondIsTagged.sk @@ -0,0 +1,16 @@ +test "CondIsTagged": + assert oak slab is tagged as item tag "slabs" with "oak slab is not a slab" + assert oak slab is not tagged as item tag "stairs" with "oak slab is somehow a stair" + assert a skeleton is tagged as entity tag "minecraft:skeletons" with "skeleton is not a skeleton" + assert a cow is not tagged as tag "unknown" with "a cow is somehow tagged as unknown" + + assert a skeleton and a stray are tagged as tag "skeletons" with "skeleton and stray are not skeletons" + assert a skeleton or a zombie is tagged as tag "skeletons" with "skeleton or zombie are not skeletons" + + set block at location(0,20,0) to oak slab + assert block at location(0,20,0) is tagged as block tag "slabs" with "oak slab is not a slab" + set block at location(0,20,0) to air + + spawn a skeleton at spawn of world "world": + assert entity is tagged as entity tag "minecraft:skeletons" with "skeleton is not a skeleton" + delete entity diff --git a/src/test/skript/tests/syntaxes/effects/EffRegisterTag.sk b/src/test/skript/tests/syntaxes/effects/EffRegisterTag.sk new file mode 100644 index 00000000000..d3cde7b75aa --- /dev/null +++ b/src/test/skript/tests/syntaxes/effects/EffRegisterTag.sk @@ -0,0 +1,7 @@ +test "register tags": + register a custom item tag named "oak" containing oak log, oak planks, and oak wood + assert custom tag "oak" is set with "tag was not registered" + assert tag contents of tag "skript:oak" contains oak wood, oak log, and oak planks with "tag doesn't contain the right items" + + register an entity type tag named "skript:little_guy" using a baby zombie + assert a baby zombie is tagged as custom tag "little_guy" with "failed to classify baby zombie as a little guy" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprTagContents.sk b/src/test/skript/tests/syntaxes/expressions/ExprTagContents.sk new file mode 100644 index 00000000000..312c545e40c --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprTagContents.sk @@ -0,0 +1,7 @@ +test "tags contents": + assert tag contents of tag "dirt" contains dirt with "dirt isn't dirt" + + assert tag contents of block tag "slabs" contains oak slab with "oak slab is not a slab" + assert tag contents of item tag "slabs" contains oak slab with "oak slab is not a slab" + + assert tag contents of entity tag "minecraft:skeletons" contains a skeleton with "skeleton is not a skeleton" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprTagsOf.sk b/src/test/skript/tests/syntaxes/expressions/ExprTagsOf.sk new file mode 100644 index 00000000000..370e6e14743 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprTagsOf.sk @@ -0,0 +1,7 @@ +test "tags of": + assert tags of dirt contains tag "dirt" with "dirt isn't dirt" + + assert block tags of oak slab contain block tag "slabs" with "oak slab is not a slab" + assert item tags of oak slab contain item tag "slabs" with "oak slab is not a slab" + + assert tags of a skeleton contain entity tag "minecraft:skeletons" with "skeleton is not a skeleton" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprTagsOfType.sk b/src/test/skript/tests/syntaxes/expressions/ExprTagsOfType.sk new file mode 100644 index 00000000000..8938b392c36 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprTagsOfType.sk @@ -0,0 +1,7 @@ +test "all tags": + assert all tags contains tag "dirt" with "dirt isn't a tag" + + assert block tags contain block tag "slabs" with "slabs is not a block tag" + assert paper item tags contain item tag "paper:doors" with "paper:doors is not an paper item tag" + + assert minecraft entity tags contain entity tag "minecraft:skeletons" with "skeletons is not a tag"