From a18c5ae87ff41e7ce6cd1ea3f14d4a066d7ec73b Mon Sep 17 00:00:00 2001 From: Moderocky Date: Sun, 29 Dec 2024 21:58:28 +0000 Subject: [PATCH] Any X (#6728) * Add any-type & named. * Basic test for ExprName. * Make existing types any-types. * Register special any-info (for checking purposes). * Register any named type. * Add converters for everything to any-named. * Basic changes to ExprName. * Add more tests. * Add any-named lang entry. * Add any-sized type. * Support current types as amount. * New amount support + test. * Change tests so they pass :) * Fix world converter for 1.13. * Support containers. * Remove converter method I didn't use. * Clean up code for Java 17. * Apply suggestions from code review Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com> * Do change requests. * Patterns. * Add docs. * Fix. * Support numbered in empty. * Switch numbers for walrus. * Fix spacing. * Add user pattern. * Fix imports. * Apply suggestions from code review Co-authored-by: Patrick Miller * Fix switch. * Change method in slot. * Remove method for walrus. * Remove throw. * ExprName Fixes * Apply suggestions from code review Co-authored-by: Patrick Miller * Fix incompatibility in dependency. * Patch entity comparison thingy. * Do requested changes. * Some changes. --------- Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com> Co-authored-by: Patrick Miller Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> --- .../java/ch/njol/skript/aliases/ItemType.java | 39 ++++- .../java/ch/njol/skript/classes/AnyInfo.java | 40 +++++ .../ch/njol/skript/classes/ClassInfo.java | 135 ++++++++-------- .../classes/data/DefaultConverters.java | 139 +++++++++++++---- .../skript/classes/data/SkriptClasses.java | 82 ++++++---- .../java/ch/njol/skript/command/Commands.java | 2 +- .../njol/skript/conditions/CondContains.java | 103 +++++++------ .../njol/skript/conditions/CondIsEmpty.java | 43 +++--- .../njol/skript/expressions/ExprAmount.java | 73 +++++++-- .../skript/expressions/ExprItemAmount.java | 2 +- .../ch/njol/skript/expressions/ExprName.java | 145 +++++++----------- .../skript/lang/util/common/AnyAmount.java | 49 ++++++ .../skript/lang/util/common/AnyContains.java | 48 ++++++ .../skript/lang/util/common/AnyNamed.java | 42 +++++ .../skript/lang/util/common/AnyProvider.java | 28 ++++ .../java/ch/njol/skript/util/slot/Slot.java | 73 ++++++++- .../skript/lang/converter/Converter.java | 21 +++ .../skript/lang/converter/Converters.java | 16 ++ src/main/resources/lang/default.lang | 3 + .../tests/syntaxes/expressions/ExprAmount.sk | 12 ++ .../tests/syntaxes/expressions/ExprName.sk | 26 ++++ .../tests/syntaxes/sections/EffSecSpawn.sk | 10 +- 22 files changed, 826 insertions(+), 305 deletions(-) create mode 100644 src/main/java/ch/njol/skript/classes/AnyInfo.java create mode 100644 src/main/java/ch/njol/skript/lang/util/common/AnyAmount.java create mode 100644 src/main/java/ch/njol/skript/lang/util/common/AnyContains.java create mode 100644 src/main/java/ch/njol/skript/lang/util/common/AnyNamed.java create mode 100644 src/main/java/ch/njol/skript/lang/util/common/AnyProvider.java create mode 100644 src/test/skript/tests/syntaxes/expressions/ExprAmount.sk create mode 100644 src/test/skript/tests/syntaxes/expressions/ExprName.sk diff --git a/src/main/java/ch/njol/skript/aliases/ItemType.java b/src/main/java/ch/njol/skript/aliases/ItemType.java index 39ce6d21962..4cbea58df59 100644 --- a/src/main/java/ch/njol/skript/aliases/ItemType.java +++ b/src/main/java/ch/njol/skript/aliases/ItemType.java @@ -4,6 +4,8 @@ import ch.njol.skript.bukkitutil.BukkitUnsafe; import ch.njol.skript.bukkitutil.ItemUtils; import ch.njol.skript.lang.Unit; +import ch.njol.skript.lang.util.common.AnyAmount; +import ch.njol.skript.lang.util.common.AnyNamed; import ch.njol.skript.localization.Adjective; import ch.njol.skript.localization.GeneralWords; import ch.njol.skript.localization.Language; @@ -35,6 +37,7 @@ import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SkullMeta; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.NotSerializableException; @@ -55,7 +58,8 @@ import java.util.stream.Collectors; @ContainerType(ItemStack.class) -public class ItemType implements Unit, Iterable, Container, YggdrasilExtendedSerializable { +public class ItemType implements Unit, Iterable, Container, YggdrasilExtendedSerializable, + AnyNamed, AnyAmount { static { // This handles updating ItemType and ItemData variable records @@ -1444,4 +1448,37 @@ public ItemType getBaseType() { return copy; } + @Override + public @Nullable String name() { + ItemMeta meta = this.getItemMeta(); + return meta.hasDisplayName() ? meta.getDisplayName() : null; + } + + @Override + public boolean supportsNameChange() { + return true; + } + + @Override + public void setName(String name) { + ItemMeta meta = this.getItemMeta(); + meta.setDisplayName(name); + this.setItemMeta(meta); + } + + @Override + public @NotNull Number amount() { + return this.getAmount(); + } + + @Override + public boolean supportsAmountChange() { + return true; + } + + @Override + public void setAmount(@Nullable Number amount) throws UnsupportedOperationException { + this.setAmount(amount != null ? amount.intValue() : 0); + } + } diff --git a/src/main/java/ch/njol/skript/classes/AnyInfo.java b/src/main/java/ch/njol/skript/classes/AnyInfo.java new file mode 100644 index 00000000000..283e0c5189b --- /dev/null +++ b/src/main/java/ch/njol/skript/classes/AnyInfo.java @@ -0,0 +1,40 @@ +package ch.njol.skript.classes; + +import ch.njol.skript.lang.util.common.AnyProvider; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * A special kind of {@link ClassInfo} for dealing with 'any'-accepting types. + * These auto-generate their user patterns (e.g. {@code named} -> {@code any named thing}). + * + * @see AnyProvider + */ +public class AnyInfo extends ClassInfo { + + /** + * @param c The class + * @param codeName The name used in patterns + */ + public AnyInfo(Class c, String codeName) { + super(c, codeName); + this.user("(any )?" + codeName + " (thing|object)s?"); + } + + @Override + public ClassInfo user(String... userInputPatterns) throws PatternSyntaxException { + if (this.userInputPatterns == null) + return super.user(userInputPatterns); + // Allow appending the patterns. + List list = new ArrayList<>(List.of(this.userInputPatterns)); + for (String pattern : userInputPatterns) { + list.add(Pattern.compile(pattern)); + } + this.userInputPatterns = list.toArray(new Pattern[0]); + return this; + } + +} diff --git a/src/main/java/ch/njol/skript/classes/ClassInfo.java b/src/main/java/ch/njol/skript/classes/ClassInfo.java index 56b24d4dceb..3c96028c58a 100644 --- a/src/main/java/ch/njol/skript/classes/ClassInfo.java +++ b/src/main/java/ch/njol/skript/classes/ClassInfo.java @@ -28,23 +28,22 @@ */ @SuppressFBWarnings("DM_STRING_VOID_CTOR") public class ClassInfo implements Debuggable { - + private final Class c; private final String codeName; private final Noun name; - + @Nullable private DefaultExpression defaultExpression = null; - + @Nullable private Parser parser = null; - + @Nullable private Cloner cloner = null; - - @Nullable - private Pattern[] userInputPatterns = null; - + + @Nullable Pattern[] userInputPatterns = null; + @Nullable private Changer changer = null; @@ -55,12 +54,12 @@ public class ClassInfo implements Debuggable { private Serializer serializer = null; @Nullable private Class serializeAs = null; - + @Nullable private Arithmetic math = null; @Nullable private Class mathRelativeType = null; - + @Nullable private String docName = null; @Nullable @@ -73,13 +72,13 @@ public class ClassInfo implements Debuggable { private String since = null; @Nullable private String[] requiredPlugins = null; - + /** * Overrides documentation id assigned from class name. */ @Nullable private String documentationId = null; - + /** * @param c The class * @param codeName The name used in patterns @@ -91,7 +90,7 @@ public ClassInfo(final Class c, final String codeName) { this.codeName = codeName; name = new Noun("types." + codeName); } - + /** * Incorrect spelling in method name. This will be removed in the future. */ @@ -99,13 +98,13 @@ public ClassInfo(final Class c, final String codeName) { public static boolean isVaildCodeName(final String name) { return isValidCodeName(name); } - + public static boolean isValidCodeName(final String name) { - return name.matches("[a-z0-9]+"); + return name.matches("(?:any-)?[a-z0-9]+"); } - + // === FACTORY METHODS === - + /** * @param parser A parser to parse values of this class or null if not applicable */ @@ -114,7 +113,7 @@ public ClassInfo parser(final Parser parser) { this.parser = parser; return this; } - + /** * @param cloner A {@link Cloner} to clone values when setting variables * or passing function arguments. @@ -124,7 +123,7 @@ public ClassInfo cloner(Cloner cloner) { this.cloner = cloner; return this; } - + /** * @param userInputPatterns Regex patterns to match this class, e.g. in the expressions loop-[type], random [type] out of ..., or as command arguments. These patterns * must be english and match singular and plural. @@ -139,7 +138,7 @@ public ClassInfo user(final String... userInputPatterns) throws PatternSyntax } return this; } - + /** * @param defaultExpression The default (event) value of this class or null if not applicable * @see EventValueExpression @@ -187,7 +186,7 @@ public ClassInfo serializer(final Serializer serializer) { serializer.register(this); return this; } - + public ClassInfo serializeAs(final Class serializeAs) { assert this.serializeAs == null; if (serializer != null) @@ -195,12 +194,12 @@ public ClassInfo serializeAs(final Class serializeAs) { this.serializeAs = serializeAs; return this; } - + @Deprecated public ClassInfo changer(final SerializableChanger changer) { return changer((Changer) changer); } - + public ClassInfo changer(final Changer changer) { assert this.changer == null; this.changer = changer; @@ -221,15 +220,15 @@ public ClassInfo math(final Class relativeType, final Arithmetic name(final String name) { this.docName = name; return this; } - + /** * Only used for Skript's documentation. - * + * * @param description * @return This ClassInfo object */ @@ -250,10 +249,10 @@ public ClassInfo description(final String... description) { this.description = description; return this; } - + /** * Only used for Skript's documentation. - * + * * @param usage * @return This ClassInfo object */ @@ -262,10 +261,10 @@ public ClassInfo usage(final String... usage) { this.usage = usage; return this; } - + /** * Only used for Skript's documentation. - * + * * @param examples * @return This ClassInfo object */ @@ -274,10 +273,10 @@ public ClassInfo examples(final String... examples) { this.examples = examples; return this; } - + /** * Only used for Skript's documentation. - * + * * @param since * @return This ClassInfo object */ @@ -286,7 +285,7 @@ public ClassInfo since(final String since) { this.since = since; return this; } - + /** * Other plugin dependencies for this ClassInfo. * @@ -300,7 +299,7 @@ public ClassInfo requiredPlugins(final String... pluginNames) { this.requiredPlugins = pluginNames; return this; } - + /** * Overrides default documentation id, which is assigned from class name. * This is especially useful for inner classes whose names are useless without @@ -313,36 +312,36 @@ public ClassInfo documentationId(String id) { this.documentationId = id; return this; } - + // === GETTERS === - + public Class getC() { return c; } - + public Noun getName() { return name; } - + public String getCodeName() { return codeName; } - + @Nullable public DefaultExpression getDefaultExpression() { return defaultExpression; } - + @Nullable public Parser getParser() { return parser; } - + @Nullable public Cloner getCloner() { return cloner; } - + /** * Clones the given object using {@link ClassInfo#cloner}, * returning the given object if no {@link Cloner} is registered. @@ -350,12 +349,12 @@ public Cloner getCloner() { public T clone(T t) { return cloner == null ? t : cloner.clone(t); } - + @Nullable public Pattern[] getUserInputPatterns() { return userInputPatterns; } - + @Nullable public Changer getChanger() { return changer; @@ -372,12 +371,12 @@ public Supplier> getSupplier() { public Serializer getSerializer() { return serializer; } - + @Nullable public Class getSerializeAs() { return serializeAs; } - + @Nullable @Deprecated public Arithmetic getMath() { @@ -389,33 +388,33 @@ public Class getSerializeAs() { public Arithmetic getRelativeMath() { return (Arithmetic) math; } - + @Nullable @Deprecated public Class getMathRelativeType() { return mathRelativeType; } - + @Nullable public String[] getDescription() { return description; } - + @Nullable public String[] getUsage() { return usage; } - + @Nullable public String[] getExamples() { return examples; } - + @Nullable public String getSince() { return since; } - + @Nullable public String getDocName() { return docName; @@ -425,7 +424,7 @@ public String getDocName() { public String[] getRequiredPlugins() { return requiredPlugins; } - + /** * Gets overridden documentation id of this this type. If no override has * been set, null is returned and the caller may try to derive this from @@ -440,13 +439,13 @@ public String getDocumentationID() { public boolean hasDocs() { return getDocName() != null && !ClassInfo.NO_DOC.equals(getDocName()); } - + // === ORDERING === - + @Nullable private Set before; private final Set after = new HashSet<>(); - + /** * Sets one or more classes that this class should occur before in the class info list. This only affects the order in which classes are parsed if it's unknown of which type * the parsed string is. @@ -454,7 +453,7 @@ public boolean hasDocs() { * Please note that subclasses will always be registered before superclasses, no matter what is defined here or in {@link #after(String...)}. *

* This list can safely contain classes that may not exist. - * + * * @param before * @return this ClassInfo */ @@ -463,7 +462,7 @@ public ClassInfo before(final String... before) { this.before = new HashSet<>(Arrays.asList(before)); return this; } - + /** * Sets one or more classes that this class should occur after in the class info list. This only affects the order in which classes are parsed if it's unknown of which type * the parsed string is. @@ -471,7 +470,7 @@ public ClassInfo before(final String... before) { * Please note that subclasses will always be registered before superclasses, no matter what is defined here or in {@link #before(String...)}. *

* This list can safely contain classes that may not exist. - * + * * @param after * @return this ClassInfo */ @@ -479,7 +478,7 @@ public ClassInfo after(final String... after) { this.after.addAll(Arrays.asList(after)); return this; } - + /** * @return Set of classes that should be after this one. May return null. */ @@ -487,26 +486,26 @@ public ClassInfo after(final String... after) { public Set before() { return before; } - + /** * @return Set of classes that should be before this one. Never returns null. */ public Set after() { return after; } - + // === GENERAL === - + @Override @NotNull public String toString() { return getName().getSingular(); } - + public String toString(final int flags) { return getName().toString(flags); } - + @Override @NotNull public String toString(final @Nullable Event event, final boolean debug) { @@ -514,5 +513,5 @@ public String toString(final @Nullable Event event, final boolean debug) { return codeName + " (" + c.getCanonicalName() + ")"; return getName().getSingular(); } - + } diff --git a/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java b/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java index 9f0731eaa7c..a73fece5a1f 100644 --- a/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java +++ b/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java @@ -1,21 +1,20 @@ package ch.njol.skript.classes.data; +import ch.njol.skript.Skript; import ch.njol.skript.aliases.ItemType; import ch.njol.skript.command.Commands; import ch.njol.skript.entity.EntityData; import ch.njol.skript.entity.EntityType; import ch.njol.skript.entity.XpOrbData; +import ch.njol.skript.lang.util.common.AnyAmount; +import ch.njol.skript.lang.util.common.AnyNamed; import ch.njol.skript.util.BlockInventoryHolder; import ch.njol.skript.util.BlockUtils; import ch.njol.skript.util.Direction; import ch.njol.skript.util.EnchantmentType; import ch.njol.skript.util.Experience; import ch.njol.skript.util.slot.Slot; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.World; +import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.DoubleChest; @@ -30,14 +29,19 @@ import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; +import org.bukkit.plugin.Plugin; +import org.bukkit.scoreboard.Objective; +import org.bukkit.scoreboard.Team; import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnknownNullability; import org.skriptlang.skript.lang.converter.Converter; import org.skriptlang.skript.lang.converter.Converters; public class DefaultConverters { - + public DefaultConverters() {} - + static { // Number to subtypes converters Converters.registerConverter(Number.class, Byte.class, Number::byteValue); @@ -82,27 +86,27 @@ public DefaultConverters() {} return (LivingEntity) e; return null; }); - + // Block - Inventory Converters.registerConverter(Block.class, Inventory.class, b -> { if (b.getState() instanceof InventoryHolder) return ((InventoryHolder) b.getState()).getInventory(); return null; }, Commands.CONVERTER_NO_COMMAND_ARGUMENTS); - + // Entity - Inventory Converters.registerConverter(Entity.class, Inventory.class, e -> { if (e instanceof InventoryHolder) return ((InventoryHolder) e).getInventory(); return null; }, Commands.CONVERTER_NO_COMMAND_ARGUMENTS); - + // Block - ItemType Converters.registerConverter(Block.class, ItemType.class, ItemType::new, Converter.NO_LEFT_CHAINING | Commands.CONVERTER_NO_COMMAND_ARGUMENTS); // Block - Location Converters.registerConverter(Block.class, Location.class, BlockUtils::getLocation, Commands.CONVERTER_NO_COMMAND_ARGUMENTS); - + // Entity - Location Converters.registerConverter(Entity.class, Location.class, Entity::getLocation, Commands.CONVERTER_NO_COMMAND_ARGUMENTS); @@ -111,21 +115,21 @@ public DefaultConverters() {} // EntityData - EntityType Converters.registerConverter(EntityData.class, EntityType.class, data -> new EntityType(data, -1)); - + // ItemType - ItemStack Converters.registerConverter(ItemType.class, ItemStack.class, ItemType::getRandom); Converters.registerConverter(ItemStack.class, ItemType.class, ItemType::new); - + // Experience - XpOrbData Converters.registerConverter(Experience.class, XpOrbData.class, e -> new XpOrbData(e.getXP())); Converters.registerConverter(XpOrbData.class, Experience.class, e -> new Experience(e.getExperience())); - + // Slot - ItemType Converters.registerConverter(Slot.class, ItemType.class, s -> { ItemStack i = s.getItem(); return new ItemType(i != null ? i : new ItemStack(Material.AIR, 1)); }); - + // Block - InventoryHolder Converters.registerConverter(Block.class, InventoryHolder.class, b -> { BlockState s = b.getState(); @@ -144,25 +148,108 @@ public DefaultConverters() {} // InventoryHolder - Entity Converters.registerConverter(InventoryHolder.class, Entity.class, holder -> { - if (holder instanceof Entity) - return (Entity) holder; + if (holder instanceof Entity entity) + return entity; return null; }, Converter.NO_CHAINING); + // Anything with a name -> AnyNamed + Converters.registerConverter(OfflinePlayer.class, AnyNamed.class, player -> player::getName, Converter.NO_RIGHT_CHAINING); + if (Skript.classExists("org.bukkit.generator.WorldInfo")) + Converters.registerConverter(World.class, AnyNamed.class, world -> world::getName, Converter.NO_RIGHT_CHAINING); + else //noinspection RedundantCast getName method is on World itself in older versions + Converters.registerConverter(World.class, AnyNamed.class, world -> () -> ((World) world).getName(), Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(GameRule.class, AnyNamed.class, rule -> rule::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Server.class, AnyNamed.class, server -> server::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Plugin.class, AnyNamed.class, plugin -> plugin::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(WorldType.class, AnyNamed.class, type -> type::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Team.class, AnyNamed.class, team -> team::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Objective.class, AnyNamed.class, objective -> objective::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Nameable.class, AnyNamed.class, // + nameable -> new AnyNamed() { + @Override + public @UnknownNullability String name() { + //noinspection deprecation + return nameable.getCustomName(); + } + + @Override + public boolean supportsNameChange() { + return true; + } + + @Override + public void setName(String name) { + //noinspection deprecation + nameable.setCustomName(name); + } + }, + // + Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Block.class, AnyNamed.class, // + block -> new AnyNamed() { + @Override + public @UnknownNullability String name() { + BlockState state = block.getState(); + if (state instanceof Nameable nameable) + //noinspection deprecation + return nameable.getCustomName(); + return null; + } + + @Override + public boolean supportsNameChange() { + return true; + } + + @Override + public void setName(String name) { + BlockState state = block.getState(); + if (state instanceof Nameable nameable) + //noinspection deprecation + nameable.setCustomName(name); + } + }, + // + Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(CommandSender.class, AnyNamed.class, thing -> thing::getName, Converter.NO_RIGHT_CHAINING); + // Command senders should be done last because there might be a better alternative above + + // Anything with an amount -> AnyAmount + Converters.registerConverter(ItemStack.class, AnyAmount.class, // + item -> new AnyAmount() { + + @Override + public @NotNull Number amount() { + return item.getAmount(); + } + + @Override + public boolean supportsAmountChange() { + return true; + } + + @Override + public void setAmount(Number amount) { + item.setAmount(amount != null ? amount.intValue() : 0); + } + }, + // + Converter.NO_RIGHT_CHAINING); + // InventoryHolder - Location // since the individual ones can't be trusted to chain. Converters.registerConverter(InventoryHolder.class, Location.class, holder -> { - if (holder instanceof Entity) - return ((Entity) holder).getLocation(); - if (holder instanceof Block) - return ((Block) holder).getLocation(); - if (holder instanceof BlockState) - return BlockUtils.getLocation(((BlockState) holder).getBlock()); - if (holder instanceof DoubleChest) { - DoubleChest doubleChest = (DoubleChest) holder; + if (holder instanceof Entity entity) + return entity.getLocation(); + if (holder instanceof Block block) + return block.getLocation(); + if (holder instanceof BlockState state) + return BlockUtils.getLocation(state.getBlock()); + if (holder instanceof DoubleChest doubleChest) { if (doubleChest.getLeftSide() != null) { return BlockUtils.getLocation(((BlockState) doubleChest.getLeftSide()).getBlock()); - } else if (((DoubleChest) holder).getRightSide() != null) { + } else if (doubleChest.getRightSide() != null) { return BlockUtils.getLocation(((BlockState) doubleChest.getRightSide()).getBlock()); } } diff --git a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java index 57704e760ee..f3e02109153 100644 --- a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java @@ -1,17 +1,20 @@ package ch.njol.skript.classes.data; +import ch.njol.skript.classes.*; +import ch.njol.skript.lang.util.common.AnyAmount; +import ch.njol.skript.lang.util.common.AnyContains; +import ch.njol.skript.lang.util.common.AnyNamed; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; + import ch.njol.skript.Skript; import ch.njol.skript.aliases.Aliases; import ch.njol.skript.aliases.ItemData; import ch.njol.skript.aliases.ItemType; import ch.njol.skript.bukkitutil.EnchantmentUtils; import ch.njol.skript.bukkitutil.ItemUtils; -import ch.njol.skript.classes.Changer; -import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.classes.EnumSerializer; -import ch.njol.skript.classes.Parser; -import ch.njol.skript.classes.Serializer; -import ch.njol.skript.classes.YggdrasilSerializer; import ch.njol.skript.expressions.base.EventValueExpression; import ch.njol.skript.lang.ParseContext; import ch.njol.skript.lang.util.SimpleLiteral; @@ -36,21 +39,17 @@ import ch.njol.skript.util.visual.VisualEffect; import ch.njol.skript.util.visual.VisualEffects; import ch.njol.yggdrasil.Fields; -import org.bukkit.Material; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.Nullable; import java.io.StreamCorruptedException; -import java.util.Arrays; import java.util.Iterator; import java.util.Locale; import java.util.regex.Pattern; +import java.util.Arrays; @SuppressWarnings("rawtypes") public class SkriptClasses { public SkriptClasses() {} - + static { //noinspection unchecked Classes.registerClass(new ClassInfo<>(ClassInfo.class, "classinfo") @@ -73,17 +72,17 @@ public SkriptClasses() {} public ClassInfo parse(final String s, final ParseContext context) { return Classes.getClassInfoFromUserInput(Noun.stripIndefiniteArticle(s)); } - + @Override public String toString(final ClassInfo c, final int flags) { return c.toString(flags); } - + @Override public String toVariableNameString(final ClassInfo c) { return c.getCodeName(); } - + @Override public String getDebugMessage(final ClassInfo c) { return c.getCodeName(); @@ -97,17 +96,17 @@ public Fields serialize(final ClassInfo c) { f.putObject("codeName", c.getCodeName()); return f; } - + @Override public boolean canBeInstantiated() { return false; } - + @Override public void deserialize(final ClassInfo o, final Fields f) throws StreamCorruptedException { assert false; } - + @Override protected ClassInfo deserialize(final Fields fields) throws StreamCorruptedException { final String codeName = fields.getObject("codeName", String.class); @@ -118,20 +117,20 @@ protected ClassInfo deserialize(final Fields fields) throws StreamCorruptedExcep throw new StreamCorruptedException("Invalid ClassInfo " + codeName); return ci; } - + // return c.getCodeName(); @Override @Nullable public ClassInfo deserialize(final String s) { return Classes.getClassInfoNoError(s); } - + @Override public boolean mustSyncDeserialization() { return false; } })); - + Classes.registerClass(new ClassInfo<>(WeatherType.class, "weathertype") .user("weather ?types?", "weather conditions?", "weathers?") .name("Weather Type") @@ -148,12 +147,12 @@ public boolean mustSyncDeserialization() { public WeatherType parse(final String s, final ParseContext context) { return WeatherType.parse(s); } - + @Override public String toString(final WeatherType o, final int flags) { return o.toString(flags); } - + @Override public String toVariableNameString(final WeatherType o) { return "" + o.name().toLowerCase(Locale.ENGLISH); @@ -161,7 +160,7 @@ public String toVariableNameString(final WeatherType o) { }) .serializer(new EnumSerializer<>(WeatherType.class))); - + Classes.registerClass(new ClassInfo<>(ItemType.class, "itemtype") .user("item ?types?", "materials?") .name("Item Type") @@ -603,7 +602,7 @@ public String toVariableNameString(final EnchantmentType o) { .since("2.0") .parser(new Parser() { private final RegexMessage pattern = new RegexMessage("types.experience.pattern", Pattern.CASE_INSENSITIVE); - + @Override @Nullable public Experience parse(String s, final ParseContext context) { @@ -616,12 +615,12 @@ public Experience parse(String s, final ParseContext context) { return new Experience(xp); return null; } - + @Override public String toString(final Experience xp, final int flags) { return xp.toString(); } - + @Override public String toVariableNameString(final Experience xp) { return "" + xp.getXP(); @@ -658,7 +657,7 @@ public String toVariableNameString(VisualEffect e) { }) .serializer(new YggdrasilSerializer<>())); - + Classes.registerClass(new ClassInfo<>(GameruleValue.class, "gamerulevalue") .user("gamerule values?") .name("Gamerule Value") @@ -668,6 +667,31 @@ public String toVariableNameString(VisualEffect e) { .since("2.5") .serializer(new YggdrasilSerializer()) ); + + Classes.registerClass(new AnyInfo<>(AnyNamed.class, "named") + .name("Any Named Thing") + .description("Something that has a name (e.g. an item).") + .usage("") + .examples("{thing}'s name") + .since("INSERT VERSION") + ); + + Classes.registerClass(new AnyInfo<>(AnyAmount.class, "numbered") + .name("Any Numbered/Sized Thing") + .description("Something that has an amount or size.") + .usage("") + .examples("the size of {thing}", "the amount of {thing}") + .since("INSERT VERSION") + ); + + Classes.registerClass(new AnyInfo<>(AnyContains.class, "containing") + .user("any container") + .name("Anything with Contents") + .description("Something that contains other things.") + .usage("") + .examples("{a} contains {b}") + .since("INSERT VERSION") + ); } - + } diff --git a/src/main/java/ch/njol/skript/command/Commands.java b/src/main/java/ch/njol/skript/command/Commands.java index 97f67d69f26..e8c778bf559 100644 --- a/src/main/java/ch/njol/skript/command/Commands.java +++ b/src/main/java/ch/njol/skript/command/Commands.java @@ -59,7 +59,7 @@ public abstract class Commands { /** * A Converter flag declaring that a Converter cannot be used for parsing command arguments. */ - public static final int CONVERTER_NO_COMMAND_ARGUMENTS = 4; + public static final int CONVERTER_NO_COMMAND_ARGUMENTS = 8; private final static Map commands = new HashMap<>(); diff --git a/src/main/java/ch/njol/skript/conditions/CondContains.java b/src/main/java/ch/njol/skript/conditions/CondContains.java index 6babfffc289..366141ae0b3 100644 --- a/src/main/java/ch/njol/skript/conditions/CondContains.java +++ b/src/main/java/ch/njol/skript/conditions/CondContains.java @@ -3,6 +3,7 @@ import ch.njol.skript.Skript; import ch.njol.skript.SkriptConfig; import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.lang.util.common.AnyContains; import org.skriptlang.skript.lang.comparator.Relation; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; @@ -18,14 +19,16 @@ import org.bukkit.event.Event; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import org.skriptlang.skript.lang.converter.Converters; import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.Objects; @Name("Contains") -@Description("Checks whether an inventory contains an item, a text contains another piece of text, " + - "or a list (e.g. {list variable::*} or 'drops') contains another object.") +@Description("Checks whether an inventory contains an item, a text contains another piece of text, " + + "a container contains something, " + + "or a list (e.g. {list variable::*} or 'drops') contains another object.") @Examples({"block contains 20 cobblestone", "player has 4 flint and 2 iron ingots", "{list::*} contains 5"}) @@ -45,16 +48,13 @@ public class CondContains extends Condition { * The type of check to perform */ private enum CheckType { - STRING, INVENTORY, OBJECTS, UNKNOWN + STRING, INVENTORY, OBJECTS, UNKNOWN, CONTAINER } - @SuppressWarnings("NotNullFieldNotInitialized") private Expression containers; - @SuppressWarnings("NotNullFieldNotInitialized") private Expression items; private boolean explicitSingle; - @SuppressWarnings("NotNullFieldNotInitialized") private CheckType checkType; @Override @@ -70,15 +70,15 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye checkType = CheckType.UNKNOWN; } - setNegated(matchedPattern % 2 == 1); + this.setNegated(matchedPattern % 2 == 1); return true; } @Override - public boolean check(Event e) { + public boolean check(Event event) { CheckType checkType = this.checkType; - Object[] containerValues = containers.getAll(e); + Object[] containerValues = containers.getAll(event); if (containerValues.length == 0) return isNegated(); @@ -88,6 +88,11 @@ public boolean check(Event e) { if (Arrays.stream(containerValues) .allMatch(Inventory.class::isInstance)) { checkType = CheckType.INVENTORY; + } else if (explicitSingle + && Arrays.stream(containerValues) + .allMatch(object -> object instanceof AnyContains + || Converters.converterExists(object.getClass(), AnyContains.class))) { + checkType = CheckType.CONTAINER; } else if (explicitSingle && Arrays.stream(containerValues) .allMatch(String.class::isInstance)) { @@ -97,48 +102,60 @@ public boolean check(Event e) { } } - if (checkType == CheckType.INVENTORY) { - return SimpleExpression.check(containerValues, o -> { + return switch (checkType) { + case INVENTORY -> SimpleExpression.check(containerValues, o -> { Inventory inventory = (Inventory) o; - return items.check(e, o1 -> { - if (o1 instanceof ItemType) - return ((ItemType) o1).isContainedIn(inventory); - else if (o1 instanceof ItemStack) - return inventory.containsAtLeast((ItemStack) o1, ((ItemStack) o1).getAmount()); - else if (o1 instanceof Inventory) + return items.check(event, o1 -> { + if (o1 instanceof ItemType type) { + return type.isContainedIn(inventory); + } else if (o1 instanceof ItemStack stack) { + return inventory.containsAtLeast(stack, stack.getAmount()); + } else if (o1 instanceof Inventory) { return Objects.equals(inventory, o1); - else - return false; - }); - }, isNegated(), containers.getAnd()); - } else if (checkType == CheckType.STRING) { - boolean caseSensitive = SkriptConfig.caseSensitive.value(); - - return SimpleExpression.check(containerValues, o -> { - String string = (String) o; - - return items.check(e, o1 -> { - if (o1 instanceof String) { - return StringUtils.contains(string, (String) o1, caseSensitive); - } else { - return false; } + return false; }); }, isNegated(), containers.getAnd()); - } else { - assert checkType == CheckType.OBJECTS; - - return items.check(e, o1 -> { - for (Object o2 : containerValues) { - if (Comparators.compare(o1, o2) == Relation.EQUAL) - return true; + case STRING -> { + boolean caseSensitive = SkriptConfig.caseSensitive.value(); + + yield SimpleExpression.check(containerValues, o -> { + String string = (String) o; + + return items.check(event, o1 -> { + if (o1 instanceof String text) { + return StringUtils.contains(string, text, caseSensitive); + } else { + return false; + } + }); + }, isNegated(), containers.getAnd()); + } + case CONTAINER -> SimpleExpression.check(containerValues, object -> { + AnyContains container; + if (object instanceof AnyContains) { + container = (AnyContains) object; + } else { + container = Converters.convert(object, AnyContains.class); } - return false; - }, isNegated()); - } + if (container == null) + return false; + return items.check(event, container::checkSafely); + }, isNegated(), containers.getAnd()); + default -> { + assert checkType == CheckType.OBJECTS; + yield items.check(event, o1 -> { + for (Object o2 : containerValues) { + if (Comparators.compare(o1, o2) == Relation.EQUAL) + return true; + } + return false; + }, isNegated()); + } + }; } - + @Override public String toString(@Nullable Event e, boolean debug) { return containers.toString(e, debug) + (isNegated() ? " doesn't contain " : " contains ") + items.toString(e, debug); diff --git a/src/main/java/ch/njol/skript/conditions/CondIsEmpty.java b/src/main/java/ch/njol/skript/conditions/CondIsEmpty.java index cf2623d88f0..d3eb1825c3e 100644 --- a/src/main/java/ch/njol/skript/conditions/CondIsEmpty.java +++ b/src/main/java/ch/njol/skript/conditions/CondIsEmpty.java @@ -1,52 +1,51 @@ package ch.njol.skript.conditions; -import org.bukkit.Material; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; - import ch.njol.skript.conditions.base.PropertyCondition; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.util.common.AnyAmount; import ch.njol.skript.util.slot.Slot; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; -/** - * @author Peter Güttinger - */ @Name("Is Empty") @Description("Checks whether an inventory, an inventory slot, or a text is empty.") @Examples("player's inventory is empty") @Since("unknown (before 2.1)") public class CondIsEmpty extends PropertyCondition { - + static { - register(CondIsEmpty.class, "empty", "inventories/slots/strings"); + register(CondIsEmpty.class, "empty", "inventories/slots/strings/numbered"); } - + @Override - public boolean check(final Object o) { - if (o instanceof String) - return ((String) o).isEmpty(); - if (o instanceof Inventory) { - for (ItemStack s : ((Inventory) o).getContents()) { - if (s != null && s.getType() != Material.AIR) + public boolean check(Object object) { + if (object instanceof String string) + return string.isEmpty(); + if (object instanceof Inventory inventory) { + for (ItemStack item : inventory.getContents()) { + if (item != null && item.getType() != Material.AIR) return false; // There is an item here! } return true; } - if (o instanceof Slot) { - final Slot s = (Slot) o; - final ItemStack i = s.getItem(); - return i == null || i.getType() == Material.AIR; + if (object instanceof Slot slot) { + final ItemStack item = slot.getItem(); + return item == null || item.getType() == Material.AIR; + } + if (object instanceof AnyAmount numbered) { + return numbered.isEmpty(); } assert false; return false; } - + @Override protected String getPropertyName() { return "empty"; } - + } diff --git a/src/main/java/ch/njol/skript/expressions/ExprAmount.java b/src/main/java/ch/njol/skript/expressions/ExprAmount.java index b9898f2f677..06abd492dc2 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprAmount.java +++ b/src/main/java/ch/njol/skript/expressions/ExprAmount.java @@ -1,6 +1,7 @@ package ch.njol.skript.expressions; import ch.njol.skript.Skript; +import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; @@ -12,18 +13,20 @@ import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.Variable; import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.lang.util.common.AnyAmount; import ch.njol.util.Kleenean; +import ch.njol.util.coll.CollectionUtils; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import java.util.Map; /** - * + * * @author Peter Güttinger */ @Name("Amount") -@Description({"The amount of something.", +@Description({"The amount or size of something.", "Please note that amount of %items% will not return the number of items, but the number of stacks, e.g. 1 for a stack of 64 torches. To get the amount of items in a stack, see the item amount expression.", "", "Also, you can get the recursive size of a list, which will return the recursive size of the list with sublists included, e.g.", @@ -42,23 +45,30 @@ "Please note that getting a list's recursive size can cause lag if the list is large, so only use this expression if you need to!"}) @Examples({"message \"There are %number of all players% players online!\""}) @Since("1.0") -public class ExprAmount extends SimpleExpression { +public class ExprAmount extends SimpleExpression { static { - Skript.registerExpression(ExprAmount.class, Long.class, ExpressionType.PROPERTY, + Skript.registerExpression(ExprAmount.class, Number.class, ExpressionType.PROPERTY, + "[the] (amount|number|size) of %numbered%", "[the] (amount|number|size) of %objects%", "[the] recursive (amount|number|size) of %objects%"); } @SuppressWarnings("null") private ExpressionList exprs; + private @Nullable Expression any; private boolean recursive; @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + if (matchedPattern == 0) { + //noinspection unchecked + this.any = (Expression) exprs[0]; + return true; + } this.exprs = exprs[0] instanceof ExpressionList ? (ExpressionList) exprs[0] : new ExpressionList<>(new Expression[]{exprs[0]}, Object.class, false); - this.recursive = matchedPattern == 1; + this.recursive = matchedPattern == 2; for (Expression expr : this.exprs.getExpressions()) { if (expr instanceof Literal) { return false; @@ -77,7 +87,9 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye @Override @SuppressWarnings("unchecked") - protected Long[] get(Event e) { + protected Number[] get(Event e) { + if (any != null) + return new Number[] {any.getOptionalSingle(e).orElse(() -> 0).amount()}; if (recursive) { int currentSize = 0; for (Expression expr : exprs.getExpressions()) { @@ -91,6 +103,45 @@ protected Long[] get(Event e) { return new Long[]{(long) exprs.getArray(e).length}; } + @Override + public @Nullable Class[] acceptChange(ChangeMode mode) { + if (any != null) { + return switch (mode) { + case SET, ADD, RESET, DELETE, REMOVE -> CollectionUtils.array(Number.class); + default -> null; + }; + } + return super.acceptChange(mode); + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + if (any == null) { + super.change(event, delta, mode); + return; + } + double amount = delta != null ? ((Number) delta[0]).doubleValue() : 1; + // It's okay to treat it as a double even if it's a whole number because there's no case in + // the set of real numbers where (x->double + y->double)->long != (x+y) + switch (mode) { + case REMOVE: + amount = -amount; + //$FALL-THROUGH$ + case ADD: + for (AnyAmount obj : any.getArray(event)) { + if (obj.supportsAmountChange()) + obj.setAmount(obj.amount().doubleValue() + amount); + } + break; + case RESET, DELETE, SET: + for (AnyAmount any : any.getArray(event)) { + if (any.supportsAmountChange()) + any.setAmount(amount); + } + break; + } + } + @SuppressWarnings("unchecked") private static int getRecursiveSize(Map map) { int count = 0; @@ -110,13 +161,15 @@ public boolean isSingle() { } @Override - public Class getReturnType() { - return Long.class; + public Class getReturnType() { + return any != null ? Number.class : Long.class; } @Override - public String toString(@Nullable Event e, boolean debug) { - return (recursive ? "recursive size of " : "amount of ") + exprs.toString(e, debug); + public String toString(@Nullable Event event, boolean debug) { + if (any != null) + return "amount of " + any.toString(event, debug); + return (recursive ? "recursive size of " : "amount of ") + exprs.toString(event, debug); } } diff --git a/src/main/java/ch/njol/skript/expressions/ExprItemAmount.java b/src/main/java/ch/njol/skript/expressions/ExprItemAmount.java index 23b3fe5eaef..be39645cd7f 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprItemAmount.java +++ b/src/main/java/ch/njol/skript/expressions/ExprItemAmount.java @@ -92,6 +92,6 @@ public Class getReturnType() { @Override protected String getPropertyName() { - return "item[[ ]stack] (amount|size|number)"; + return "item amount"; } } diff --git a/src/main/java/ch/njol/skript/expressions/ExprName.java b/src/main/java/ch/njol/skript/expressions/ExprName.java index 6149bfe7667..ae6b295e670 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprName.java +++ b/src/main/java/ch/njol/skript/expressions/ExprName.java @@ -4,13 +4,14 @@ import java.util.List; import ch.njol.skript.bukkitutil.InventoryUtils; -import ch.njol.skript.bukkitutil.ItemUtils; +import ch.njol.skript.lang.util.common.AnyNamed; import org.bukkit.Bukkit; import org.bukkit.GameRule; import org.bukkit.Nameable; import org.bukkit.OfflinePlayer; import org.bukkit.block.Block; import org.bukkit.block.BlockState; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Entity; import org.bukkit.entity.HumanEntity; import org.bukkit.entity.LivingEntity; @@ -99,17 +100,16 @@ public class ExprName extends SimplePropertyExpression { @Nullable private static BungeeComponentSerializer serializer; - static final boolean HAS_GAMERULES; static { // Check for Adventure API if (Skript.classExists("net.kyori.adventure.text.Component") && Skript.methodExists(Bukkit.class, "createInventory", InventoryHolder.class, int.class, Component.class)) serializer = BungeeComponentSerializer.get(); - HAS_GAMERULES = Skript.classExists("org.bukkit.GameRule"); - register(ExprName.class, String.class, "(1¦name[s]|2¦(display|nick|chat|custom)[ ]name[s])", "offlineplayers/entities/blocks/itemtypes/inventories/slots/worlds" - + (HAS_GAMERULES ? "/gamerules" : "")); - register(ExprName.class, String.class, "(3¦(player|tab)[ ]list name[s])", "players"); + register(ExprName.class, String.class, "(1:name[s])", "offlineplayers/entities/inventories/named"); + register(ExprName.class, String.class, "(2:(display|nick|chat|custom)[ ]name[s])", "offlineplayers/entities/inventories/named"); + register(ExprName.class, String.class, "(3:(player|tab)[ ]list name[s])", "players"); + // we keep the entity input because we want to do something special with entities } /* @@ -123,54 +123,36 @@ public class ExprName extends SimplePropertyExpression { public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { mark = parseResult.mark; setExpr(exprs[0]); - if (mark != 1 && World.class.isAssignableFrom(getExpr().getReturnType())) { - Skript.error("Can't use 'display name' with worlds. Use 'name' instead."); - return false; - } return true; } @Override - @Nullable - public String convert(Object object) { - if (object instanceof OfflinePlayer && ((OfflinePlayer) object).isOnline()) - object = ((OfflinePlayer) object).getPlayer(); - - if (object instanceof Player) { - switch (mark) { - case 1: - return ((Player) object).getName(); - case 2: - return ((Player) object).getDisplayName(); - case 3: - return ((Player) object).getPlayerListName(); + public @Nullable String convert(Object object) { + if (object instanceof OfflinePlayer offlinePlayer) { + if (offlinePlayer.isOnline()) { // Defer to player check below + object = offlinePlayer.getPlayer(); + } else { // We can only support "name" + return mark == 1 ? offlinePlayer.getName() : null; } - } else if (object instanceof OfflinePlayer) { - return mark == 1 ? ((OfflinePlayer) object).getName() : null; - } else if (object instanceof Entity) { - return ((Entity) object).getCustomName(); - } else if (object instanceof Block) { - BlockState state = ((Block) object).getState(); - if (state instanceof Nameable) - return ((Nameable) state).getCustomName(); - } else if (object instanceof ItemType) { - ItemMeta m = ((ItemType) object).getItemMeta(); - return m.hasDisplayName() ? m.getDisplayName() : null; - } else if (object instanceof Inventory) { - Inventory inventory = (Inventory) object; + } + + if (object instanceof Player player) { + return switch (mark) { + case 1 -> player.getName(); + case 2 -> player.getDisplayName(); + case 3 -> player.getPlayerListName(); + default -> throw new IllegalStateException("Unexpected value: " + mark); + }; + } else if (object instanceof Nameable nameable) { + if (mark == 1 && nameable instanceof CommandSender sender) + return sender.getName(); + return nameable.getCustomName(); + } else if (object instanceof Inventory inventory) { if (inventory.getViewers().isEmpty()) return null; return InventoryUtils.getTitle(inventory.getViewers().get(0).getOpenInventory()); - } else if (object instanceof Slot) { - ItemStack is = ((Slot) object).getItem(); - if (is != null && is.hasItemMeta()) { - ItemMeta m = is.getItemMeta(); - return m.hasDisplayName() ? m.getDisplayName() : null; - } - } else if (object instanceof World) { - return ((World) object).getName(); - } else if (HAS_GAMERULES && object instanceof GameRule) { - return ((GameRule) object).getName(); + } else if (object instanceof AnyNamed named) { + return named.name(); } return null; } @@ -196,41 +178,32 @@ public Class[] acceptChange(ChangeMode mode) { public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { String name = delta != null ? (String) delta[0] : null; for (Object object : getExpr().getArray(event)) { - if (object instanceof Player) { + if (object instanceof Player player) { switch (mark) { case 2: - ((Player) object).setDisplayName(name != null ? name + ChatColor.RESET : ((Player) object).getName()); + player.setDisplayName(name != null ? name + ChatColor.RESET : ((Player) object).getName()); break; case 3: // Null check not necessary. This method will use the player's name if 'name' is null. - ((Player) object).setPlayerListName(name); + player.setPlayerListName(name); break; } - } else if (object instanceof Entity) { - ((Entity) object).setCustomName(name); + } else if (object instanceof Entity entity) { + entity.setCustomName(name); if (mark == 2 || mode == ChangeMode.RESET) // Using "display name" - ((Entity) object).setCustomNameVisible(name != null); - if (object instanceof LivingEntity) - ((LivingEntity) object).setRemoveWhenFarAway(name == null); - } else if (object instanceof Block) { - BlockState state = ((Block) object).getState(); - if (state instanceof Nameable) { - ((Nameable) state).setCustomName(name); - state.update(); - } - } else if (object instanceof ItemType) { - ItemType i = (ItemType) object; - ItemMeta m = i.getItemMeta(); - m.setDisplayName(name); - i.setItemMeta(m); - } else if (object instanceof Inventory) { - Inventory inv = (Inventory) object; - - if (inv.getViewers().isEmpty()) + entity.setCustomNameVisible(name != null); + if (object instanceof LivingEntity living) + living.setRemoveWhenFarAway(name == null); + } else if (object instanceof AnyNamed named) { + if (named.supportsNameChange()) + named.setName(name); + } else if (object instanceof Inventory inventory) { + + if (inventory.getViewers().isEmpty()) return; // Create a clone to avoid a ConcurrentModificationException - List viewers = new ArrayList<>(inv.getViewers()); + List viewers = new ArrayList<>(inventory.getViewers()); - InventoryType type = inv.getType(); + InventoryType type = inventory.getType(); if (!type.isCreatable()) return; @@ -239,9 +212,9 @@ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { if (name == null) name = type.getDefaultTitle(); if (type == InventoryType.CHEST) { - copy = Bukkit.createInventory(inv.getHolder(), inv.getSize(), name); + copy = Bukkit.createInventory(inventory.getHolder(), inventory.getSize(), name); } else { - copy = Bukkit.createInventory(inv.getHolder(), type, name); + copy = Bukkit.createInventory(inventory.getHolder(), type, name); } } else { Component component = type.defaultTitle(); @@ -250,22 +223,13 @@ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { component = serializer.deserialize(components); } if (type == InventoryType.CHEST) { - copy = Bukkit.createInventory(inv.getHolder(), inv.getSize(), component); + copy = Bukkit.createInventory(inventory.getHolder(), inventory.getSize(), component); } else { - copy = Bukkit.createInventory(inv.getHolder(), type, component); + copy = Bukkit.createInventory(inventory.getHolder(), type, component); } } - copy.setContents(inv.getContents()); + copy.setContents(inventory.getContents()); viewers.forEach(viewer -> viewer.openInventory(copy)); - } else if (object instanceof Slot) { - Slot s = (Slot) object; - ItemStack is = s.getItem(); - if (is != null && !ItemUtils.isAir(is.getType())) { - ItemMeta m = is.hasItemMeta() ? is.getItemMeta() : Bukkit.getItemFactory().getItemMeta(is.getType()); - m.setDisplayName(name); - is.setItemMeta(m); - s.setItem(is); - } } } } @@ -277,12 +241,11 @@ public Class getReturnType() { @Override protected String getPropertyName() { - switch (mark) { - case 1: return "name"; - case 2: return "display name"; - case 3: return "tablist name"; - default: return "name"; - } + return switch (mark) { + case 2 -> "display name"; + case 3 -> "tablist name"; + default -> "name"; + }; } } diff --git a/src/main/java/ch/njol/skript/lang/util/common/AnyAmount.java b/src/main/java/ch/njol/skript/lang/util/common/AnyAmount.java new file mode 100644 index 00000000000..9232f45826e --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/util/common/AnyAmount.java @@ -0,0 +1,49 @@ +package ch.njol.skript.lang.util.common; + +import org.jetbrains.annotations.NotNull; + +/** + * A provider for anything with a (number) amount/size. + * Anything implementing this (or convertible to this) can be used by the {@link ch.njol.skript.expressions.ExprAmount} + * property expression. + * + * @see AnyProvider + */ +@FunctionalInterface +public interface AnyAmount extends AnyProvider { + + /** + * @return This thing's amount/size + */ + @NotNull Number amount(); + + /** + * This is called before {@link #setAmount(Number)}. + * If the result is false, setting the name will never be attempted. + * + * @return Whether this supports being set + */ + default boolean supportsAmountChange() { + return false; + } + + /** + * The behaviour for changing this thing's name, if possible. + * If not possible, then {@link #supportsAmountChange()} should return false and this + * may throw an error. + * + * @param amount The name to change + * @throws UnsupportedOperationException If this is impossible + */ + default void setAmount(Number amount) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * @return Whether the amount of this is zero, i.e. empty + */ + default boolean isEmpty() { + return this.amount().intValue() == 0; + } + +} diff --git a/src/main/java/ch/njol/skript/lang/util/common/AnyContains.java b/src/main/java/ch/njol/skript/lang/util/common/AnyContains.java new file mode 100644 index 00000000000..c56472e9b55 --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/util/common/AnyContains.java @@ -0,0 +1,48 @@ +package ch.njol.skript.lang.util.common; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.UnknownNullability; + +/** + * A provider for anything that contains other things. + * Anything implementing this (or convertible to this) can be used by the {@link ch.njol.skript.conditions.CondContains} + * conditions. + * + * @see AnyProvider + */ +@FunctionalInterface +public interface AnyContains extends AnyProvider { + + /** + * If {@link #isSafeToCheck(Object)} returns false, values will not be passed to this + * method and will instead return false. + *
+ * The null-ness of the parameter depends on whether {@link #isSafeToCheck(Object)} permits null values. + * + * @param value The value to test + * @return Whether this contains {@code value} + */ + boolean contains(@UnknownNullability Type value); + + /** + * Objects are checked versus this before being cast for {@link #contains(Object)}. + * If your contains method doesn't accept all objects (e.g. for a {@link java.util.List#contains(Object)} call) + * then it can exclude unwanted types (or null values) here. + * + * @param value The value to check + * @return Whether the value is safe to call {@link #contains(Object)} with + */ + default boolean isSafeToCheck(Object value) { + return true; + } + + /** + * The internal method used to verify an object and then check its container. + */ + @ApiStatus.Internal + default boolean checkSafely(Object value) { + //noinspection unchecked + return this.isSafeToCheck(value) && this.contains((Type) value); + } + +} diff --git a/src/main/java/ch/njol/skript/lang/util/common/AnyNamed.java b/src/main/java/ch/njol/skript/lang/util/common/AnyNamed.java new file mode 100644 index 00000000000..4da763d229d --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/util/common/AnyNamed.java @@ -0,0 +1,42 @@ +package ch.njol.skript.lang.util.common; + +import org.jetbrains.annotations.UnknownNullability; + +/** + * A provider for anything with a (text) name. + * Anything implementing this (or convertible to this) can be used by the {@link ch.njol.skript.expressions.ExprName} + * property expression. + * + * @see AnyProvider + */ +@FunctionalInterface +public interface AnyNamed extends AnyProvider { + + /** + * @return This thing's name + */ + @UnknownNullability String name(); + + /** + * This is called before {@link #setName(String)}. + * If the result is false, setting the name will never be attempted. + * + * @return Whether this supports being set + */ + default boolean supportsNameChange() { + return false; + } + + /** + * The behaviour for changing this thing's name, if possible. + * If not possible, then {@link #supportsNameChange()} should return false and this + * may throw an error. + * + * @param name The name to change + * @throws UnsupportedOperationException If this is impossible + */ + default void setName(String name) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/ch/njol/skript/lang/util/common/AnyProvider.java b/src/main/java/ch/njol/skript/lang/util/common/AnyProvider.java new file mode 100644 index 00000000000..110a96ce810 --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/util/common/AnyProvider.java @@ -0,0 +1,28 @@ +package ch.njol.skript.lang.util.common; + +/** + * 'AnyProvider' types are holders for common properties (e.g. name, size) where + * it is highly likely that things other than Skript may wish to register + * exponents of the property. + *
+ *
+ * If possible, types should implement an {@link AnyProvider} subtype directly for + * the best possible parsing efficiency. + * However, implementing the interface may not be possible if: + *
    + *
  • registering an existing class from a third-party library
  • + *
  • the subtype getter method conflicts with the type's own methods + * or erasure
  • + *
  • the presence of the supertype might confuse the class's design
  • + *
+ * In these cases, a converter from the class to the AnyX type can be registered. + * The converter should not permit right-chaining or unsafe casts. + *
+ *
+ * The root provider supertype cannot include its own common methods, since these + * may conflict between things that provide two values (e.g. something declaring + * both a name and a size) + */ +public interface AnyProvider { + +} diff --git a/src/main/java/ch/njol/skript/util/slot/Slot.java b/src/main/java/ch/njol/skript/util/slot/Slot.java index 821dac0d750..515cdabfa46 100644 --- a/src/main/java/ch/njol/skript/util/slot/Slot.java +++ b/src/main/java/ch/njol/skript/util/slot/Slot.java @@ -1,32 +1,41 @@ package ch.njol.skript.util.slot; +import ch.njol.skript.aliases.Aliases; +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.bukkitutil.ItemUtils; +import ch.njol.skript.lang.util.common.AnyAmount; +import ch.njol.skript.lang.util.common.AnyNamed; +import org.bukkit.Bukkit; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.jetbrains.annotations.Nullable; import ch.njol.skript.lang.Debuggable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnknownNullability; /** * Represents a container for a single item. It could be an ordinary inventory * slot or perhaps an item frame. */ -public abstract class Slot implements Debuggable { - +public abstract class Slot implements Debuggable, AnyNamed, AnyAmount { + protected Slot() {} - + @Nullable public abstract ItemStack getItem(); - + public abstract void setItem(final @Nullable ItemStack item); - + public abstract int getAmount(); - + public abstract void setAmount(int amount); - + @Override public final String toString() { return toString(null, false); } - + /** * Checks if given slot is in same position with this. * Ignores slot contents. @@ -34,4 +43,52 @@ public final String toString() { * @return True if positions equal, false otherwise. */ public abstract boolean isSameSlot(Slot o); + + /** + * @return The name of the item in this slot + */ + @Override + public @UnknownNullability String name() { + ItemStack stack = this.getItem(); + if (stack != null && stack.hasItemMeta()) { + ItemMeta meta = stack.getItemMeta(); + return meta.hasDisplayName() ? meta.getDisplayName() : null; + } + return null; + } + + @Override + public boolean supportsNameChange() { + return true; + } + + /** + * @param name The name to change + */ + @Override + public void setName(String name) { + ItemStack stack = this.getItem(); + if (stack != null && !ItemUtils.isAir(stack.getType())) { + ItemMeta meta = stack.hasItemMeta() ? stack.getItemMeta() : Bukkit.getItemFactory().getItemMeta(stack.getType()); + meta.setDisplayName(name); + stack.setItemMeta(meta); + this.setItem(stack); + } + } + + @Override + public @NotNull Number amount() { + return this.getAmount(); + } + + @Override + public boolean supportsAmountChange() { + return true; + } + + @Override + public void setAmount(@Nullable Number amount) { + this.setAmount(amount != null ? amount.intValue() : 0); + } + } diff --git a/src/main/java/org/skriptlang/skript/lang/converter/Converter.java b/src/main/java/org/skriptlang/skript/lang/converter/Converter.java index cb7bc8d411e..6c7df376f45 100644 --- a/src/main/java/org/skriptlang/skript/lang/converter/Converter.java +++ b/src/main/java/org/skriptlang/skript/lang/converter/Converter.java @@ -19,15 +19,36 @@ public interface Converter { /** * A Converter flag declaring this Converter cannot be chained to another Converter. * This means that this Converter must be the beginning of a chain. + *
+ * Note: unchecked casts are not permitted before this converter + * (e.g. {@code Object} to {@param }). */ int NO_LEFT_CHAINING = 1; /** * A Converter flag declaring that another Converter cannot be chained to this Converter. * This means that this Converter must be the end of a chain. + *
+ * Note: unchecked casts are not permitted after this converter + * (e.g. {@param } to {@param }). */ int NO_RIGHT_CHAINING = 2; + /** + * A Converter flag declaring that the input/output of this can use an unchecked cast, + * when combined with {@link #NO_LEFT_CHAINING} or {@link #NO_RIGHT_CHAINING}. + *
+ * An unchecked cast would be {@code Number -> Integer}. (Not all numbers are integers, some are floats!) + *
+ *
+ * When combined with {@link #NO_RIGHT_CHAINING} the output can be conformed with an unchecked cast, + * e.g. {@code String -> Number (-> cast Integer)}. + *
+ * When combined with {@link #NO_RIGHT_CHAINING} the output can be conformed with an unchecked cast, + * e.g. {@code (cast Object ->) Integer -> String}. + */ + int ALLOW_UNSAFE_CASTS = 4; + /** * A Converter flag declaring that this Converter cannot be a part of a chain. */ diff --git a/src/main/java/org/skriptlang/skript/lang/converter/Converters.java b/src/main/java/org/skriptlang/skript/lang/converter/Converters.java index 1471d78580e..1efeb559676 100644 --- a/src/main/java/org/skriptlang/skript/lang/converter/Converters.java +++ b/src/main/java/org/skriptlang/skript/lang/converter/Converters.java @@ -286,8 +286,13 @@ private static Converte // Attempt to find converters that have either 'from' OR 'to' not exactly matching for (ConverterInfo unknownInfo : CONVERTERS) { + int flags = unknownInfo.getFlags(); if (unknownInfo.getFrom().isAssignableFrom(fromType) && unknownInfo.getTo().isAssignableFrom(toType)) { ConverterInfo info = (ConverterInfo) unknownInfo; + if ((flags & Converter.ALLOW_UNSAFE_CASTS) == 0) { + if ((flags & Converter.NO_RIGHT_CHAINING) == Converter.NO_RIGHT_CHAINING) + continue; + } // 'to' doesn't exactly match and needs to be filtered // Basically, this converter might convert 'F' into something that's shares a parent with 'T' @@ -301,6 +306,10 @@ private static Converte } else if (fromType.isAssignableFrom(unknownInfo.getFrom()) && toType.isAssignableFrom(unknownInfo.getTo())) { ConverterInfo info = (ConverterInfo) unknownInfo; + if ((flags & Converter.ALLOW_UNSAFE_CASTS) == 0) { + if ((flags & Converter.NO_LEFT_CHAINING) == Converter.NO_LEFT_CHAINING) + continue; + } // 'from' doesn't exactly match and needs to be filtered // Basically, this converter will only convert certain 'F' objects @@ -318,6 +327,13 @@ private static Converte for (ConverterInfo unknownInfo : CONVERTERS) { if (fromType.isAssignableFrom(unknownInfo.getFrom()) && unknownInfo.getTo().isAssignableFrom(toType)) { ConverterInfo info = (ConverterInfo) unknownInfo; + int flags = unknownInfo.getFlags(); + if ((flags & Converter.ALLOW_UNSAFE_CASTS) == 0) { + if ((flags & Converter.NO_LEFT_CHAINING) == Converter.NO_LEFT_CHAINING) + continue; + if ((flags & Converter.NO_RIGHT_CHAINING) == Converter.NO_RIGHT_CHAINING) + continue; + } // 'from' and 'to' both don't exactly match and need to be filtered // Basically, this converter will only convert certain 'F' objects diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index c35f69dfefc..28d608aa7a9 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2594,6 +2594,9 @@ types: experience.pattern: (e?xp|experience( points?)?) classinfo: type¦s @a visualeffect: visual effect¦s @a + named: named thing¦s @a + numbered: numbered thing¦s @a + containing: container¦s @a # Hooks money: money diff --git a/src/test/skript/tests/syntaxes/expressions/ExprAmount.sk b/src/test/skript/tests/syntaxes/expressions/ExprAmount.sk new file mode 100644 index 00000000000..d4cc336b031 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprAmount.sk @@ -0,0 +1,12 @@ +test "amount of objects": + set {_objects::*} to (1 and 2) + set {_amount} to amount of {_objects::*} + assert {_amount} is 2 with "was wrong" + set {_objects::*} to ("hello", "there" and 1) + set {_amount} to amount of {_objects::*} + assert {_amount} is 3 with "was wrong" + +test "amount of items": + assert amount of (3 of stone) is 3 with "was wrong" + set {_item} to 3 of stone + assert amount of {_item} is 3 with "was wrong" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprName.sk b/src/test/skript/tests/syntaxes/expressions/ExprName.sk new file mode 100644 index 00000000000..142e727de34 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprName.sk @@ -0,0 +1,26 @@ +test "name of world": + set {_thing} to the world "world" + assert name of {_thing} is "world" with "name was wrong" + set the name of {_thing} to "blob" + assert name of {_thing} is "world" with "world name changed" + +test "name of entity": + set {_before} to 5 + spawn a pig at spawn of "world": + assert event-entity is a pig with "entity not a pig" + set {_test} to event-entity + set event-entity's name to "foo" + assert {_test} is set with "entity not set" + assert {_test} is a pig with "entity variable not a pig" + assert event-entity's name is "foo" with "name didn't change" + assert {_test} exists with "entity didn't carry out" + assert {_test}'s name is "foo" with "name didn't carry out" + set {_test}'s name to "bar" + assert {_test}'s name is "bar" with "name didn't change" + delete the last spawned pig + +test "name of item": + set {_thing} to 3 of stone + assert name of {_thing} does not exist with "name was set" + set the name of {_thing} to "blob" + assert name of {_thing} is "blob" with "item name didn't change" diff --git a/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk b/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk index 30d4fc0cbf0..f19fdcd50c2 100644 --- a/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk +++ b/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk @@ -54,13 +54,13 @@ test "spawn salmon by variant" when running minecraft "1.21.2": set {_l} to test-location spawn 5 small salmon at {_l} assert size of all small salmons = 5 with "Size of all small salmons is not 5" - assert size of all salmons = 5 with "Size of all salmons is not 5" + assert size of all entities of type salmon = 5 with "Size of all salmons is not 5" spawn 3 medium salmon at {_l} assert size of all medium salmons = 3 with "Size of all medium salmons is not 3" - assert size of all salmons = 8 with "Size of all salmons is not 8" + assert size of all entities of type salmon = 8 with "Size of all salmons is not 8" spawn 2 large salmon at {_l} assert size of all large salmons = 2 with "Size of all large salmon is not 2" - assert size of all salmons = 10 with "Size of all salmon is not 10" + assert size of all entities of type salmon = 10 with "Size of all salmon is not 10" delete all large salmons assert size of all large salmons = 0 with "Large salmons did not get cleared" delete all medium salmons @@ -68,9 +68,9 @@ test "spawn salmon by variant" when running minecraft "1.21.2": delete all small salmons assert size of all small salmons = 0 with "Small salmons did not get cleared" spawn 15 of any salmon at {_l} - assert size of all salmons = 15 with "Size of all salmons is not 15" + assert size of all entities of type salmon = 15 with "Size of all salmons is not 15" clear all salmons - assert size of all salmons = 0 with "All salmons did not get cleared" + assert size of all entities of type salmon = 0 with "All salmons did not get cleared" test "spawn entities": set {_entities::*} to "allay", "axolotl", "bat", "bee", "blaze", "cat", "cave spider", "chicken" and "cod"