diff --git a/build.gradle b/build.gradle index d6a60fa9e37..486220cf22e 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ dependencies { shadow group: 'org.bstats', name: 'bstats-bukkit', version: '3.0.2' shadow group: 'net.kyori', name: 'adventure-text-serializer-bungeecord', version: '4.3.2' - implementation group: 'io.papermc.paper', name: 'paper-api', version: '1.20.6-R0.1-SNAPSHOT' + implementation group: 'io.papermc.paper', name: 'paper-api', version: '1.21-R0.1-SNAPSHOT' implementation group: 'org.eclipse.jdt', name: 'org.eclipse.jdt.annotation', version: '2.2.700' implementation group: 'com.google.code.findbugs', name: 'findbugs', version: '3.0.1' implementation group: 'com.sk89q.worldguard', name: 'worldguard-legacy', version: '7.0.0-SNAPSHOT' @@ -235,7 +235,7 @@ def java21 = 21 def java17 = 17 def java11 = 11 -def latestEnv = 'java21/paper-1.20.6.json' +def latestEnv = 'java21/paper-1.21.0.json' def latestJava = java21 def oldestJava = java11 diff --git a/gradle.properties b/gradle.properties index f78ae1766eb..469d1e072ce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,5 +7,5 @@ groupid=ch.njol name=skript version=2.8.7 jarName=Skript.jar -testEnv=java21/paper-1.20.6 +testEnv=java21/paper-1.21.0 testEnvJavaVersion=21 diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index d98a51a7763..3755525edb1 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -752,6 +752,8 @@ protected void afterErrors() { TestTracker.JUnitTestFailed(test, message); Skript.exception(failure.getException(), "JUnit test '" + failure.getTestHeader() + " failed."); }); + if (SkriptJUnitTest.class.isAssignableFrom(clazz)) + ((SkriptJUnitTest) clazz.getConstructor().newInstance()).cleanup(); SkriptJUnitTest.clearJUnitTest(); } } catch (IOException e) { @@ -759,6 +761,8 @@ protected void afterErrors() { } catch (ClassNotFoundException e) { // Should be the Skript test jar gradle task. assert false : "Class 'ch.njol.skript.variables.FlatFileStorageTest' was not found."; + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { + Skript.exception(e, "Failed to initalize test JUnit classes."); } if (ignored > 0) Skript.warning("There were " + ignored + " ignored test cases! This can mean they are not properly setup in order in that class!"); diff --git a/src/main/java/ch/njol/skript/aliases/AliasesProvider.java b/src/main/java/ch/njol/skript/aliases/AliasesProvider.java index a3b8b215b75..cccf91618ed 100644 --- a/src/main/java/ch/njol/skript/aliases/AliasesProvider.java +++ b/src/main/java/ch/njol/skript/aliases/AliasesProvider.java @@ -304,16 +304,19 @@ public void addAlias(AliasName name, String id, @Nullable Map ta } // Apply (NBT) tags to item stack - ItemStack stack = new ItemStack(material); + ItemStack stack = null; int itemFlags = 0; - if (tags != null) { - itemFlags = applyTags(stack, new HashMap<>(tags)); + if (material.isItem()) { + stack = new ItemStack(material); + if (tags != null) { + itemFlags = applyTags(stack, new HashMap<>(tags)); + } } // Parse block state to block values BlockValues blockValues = BlockCompat.INSTANCE.createBlockValues(material, blockStates, stack, itemFlags); - ItemData data = new ItemData(stack, blockValues); + ItemData data = stack != null ? new ItemData(stack, blockValues) : new ItemData(material, blockValues); data.isAlias = true; data.itemFlags = itemFlags; diff --git a/src/main/java/ch/njol/skript/aliases/ItemData.java b/src/main/java/ch/njol/skript/aliases/ItemData.java index 32323c57e6f..0f68279f43a 100644 --- a/src/main/java/ch/njol/skript/aliases/ItemData.java +++ b/src/main/java/ch/njol/skript/aliases/ItemData.java @@ -37,7 +37,7 @@ import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; -import org.eclipse.jdt.annotation.Nullable; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.NotSerializableException; @@ -91,11 +91,6 @@ public static class OldItemData { @Deprecated public static final boolean itemDataValues = false; - /** - * ItemStack, which is used for everything but serialization. - */ - transient ItemStack stack; - /** * Type of the item as Bukkit material. Serialized manually. */ @@ -105,14 +100,18 @@ public static class OldItemData { * If this represents all possible items. */ boolean isAnything; - + + /** + * ItemStack, which is used for everything but serialization. + */ + transient @Nullable ItemStack stack; + /** * When this ItemData represents a block, this contains information to * allow comparing it against other blocks. */ - @Nullable - BlockValues blockValues; - + @Nullable BlockValues blockValues; + /** * Whether this represents an item (that definitely cannot have * block states) or a block, which might have them. @@ -140,9 +139,10 @@ public static class OldItemData { public ItemData(Material type, @Nullable String tags) { this.type = type; - - this.stack = new ItemStack(type); - this.blockValues = BlockCompat.INSTANCE.getBlockValues(stack); + + if (type.isItem()) + this.stack = new ItemStack(type); + this.blockValues = BlockCompat.INSTANCE.getBlockValues(type); if (tags != null) { applyTags(tags); } @@ -150,8 +150,9 @@ public ItemData(Material type, @Nullable String tags) { public ItemData(Material type, int amount) { this.type = type; - this.stack = new ItemStack(type, Math.abs(amount)); - this.blockValues = BlockCompat.INSTANCE.getBlockValues(stack); + if (type.isItem()) + this.stack = new ItemStack(type, Math.abs(amount)); + this.blockValues = BlockCompat.INSTANCE.getBlockValues(type); } public ItemData(Material type) { @@ -159,13 +160,18 @@ public ItemData(Material type) { } public ItemData(ItemData data) { - this.stack = data.stack.clone(); + this.stack = data.stack != null ? data.stack.clone() : null; this.type = data.type; this.blockValues = data.blockValues; this.isAlias = data.isAlias; this.plain = data.plain; this.itemFlags = data.itemFlags; } + + public ItemData(Material material, @Nullable BlockValues values) { + this.type = material; + this.blockValues = values; + } public ItemData(ItemStack stack, @Nullable BlockValues values) { this.stack = stack; @@ -200,7 +206,8 @@ public ItemData(BlockState blockState) { public ItemData(BlockData blockData) { this.type = blockData.getMaterial(); - this.stack = new ItemStack(type); + if (type.isItem()) + this.stack = new ItemStack(type); this.blockValues = BlockCompat.INSTANCE.getBlockValues(blockData); } @@ -227,13 +234,12 @@ public boolean isOfType(@Nullable ItemStack item) { if (type != item.getType()) return false; // Obvious mismatch - if (itemFlags != 0) { // Either stack has tags (or durability) + if (stack != null && itemFlags != 0) { // Either stack has tags (or durability) if (ItemUtils.getDamage(stack) != ItemUtils.getDamage(item)) return false; // On 1.12 and below, damage is not in meta if (stack.hasItemMeta() == item.hasItemMeta()) // Compare ItemMeta as in isSimilar() of ItemStack - return stack.hasItemMeta() ? itemFactory.equals(stack.getItemMeta(), item.getItemMeta()) : true; - else - return false; + return !stack.hasItemMeta() || itemFactory.equals(stack.getItemMeta(), item.getItemMeta()); + return false; } return true; } @@ -249,7 +255,7 @@ public String toString() { public String toString(final boolean debug, final boolean plural) { StringBuilder builder = new StringBuilder(Aliases.getMaterialName(this, plural)); - ItemMeta meta = stack.getItemMeta(); + ItemMeta meta = stack != null ? stack.getItemMeta() : null; if (meta != null && meta.hasDisplayName()) { builder.append(" ").append(m_named).append(" "); builder.append(meta.getDisplayName()); @@ -282,7 +288,7 @@ public boolean equals(final @Nullable Object obj) { @Override public int hashCode() { int hash = type.hashCode(); // Has collisions, but probably not too many of them - if (blockValues == null || (blockValues != null && blockValues.isDefault())) { + if (blockValues == null || blockValues.isDefault()) { hash = hash * 37 + 1; } return hash; @@ -351,7 +357,7 @@ public MatchQuality matchAlias(ItemData item) { } // See if we need to compare item metas (excluding durability) - if (quality.isAtLeast(MatchQuality.SAME_ITEM) && stack.hasItemMeta() || item.stack.hasItemMeta()) { // Item meta checks could lower this + if (quality.isAtLeast(MatchQuality.SAME_ITEM) && this.hasItemMeta() || item.hasItemMeta()) { // Item meta checks could lower this MatchQuality metaQuality = compareItemMetas(getItemMeta(), item.getItemMeta()); // If given item doesn't care about meta, promote to SAME_ITEM @@ -489,9 +495,13 @@ public ItemData intersection(final ItemData other) { * It is not a copy, so please be careful. * @return Item stack. */ - public ItemStack getStack() { + public @Nullable ItemStack getStack() { return stack; } + + private boolean hasItemMeta() { + return stack != null && stack.hasItemMeta(); + } @Override public ItemData clone() { @@ -508,7 +518,7 @@ public BlockValues getBlockValues() { } public ItemMeta getItemMeta() { - ItemMeta meta = stack.getItemMeta(); + ItemMeta meta = stack != null ? stack.getItemMeta() : null; if (meta == null) { // AIR has null item meta! meta = itemFactory.getItemMeta(Material.STONE); } @@ -517,6 +527,8 @@ public ItemMeta getItemMeta() { } public void setItemMeta(ItemMeta meta) { + if (stack == null) + return; stack.setItemMeta(meta); isAlias = false; // This is no longer exact alias plain = false; // This is no longer a plain item @@ -524,10 +536,14 @@ public void setItemMeta(ItemMeta meta) { } public int getDurability() { + if (stack == null) + return 0; // no damage? return ItemUtils.getDamage(stack); } public void setDurability(int durability) { + if (stack == null) + return; ItemUtils.setDamage(stack, durability); isAlias = false; // Change happened plain = false; // This is no longer a plain item @@ -567,7 +583,7 @@ public boolean matchPlain(ItemData other) { public Fields serialize() throws NotSerializableException { Fields fields = new Fields(this); // ItemStack is transient, will be ignored fields.putPrimitive("id", type.ordinal()); - fields.putObject("meta", stack.getItemMeta()); + fields.putObject("meta", stack != null ? stack.getItemMeta() : null); return fields; } @@ -579,8 +595,10 @@ public void deserialize(Fields fields) throws StreamCorruptedException, NotSeria ItemMeta meta = fields.getAndRemoveObject("meta", ItemMeta.class); // Initialize ItemStack - this.stack = new ItemStack(type); - stack.setItemMeta(meta); // Just set meta to it + if (meta != null && type.isItem()) { + this.stack = new ItemStack(type); + stack.setItemMeta(meta); // Just set meta to it + } fields.setFields(this); // Everything but ItemStack and Material } @@ -598,17 +616,17 @@ public void deserialize(Fields fields) throws StreamCorruptedException, NotSeria */ public ItemData aliasCopy() { ItemData data = new ItemData(); - data.stack = new ItemStack(type, 1); - - if (stack.hasItemMeta()) { - ItemMeta meta = stack.getItemMeta(); // Creates a copy - meta.setDisplayName(null); // Clear display name - if (!itemFactory.getItemMeta(type).equals(meta)) // there may be different tags (e.g. potions) - data.itemFlags |= ItemFlags.CHANGED_TAGS; - data.stack.setItemMeta(meta); + if (stack != null) { + data.stack = new ItemStack(type, 1); + if (stack.hasItemMeta()) { + ItemMeta meta = stack.getItemMeta(); // Creates a copy + meta.setDisplayName(null); // Clear display name + if (!itemFactory.getItemMeta(type).equals(meta)) // there may be different tags (e.g. potions) + data.itemFlags |= ItemFlags.CHANGED_TAGS; + data.stack.setItemMeta(meta); + } + ItemUtils.setDamage(data.stack, 0); // Set to undamaged } - ItemUtils.setDamage(data.stack, 0); // Set to undamaged - data.type = type; data.blockValues = blockValues; data.itemForm = itemForm; @@ -620,6 +638,8 @@ public ItemData aliasCopy() { * @param tags Tags in Mojang's JSON format. */ public void applyTags(String tags) { + if (stack == null) + return; BukkitUnsafe.modifyItemStack(stack, tags); itemFlags |= ItemFlags.CHANGED_TAGS; } diff --git a/src/main/java/ch/njol/skript/aliases/ItemType.java b/src/main/java/ch/njol/skript/aliases/ItemType.java index 7341667fa91..0b47617ef5c 100644 --- a/src/main/java/ch/njol/skript/aliases/ItemType.java +++ b/src/main/java/ch/njol/skript/aliases/ItemType.java @@ -68,6 +68,7 @@ import java.util.Random; import java.util.RandomAccess; import java.util.Set; +import java.util.stream.Collectors; @ContainerType(ItemStack.class) public class ItemType implements Unit, Iterable, Container, YggdrasilExtendedSerializable { @@ -315,7 +316,7 @@ public boolean isOfType(Material id, @Nullable String tags) { public boolean isOfType(Material id) { // TODO avoid object creation - return isOfType(new ItemData(id, null)); + return isOfType(new ItemData(id, (String) null)); } /** @@ -343,7 +344,7 @@ public ItemType getBlock() { */ public boolean hasItem() { for (ItemData d : types) { - if (!d.type.isBlock()) + if (d.type.isItem()) return true; } return false; @@ -487,9 +488,13 @@ public boolean hasNext() { @Override public ItemStack next() { - if (!hasNext()) - throw new NoSuchElementException(); - ItemStack is = iter.next().getStack().clone(); + ItemStack is = null; + while (is == null) { + if (!hasNext()) + throw new NoSuchElementException(); + is = iter.next().getStack(); + } + is = is.clone(); is.setAmount(getAmount()); return is; } @@ -588,10 +593,17 @@ public ItemType clone() { * @see #removeFrom(ItemStack) * @see #removeFrom(List...) */ - public ItemStack getRandom() { - int numItems = types.size(); + public @Nullable ItemStack getRandom() { + List datas = types.stream() + .filter(data -> data.stack != null) + .collect(Collectors.toList()); + if (datas.isEmpty()) + return null; + int numItems = datas.size(); int index = random.nextInt(numItems); - ItemStack is = types.get(index).getStack().clone(); + ItemStack is = datas.get(index).getStack(); + assert is != null; // verified above + is = is.clone(); is.setAmount(getAmount()); return is; } @@ -869,7 +881,9 @@ public final boolean removeFrom(boolean replaceWithNull, List... list */ public void addTo(final List list) { if (!isAll()) { - list.add(getItem().getRandom()); + ItemStack random = getItem().getRandom(); + if (random != null) + list.add(getItem().getRandom()); return; } for (final ItemStack is : getItem().getAll()) @@ -936,7 +950,9 @@ private static boolean addTo(@Nullable ItemStack is, ItemStack[] buf) { public boolean addTo(final ItemStack[] buf) { if (!isAll()) { - return addTo(getItem().getRandom(), buf); + ItemStack random = getItem().getRandom(); + if (random != null) + return addTo(getItem().getRandom(), buf); } boolean ok = true; for (ItemStack is : getItem().getAll()) { diff --git a/src/main/java/ch/njol/skript/bukkitutil/HealthUtils.java b/src/main/java/ch/njol/skript/bukkitutil/HealthUtils.java index 20b5120a0f1..be8675ccabb 100644 --- a/src/main/java/ch/njol/skript/bukkitutil/HealthUtils.java +++ b/src/main/java/ch/njol/skript/bukkitutil/HealthUtils.java @@ -26,13 +26,13 @@ import org.bukkit.damage.DamageSource; import org.bukkit.damage.DamageType; import org.bukkit.entity.Damageable; +import org.bukkit.entity.LivingEntity; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityDamageEvent.DamageCause; import org.jetbrains.annotations.Nullable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; public class HealthUtils { @@ -112,8 +112,11 @@ public static double getFinalDamage(EntityDamageEvent e) { return e.getFinalDamage() / 2; } - public static void setDamage(EntityDamageEvent e, double damage) { - e.setDamage(damage * 2); + public static void setDamage(EntityDamageEvent event, double damage) { + event.setDamage(damage * 2); + // Set last damage manually as Bukkit doesn't appear to do that + if (event.getEntity() instanceof LivingEntity) + ((LivingEntity) event.getEntity()).setLastDamage(damage * 2); } @Nullable diff --git a/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java b/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java index 31bf99c2d56..216be44b918 100644 --- a/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java +++ b/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java @@ -42,9 +42,17 @@ public class ItemUtils { * @return Damage. */ public static int getDamage(ItemStack itemStack) { - ItemMeta meta = itemStack.getItemMeta(); - if (meta instanceof Damageable) - return ((Damageable) meta).getDamage(); + return getDamage(itemStack.getItemMeta()); + } + + /** + * Gets damage/durability of an itemmeta, or 0 if it does not have damage. + * @param itemMeta ItemMeta. + * @return Damage. + */ + public static int getDamage(ItemMeta itemMeta) { + if (itemMeta instanceof Damageable) + return ((Damageable) itemMeta).getDamage(); return 0; // Non damageable item } @@ -143,15 +151,22 @@ public static Material asItem(Material type) { /** * Tests whether two item stacks are of the same type, i.e. it ignores the amounts. * - * @param is1 - * @param is2 + * @param itemStack1 + * @param itemStack2 * @return Whether the item stacks are of the same type */ - public static boolean itemStacksEqual(final @Nullable ItemStack is1, final @Nullable ItemStack is2) { - if (is1 == null || is2 == null) - return is1 == is2; - return is1.getType() == is2.getType() && ItemUtils.getDamage(is1) == ItemUtils.getDamage(is2) - && is1.getItemMeta().equals(is2.getItemMeta()); + public static boolean itemStacksEqual(@Nullable ItemStack itemStack1, @Nullable ItemStack itemStack2) { + if (itemStack1 == null || itemStack2 == null) + return itemStack1 == itemStack2; + if (itemStack1.getType() != itemStack2.getType()) + return false; + + ItemMeta itemMeta1 = itemStack1.getItemMeta(); + ItemMeta itemMeta2 = itemStack2.getItemMeta(); + if (itemMeta1 == null || itemMeta2 == null) + return itemMeta1 == itemMeta2; + + return itemStack1.getItemMeta().equals(itemStack2.getItemMeta()); } // Only 1.15 and versions after have Material#isAir method diff --git a/src/main/java/ch/njol/skript/bukkitutil/block/BlockCompat.java b/src/main/java/ch/njol/skript/bukkitutil/block/BlockCompat.java index eef0866a01b..87bc45a6ec8 100644 --- a/src/main/java/ch/njol/skript/bukkitutil/block/BlockCompat.java +++ b/src/main/java/ch/njol/skript/bukkitutil/block/BlockCompat.java @@ -65,6 +65,9 @@ default BlockValues getBlockValues(Block block) { return getBlockValues(block.getBlockData()); } + @Nullable + BlockValues getBlockValues(Material material); + @Nullable BlockValues getBlockValues(BlockData blockData); diff --git a/src/main/java/ch/njol/skript/bukkitutil/block/NewBlockCompat.java b/src/main/java/ch/njol/skript/bukkitutil/block/NewBlockCompat.java index 2a0ecd36da8..e5cf9c0567b 100644 --- a/src/main/java/ch/njol/skript/bukkitutil/block/NewBlockCompat.java +++ b/src/main/java/ch/njol/skript/bukkitutil/block/NewBlockCompat.java @@ -336,6 +336,13 @@ public BlockValues getBlockValues(BlockState blockState) { return getBlockValues(blockState.getBlockData()); } + @Override + public @Nullable BlockValues getBlockValues(Material material) { + if (material.isBlock()) + return new NewBlockValues(material, Bukkit.createBlockData(material), false); + return null; + } + @Nullable @Override public BlockValues getBlockValues(BlockData blockData) { diff --git a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java index bd8840d0e88..5e301b605ab 100644 --- a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java @@ -932,6 +932,7 @@ public String toVariableNameString(InventoryHolder holder) { .since("1.0") .after("number") .supplier(() -> Arrays.stream(Material.values()) + .filter(Material::isItem) .map(ItemStack::new) .iterator()) .parser(new Parser() { @@ -1395,12 +1396,13 @@ public String toVariableNameString(FireworkEffect effect) { .since("2.4") .requiredPlugins("Minecraft 1.14 or newer")); } + Classes.registerClass(new EnumClassInfo<>(RegainReason.class, "healreason", "heal reasons") - .user("(regen|heal) (reason|cause)") - .name("Heal Reason") - .description("The heal reason in a heal event.") - .examples("") - .since("2.5")); + .user("(regen|heal) (reason|cause)") + .name("Heal Reason") + .description("The health regain reason in a heal event.") + .since("2.5")); + if (Skript.classExists("org.bukkit.entity.Cat$Type")) { ClassInfo catTypeClassInfo; if (BukkitUtils.registryExists("CAT_VARIANT")) { diff --git a/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java b/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java index 76e098cb602..6a49566c827 100644 --- a/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java +++ b/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java @@ -101,6 +101,8 @@ import org.bukkit.event.entity.EntityDropItemEvent; import org.bukkit.event.entity.EntityEvent; import org.bukkit.event.entity.EntityPickupItemEvent; +import org.bukkit.event.entity.EntityRegainHealthEvent; +import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason; import org.bukkit.event.entity.EntityResurrectEvent; import org.bukkit.event.entity.EntityTameEvent; import org.bukkit.event.entity.EntityTransformEvent; @@ -1908,5 +1910,14 @@ public ItemStack get(InventoryMoveItemEvent event) { return event.getItem(); } }, EventValues.TIME_NOW); + + // EntityRegainHealthEvent + EventValues.registerEventValue(EntityRegainHealthEvent.class, RegainReason.class, new Getter() { + @Override + @Nullable + public RegainReason get(EntityRegainHealthEvent event) { + return event.getRegainReason(); + } + }, EventValues.TIME_NOW); } } 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 00f419f4901..a90de6957d3 100644 --- a/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java +++ b/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java @@ -176,6 +176,14 @@ public DefaultConverters() {} return ((Block) holder).getLocation(); if (holder instanceof BlockState) return BlockUtils.getLocation(((BlockState) holder).getBlock()); + if (holder instanceof DoubleChest) { + DoubleChest doubleChest = (DoubleChest) holder; + if (doubleChest.getLeftSide() != null) { + return BlockUtils.getLocation(((BlockState) doubleChest.getLeftSide()).getBlock()); + } else if (((DoubleChest) holder).getRightSide() != null) { + return BlockUtils.getLocation(((BlockState) doubleChest.getRightSide()).getBlock()); + } + } return null; }); diff --git a/src/main/java/ch/njol/skript/doc/HTMLGenerator.java b/src/main/java/ch/njol/skript/doc/HTMLGenerator.java index e882057cebd..d79b1e65320 100644 --- a/src/main/java/ch/njol/skript/doc/HTMLGenerator.java +++ b/src/main/java/ch/njol/skript/doc/HTMLGenerator.java @@ -36,6 +36,9 @@ import com.google.common.collect.Lists; import com.google.common.io.Files; import org.apache.commons.lang.StringUtils; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.block.BlockCanBuildEvent; import org.eclipse.jdt.annotation.Nullable; import org.skriptlang.skript.lang.entry.EntryData; import org.skriptlang.skript.lang.entry.EntryValidator; @@ -481,6 +484,9 @@ private String generateAnnotated(String descTemp, SyntaxElementInfo info, @Nu } desc = desc.replace("${element.id}", ID); + // Cancellable + desc = handleIf(desc, "${if cancellable}", false); + // Events Events events = c.getAnnotation(Events.class); desc = handleIf(desc, "${if events}", events != null); @@ -603,6 +609,17 @@ private String generateEvent(String descTemp, SkriptEventInfo info, @Nullable String[] keywords = info.getKeywords(); desc = desc.replace("${element.keywords}", keywords == null ? "" : Joiner.on(", ").join(keywords)); + // Cancellable + boolean cancellable = false; + for (Class event : info.events) { + if (Cancellable.class.isAssignableFrom(event) || BlockCanBuildEvent.class.isAssignableFrom(event)) { + cancellable = true; // let's assume all are cancellable otherwise EffCancelEvent would do the rest in action + break; + } + } + desc = handleIf(desc, "${if cancellable}", cancellable); + desc = desc.replace("${element.cancellable}", cancellable ? "Yes" : ""); // if not cancellable the section is hidden + // Documentation ID String ID = info.getDocumentationID() != null ? info.getDocumentationID() : info.getId(); // Fix duplicated IDs @@ -719,6 +736,9 @@ private String generateClass(String descTemp, ClassInfo info, @Nullable Strin } desc = desc.replace("${element.id}", ID); + // Cancellable + desc = handleIf(desc, "${if cancellable}", false); + // Events Events events = c.getAnnotation(Events.class); desc = handleIf(desc, "${if events}", events != null); @@ -821,6 +841,9 @@ private String generateFunction(String descTemp, JavaFunction info) { // Documentation ID desc = desc.replace("${element.id}", info.getName()); + // Cancellable + desc = handleIf(desc, "${if cancellable}", false); + // Events desc = handleIf(desc, "${if events}", false); // Functions do not require events nor plugins (at time writing this) diff --git a/src/main/java/ch/njol/skript/effects/EffBroadcast.java b/src/main/java/ch/njol/skript/effects/EffBroadcast.java index 5e2b8fbdefb..fd6ef3ff245 100644 --- a/src/main/java/ch/njol/skript/effects/EffBroadcast.java +++ b/src/main/java/ch/njol/skript/effects/EffBroadcast.java @@ -18,6 +18,13 @@ */ package ch.njol.skript.effects; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; @@ -31,19 +38,20 @@ import ch.njol.skript.lang.VariableString; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.LiteralUtils; +import ch.njol.skript.util.SkriptColor; +import ch.njol.skript.util.Utils; import ch.njol.skript.util.chat.BungeeConverter; import ch.njol.skript.util.chat.ChatMessages; import ch.njol.util.Kleenean; +import ch.njol.util.StringUtils; import ch.njol.util.coll.CollectionUtils; import net.md_5.bungee.api.chat.BaseComponent; import org.bukkit.Bukkit; import org.bukkit.World; import org.bukkit.command.CommandSender; import org.bukkit.event.Event; -import org.eclipse.jdt.annotation.Nullable; - -import java.util.ArrayList; -import java.util.List; +import org.bukkit.event.server.BroadcastMessageEvent; +import org.jetbrains.annotations.Nullable; @Name("Broadcast") @Description("Broadcasts a message to the server.") @@ -54,6 +62,8 @@ @Since("1.0, 2.6 (broadcasting objects), 2.6.1 (using advanced formatting)") public class EffBroadcast extends Effect { + private static final Pattern HEX_PATTERN = Pattern.compile("(?i)&x((?:&\\p{XDigit}){6})"); + static { Skript.registerEffect(EffBroadcast.class, "broadcast %objects% [(to|in) %-worlds%]"); } @@ -65,8 +75,8 @@ public class EffBroadcast extends Effect { @Nullable private Expression worlds; - @SuppressWarnings("unchecked") @Override + @SuppressWarnings("unchecked") public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { messageExpr = LiteralUtils.defendExpression(exprs[0]); messages = messageExpr instanceof ExpressionList ? @@ -74,37 +84,51 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye worlds = (Expression) exprs[1]; return LiteralUtils.canInitSafely(messageExpr); } - + + /** + * This effect will call {@link BroadcastMessageEvent} as of INSERT_VERSION. + */ @Override @SuppressWarnings("deprecation") - public void execute(Event e) { + public void execute(Event event) { List receivers = new ArrayList<>(); if (worlds == null) { receivers.addAll(Bukkit.getOnlinePlayers()); receivers.add(Bukkit.getConsoleSender()); } else { - for (World world : worlds.getArray(e)) + for (World world : worlds.getArray(event)) receivers.addAll(world.getPlayers()); } for (Expression message : getMessages()) { if (message instanceof VariableString) { - BaseComponent[] components = BungeeConverter.convert(((VariableString) message).getMessageComponents(e)); + if (!dispatchEvent(getRawString(event, (VariableString) message), receivers)) + continue; + BaseComponent[] components = BungeeConverter.convert(((VariableString) message).getMessageComponents(event)); receivers.forEach(receiver -> receiver.spigot().sendMessage(components)); } else if (message instanceof ExprColoured && ((ExprColoured) message).isUnsafeFormat()) { // Manually marked as trusted - for (Object realMessage : message.getArray(e)) { + for (Object realMessage : message.getArray(event)) { + if (!dispatchEvent(Utils.replaceChatStyles((String) realMessage), receivers)) + continue; BaseComponent[] components = BungeeConverter.convert(ChatMessages.parse((String) realMessage)); receivers.forEach(receiver -> receiver.spigot().sendMessage(components)); } } else { - for (Object messageObject : message.getArray(e)) { + for (Object messageObject : message.getArray(event)) { String realMessage = messageObject instanceof String ? (String) messageObject : Classes.toString(messageObject); + if (!dispatchEvent(Utils.replaceChatStyles(realMessage), receivers)) + continue; receivers.forEach(receiver -> receiver.sendMessage(realMessage)); } } } } + @Override + public String toString(@Nullable Event event, boolean debug) { + return "broadcast " + messageExpr.toString(event, debug) + (worlds == null ? "" : " to " + worlds.toString(event, debug)); + } + private Expression[] getMessages() { if (messageExpr instanceof ExpressionList && !messageExpr.getAnd()) { return new Expression[] {CollectionUtils.getRandom(messages)}; @@ -112,9 +136,35 @@ private Expression[] getMessages() { return messages; } - @Override - public String toString(@Nullable Event e, boolean debug) { - return "broadcast " + messageExpr.toString(e, debug) + (worlds == null ? "" : " to " + worlds.toString(e, debug)); + /** + * Manually calls a {@link BroadcastMessageEvent}. + * @param message the message + * @return true if the dispatched event does not get cancelled + */ + @SuppressWarnings("deprecation") + private static boolean dispatchEvent(String message, List receivers) { + Set recipients = Collections.unmodifiableSet(new HashSet<>(receivers)); + BroadcastMessageEvent broadcastEvent; + if (Skript.isRunningMinecraft(1, 14)) { + broadcastEvent = new BroadcastMessageEvent(!Bukkit.isPrimaryThread(), message, recipients); + } else { + broadcastEvent = new BroadcastMessageEvent(message, recipients); + } + Bukkit.getPluginManager().callEvent(broadcastEvent); + return !broadcastEvent.isCancelled(); } - + + @Nullable + private static String getRawString(Event event, Expression string) { + if (string instanceof VariableString) + return ((VariableString) string).toUnformattedString(event); + String rawString = string.getSingle(event); + rawString = SkriptColor.replaceColorChar(rawString); + if (rawString.toLowerCase().contains("&x")) { + rawString = StringUtils.replaceAll(rawString, HEX_PATTERN, matchResult -> + "<#" + matchResult.group(1).replace("&", "") + '>'); + } + return rawString; + } + } diff --git a/src/main/java/ch/njol/skript/effects/EffSort.java b/src/main/java/ch/njol/skript/effects/EffSort.java new file mode 100644 index 00000000000..40061777d50 --- /dev/null +++ b/src/main/java/ch/njol/skript/effects/EffSort.java @@ -0,0 +1,167 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package ch.njol.skript.effects; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.Changer.ChangeMode; +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.ExprInput; +import ch.njol.skript.expressions.ExprSortedList; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.InputSource; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.Variable; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.util.Kleenean; +import ch.njol.util.Pair; +import org.bukkit.event.Event; +import org.eclipse.jdt.annotation.Nullable; +import org.jetbrains.annotations.UnknownNullability; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +@Name("Sort") +@Description({ + "Sorts a list variable using either the natural ordering of the contents or the results of the given expression.", + "Be warned, this will overwrite the indices of the list variable." +}) +@Examples({ + "set {_words::*} to \"pineapple\", \"banana\", \"yoghurt\", and \"apple\"", + "sort {_words::*} # alphabetical sort", + "sort {_words::*} by length of input # shortest to longest", + "sort {_words::*} based on {tastiness::%input%} # sort based on custom value" +}) +@Since("INSERT VERSION") +@Keywords("input") +public class EffSort extends Effect implements InputSource { + + static { + Skript.registerEffect(EffSort.class, "sort %~objects% [(by|based on) <.+>]"); + if (!ParserInstance.isRegistered(InputData.class)) + ParserInstance.registerData(InputData.class, InputData::new); + } + + @Nullable + private Expression mappingExpr; + @Nullable + private String unparsedExpression; + private Variable unsortedObjects; + + private Set> dependentInputs = new HashSet<>(); + + @Nullable + private Object currentValue; + @UnknownNullability + private String currentIndex; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + if (expressions[0].isSingle() || !(expressions[0] instanceof Variable)) { + Skript.error("You can only sort list variables!"); + return false; + } + unsortedObjects = (Variable) expressions[0]; + + if (!parseResult.regexes.isEmpty()) { + unparsedExpression = parseResult.regexes.get(0).group(); + assert unparsedExpression != null; + InputData inputData = getParser().getData(InputData.class); + InputSource originalSource = inputData.getSource(); + inputData.setSource(this); + mappingExpr = new SkriptParser(unparsedExpression, SkriptParser.PARSE_EXPRESSIONS, ParseContext.DEFAULT) + .parseExpression(Object.class); + inputData.setSource(originalSource); + return mappingExpr != null && mappingExpr.isSingle(); + } + return true; + } + + @Override + protected void execute(Event event) { + Object[] sorted; + if (mappingExpr == null) { + try { + sorted = unsortedObjects.stream(event) + .sorted(ExprSortedList::compare) + .toArray(); + } catch (IllegalArgumentException | ClassCastException e) { + return; + } + } else { + Map valueToMappedValue = new LinkedHashMap<>(); + for (Iterator> it = unsortedObjects.variablesIterator(event); it.hasNext(); ) { + Pair pair = it.next(); + currentIndex = pair.getKey(); + currentValue = pair.getValue(); + Object mappedValue = mappingExpr.getSingle(event); + if (mappedValue == null) + return; + valueToMappedValue.put(currentValue, mappedValue); + } + try { + sorted = valueToMappedValue.entrySet().stream() + .sorted(Map.Entry.comparingByValue(ExprSortedList::compare)) + .map(Map.Entry::getKey) + .toArray(); + } catch (IllegalArgumentException | ClassCastException e) { + return; + } + } + + unsortedObjects.change(event, sorted, ChangeMode.SET); + } + + @Override + public Set> getDependentInputs() { + return dependentInputs; + } + + @Override + public @Nullable Object getCurrentValue() { + return currentValue; + } + + @Override + public boolean hasIndices() { + return true; + } + + @Override + public @UnknownNullability String getCurrentIndex() { + return currentIndex; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "sort" + unsortedObjects.toString(event, debug) + + (mappingExpr == null ? "" : " by " + mappingExpr.toString(event, debug)); + } + +} diff --git a/src/main/java/ch/njol/skript/effects/EffTeleport.java b/src/main/java/ch/njol/skript/effects/EffTeleport.java index 3ba06d7d150..990e1aa7ec1 100644 --- a/src/main/java/ch/njol/skript/effects/EffTeleport.java +++ b/src/main/java/ch/njol/skript/effects/EffTeleport.java @@ -19,7 +19,6 @@ package ch.njol.skript.effects; import ch.njol.skript.Skript; -import ch.njol.skript.sections.EffSecSpawn; import ch.njol.skript.sections.EffSecSpawn.SpawnEvent; import ch.njol.skript.bukkitutil.EntityUtils; import ch.njol.skript.doc.Description; @@ -31,7 +30,6 @@ import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.Trigger; import ch.njol.skript.lang.TriggerItem; -import ch.njol.skript.lang.TriggerSection; import ch.njol.skript.timings.SkriptTimings; import ch.njol.skript.util.Direction; import ch.njol.skript.variables.Variables; @@ -53,7 +51,7 @@ "which may cause lag spikes or server crashes when using this effect to teleport entities to unloaded chunks." }) @Examples({ - "teleport the player to {homes.%player%}", + "teleport the player to {homes::%player%}", "teleport the attacker to the victim" }) @Since("1.0") @@ -101,6 +99,7 @@ protected TriggerItem walk(Event e) { Location loc = location.getSingle(e); if (loc == null) return next; + boolean unknownWorld = !loc.isWorldLoaded(); Entity[] entityArray = entities.getArray(e); // We have to fetch this before possible async execution to avoid async local variable access. if (entityArray.length == 0) @@ -108,11 +107,17 @@ protected TriggerItem walk(Event e) { if (!delayed) { if (e instanceof PlayerRespawnEvent && entityArray.length == 1 && entityArray[0].equals(((PlayerRespawnEvent) e).getPlayer())) { + if (unknownWorld) + return next; ((PlayerRespawnEvent) e).setRespawnLocation(loc); return next; } if (e instanceof PlayerMoveEvent && entityArray.length == 1 && entityArray[0].equals(((PlayerMoveEvent) e).getPlayer())) { + if (unknownWorld) { // we can approximate the world + loc = loc.clone(); + loc.setWorld(((PlayerMoveEvent) e).getFrom().getWorld()); + } ((PlayerMoveEvent) e).setTo(loc); return next; } @@ -125,6 +130,19 @@ protected TriggerItem walk(Event e) { return next; } + if (unknownWorld) { // we can't fetch the chunk without a world + if (entityArray.length == 1) { // if there's 1 thing we can borrow its world + Entity entity = entityArray[0]; + if (entity == null) + return next; + // assume it's a local teleport, use the first entity we find as a reference + loc = loc.clone(); + loc.setWorld(entity.getWorld()); + } else { + return next; // no entities = no chunk = nobody teleporting + } + } + final Location fixed = loc; Delay.addDelayedEvent(e); Object localVars = Variables.removeLocals(e); @@ -132,7 +150,7 @@ protected TriggerItem walk(Event e) { PaperLib.getChunkAtAsync(loc).thenAccept(chunk -> { // The following is now on the main thread for (Entity entity : entityArray) { - EntityUtils.teleport(entity, loc); + EntityUtils.teleport(entity, fixed); } // Re-set local variables diff --git a/src/main/java/ch/njol/skript/events/EvtHealing.java b/src/main/java/ch/njol/skript/events/EvtHealing.java new file mode 100644 index 00000000000..21dd2c2e642 --- /dev/null +++ b/src/main/java/ch/njol/skript/events/EvtHealing.java @@ -0,0 +1,100 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package ch.njol.skript.events; + +import org.bukkit.entity.Entity; +import org.bukkit.event.Event; +import org.bukkit.event.entity.EntityRegainHealthEvent; +import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason; +import org.eclipse.jdt.annotation.Nullable; + +import ch.njol.skript.Skript; +import ch.njol.skript.entity.EntityData; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptEvent; +import ch.njol.skript.lang.SkriptParser.ParseResult; + +public class EvtHealing extends SkriptEvent { + + static { + Skript.registerEvent("Heal", EvtHealing.class, EntityRegainHealthEvent.class, "heal[ing] [of %-entitydatas%] [(from|due to|by) %-healreasons%]", "%entitydatas% heal[ing] [(from|due to|by) %-healreasons%]") + .description("Called when an entity is healed, e.g. by eating (players), being fed (pets), or by the effect of a potion of healing (overworld mobs) or harm (nether mobs).") + .examples( + "on heal:", + "on player healing from a regeneration potion:", + "on healing of a zombie, cow or a wither:", + "\theal reason is healing potion", + "\tcancel event" + ) + .since("1.0, INSERT VERSION (by reason)"); + } + + @Nullable + private Literal> entityDatas; + + @Nullable + private Literal healReasons; + + @Override + @SuppressWarnings("unchecked") + public boolean init(Literal[] args, int matchedPattern, ParseResult parser) { + entityDatas = (Literal>) args[0]; + healReasons = (Literal) args[1]; + return true; + } + + @Override + public boolean check(Event event) { + if (!(event instanceof EntityRegainHealthEvent)) + return false; + EntityRegainHealthEvent healthEvent = (EntityRegainHealthEvent) event; + if (entityDatas != null) { + Entity compare = healthEvent.getEntity(); + boolean result = false; + for (EntityData entityData : entityDatas.getAll()) { + if (entityData.isInstance(compare)) { + result = true; + break; + } + } + if (!result) + return false; + } + if (healReasons != null) { + RegainReason compare = healthEvent.getRegainReason(); + boolean result = false; + for (RegainReason healReason : healReasons.getAll()) { + if (healReason == compare) { + result = true; + break; + } + } + if (!result) + return false; + } + return true; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "heal" + (entityDatas != null ? " of " + entityDatas.toString(event, debug) : "") + + (healReasons != null ? " by " + healReasons.toString(event, debug) : ""); + } + +} diff --git a/src/main/java/ch/njol/skript/events/SimpleEvents.java b/src/main/java/ch/njol/skript/events/SimpleEvents.java index c470d68a07d..3ac8cce5e1a 100644 --- a/src/main/java/ch/njol/skript/events/SimpleEvents.java +++ b/src/main/java/ch/njol/skript/events/SimpleEvents.java @@ -57,7 +57,6 @@ import org.bukkit.event.entity.EntityMountEvent; import org.bukkit.event.entity.EntityPortalEnterEvent; import org.bukkit.event.entity.EntityPortalEvent; -import org.bukkit.event.entity.EntityRegainHealthEvent; import org.bukkit.event.entity.EntityResurrectEvent; import org.bukkit.event.entity.EntityTameEvent; import org.bukkit.event.entity.EntityToggleGlideEvent; @@ -209,10 +208,6 @@ public class SimpleEvents { .description("Called when an entity enters a nether portal or an end portal. Please note that this event will be fired many times for a nether portal.") .examples("on portal enter:") .since("1.0"); - Skript.registerEvent("Heal", SimpleEvent.class, EntityRegainHealthEvent.class, "heal[ing]") - .description("Called when an entity is healed, e.g. by eating (players), being fed (pets), or by the effect of a potion of healing (overworld mobs) or harm (nether mobs).") - .examples("on heal:") - .since("1.0"); Skript.registerEvent("Tame", SimpleEvent.class, EntityTameEvent.class, "[entity] tam(e|ing)") .description("Called when a player tames a wolf or ocelot. Can be cancelled to prevent the entity from being tamed.") .examples("on tame:") diff --git a/src/main/java/ch/njol/skript/expressions/ExprFilter.java b/src/main/java/ch/njol/skript/expressions/ExprFilter.java index 239b720e418..a9747bbc3fc 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprFilter.java +++ b/src/main/java/ch/njol/skript/expressions/ExprFilter.java @@ -27,83 +27,97 @@ import ch.njol.skript.lang.Condition; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; -import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.InputSource; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.Variable; +import ch.njol.skript.lang.parser.ParserInstance; import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Pair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; import org.skriptlang.skript.lang.converter.Converters; import ch.njol.skript.util.LiteralUtils; -import ch.njol.skript.util.Utils; import ch.njol.util.Kleenean; -import ch.njol.util.coll.iterator.ArrayIterator; import com.google.common.collect.Iterators; import org.bukkit.event.Event; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.Nullable; -import java.lang.reflect.Array; -import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; -import java.util.List; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.regex.Pattern; +import java.util.stream.StreamSupport; @Name("Filter") -@Description("Filters a list based on a condition. " + - "For example, if you ran 'broadcast \"something\" and \"something else\" where [string input is \"something\"]', " + - "only \"something\" would be broadcast as it is the only string that matched the condition.") +@Description({ + "Filters a list based on a condition. ", + "For example, if you ran 'broadcast \"something\" and \"something else\" where [string input is \"something\"]', ", + "only \"something\" would be broadcast as it is the only string that matched the condition." +}) @Examples("send \"congrats on being staff!\" to all players where [player input has permission \"staff\"]") @Since("2.2-dev36") @SuppressWarnings({"null", "unchecked"}) -public class ExprFilter extends SimpleExpression { - - @Nullable - private static ExprFilter parsing; +public class ExprFilter extends SimpleExpression implements InputSource { static { Skript.registerExpression(ExprFilter.class, Object.class, ExpressionType.COMBINED, "%objects% (where|that match) \\[<.+>\\]"); + if (!ParserInstance.isRegistered(InputData.class)) + ParserInstance.registerData(InputData.class, InputData::new); } - private Object current; - private List> children = new ArrayList<>(); - private Condition condition; - private String rawCond; - private Expression objects; + private Condition filterCondition; + private String unparsedCondition; + private Expression unfilteredObjects; + private Set> dependentInputs = new HashSet<>(); @Nullable - public static ExprFilter getParsing() { - return parsing; - } + private Object currentFilterValue; + @Nullable + private String currentFilterIndex; @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - try { - parsing = this; - objects = LiteralUtils.defendExpression(exprs[0]); - if (objects.isSingle()) - return false; - rawCond = parseResult.regexes.get(0).group(); - condition = Condition.parse(rawCond, "Can't understand this condition: " + rawCond); - } finally { - parsing = null; - } - return condition != null && LiteralUtils.canInitSafely(objects); + unfilteredObjects = LiteralUtils.defendExpression(exprs[0]); + if (unfilteredObjects.isSingle() || !LiteralUtils.canInitSafely(unfilteredObjects)) + return false; + unparsedCondition = parseResult.regexes.get(0).group(); + InputData inputData = getParser().getData(InputData.class); + InputSource originalSource = inputData.getSource(); + inputData.setSource(this); + filterCondition = Condition.parse(unparsedCondition, "Can't understand this condition: " + unparsedCondition); + inputData.setSource(originalSource); + return filterCondition != null; } - @NonNull + @NotNull @Override public Iterator iterator(Event event) { - Iterator objIterator = this.objects.iterator(event); - if (objIterator == null) + if (unfilteredObjects instanceof Variable) { + Iterator> variableIterator = ((Variable) unfilteredObjects).variablesIterator(event); + return StreamSupport.stream(Spliterators.spliteratorUnknownSize(variableIterator, Spliterator.ORDERED), false) + .filter(pair -> { + currentFilterValue = pair.getValue(); + currentFilterIndex = pair.getKey(); + return filterCondition.check(event); + }) + .map(Pair::getValue) + .iterator(); + } + + // clear current index just to be safe + currentFilterIndex = null; + + Iterator unfilteredObjectIterator = unfilteredObjects.iterator(event); + if (unfilteredObjectIterator == null) return Collections.emptyIterator(); - try { - return Iterators.filter(objIterator, object -> { - current = object; - return condition.check(event); - }); - } finally { - current = null; - } + return Iterators.filter(unfilteredObjectIterator, candidateObject -> { + currentFilterValue = candidateObject; + return filterCondition.check(event); + }); } @Override @@ -115,148 +129,64 @@ protected Object[] get(Event event) { } } - public Object getCurrent() { - return current; - } - - private void addChild(ExprInput child) { - children.add(child); - } - - private void removeChild(ExprInput child) { - children.remove(child); + @Override + public boolean isSingle() { + return false; } @Override public Class getReturnType() { - return objects.getReturnType(); + return unfilteredObjects.getReturnType(); } - @Override - public boolean isSingle() { - return objects.isSingle(); - } @Override public String toString(Event event, boolean debug) { - return String.format("%s where [%s]", objects.toString(event, debug), rawCond); + return unfilteredObjects.toString(event, debug) + " that match [" + unparsedCondition + "]"; } - @Override - public boolean isLoopOf(String s) { - for (ExprInput child : children) { // if they used player input, let's assume loop-player is valid - if (child.getClassInfo() == null || child.getClassInfo().getUserInputPatterns() == null) - continue; + private boolean matchesAnySpecifiedTypes(String candidateString) { + for (ExprInput dependentInput : dependentInputs) { + ClassInfo specifiedType = dependentInput.getSpecifiedType(); + if (specifiedType == null) + return false; + Pattern[] specifiedTypePatterns = specifiedType.getUserInputPatterns(); + if (specifiedTypePatterns == null) + return false; - for (Pattern pattern : child.getClassInfo().getUserInputPatterns()) { - if (pattern.matcher(s).matches()) + for (Pattern typePattern : specifiedTypePatterns) { + if (typePattern.matcher(candidateString).matches()) { return true; + } } } - return objects.isLoopOf(s); // nothing matched, so we'll rely on the object expression's logic + return false; } - @Name("Filter Input") - @Description("Represents the input in a filter expression. " + - "For example, if you ran 'broadcast \"something\" and \"something else\" where [input is \"something\"]" + - "the condition would be checked twice, using \"something\" and \"something else\" as the inputs.") - @Examples("send \"congrats on being staff!\" to all players where [input has permission \"staff\"]") - @Since("2.2-dev36") - public static class ExprInput extends SimpleExpression { - - static { - Skript.registerExpression(ExprInput.class, Object.class, ExpressionType.COMBINED, - "input", - "%*classinfo% input" - ); - } - - @Nullable - private final ExprInput source; - private final Class[] types; - private final Class superType; - @SuppressWarnings("NotNullFieldNotInitialized") - private ExprFilter parent; - @Nullable - private ClassInfo inputType; - - public ExprInput() { - this(null, (Class) Object.class); - } - - public ExprInput(@Nullable ExprInput source, Class... types) { - this.source = source; - if (source != null) { - this.parent = source.parent; - this.inputType = source.inputType; - parent.removeChild(source); - parent.addChild(this); - } - - this.types = types; - this.superType = (Class) Utils.getSuperType(types); - } - - @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - parent = ExprFilter.getParsing(); - - if (parent == null) - return false; - - parent.addChild(this); - inputType = matchedPattern == 0 ? null : ((Literal>) exprs[0]).getSingle(); - return true; - } - - @Override - protected T[] get(Event event) { - Object current = parent.getCurrent(); - if (inputType != null && !inputType.getC().isInstance(current)) { - return null; - } - - try { - return Converters.convert(new Object[]{current}, types, superType); - } catch (ClassCastException e1) { - return (T[]) Array.newInstance(superType, 0); - } - } - - public void setParent(ExprFilter parent) { - this.parent = parent; - } - - @Override - public Expression getConvertedExpression(Class... to) { - return new ExprInput<>(this, to); - } - @Override - public Expression getSource() { - return source == null ? this : source; - } - - @Override - public Class getReturnType() { - return superType; - } + @Override + public boolean isLoopOf(String candidateString) { + return unfilteredObjects.isLoopOf(candidateString) || matchesAnySpecifiedTypes(candidateString); + } - @Nullable - private ClassInfo getClassInfo() { - return inputType; - } + public Set> getDependentInputs() { + return dependentInputs; + } - @Override - public boolean isSingle() { - return true; - } + @Nullable + public Object getCurrentValue() { + return currentFilterValue; + } - @Override - public String toString(Event event, boolean debug) { - return inputType == null ? "input" : inputType.getCodeName() + " input"; - } + @Override + public boolean hasIndices() { + return unfilteredObjects instanceof Variable; + } + @Override + @UnknownNullability + public String getCurrentIndex() { + return currentFilterIndex; } } diff --git a/src/main/java/ch/njol/skript/expressions/ExprHealAmount.java b/src/main/java/ch/njol/skript/expressions/ExprHealAmount.java index 9e74f6aa68e..ae905e96fd7 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprHealAmount.java +++ b/src/main/java/ch/njol/skript/expressions/ExprHealAmount.java @@ -25,6 +25,7 @@ import ch.njol.skript.Skript; import ch.njol.skript.classes.Changer; +import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Events; import ch.njol.skript.doc.Examples; @@ -32,49 +33,49 @@ import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; -import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.skript.log.ErrorQuality; import ch.njol.util.Kleenean; import ch.njol.util.coll.CollectionUtils; @Name("Heal Amount") -@Description("The amount of health healed in a healing event.") -@Examples({"increase heal amount by 2", - "remove 0.5 from heal amount"}) -@Since("2.5.1") +@Description("The amount of health healed in a heal event.") +@Examples({ + "on player healing:", + "\tincrease the heal amount by 2", + "\tremove 0.5 from the healing amount" +}) @Events("heal") -public class ExprHealAmount extends SimpleExpression { - +@Since("2.5.1") +public class ExprHealAmount extends SimpleExpression { + static { - Skript.registerExpression(ExprHealAmount.class, Number.class, ExpressionType.SIMPLE, "[the] heal amount"); + Skript.registerExpression(ExprHealAmount.class, Double.class, ExpressionType.SIMPLE, "[the] heal[ing] amount"); } - - @SuppressWarnings("null") + private Kleenean delay; - + @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { if (!getParser().isCurrentEvent(EntityRegainHealthEvent.class)) { - Skript.error("The expression 'heal amount' may only be used in a healing event", ErrorQuality.SEMANTIC_ERROR); + Skript.error("The expression 'heal amount' may only be used in a healing event"); return false; } delay = isDelayed; return true; } - + @Nullable @Override - protected Number[] get(Event e) { - if (!(e instanceof EntityRegainHealthEvent)) + protected Double[] get(Event event) { + if (!(event instanceof EntityRegainHealthEvent)) return null; - - return new Number[]{((EntityRegainHealthEvent) e).getAmount()}; + return new Double[]{((EntityRegainHealthEvent) event).getAmount()}; } - + @Nullable @Override - public Class[] acceptChange(Changer.ChangeMode mode) { + public Class[] acceptChange(ChangeMode mode) { if (delay != Kleenean.FALSE) { Skript.error("The heal amount cannot be changed after the event has already passed"); return null; @@ -83,42 +84,48 @@ public Class[] acceptChange(Changer.ChangeMode mode) { return null; return CollectionUtils.array(Number.class); } - + @Override - public void change(Event e, @Nullable Object[] delta, Changer.ChangeMode mode) { - if (!(e instanceof EntityRegainHealthEvent)) + public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { + if (!(event instanceof EntityRegainHealthEvent)) return; + EntityRegainHealthEvent healthEvent = (EntityRegainHealthEvent) event; double value = delta == null ? 0 : ((Number) delta[0]).doubleValue(); switch (mode) { case SET: case DELETE: - ((EntityRegainHealthEvent) e).setAmount(value); + healthEvent.setAmount(value); break; case ADD: - ((EntityRegainHealthEvent) e).setAmount(((EntityRegainHealthEvent) e).getAmount() + value); + healthEvent.setAmount(healthEvent.getAmount() + value); break; case REMOVE: - ((EntityRegainHealthEvent) e).setAmount(((EntityRegainHealthEvent) e).getAmount() - value); + healthEvent.setAmount(healthEvent.getAmount() - value); break; default: break; } } - + + @Override + public boolean setTime(int time) { + return super.setTime(time, EntityRegainHealthEvent.class); + } + @Override public boolean isSingle() { return true; } - + @Override - public Class getReturnType() { - return Number.class; + public Class getReturnType() { + return Double.class; } - + @Override - public String toString(@Nullable Event e, boolean debug) { + public String toString(@Nullable Event event, boolean debug) { return "heal amount"; } - + } diff --git a/src/main/java/ch/njol/skript/expressions/ExprHealReason.java b/src/main/java/ch/njol/skript/expressions/ExprHealReason.java index 12dc0270976..330ec4ed4d7 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprHealReason.java +++ b/src/main/java/ch/njol/skript/expressions/ExprHealReason.java @@ -18,67 +18,40 @@ */ package ch.njol.skript.expressions; -import org.bukkit.event.Event; -import org.bukkit.event.entity.EntityRegainHealthEvent; -import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason; -import org.eclipse.jdt.annotation.Nullable; - -import ch.njol.skript.Skript; import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Events; import ch.njol.skript.doc.Examples; 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.log.ErrorQuality; -import ch.njol.util.Kleenean; +import ch.njol.skript.expressions.base.EventValueExpression; +import ch.njol.skript.registrations.EventValues; +import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason; @Name("Heal Reason") -@Description("The heal reason of a heal event. Please click on the link for more information.") -@Examples({"on heal:", - "\tif heal reason = satiated:", - "\t\tsend \"You ate enough food and gained health back!\" to player"}) +@Description("The heal reason of a heal event.") +@Examples({ + "on heal:", + "\theal reason is satiated", + "\tsend \"You ate enough food and gained full health back!\"" +}) +@Events("heal") @Since("2.5") -public class ExprHealReason extends SimpleExpression { - +public class ExprHealReason extends EventValueExpression { + static { - Skript.registerExpression(ExprHealReason.class, RegainReason.class, ExpressionType.SIMPLE, "(regen|health regain|heal) (reason|cause)"); + register(ExprHealReason.class, RegainReason.class, "(regen|health regain|heal[ing]) (reason|cause)"); } - - @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - if (!getParser().isCurrentEvent(EntityRegainHealthEvent.class)) { - Skript.error("Heal reason can only be used in an on heal event", ErrorQuality.SEMANTIC_ERROR); - return false; - } - return true; - } - - @Nullable - @Override - protected RegainReason[] get(Event e) { - if (!(e instanceof EntityRegainHealthEvent)) - return null; - return new RegainReason[]{((EntityRegainHealthEvent) e).getRegainReason()}; + public ExprHealReason() { + super(RegainReason.class); } - - @Override - public boolean isSingle() { - return true; - } - - @Override - public Class getReturnType() { - return RegainReason.class; - } - + @Override - public String toString(@Nullable Event e, boolean debug) { - return "heal reason"; + public boolean setTime(int time) { + if (time == EventValues.TIME_FUTURE) + return false; + return super.setTime(time); } - + } diff --git a/src/main/java/ch/njol/skript/expressions/ExprInput.java b/src/main/java/ch/njol/skript/expressions/ExprInput.java new file mode 100644 index 00000000000..334fc79cd93 --- /dev/null +++ b/src/main/java/ch/njol/skript/expressions/ExprInput.java @@ -0,0 +1,170 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package ch.njol.skript.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +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.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.InputSource; +import ch.njol.skript.lang.InputSource.InputData; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.registrations.DefaultClasses; +import ch.njol.skript.util.ClassInfoReference; +import ch.njol.skript.util.Utils; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.eclipse.jdt.annotation.Nullable; +import org.skriptlang.skript.lang.converter.Converters; + +import java.lang.reflect.Array; +import java.util.Set; + +@Name("Input") +@Description({ + "Represents the input in a filter expression or sort effect.", + "For example, if you ran 'broadcast \"something\" and \"something else\" where [input is \"something\"]", + "the condition would be checked twice, using \"something\" and \"something else\" as the inputs.", + "The 'input index' pattern can be used when acting on a variable to access the index of the input." +}) +@Examples({ + "send \"congrats on being staff!\" to all players where [input has permission \"staff\"]", + "sort {_list::*} based on length of input index" +}) +@Since("2.2-dev36, INSERT_VERSION (input index)") +public class ExprInput extends SimpleExpression { + + static { + Skript.registerExpression(ExprInput.class, Object.class, ExpressionType.COMBINED, + "input", + "%*classinfo% input", + "input index" + ); + } + + @Nullable + private final ExprInput source; + private final Class[] types; + private final Class superType; + + private InputSource inputSource; + + @Nullable + private ClassInfo specifiedType; + private boolean isIndex = false; + + public ExprInput() { + this(null, (Class) Object.class); + } + + public ExprInput(@Nullable ExprInput source, Class... types) { + this.source = source; + if (source != null) { + isIndex = source.isIndex; + specifiedType = source.specifiedType; + inputSource = source.inputSource; + Set> dependentInputs = inputSource.getDependentInputs(); + dependentInputs.remove(this.source); + dependentInputs.add(this); + } + this.types = types; + this.superType = (Class) Utils.getSuperType(types); + } + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + inputSource = getParser().getData(InputData.class).getSource(); + if (inputSource == null) + return false; + switch (matchedPattern) { + case 1: + ClassInfoReference classInfoReference = ((Literal) ClassInfoReference.wrap((Expression>) exprs[0])).getSingle(); + if (classInfoReference.isPlural().isTrue()) { + Skript.error("An input can only be a single value! Please use a singular type (for example: players input -> player input)."); + return false; + } + specifiedType = classInfoReference.getClassInfo(); + break; + case 2: + if (!inputSource.hasIndices()) { + Skript.error("You cannot use 'input index' on lists without indices!"); + return false; + } + specifiedType = DefaultClasses.STRING; + isIndex = true; + break; + default: + specifiedType = null; + } + return true; + } + + @Override + protected T[] get(Event event) { + Object currentValue = isIndex ? inputSource.getCurrentIndex() : inputSource.getCurrentValue(); + if (currentValue == null || (specifiedType != null && !specifiedType.getC().isInstance(currentValue))) + return (T[]) Array.newInstance(superType, 0); + + try { + return Converters.convert(new Object[]{currentValue}, types, superType); + } catch (ClassCastException exception) { + return (T[]) Array.newInstance(superType, 0); + } + } + + @Override + public Expression getConvertedExpression(Class... to) { + return new ExprInput<>(this, to); + } + + @Override + public Expression getSource() { + return source == null ? this : source; + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return superType; + } + + @Nullable + public ClassInfo getSpecifiedType() { + return specifiedType; + } + + + @Override + public String toString(Event event, boolean debug) { + if (isIndex) + return "input index"; + return specifiedType == null ? "input" : specifiedType.getCodeName() + " input"; + } + +} diff --git a/src/main/java/ch/njol/skript/expressions/ExprLastDamage.java b/src/main/java/ch/njol/skript/expressions/ExprLastDamage.java index 0b62cd60a9d..ede427b156f 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprLastDamage.java +++ b/src/main/java/ch/njol/skript/expressions/ExprLastDamage.java @@ -19,47 +19,34 @@ */ package ch.njol.skript.expressions; -import org.bukkit.entity.LivingEntity; -import org.bukkit.event.Event; -import org.eclipse.jdt.annotation.Nullable; - import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.SkriptParser; -import ch.njol.util.Kleenean; import ch.njol.util.coll.CollectionUtils; +import org.bukkit.entity.LivingEntity; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; @Name("Last Damage") @Description("The last damage that was done to an entity. Note that changing it doesn't deal more/less damage.") @Examples({"set last damage of event-entity to 2"}) @Since("2.5.1") public class ExprLastDamage extends SimplePropertyExpression { - + static { register(ExprLastDamage.class, Number.class, "last damage", "livingentities"); } - - @Nullable - private ExprDamage damageExpr; - - @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { - damageExpr = new ExprDamage(); - return true; - } - + @Nullable @Override @SuppressWarnings("null") public Number convert(LivingEntity livingEntity) { - return damageExpr.get(livingEntity.getLastDamageCause())[0]; + return livingEntity.getLastDamage() / 2; } - + @Nullable @Override public Class[] acceptChange(ChangeMode mode) { @@ -72,35 +59,38 @@ public Class[] acceptChange(ChangeMode mode) { return null; } } - + + @SuppressWarnings("ConstantValue") @Override public void change(Event e, @Nullable Object[] delta, ChangeMode mode) { - if (delta != null) { + if (delta != null && delta[0] instanceof Number) { + double damage = ((Number) delta[0]).doubleValue() * 2; + switch (mode) { case SET: for (LivingEntity entity : getExpr().getArray(e)) - entity.setLastDamage((Long) delta[0]); + entity.setLastDamage(damage); break; case REMOVE: - delta[0] = (Long) delta[0] * -1; + damage = damage * -1; case ADD: for (LivingEntity entity : getExpr().getArray(e)) - entity.setLastDamage((Long) delta[0] + entity.getLastDamage()); + entity.setLastDamage(damage + entity.getLastDamage()); break; default: assert false; } } } - + @Override public Class getReturnType() { return Number.class; } - + @Override protected String getPropertyName() { return "last damage"; } - + } diff --git a/src/main/java/ch/njol/skript/expressions/ExprYawPitch.java b/src/main/java/ch/njol/skript/expressions/ExprYawPitch.java index cbbc3ba085d..6aca873d614 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprYawPitch.java +++ b/src/main/java/ch/njol/skript/expressions/ExprYawPitch.java @@ -18,8 +18,13 @@ */ package ch.njol.skript.expressions; +import ch.njol.skript.ServerPlatform; +import ch.njol.skript.Skript; +import ch.njol.skript.doc.RequiredPlugins; import ch.njol.util.VectorMath; import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; import org.bukkit.event.Event; import ch.njol.skript.classes.Changer.ChangeMode; @@ -33,125 +38,193 @@ import ch.njol.util.Kleenean; import ch.njol.util.coll.CollectionUtils; import org.bukkit.util.Vector; +import org.eclipse.jdt.annotation.Nullable; @Name("Yaw / Pitch") -@Description("The yaw or pitch of a location or vector.") -@Examples({"log \"%player%: %location of player%, %player's yaw%, %player's pitch%\" to \"playerlocs.log\"", - "set {_yaw} to yaw of player", - "set {_p} to pitch of target entity"}) -@Since("2.0, 2.2-dev28 (vector yaw/pitch)") -public class ExprYawPitch extends SimplePropertyExpression { +@Description({ + "The yaw or pitch of a location or vector.", + "A yaw of 0 or 360 represents the positive z direction. Adding a positive number to the yaw of a player will rotate it clockwise.", + "A pitch of 90 represents the negative y direction, or downward facing. A pitch of -90 represents upward facing. Adding a positive number to the pitch will rotate the direction downwards.", + "Only Paper 1.19+ users may directly change the yaw/pitch of players." +}) +@Examples({ + "log \"%player%: %location of player%, %player's yaw%, %player's pitch%\" to \"playerlocs.log\"", + "set {_yaw} to yaw of player", + "set {_p} to pitch of target entity", + "set pitch of player to -90 # Makes the player look upwards, Paper 1.19+ only", + "add 180 to yaw of target of player # Makes the target look behind themselves" +}) +@Since("2.0, 2.2-dev28 (vector yaw/pitch), INSERT VERSION (entity changers)") +@RequiredPlugins("Paper 1.19+ (player changers)") +public class ExprYawPitch extends SimplePropertyExpression { static { - register(ExprYawPitch.class, Number.class, "(0¦yaw|1¦pitch)", "locations/vectors"); + register(ExprYawPitch.class, Float.class, "(:yaw|pitch)", "entities/locations/vectors"); } + // For non-Paper versions lower than 1.19, changing the rotation of an entity is not supported for players. + private static final boolean SUPPORTS_PLAYERS = Skript.isRunningMinecraft(1, 19) && Skript.getServerPlatform() == ServerPlatform.BUKKIT_PAPER; + private boolean usesYaw; @Override - public boolean init(final Expression[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parseResult) { - usesYaw = parseResult.mark == 0; + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + usesYaw = parseResult.hasTag("yaw"); return super.init(exprs, matchedPattern, isDelayed, parseResult); } @Override - public Number convert(final Object object) { - if (object instanceof Location) { - Location l = ((Location) object); - return usesYaw ? convertToPositive(l.getYaw()) : l.getPitch(); + public Float convert(Object object) { + if (object instanceof Entity) { + Location location = ((Entity) object).getLocation(); + return usesYaw + ? normalizeYaw(location.getYaw()) + : location.getPitch(); + } else if (object instanceof Location) { + Location location = (Location) object; + return usesYaw + ? normalizeYaw(location.getYaw()) + : location.getPitch(); } else if (object instanceof Vector) { - Vector vector = ((Vector) object); - if (usesYaw) - return VectorMath.skriptYaw(VectorMath.getYaw(vector)); - return VectorMath.skriptPitch(VectorMath.getPitch(vector)); + Vector vector = (Vector) object; + return usesYaw + ? VectorMath.skriptYaw((VectorMath.getYaw(vector))) + : VectorMath.skriptPitch(VectorMath.getPitch(vector)); } return null; } - @SuppressWarnings({"null"}) @Override - public Class[] acceptChange(final ChangeMode mode) { - if (mode == ChangeMode.SET || mode == ChangeMode.ADD || mode == ChangeMode.REMOVE) - return CollectionUtils.array(Number.class); - return null; + public Class[] acceptChange(ChangeMode mode) { + if (Player.class.isAssignableFrom(getExpr().getReturnType()) && !SUPPORTS_PLAYERS) + return null; + + switch (mode) { + case SET: + case ADD: + case REMOVE: + return CollectionUtils.array(Number.class); + case RESET: + return new Class[0]; + default: + return null; + } } - @SuppressWarnings("null") @Override - public void change(Event e, Object[] delta, ChangeMode mode) { - if (delta == null) + public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { + if (delta == null && mode != ChangeMode.RESET) return; float value = ((Number) delta[0]).floatValue(); - for (Object single : getExpr().getArray(e)) { - if (single instanceof Location) { - changeLocation(((Location) single), value, mode); - } else if (single instanceof Vector) { - changeVector(((Vector) single), value, mode); + for (Object object : getExpr().getArray(event)) { + if (object instanceof Player && !SUPPORTS_PLAYERS) + continue; + + if (object instanceof Entity) { + changeForEntity((Entity) object, value, mode); + } else if (object instanceof Location) { + changeForLocation(((Location) object), value, mode); + } else if (object instanceof Vector) { + changeForVector(((Vector) object), value, mode); } } } - private void changeLocation(Location l, float value, ChangeMode mode) { + private void changeForEntity(Entity entity, float value, ChangeMode mode) { + Location location = entity.getLocation(); switch (mode) { case SET: - if (usesYaw) - l.setYaw(convertToPositive(value)); - else - l.setPitch(value); + if (usesYaw) { + entity.setRotation(value, location.getPitch()); + } else { + entity.setRotation(location.getYaw(), value); + } break; + case REMOVE: + value = -value; case ADD: - if (usesYaw) - l.setYaw(convertToPositive(l.getYaw()) + value); - else - l.setPitch(l.getPitch() + value); + if (usesYaw) { + entity.setRotation(location.getYaw() + value, location.getPitch()); + } else { + // Subtracting because of Minecraft's upside-down pitch. + entity.setRotation(location.getYaw(), location.getPitch() - value); + } + break; + case RESET: + if (usesYaw) { + entity.setRotation(0, location.getPitch()); + } else { + entity.setRotation(location.getYaw(), 0); + } + break; + default: + break; + } + } + + private void changeForLocation(Location location, float value, ChangeMode mode) { + switch (mode) { + case SET: + if (usesYaw) { + location.setYaw(value); + } else { + location.setPitch(value); + } break; case REMOVE: - if (usesYaw) - l.setYaw(convertToPositive(l.getYaw()) - value); - else - l.setPitch(l.getPitch() - value); + value = -value; + case ADD: + if (usesYaw) { + location.setYaw(location.getYaw() + value); + } else { + // Subtracting because of Minecraft's upside-down pitch. + location.setPitch(location.getPitch() - value); + } break; + case RESET: + if (usesYaw) { + location.setYaw(0); + } else { + location.setPitch(0); + } default: break; } } - private void changeVector(Vector vector, float n, ChangeMode mode) { + private void changeForVector(Vector vector, float value, ChangeMode mode) { float yaw = VectorMath.getYaw(vector); float pitch = VectorMath.getPitch(vector); switch (mode) { case REMOVE: - n = -n; - //$FALL-THROUGH$ + value = -value; + // $FALL-THROUGH$ case ADD: - if (usesYaw) - yaw += n; - else - pitch -= n; // Negative because of Minecraft's / Skript's upside down pitch - Vector newVector = VectorMath.fromYawAndPitch(yaw, pitch).multiply(vector.length()); - VectorMath.copyVector(vector, newVector); + if (usesYaw) { + yaw += value; + } else { + // Subtracting because of Minecraft's upside-down pitch. + pitch -= value; + } break; case SET: if (usesYaw) - yaw = VectorMath.fromSkriptYaw(n); + yaw = VectorMath.fromSkriptYaw(value); else - pitch = VectorMath.fromSkriptPitch(n); - newVector = VectorMath.fromYawAndPitch(yaw, pitch).multiply(vector.length()); - VectorMath.copyVector(vector, newVector); + pitch = VectorMath.fromSkriptPitch(value); } + Vector newVector = VectorMath.fromYawAndPitch(yaw, pitch).multiply(vector.length()); + VectorMath.copyVector(vector, newVector); } - - //Some random method decided to use for converting to positive values. - public float convertToPositive(float f) { - if (f != 0 && f * -1 == Math.abs(f)) - return 360 + f; - return f; + private static float normalizeYaw(float yaw) { + yaw = Location.normalizeYaw(yaw); + return yaw < 0 ? yaw + 360 : yaw; } @Override - public Class getReturnType() { - return Number.class; + public Class getReturnType() { + return Float.class; } @Override diff --git a/src/main/java/ch/njol/skript/lang/InputSource.java b/src/main/java/ch/njol/skript/lang/InputSource.java new file mode 100644 index 00000000000..df541a0e40f --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/InputSource.java @@ -0,0 +1,101 @@ +/** + * This file is part of Skript. + * + * Skript is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Skript is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Skript. If not, see . + * + * Copyright Peter Güttinger, SkriptLang team and contributors + */ +package ch.njol.skript.lang; + +import ch.njol.skript.expressions.ExprInput; +import ch.njol.skript.lang.parser.ParserInstance; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; + +import java.util.Set; + +/** + * An InputSource represents a syntax that can provide a + * value for {@link ExprInput} to use. + *
+ * @see ch.njol.skript.expressions.ExprFilter + * @see ch.njol.skript.effects.EffSort + */ +public interface InputSource { + + /** + * @return A mutable {@link Set} of {@link ExprInput}s that depend on this source. + */ + Set> getDependentInputs(); + + /** + * @return The current value that {@link ExprInput} should use. + */ + @Nullable Object getCurrentValue(); + + /** + * {@link InputSource}s that can supply indices along with values should override this + * method to indicate their ability. + * + * @return Whether this source can return indices. + */ + default boolean hasIndices() { + return false; + } + + /** + * This should only be used by {@link InputSource}s that return true for {@link InputSource#hasIndices()}. + * + * @return The current value's index. + */ + default @UnknownNullability String getCurrentIndex() { + return null; + } + + /** + * A {@link ch.njol.skript.lang.parser.ParserInstance.Data} used for + * linking {@link InputSource}s and {@link ExprInput}s. + */ + class InputData extends ParserInstance.Data { + + @Nullable + private InputSource source; + + public InputData(ParserInstance parserInstance) { + super(parserInstance); + } + + /** + * {@link InputSource} should call this during init() to declare that they are the current source for future + * {@link ExprInput}s, and then reset it to its previous value once out of scope. + * + * @param source the source of information. + */ + public void setSource(@Nullable InputSource source) { + this.source = source; + } + + /** + * ExprInput should use this to get the information source, and then call + * {@link InputSource#getCurrentValue()} to get the current value of the source. + * + * @return the source of information. + */ + @Nullable + public InputSource getSource() { + return source; + } + + } +} diff --git a/src/main/java/ch/njol/skript/test/runner/SkriptJUnitTest.java b/src/main/java/ch/njol/skript/test/runner/SkriptJUnitTest.java index 65a52f0ac1a..6988d537cff 100644 --- a/src/main/java/ch/njol/skript/test/runner/SkriptJUnitTest.java +++ b/src/main/java/ch/njol/skript/test/runner/SkriptJUnitTest.java @@ -27,8 +27,6 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.EntityType; import org.bukkit.entity.Pig; -import org.junit.After; -import org.junit.Before; import ch.njol.skript.Skript; @@ -79,8 +77,6 @@ public static void setShutdownDelay(long delay) { /** * Override this method if your JUnit test requires block modification with delay over 1 tick. */ - @Before - @After public void cleanup() { getTestWorld().getEntities().forEach(Entity::remove); setBlock(Material.AIR); diff --git a/src/test/skript/environments/java21/paper-1.21.0.json b/src/test/skript/environments/java21/paper-1.21.0.json new file mode 100644 index 00000000000..22a72e3a73b --- /dev/null +++ b/src/test/skript/environments/java21/paper-1.21.0.json @@ -0,0 +1,17 @@ +{ + "name": "paper-1.21.0", + "resources": [ + {"source": "server.properties.generic", "target": "server.properties"} + ], + "paperDownloads": [ + { + "version": "1.21", + "target": "paperclip.jar" + } + ], + "skriptTarget": "plugins/Skript.jar", + "commandLine": [ + "-Dcom.mojang.eula.agree=true", + "-jar", "paperclip.jar", "--nogui" + ] +} diff --git a/src/test/skript/tests/regressions/5491-xp orb merge overwrite.sk b/src/test/skript/tests/regressions/5491-xp orb merge overwrite.sk index ca5b9b46d6e..c925f51c590 100644 --- a/src/test/skript/tests/regressions/5491-xp orb merge overwrite.sk +++ b/src/test/skript/tests/regressions/5491-xp orb merge overwrite.sk @@ -1,6 +1,10 @@ test "spawn xp orb overwriting merged value" when running minecraft "1.14.4": # 1.13.2 seems not to merge xp orbs in the same way, so this test is skipped + # 1.21 also does not merge orbs, so this test is disabled. + # TODO: figure out how to force the orbs to merge on 1.21+ + running below minecraft "1.21" + # sanitize kill all xp orbs set {_spawn} to spawn of world "world" diff --git a/src/test/skript/tests/regressions/6811-inventory-holder-location-doublechest.sk b/src/test/skript/tests/regressions/6811-inventory-holder-location-doublechest.sk new file mode 100644 index 00000000000..f46d4b52794 --- /dev/null +++ b/src/test/skript/tests/regressions/6811-inventory-holder-location-doublechest.sk @@ -0,0 +1,20 @@ +test "inventory holder location double chest": + set {_b::1} to the block at spawn of world "world" + set {_b::2} to the block north of {_b::1} + set {_prev::1} to type of block at {_b::1} + set {_prev::2} to type of block at {_b::2} + + set block at {_b::1} to chest[facing=east;type=right] + set block at {_b::2} to chest[facing=east;type=left] + + set {_inv} to inventory of {_b::1} + set {_holder} to holder of {_inv} + + set {_a-loc} to location of {_holder} + set {_b-loc::*} to location of {_b::1} and location of {_b::2} + + # clean up first in case assert fails + set block at {_b::1} to {_prev::1} + set block at {_b::2} to {_prev::2} + + assert {_b-loc::*} contains {_a-loc} with "holder location of double chest differs from block location" diff --git a/src/test/skript/tests/regressions/6830-remove air from slot.sk b/src/test/skript/tests/regressions/6830-remove air from slot.sk new file mode 100644 index 00000000000..13eaa29b1d3 --- /dev/null +++ b/src/test/skript/tests/regressions/6830-remove air from slot.sk @@ -0,0 +1,4 @@ +test "remove air from air slot": + set {_chest} to chest inventory with 3 rows + # throws exception if not fixed + remove 1 of (slot 0 of {_chest}) from (slot 0 of {_chest}) diff --git a/src/test/skript/tests/syntaxes/effects/EffGlowingText.sk b/src/test/skript/tests/syntaxes/effects/EffGlowingText.sk index b43a5786755..d39b3e6988a 100644 --- a/src/test/skript/tests/syntaxes/effects/EffGlowingText.sk +++ b/src/test/skript/tests/syntaxes/effects/EffGlowingText.sk @@ -10,7 +10,7 @@ test "glowing sign blocks" when running minecraft "1.17.1": set block at {_loc} to {_original block} test "glowing sign items" when running minecraft "1.17.1": - set {_sign} to sign + set {_sign} to floor sign assert {_sign} doesn't have glowing text with "Sign had glowing text erroneously (1)" make {_sign} have glowing text assert {_sign} has glowing text with "Sign had normal text erroneously" diff --git a/src/test/skript/tests/syntaxes/effects/EffSort.sk b/src/test/skript/tests/syntaxes/effects/EffSort.sk new file mode 100644 index 00000000000..4d832cd61fa --- /dev/null +++ b/src/test/skript/tests/syntaxes/effects/EffSort.sk @@ -0,0 +1,47 @@ +test "sorting": + set {_numbers::*} to shuffled integers from 1 to 50 + sort {_numbers::*} + assert {_numbers::*} is integers from 1 to 50 with "improper sorting of numbers" + + set {_numbers::*} to shuffled integers from 1 to 5 + sort {_numbers::*} by input * 20 + 4 - 3 # linear transformations don't affect order + assert {_numbers::*} is integers from 1 to 5 with "improper custom sorting of numbers" + + set {_numbers::*} to shuffled integers from 1 to 5 + set {_pre-sort-numbers::*} to {_numbers::*} + sort {_numbers::*} by "%input%" parsed as time # map expression returns null + assert {_numbers::*} is {_pre-sort-numbers::*} with "Invalid sorting expression adjusted list" + + set {_numbers::*} to shuffled integers from 1 to 5 + set {_pre-sort-numbers::*} to {_numbers::*} + sort {_numbers::*} by {_} + assert {_numbers::*} is {_pre-sort-numbers::*} with "Invalid sorting expression adjusted list" + + set {_numbers::*} to {_} + sort {_numbers::*} by input + 3 + assert {_numbers::*} is not set with "Invalid sorting of unset list" + + set {_chars::*} to shuffled characters between "a" and "f" + sort {_chars::*} + assert {_chars::*} is characters between "a" and "f" with "improper sorting of chars" + + set {_chars::*} to shuffled characters between "a" and "f" + sort {_chars::*} based on codepoint of input + assert {_chars::*} is characters between "a" and "f" with "improper custom sorting of chars" + + set {_mixed::*} to shuffled (characters between "a" and "f", integers from 1 to 5) + set {_pre-sort-mixed::*} to {_mixed::*} + sort {_mixed::*} + assert {_mixed::*} is {_pre-sort-mixed::*} with "incomparable mixed list was adjusted" + + set {_mixed::*} to shuffled (characters between "a" and "f", integers from 1 to 5) + sort {_mixed::*} by "%input%" + assert {_mixed::*} is 1, 2, 3, 4, 5, and characters between "a" and "f" with "improper custom sorting of mixed list" + + set {_list::x} to 1 + set {_list::aa} to 2 + set {_list::bxs} to 3 + set {_list::zysa} to 4 + set {_list::aaaaa} to 5 + sort {_list::*} by length of input index + assert {_list::*} is integers from 1 to 5 with "improper custom sorting based on index" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprAmountOfItems.sk b/src/test/skript/tests/syntaxes/expressions/ExprAmountOfItems.sk new file mode 100644 index 00000000000..9e40ff707ca --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprAmountOfItems.sk @@ -0,0 +1,14 @@ +test "amount of items": + set {_inventory} to a hopper inventory named "test" + assert the amount of stone in {_inventory} is 0 with "default amount failed" + add stone to {_inventory} + assert the amount of stone in {_inventory} is 1 with "single amount failed" + add stone named "bread" to {_inventory} + assert the amount of stone in {_inventory} is 2 with "different named items amount failed" + add 100 of iron ingot to {_inventory} + assert the amount of stone in {_inventory} is 2 with "add different item amount failed" + assert the amount of iron ingot in {_inventory} is 100 with "add 100 item amount failed" + remove stone from {_inventory} + assert the amount of stone in {_inventory} is 1 with "removed one amount failed" + remove stone from {_inventory} + assert the amount of stone in {_inventory} is 0 with "removed all amount failed" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprFilter.sk b/src/test/skript/tests/syntaxes/expressions/ExprFilter.sk index b1f1738f20d..bc195b894c8 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprFilter.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprFilter.sk @@ -3,4 +3,15 @@ test "where filter": assert first element of ({_list::*} where [string input is "foo"]) is "foo" with "ExprFilter filtered incorrectly" assert {_list::*} where [number input is set] is not set with "ExprFilter provided input value when classinfo did not match" assert first element of ({_list::*} where [input is "foo"]) is "foo" with "ExprFilter filtered object input incorrectly" + assert first element of ({_list::*} where [input is "bar"]) is "bar" with "ExprFilter filtered object input incorrectly" + assert size of ({_list::*} where [input is "bar"]) is 1 with "ExprFilter filtered object input incorrectly" + assert first element of ({_list::*} where [input is "bar"]) is "bar" with "ExprFilter filtered object input incorrectly" + assert size of ({_list::*} where [input is "bar"]) is 1 with "ExprFilter filtered object input incorrectly" + assert first element of ({_list::*} where [input is "foobar"]) is "foobar" with "ExprFilter filtered object input incorrectly" + assert size of ({_list::*} where [input is "foobar"]) is 1 with "ExprFilter filtered object input incorrectly" + assert size of ({_list::*} where [input is "foo" or "bar"]) is 2 with "ExprFilter filtered object input incorrectly" + assert size of ({_list::*} where [input is set]) is 3 with "ExprFilter filtered object input incorrectly" assert {_list::*} where [false is true] is not set with "ExprFilter returned objects with false condition" + assert ({_list::*} where [input is (("foo" and "bar") where [input is "bar"])]) is "bar" with "Failed filter with filter within condition" + assert (({_list::*} where [input is "foo"]) where [input is "foo"]) is "foo" with "Failed chained filters" + assert {_list::*} where [input index is "2" or "3"] is "bar" and "foobar" with "Failed input index filter" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprItemWithCustomModelData.sk b/src/test/skript/tests/syntaxes/expressions/ExprItemWithCustomModelData.sk new file mode 100644 index 00000000000..098cda9abee --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprItemWithCustomModelData.sk @@ -0,0 +1,13 @@ +test "item with custom model data" when minecraft version is not "1.13.2": + set {_i} to stone + assert the custom model data of {_i} is 0 with "default model data failed" + set {_i} to stone with custom model data 5 + assert the custom model data of {_i} is 5 with "simple model data set failed" + set {_i} to stone with custom model data -1 + assert the custom model data of {_i} is -1 with "negative model data set failed" + set {_i} to {_i} with custom model data 2 + assert the custom model data of {_i} is 2 with "existing item model data set failed" + set {_i} to {_i} with custom model data 3.3 + assert the custom model data of {_i} is 3 with "decimal item model data set failed" + set {_i} to {_i} with custom model data 3.999 + assert the custom model data of {_i} is 3 with "close decimal item model data set failed" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprItemsIn.sk b/src/test/skript/tests/syntaxes/expressions/ExprItemsIn.sk index b5ef324b555..ce5253d40b0 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprItemsIn.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprItemsIn.sk @@ -1,3 +1,29 @@ + +test "items in (inventory)": + set {_inventory} to a hopper inventory named "test" + add stone to {_inventory} + add stone named "bread" to {_inventory} + add 100 of iron ingot to {_inventory} + loop items in {_inventory}: + if loop-value is stone: + continue + else if loop-value is iron ingot: + continue + else: + assert true is false with "unexpected item in the inventory area: %loop-value%" + set {_list::*} to items in {_inventory} + assert size of {_list::*} is 4 with "size of items in failed" + assert {_list::1} is stone with "first item failed" + assert {_list::2} is stone named "bread" with "second item failed" + assert {_list::3} is 64 of iron ingot with "third item failed" + assert {_list::4} is 36 of iron ingot with "split fourth item failed" + remove stone from {_inventory} + set {_list::*} to items in {_inventory} + assert size of {_list::*} is 3 with "size of second items in failed" + assert {_list::1} is stone named "bread" with "new first item failed" + assert {_list::2} is 64 of iron ingot with "new second item failed" + assert {_list::3} is 36 of iron ingot with "new third item failed" + test "filtering ExprItemsIn": set {_world} to random world out of all worlds set block at spawn of {_world} to chest diff --git a/src/test/skript/tests/syntaxes/expressions/ExprLastDamage.sk b/src/test/skript/tests/syntaxes/expressions/ExprLastDamage.sk new file mode 100644 index 00000000000..0ed3f3f3e4c --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprLastDamage.sk @@ -0,0 +1,15 @@ +test "last damage": + set {_l} to location 0.5 above highest block at location(1,1,1) + spawn a sheep at {_l} + set {_e} to last spawned entity + + assert last damage of {_e} = 0 with "last damage of newly spawned entity should be 0" + + damage {_e} by 1 + assert last damage of {_e} = 1 with "last damage of entity should be 1 after damaging it by 1" + + set last damage of {_e} to 3 + assert last damage of {_e} = 3 with "last damage of entity should be 3 after setting to 3" + + # thank you for your service + delete entity in {_e}