From aa71764e4a7be7e990b74292f92647e9793293c2 Mon Sep 17 00:00:00 2001 From: _tud <98935832+UnderscoreTud@users.noreply.github.com> Date: Sun, 15 Dec 2024 03:24:59 +0300 Subject: [PATCH 01/19] Fix the REMOVE changer of variables (#7163) * Fix the REMOVE changer of variables * Fix wording * Fix tests * Update Variable.java --------- Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com> --- .../java/ch/njol/skript/lang/Variable.java | 47 +++++-------------- ...removing from variable skips duplicates.sk | 6 +++ 2 files changed, 18 insertions(+), 35 deletions(-) create mode 100644 src/test/skript/tests/regressions/7162-removing from variable skips duplicates.sk diff --git a/src/main/java/ch/njol/skript/lang/Variable.java b/src/main/java/ch/njol/skript/lang/Variable.java index 1b7a55f42b6..173ee1aeea3 100644 --- a/src/main/java/ch/njol/skript/lang/Variable.java +++ b/src/main/java/ch/njol/skript/lang/Variable.java @@ -1,23 +1,10 @@ -/** - * 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 java.lang.reflect.Array; +import java.util.*; +import java.util.Map.Entry; +import java.util.function.Function; + import ch.njol.skript.Skript; import ch.njol.skript.SkriptAPIException; import ch.njol.skript.SkriptConfig; @@ -55,17 +42,6 @@ import org.skriptlang.skript.lang.script.Script; import org.skriptlang.skript.lang.script.ScriptWarning; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.NoSuchElementException; -import java.util.TreeMap; -import java.util.function.Function; - public class Variable implements Expression { private final static String SINGLE_SEPARATOR_CHAR = ":"; @@ -579,14 +555,15 @@ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) throw if (mode == ChangeMode.REMOVE) { if (map == null) return; - ArrayList toRemove = new ArrayList<>(); // prevents CMEs + Set toRemove = new HashSet<>(); // prevents CMEs for (Object value : delta) { for (Entry entry : map.entrySet()) { + String key = entry.getKey(); + if (key == null) + continue; // This is NOT a part of list variable + if (toRemove.contains(key)) + continue; // Skip if we've already marked this key to be removed if (Relation.EQUAL.isImpliedBy(Comparators.compare(entry.getValue(), value))) { - String key = entry.getKey(); - if (key == null) - continue; // This is NOT a part of list variable - // Otherwise, we'll mark that key to be set to null toRemove.add(key); break; @@ -600,7 +577,7 @@ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) throw } else if (mode == ChangeMode.REMOVE_ALL) { if (map == null) return; - ArrayList toRemove = new ArrayList<>(); // prevents CMEs + Set toRemove = new HashSet<>(); // prevents CMEs for (Entry i : map.entrySet()) { for (Object value : delta) { if (Relation.EQUAL.isImpliedBy(Comparators.compare(i.getValue(), value))) diff --git a/src/test/skript/tests/regressions/7162-removing from variable skips duplicates.sk b/src/test/skript/tests/regressions/7162-removing from variable skips duplicates.sk new file mode 100644 index 00000000000..c4fadc84163 --- /dev/null +++ b/src/test/skript/tests/regressions/7162-removing from variable skips duplicates.sk @@ -0,0 +1,6 @@ +test "removing from variables skips duplicates": + set {_list::*} to 1, 1, 2, 3, 4, 5, 6 and 6 + remove 1 and 1 from {_list::*} + assert {_list::*} is 2, 3, 4, 5, 6 and 6 with "didn't remove all instances of 1 from the list" + remove first 6 elements out of {_list::*} from {_list::*} + assert {_list::*} is not set with "didn't remove all elements" From 48f931a9f92dcb3b6486513c862fd788a1ba8038 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Tue, 17 Dec 2024 11:46:12 -0500 Subject: [PATCH 02/19] Fix Improperly Typed Array in ExprXOf (#7268) --- src/main/java/ch/njol/skript/expressions/ExprXOf.java | 4 +++- .../skript/tests/regressions/6817-null of x incorrect type.sk | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 src/test/skript/tests/regressions/6817-null of x incorrect type.sk diff --git a/src/main/java/ch/njol/skript/expressions/ExprXOf.java b/src/main/java/ch/njol/skript/expressions/ExprXOf.java index 06393a0423a..04dc49c8971 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprXOf.java +++ b/src/main/java/ch/njol/skript/expressions/ExprXOf.java @@ -36,6 +36,8 @@ import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.Array; + @Name("X of Item") @Description("An expression to be able to use a certain amount of items where the amount can be any expression. Please note that this expression is not stable and might be replaced in the future.") @Examples("give level of player of pickaxes to the player") @@ -62,7 +64,7 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye protected Object[] get(Event e, Object[] source) { Number a = amount.getSingle(e); if (a == null) - return new Object[0]; + return (Object[]) Array.newInstance(getReturnType(), 0); return get(source, o -> { if (o instanceof ItemStack) { diff --git a/src/test/skript/tests/regressions/6817-null of x incorrect type.sk b/src/test/skript/tests/regressions/6817-null of x incorrect type.sk new file mode 100644 index 00000000000..ac93553d712 --- /dev/null +++ b/src/test/skript/tests/regressions/6817-null of x incorrect type.sk @@ -0,0 +1,3 @@ +test "null of x incorrect type": + # would cause an exception + spawn {_null} of pig above {_null} From 219e358711e3db3172d836d5c86ec54f5b7d2af5 Mon Sep 17 00:00:00 2001 From: Moderocky Date: Thu, 26 Dec 2024 20:44:35 +0000 Subject: [PATCH 03/19] Add boat data check to prevent error. (#7301) --- .../java/ch/njol/skript/entity/BoatData.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/ch/njol/skript/entity/BoatData.java b/src/main/java/ch/njol/skript/entity/BoatData.java index 22b8d58b251..47d647ee61f 100644 --- a/src/main/java/ch/njol/skript/entity/BoatData.java +++ b/src/main/java/ch/njol/skript/entity/BoatData.java @@ -55,22 +55,22 @@ public class BoatData extends EntityData { } - + public BoatData(){ this(0); } - + public BoatData(@Nullable TreeSpecies type){ this(type != null ? type.ordinal() + 2 : 1); } - + private BoatData(int type){ matchedPattern = type; } - + @Override protected boolean init(Literal[] exprs, int matchedPattern, ParseResult parseResult) { - + return true; } @@ -96,7 +96,7 @@ protected boolean match(Boat entity) { @Override public Class getType() { - if (IS_RUNNING_1_21_3) + if (IS_RUNNING_1_21_3 && matchedPattern > 1) return typeToClassMap.get(TreeSpecies.values()[matchedPattern - 2]); return Boat.class; } @@ -124,7 +124,7 @@ public boolean isSupertypeOf(EntityData e) { return matchedPattern <= 1 || matchedPattern == ((BoatData)e).matchedPattern; return false; } - + public boolean isOfItemType(ItemType i){ int ordinal = -1; @@ -142,7 +142,7 @@ else if (type == Material.ACACIA_BOAT) else if (type == Material.DARK_OAK_BOAT) ordinal = TreeSpecies.DARK_OAK.ordinal(); return hashCode_i() == ordinal + 2 || (matchedPattern + ordinal == 0) || ordinal == 0; - + } } From 3c940632d60327cd019d8ab6d411c0aeb1750f97 Mon Sep 17 00:00:00 2001 From: kyoshuske <65446070+kyoshuske@users.noreply.github.com> Date: Thu, 26 Dec 2024 21:55:49 +0100 Subject: [PATCH 04/19] Fix BlockIgniteEvent (#7252) Update BukkitEventValues.java Co-authored-by: Moderocky --- .../java/ch/njol/skript/classes/data/BukkitEventValues.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 82fca450068..f40aa638d9e 100644 --- a/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java +++ b/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java @@ -423,7 +423,7 @@ public Player get(final BlockIgniteEvent e) { @Override @Nullable public Block get(final BlockIgniteEvent e) { - return e.getIgnitingBlock(); + return e.getBlock(); } }, 0); // BlockDispenseEvent From dde9aee3505b8725a1b40650b5c10728158b2f6b Mon Sep 17 00:00:00 2001 From: Shane Bee Date: Sat, 28 Dec 2024 11:13:03 -0800 Subject: [PATCH 05/19] RegistryClassInfo - fix comparator (Failing Tests) (#7319) RegistryClassInfo - fix comparator --- .../java/ch/njol/skript/classes/registry/RegistryClassInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/njol/skript/classes/registry/RegistryClassInfo.java b/src/main/java/ch/njol/skript/classes/registry/RegistryClassInfo.java index 012cbf2eb90..be7c4c8076b 100644 --- a/src/main/java/ch/njol/skript/classes/registry/RegistryClassInfo.java +++ b/src/main/java/ch/njol/skript/classes/registry/RegistryClassInfo.java @@ -66,7 +66,7 @@ public RegistryClassInfo(Class registryClass, Registry registry, String co .parser(registryParser); if (registerComparator) - Comparators.registerComparator(registryClass, registryClass, (o1, o2) -> Relation.get(o1.getKey() == o2.getKey())); + Comparators.registerComparator(registryClass, registryClass, (o1, o2) -> Relation.get(o1.getKey().equals(o2.getKey()))); } } From 5501f28bc3097b6e986f380ff130fce28769cc4a Mon Sep 17 00:00:00 2001 From: Efnilite <35348263+Efnilite@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:12:28 +0100 Subject: [PATCH 06/19] Supported events (#7281) * init commit * update error message * update docs, update message * move to interface * update * le fix * update doc * implement * oops! --------- Co-authored-by: Moderocky --- .../njol/skript/expressions/ExprArgument.java | 16 ++++++---- .../njol/skript/expressions/ExprAttacked.java | 15 ++++++---- .../njol/skript/expressions/ExprAttacker.java | 16 ++++++---- .../njol/skript/expressions/ExprCommand.java | 15 ++++++---- .../ch/njol/skript/expressions/ExprDrops.java | 12 ++++---- .../expressions/ExprExplodedBlocks.java | 16 +++++----- .../skript/expressions/ExprPushedBlocks.java | 15 +++++----- .../skript/lang/EventRestrictedSyntax.java | 25 ++++++++++++++++ .../ch/njol/skript/lang/SkriptParser.java | 30 ++++++++++++++----- .../skript/tests/misc/supported events.sk | 5 ++++ 10 files changed, 114 insertions(+), 51 deletions(-) create mode 100644 src/main/java/ch/njol/skript/lang/EventRestrictedSyntax.java create mode 100644 src/test/skript/tests/misc/supported events.sk diff --git a/src/main/java/ch/njol/skript/expressions/ExprArgument.java b/src/main/java/ch/njol/skript/expressions/ExprArgument.java index a6c9f666525..db91407e222 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprArgument.java +++ b/src/main/java/ch/njol/skript/expressions/ExprArgument.java @@ -3,6 +3,8 @@ import java.util.List; import java.util.regex.MatchResult; +import ch.njol.skript.lang.EventRestrictedSyntax; +import ch.njol.util.coll.CollectionUtils; import org.bukkit.event.Event; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.server.ServerCommandEvent; @@ -43,7 +45,7 @@ "heal the last argument" }) @Since("1.0, 2.7 (support for command events)") -public class ExprArgument extends SimpleExpression { +public class ExprArgument extends SimpleExpression implements EventRestrictedSyntax { static { Skript.registerExpression(ExprArgument.class, Object.class, ExpressionType.SIMPLE, @@ -68,10 +70,6 @@ public class ExprArgument extends SimpleExpression { @SuppressWarnings("unchecked") public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { boolean scriptCommand = getParser().isCurrentEvent(ScriptCommandEvent.class); - if (!scriptCommand && !getParser().isCurrentEvent(PlayerCommandPreprocessEvent.class, ServerCommandEvent.class)) { - Skript.error("The 'argument' expression can only be used in a script command or command event"); - return false; - } switch (matchedPattern) { case 0: @@ -192,7 +190,13 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye return true; } - + + @Override + public Class[] supportedEvents() { + return CollectionUtils.array(ScriptCommandEvent.class, PlayerCommandPreprocessEvent.class, + ServerCommandEvent.class); + } + @Override @Nullable protected Object[] get(final Event e) { diff --git a/src/main/java/ch/njol/skript/expressions/ExprAttacked.java b/src/main/java/ch/njol/skript/expressions/ExprAttacked.java index c8f2a8dd1d3..5b2ed4e51cb 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprAttacked.java +++ b/src/main/java/ch/njol/skript/expressions/ExprAttacked.java @@ -2,6 +2,8 @@ import java.lang.reflect.Array; +import ch.njol.skript.lang.EventRestrictedSyntax; +import ch.njol.util.coll.CollectionUtils; import org.bukkit.entity.Entity; import org.bukkit.event.Event; import org.bukkit.event.entity.EntityDamageEvent; @@ -36,7 +38,7 @@ "\tdamage the attacked by 1 heart"}) @Since("1.3, 2.6.1 (projectile hit event)") @Events({"damage", "death", "projectile hit"}) -public class ExprAttacked extends SimpleExpression { +public class ExprAttacked extends SimpleExpression implements EventRestrictedSyntax { private static final boolean SUPPORT_PROJECTILE_HIT = Skript.methodExists(ProjectileHitEvent.class, "getHitEntity"); @@ -49,11 +51,6 @@ public class ExprAttacked extends SimpleExpression { @Override public boolean init(Expression[] vars, int matchedPattern, Kleenean isDelayed, ParseResult parser) { - if (!getParser().isCurrentEvent(EntityDamageEvent.class, EntityDeathEvent.class, VehicleDamageEvent.class, VehicleDestroyEvent.class, ProjectileHitEvent.class) - || !SUPPORT_PROJECTILE_HIT && getParser().isCurrentEvent(ProjectileHitEvent.class)) { - Skript.error("The expression 'victim' can only be used in a damage" + (SUPPORT_PROJECTILE_HIT ? ", death, or projectile hit" : " or death") + " event"); - return false; - } String type = parser.regexes.size() == 0 ? null : parser.regexes.get(0).group(); if (type == null) { this.type = EntityData.fromClass(Entity.class); @@ -68,6 +65,12 @@ public boolean init(Expression[] vars, int matchedPattern, Kleenean isDelayed return true; } + @Override + public Class[] supportedEvents() { + return CollectionUtils.array(EntityDamageEvent.class, EntityDeathEvent.class, + VehicleDamageEvent.class, VehicleDestroyEvent.class, ProjectileHitEvent.class); + } + @Override @Nullable protected Entity[] get(Event e) { diff --git a/src/main/java/ch/njol/skript/expressions/ExprAttacker.java b/src/main/java/ch/njol/skript/expressions/ExprAttacker.java index 435a3a33b5f..bf1e6d9f20e 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprAttacker.java +++ b/src/main/java/ch/njol/skript/expressions/ExprAttacker.java @@ -1,5 +1,7 @@ package ch.njol.skript.expressions; +import ch.njol.skript.lang.EventRestrictedSyntax; +import ch.njol.util.coll.CollectionUtils; import org.bukkit.entity.Entity; import org.bukkit.entity.Projectile; import org.bukkit.event.Event; @@ -36,7 +38,7 @@ " damage victim by 1 heart"}) @Since("1.3") @Events({"damage", "death", "destroy"}) -public class ExprAttacker extends SimpleExpression { +public class ExprAttacker extends SimpleExpression implements EventRestrictedSyntax { static { Skript.registerExpression(ExprAttacker.class, Entity.class, ExpressionType.SIMPLE, "[the] (attacker|damager)"); @@ -44,13 +46,15 @@ public class ExprAttacker extends SimpleExpression { @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parser) { - if (!getParser().isCurrentEvent(EntityDamageEvent.class, EntityDeathEvent.class, VehicleDamageEvent.class, VehicleDestroyEvent.class)) { - Skript.error("Cannot use 'attacker' outside of a damage/death/destroy event", ErrorQuality.SEMANTIC_ERROR); - return false; - } return true; } - + + @Override + public Class[] supportedEvents() { + return CollectionUtils.array(EntityDamageEvent.class, EntityDeathEvent.class, + VehicleDamageEvent.class, VehicleDestroyEvent.class); + } + @Override protected Entity[] get(Event e) { return new Entity[] {getAttacker(e)}; diff --git a/src/main/java/ch/njol/skript/expressions/ExprCommand.java b/src/main/java/ch/njol/skript/expressions/ExprCommand.java index b2173d00cb3..8cb71f77e34 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprCommand.java +++ b/src/main/java/ch/njol/skript/expressions/ExprCommand.java @@ -1,6 +1,8 @@ package ch.njol.skript.expressions; import ch.njol.skript.command.ScriptCommandEvent; +import ch.njol.skript.lang.EventRestrictedSyntax; +import ch.njol.util.coll.CollectionUtils; import org.bukkit.event.Event; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.server.ServerCommandEvent; @@ -31,7 +33,7 @@ "\t\t\tcancel the event"}) @Since("2.0, 2.7 (support for script commands)") @Events("command") -public class ExprCommand extends SimpleExpression { +public class ExprCommand extends SimpleExpression implements EventRestrictedSyntax { static { Skript.registerExpression(ExprCommand.class, String.class, ExpressionType.SIMPLE, @@ -44,13 +46,14 @@ public class ExprCommand extends SimpleExpression { @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - if (!getParser().isCurrentEvent(PlayerCommandPreprocessEvent.class, ServerCommandEvent.class, ScriptCommandEvent.class)) { - Skript.error("The 'command' expression can only be used in a script command or command event"); - return false; - } fullCommand = matchedPattern == 0; return true; } + + @Override + public Class[] supportedEvents() { + return CollectionUtils.array(PlayerCommandPreprocessEvent.class, ServerCommandEvent.class, ScriptCommandEvent.class); + } @Override @Nullable @@ -88,5 +91,5 @@ public Class getReturnType() { public String toString(@Nullable Event e, boolean debug) { return fullCommand ? "the full command" : "the command"; } - + } diff --git a/src/main/java/ch/njol/skript/expressions/ExprDrops.java b/src/main/java/ch/njol/skript/expressions/ExprDrops.java index 0bdc73304da..02809a05502 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprDrops.java +++ b/src/main/java/ch/njol/skript/expressions/ExprDrops.java @@ -8,6 +8,7 @@ import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.EventRestrictedSyntax; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; @@ -38,7 +39,7 @@ "remove 4 planks from the drops"}) @Since("1.0") @Events("death") -public class ExprDrops extends SimpleExpression { +public class ExprDrops extends SimpleExpression implements EventRestrictedSyntax { static { Skript.registerExpression(ExprDrops.class, ItemType.class, ExpressionType.SIMPLE, "[the] drops"); @@ -48,15 +49,16 @@ public class ExprDrops extends SimpleExpression { @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - if (!getParser().isCurrentEvent(EntityDeathEvent.class, BlockDropItemEvent.class)) { - Skript.error("The expression 'drops' can only be used in death events and block drop events"); - return false; - } if (getParser().isCurrentEvent(EntityDeathEvent.class)) isDeathEvent = true; return true; } + @Override + public Class[] supportedEvents() { + return CollectionUtils.array(EntityDeathEvent.class, BlockDropItemEvent.class); + } + @Override protected ItemType @Nullable [] get(Event event) { if (event instanceof EntityDeathEvent entityDeathEvent) { diff --git a/src/main/java/ch/njol/skript/expressions/ExprExplodedBlocks.java b/src/main/java/ch/njol/skript/expressions/ExprExplodedBlocks.java index 9eaccb66a3b..d67b502f56b 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprExplodedBlocks.java +++ b/src/main/java/ch/njol/skript/expressions/ExprExplodedBlocks.java @@ -3,6 +3,7 @@ import java.util.List; import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.EventRestrictedSyntax; import ch.njol.util.coll.CollectionUtils; import org.bukkit.block.Block; import org.bukkit.event.Event; @@ -43,21 +44,22 @@ "\tadd blocks above event-entity to exploded blocks"}) @Events("explode") @Since("2.5, 2.8.6 (modify blocks)") -public class ExprExplodedBlocks extends SimpleExpression { +public class ExprExplodedBlocks extends SimpleExpression implements EventRestrictedSyntax { static { Skript.registerExpression(ExprExplodedBlocks.class, Block.class, ExpressionType.COMBINED, "[the] exploded blocks"); } - + @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { - if (!getParser().isCurrentEvent(EntityExplodeEvent.class)) { - Skript.error("Exploded blocks can only be retrieved from an explode event."); - return false; - } return true; } - + + @Override + public Class[] supportedEvents() { + return CollectionUtils.array(EntityExplodeEvent.class); + } + @Nullable @Override protected Block[] get(Event e) { diff --git a/src/main/java/ch/njol/skript/expressions/ExprPushedBlocks.java b/src/main/java/ch/njol/skript/expressions/ExprPushedBlocks.java index a1b93a4d1c1..bdb6bb5a6bb 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprPushedBlocks.java +++ b/src/main/java/ch/njol/skript/expressions/ExprPushedBlocks.java @@ -1,5 +1,6 @@ package ch.njol.skript.expressions; +import ch.njol.skript.lang.EventRestrictedSyntax; import ch.njol.util.coll.CollectionUtils; import org.bukkit.block.Block; import org.bukkit.event.Event; @@ -23,7 +24,7 @@ @Description("Blocks which are moved in a piston event. Cannot be used outside of piston events.") @Examples("the moved blocks") @Since("2.2-dev27") -public class ExprPushedBlocks extends SimpleExpression { +public class ExprPushedBlocks extends SimpleExpression implements EventRestrictedSyntax { static { Skript.registerExpression(ExprPushedBlocks.class, Block.class, ExpressionType.SIMPLE, "[the] moved blocks"); @@ -31,14 +32,14 @@ public class ExprPushedBlocks extends SimpleExpression { @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - if (!getParser().isCurrentEvent(BlockPistonExtendEvent.class, BlockPistonRetractEvent.class)) { - Skript.error("The moved blocks are only usable in piston extend and retract events", ErrorQuality.SEMANTIC_ERROR); - return false; - } - return true; } - + + @Override + public Class[] supportedEvents() { + return CollectionUtils.array(BlockPistonExtendEvent.class, BlockPistonRetractEvent.class); + } + @Override @Nullable protected Block[] get(Event e) { diff --git a/src/main/java/ch/njol/skript/lang/EventRestrictedSyntax.java b/src/main/java/ch/njol/skript/lang/EventRestrictedSyntax.java new file mode 100644 index 00000000000..901c23d6ece --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/EventRestrictedSyntax.java @@ -0,0 +1,25 @@ +package ch.njol.skript.lang; + +import ch.njol.util.Kleenean; +import ch.njol.util.coll.CollectionUtils; +import org.bukkit.event.Event; + +/** + * A syntax element that restricts the events it can be used in. + */ +public interface EventRestrictedSyntax { + + /** + * Returns all supported events for this syntax element. + *

+ * Before {@link SyntaxElement#init(Expression[], int, Kleenean, SkriptParser.ParseResult)} is called, checks + * to see if the current event is supported by this syntax element. + * If it is not, an error will be printed and the syntax element will not be initialised. + *

+ * + * @return All supported event classes. + * @see CollectionUtils#array(Object[]) + */ + Class[] supportedEvents(); + +} diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index 6deca5e4cc6..79d94bf6ebc 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -31,19 +31,14 @@ import ch.njol.util.StringUtils; import ch.njol.util.coll.CollectionUtils; import com.google.common.primitives.Booleans; +import org.bukkit.event.Event; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.script.Script; import org.skriptlang.skript.lang.script.ScriptWarning; -import java.util.ArrayList; -import java.util.Deque; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; @@ -225,7 +220,26 @@ public boolean hasTag(String tag) { } } T element = info.getElementClass().newInstance(); - if (element.init(parseResult.exprs, patternIndex, getParser().getHasDelayBefore(), parseResult)) { + + if (element instanceof EventRestrictedSyntax eventRestrictedSyntax) { + Class[] supportedEvents = eventRestrictedSyntax.supportedEvents(); + if (!getParser().isCurrentEvent(supportedEvents)) { + Iterator iterator = Arrays.stream(supportedEvents) + .map(it -> "the " + it.getSimpleName() + .replaceAll("([A-Z])", " $1") + .toLowerCase() + .trim()) + .iterator(); + + String events = StringUtils.join(iterator, ", ", " or "); + + Skript.error("'" + parseResult.expr + "' can only be used in " + events); + continue; + } + } + + boolean success = element.init(parseResult.exprs, patternIndex, getParser().getHasDelayBefore(), parseResult); + if (success) { log.printLog(); return element; } diff --git a/src/test/skript/tests/misc/supported events.sk b/src/test/skript/tests/misc/supported events.sk new file mode 100644 index 00000000000..f0cfc2f8a16 --- /dev/null +++ b/src/test/skript/tests/misc/supported events.sk @@ -0,0 +1,5 @@ +test "supported events": + parse: + set {_x} to the exploded blocks + + assert last parse logs contain "'the exploded blocks' can only be used in the entity explode event" with "supported events message did not get sent correctly" From 27c4591429336e50ab0501cc25f8b84efa136204 Mon Sep 17 00:00:00 2001 From: Efnilite <35348263+Efnilite@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:19:55 +0100 Subject: [PATCH 07/19] Add unicode tag (#7312) * init commit * remove statics --------- Co-authored-by: Moderocky --- src/main/java/ch/njol/skript/Skript.java | 1 - src/main/java/ch/njol/skript/util/Utils.java | 170 +++++++++--------- .../njol/skript/util/chat/ChatMessages.java | 13 +- src/test/skript/tests/misc/unicode.sk | 6 + 4 files changed, 92 insertions(+), 98 deletions(-) create mode 100644 src/test/skript/tests/misc/unicode.sk diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 3c245a78615..8bcbffa039b 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -209,7 +209,6 @@ public Skript() throws IllegalStateException { /** * Check minecraft version and assign it to minecraftVersion field * This method is created to update MC version before onEnable method - * To fix {@link Utils#HEX_SUPPORTED} being assigned before minecraftVersion is properly assigned */ public static void updateMinecraftVersion() { String bukkitV = Bukkit.getBukkitVersion(); diff --git a/src/main/java/ch/njol/skript/util/Utils.java b/src/main/java/ch/njol/skript/util/Utils.java index 7b54dd78ccf..dc2b253e158 100644 --- a/src/main/java/ch/njol/skript/util/Utils.java +++ b/src/main/java/ch/njol/skript/util/Utils.java @@ -1,5 +1,27 @@ package ch.njol.skript.util; +import ch.njol.skript.Skript; +import ch.njol.skript.effects.EffTeleport; +import ch.njol.skript.localization.Language; +import ch.njol.skript.localization.LanguageChangeListener; +import ch.njol.skript.registrations.Classes; +import ch.njol.util.*; +import ch.njol.util.coll.CollectionUtils; +import ch.njol.util.coll.iterator.EnumerationIterable; +import com.google.common.collect.Iterables; +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; +import net.md_5.bungee.api.ChatColor; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.messaging.Messenger; +import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -13,34 +35,6 @@ import java.util.regex.Pattern; import java.util.stream.Stream; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.plugin.messaging.Messenger; -import org.bukkit.plugin.messaging.PluginMessageListener; - -import com.google.common.collect.Iterables; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; - -import ch.njol.skript.Skript; -import ch.njol.skript.effects.EffTeleport; -import ch.njol.skript.localization.Language; -import ch.njol.skript.localization.LanguageChangeListener; -import ch.njol.skript.registrations.Classes; -import ch.njol.util.Callback; -import ch.njol.util.Checker; -import ch.njol.util.NonNullPair; -import ch.njol.util.Pair; -import ch.njol.util.StringUtils; -import ch.njol.util.coll.CollectionUtils; -import ch.njol.util.coll.iterator.EnumerationIterable; -import net.md_5.bungee.api.ChatColor; -import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.NotNull; - /** * Utility class. * @@ -572,9 +566,6 @@ public static CompletableFuture sendPluginMessage(Player pla final static Map chat = new HashMap<>(); final static Map englishChat = new HashMap<>(); - public final static boolean HEX_SUPPORTED = Skript.isRunningMinecraft(1, 16); - public final static boolean COPY_SUPPORTED = Skript.isRunningMinecraft(1, 15); - static { Language.addListener(new LanguageChangeListener() { @Override @@ -601,43 +592,17 @@ public static String getChatStyle(final String s) { return chat.get(s); } - private final static Pattern stylePattern = Pattern.compile("<([^<>]+)>"); - /** * Replaces <chat styles> in the message * * @param message * @return message with localised chat styles converted to Minecraft's format */ - public static String replaceChatStyles(final String message) { + public static @NotNull String replaceChatStyles(String message) { if (message.isEmpty()) return message; - String m = StringUtils.replaceAll(Matcher.quoteReplacement("" + message.replace("<>", "")), stylePattern, new Callback() { - @Override - public String run(final Matcher m) { - SkriptColor color = SkriptColor.fromName("" + m.group(1)); - if (color != null) - return color.getFormattedChat(); - final String tag = m.group(1).toLowerCase(Locale.ENGLISH); - final String f = chat.get(tag); - if (f != null) - return f; - if (HEX_SUPPORTED && tag.startsWith("#")) { // Check for parsing hex colors - ChatColor chatColor = parseHexColor(tag); - if (chatColor != null) - return chatColor.toString(); - } - return "" + m.group(); - } - }); - assert m != null; - // Restore user input post-sanitization - // Sometimes, the message has already been restored - if (!message.equals(m)) { - m = m.replace("\\$", "$").replace("\\\\", "\\"); - } - m = ChatColor.translateAlternateColorCodes('&', "" + m); - return "" + m; + + return replaceChatStyle(message.replace("<>", "")); } /** @@ -647,54 +612,81 @@ public String run(final Matcher m) { * @param message * @return message with english chat styles converted to Minecraft's format */ - public static String replaceEnglishChatStyles(final String message) { + public static @NotNull String replaceEnglishChatStyles(String message) { if (message.isEmpty()) return message; - String m = StringUtils.replaceAll(Matcher.quoteReplacement(message), stylePattern, new Callback() { - @Override - public String run(final Matcher m) { - SkriptColor color = SkriptColor.fromName("" + m.group(1)); - if (color != null) - return color.getFormattedChat(); - final String tag = m.group(1).toLowerCase(Locale.ENGLISH); - final String f = englishChat.get(tag); - if (f != null) - return f; - if (HEX_SUPPORTED && tag.startsWith("#")) { // Check for parsing hex colors - ChatColor chatColor = parseHexColor(tag); - if (chatColor != null) - return chatColor.toString(); - } - return "" + m.group(); + + return replaceChatStyle(message); + } + + private final static Pattern STYLE_PATTERN = Pattern.compile("<([^<>]+)>"); + + private static @NotNull String replaceChatStyle(String message) { + String m = StringUtils.replaceAll(Matcher.quoteReplacement(message), STYLE_PATTERN, matcher -> { + SkriptColor color = SkriptColor.fromName(matcher.group(1)); + if (color != null) + return color.getFormattedChat(); + + String tag = matcher.group(1).toLowerCase(Locale.ENGLISH); + String f = englishChat.get(tag); + if (f != null) + return f; + + if (tag.startsWith("#")) { + ChatColor chatColor = parseHexColor(tag); + if (chatColor != null) + return chatColor.toString(); + } else if (tag.startsWith("u:") || tag.startsWith("unicode:")) { + String character = parseUnicode(tag); + if (character != null) + return character; } + return matcher.group(); }); - assert m != null; + // Restore user input post-sanitization // Sometimes, the message has already been restored if (!message.equals(m)) { m = m.replace("\\$", "$").replace("\\\\", "\\"); } - m = ChatColor.translateAlternateColorCodes('&', "" + m); - return "" + m; + + return ChatColor.translateAlternateColorCodes('&', m); } - private static final Pattern HEX_PATTERN = Pattern.compile("(?i)#{0,2}[0-9a-f]{6}"); + private static final Pattern UNICODE_PATTERN = Pattern.compile("(?i)u(?:nicode)?:(?[0-9a-f]{4,})"); + + /** + * Tries to extract a Unicode character from the given string. + * @param string The string. + * @return The Unicode character, or null if it could not be parsed. + */ + public static @Nullable String parseUnicode(String string) { + Matcher matcher = UNICODE_PATTERN.matcher(string); + if (!matcher.matches()) + return null; + + try { + return Character.toString(Integer.parseInt(matcher.group("code"), 16)); + } catch (IllegalArgumentException ex) { + return null; + } + } + + private static final Pattern HEX_PATTERN = Pattern.compile("(?i)#{0,2}(?[0-9a-f]{6})"); /** * Tries to get a {@link ChatColor} from the given string. - * @param hex The hex code to parse. + * @param string The string code to parse. * @return The ChatColor, or null if it couldn't be parsed. */ - @SuppressWarnings("null") - @Nullable - public static ChatColor parseHexColor(String hex) { - if (!HEX_SUPPORTED || !HEX_PATTERN.matcher(hex).matches()) // Proper hex code validation + public static @Nullable ChatColor parseHexColor(String string) { + Matcher matcher = HEX_PATTERN.matcher(string); + if (!matcher.matches()) return null; - hex = hex.replace("#", ""); try { - return ChatColor.of('#' + hex.substring(0, 6)); - } catch (IllegalArgumentException e) { + return ChatColor.of('#' + matcher.group("code")); + } catch (IllegalArgumentException ex) { return null; } } diff --git a/src/main/java/ch/njol/skript/util/chat/ChatMessages.java b/src/main/java/ch/njol/skript/util/chat/ChatMessages.java index 5498f829179..b8bcd5d4ed9 100644 --- a/src/main/java/ch/njol/skript/util/chat/ChatMessages.java +++ b/src/main/java/ch/njol/skript/util/chat/ChatMessages.java @@ -77,8 +77,6 @@ public void onLanguageChange() { Skript.debug("Parsing message style lang files"); for (SkriptChatCode code : SkriptChatCode.values()) { assert code != null; - if (code == SkriptChatCode.copy_to_clipboard && !Utils.COPY_SUPPORTED) - continue; registerChatCode(code); } @@ -213,7 +211,7 @@ else if (c2 == '>') } name = name.toLowerCase(Locale.ENGLISH); // Tags are case-insensitive - boolean tryHex = Utils.HEX_SUPPORTED && name.startsWith("#"); + boolean tryHex = name.startsWith("#"); ChatColor chatColor = null; if (tryHex) { chatColor = Utils.parseHexColor(name); @@ -261,7 +259,7 @@ else if (c2 == '>') char color = chars[i + 1]; - boolean tryHex = Utils.HEX_SUPPORTED && color == 'x'; + boolean tryHex = color == 'x'; ChatColor chatColor = null; if (tryHex && i + 14 < chars.length) { // Try to parse hex "&x&1&2&3&4&5&6" chatColor = Utils.parseHexColor(msg.substring(i + 2, i + 14).replace("&", "").replace("§", "")); @@ -424,7 +422,7 @@ public static List fromParsedString(String msg) { char color = chars[i + 1]; - boolean tryHex = Utils.HEX_SUPPORTED && color == 'x'; + boolean tryHex = color == 'x'; ChatColor chatColor = null; if (tryHex && i + 14 < chars.length) { // Try to parse hex "&x&1&2&3&4&5&6" chatColor = Utils.parseHexColor(msg.substring(i + 2, i + 14).replace("&", "").replace("§", "")); @@ -582,9 +580,8 @@ public static String stripStyles(String text) { builder.append(component.text); } String plain = builder.toString(); - - if (Utils.HEX_SUPPORTED) // Strip '§x', '&x' - plain = HEX_COLOR_PATTERN.matcher(plain).replaceAll(""); + + plain = HEX_COLOR_PATTERN.matcher(plain).replaceAll(""); result = ANY_COLOR_PATTERN.matcher(plain).replaceAll(""); // strips colors & or § (ex. &5) } while (!previous.equals(result)); diff --git a/src/test/skript/tests/misc/unicode.sk b/src/test/skript/tests/misc/unicode.sk new file mode 100644 index 00000000000..3a6e9ba7816 --- /dev/null +++ b/src/test/skript/tests/misc/unicode.sk @@ -0,0 +1,6 @@ +test "unicode": + assert "" is "§" with "single symbol did not get replaced" + assert "" is "§" with "single short symbol did not get replaced" + + assert "aB" is "a🐛B" with "symbol did not get replaced" + assert "aB" is "a🐛B" with "short symbol did not get replaced" From 42ba3ea2cea4045d389f2f853f2859995b32c5a5 Mon Sep 17 00:00:00 2001 From: Efnilite <35348263+Efnilite@users.noreply.github.com> Date: Sun, 29 Dec 2024 22:21:52 +0100 Subject: [PATCH 08/19] Add event-block to command struct (#7320) init commit --- .../java/ch/njol/skript/classes/data/BukkitEventValues.java | 4 ++++ 1 file changed, 4 insertions(+) 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 f35f934789d..ab91438aaf2 100644 --- a/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java +++ b/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java @@ -4,6 +4,7 @@ import ch.njol.skript.aliases.ItemType; import ch.njol.skript.bukkitutil.InventoryUtils; import ch.njol.skript.command.CommandEvent; +import ch.njol.skript.command.ScriptCommandEvent; import ch.njol.skript.events.bukkit.ScriptEvent; import ch.njol.skript.events.bukkit.SkriptStartEvent; import ch.njol.skript.events.bukkit.SkriptStopEvent; @@ -25,6 +26,7 @@ import org.bukkit.block.BlockFace; import org.bukkit.block.BlockState; import org.bukkit.block.data.BlockData; +import org.bukkit.command.BlockCommandSender; import org.bukkit.command.CommandSender; import org.bukkit.entity.*; import org.bukkit.event.block.*; @@ -965,6 +967,8 @@ public World get(final CommandEvent e) { return e.getSender() instanceof Player ? ((Player) e.getSender()).getWorld() : null; } }, 0); + EventValues.registerEventValue(CommandEvent.class, Block.class, + event -> event.getSender() instanceof BlockCommandSender sender ? sender.getBlock() : null); // === ServerEvents === // Script load/unload event From a0f3a5b586a02e36c2914c063643336903cd120b Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Sun, 29 Dec 2024 16:51:36 -0500 Subject: [PATCH 09/19] New Registration API (#6246) * First api design * Rework and implement for expression * Implement for all syntax elements * Rename to Skript and other small refactors * Registration closing * Child key * Cleanup * Moving * hashcode * Oops * TODO * Move event pattern transformation to the correct place * Moves key implementation away * Apply suggestions from code review Co-authored-by: Patrick Miller * Apply suggestions from code review Co-authored-by: Patrick Miller * Requested changes * Package rename * Refactoring * Deprecation * Builders * Use builders * Certified license header moment * Not too proud of this one * Fix tests * Refactoring * Replace with shorter version * Fix order * Refactoring * Cherry-pick docs-tool into api-rework * Fix documentation * Double tabbing * Fix registrations * Attempt 2 * Apply suggestions from code review Co-authored-by: Patrick Miller Co-authored-by: LimeGlass <16087552+TheLimeGlass@users.noreply.github.com> * Allows updating of Skript instance. * Fix error * Deprecation * Simpler keys * SimpleSkriptRegistry * Priority stuff * Allow for multiple Skript instances * Suppliers for syntax elements * Fixes for event parsing changes * Improve BukkitOrigin; Remove LegacyEventImpl * Origin improvements * Rework SyntaxInfo/Builder system * Improve Registry and Key system * Improve SyntaxRegister docs * Improve SyntaxInfo implementations * Add missing JavaDocs * Require builders for creating SyntaxInfos * Initial implementation of new Addon API * Address UnderscoreTud's review * Address Moderocky's review * Improve compatibility implementation * Remove Skript State system * Minor Tweaks * Localizer system draft * Localizer Fixes * Add support for property registration methods * Add support for unregistering syntax infos * First pass at SkriptAddon rework This will not build. This is a design pivot to have SkriptAddon instances returned by registering an addon by name rather than implementing the interface. * Implement child registries for addons * Fix language and addon compatibility * Fix building and tests * Block registration of interfaces/abstract classes They are permitted if a creation supplier is provided * Origin reworks Added a default SkriptAddon origin and pivoted internally to avoid using an "unknown" origin * First pass at priority rework * Add priority support to info builders * Revert Structure priority changes * Pivot to a relational priority system * Replace ExpressionType with priority system * Revert some unnecessary changes * Add missing EventValueExpression registration api * Rework Expression priorities to be for all SyntaxInfos * Change Skript.createInstance to also return modifiable addon * Add SyntaxPriority An implementation of Priority that enables positioning itself around specific SyntaxElement classes. * Move module loading out of registration * Add missing PriorityImpl checks * SyntaxPriority improvements and clarifications * Limitations on SyntaxPriority * Alternative module loading method * Remove unnecessary SyntaxRegister interface * Fix simple structure support * Implement Registry interface * Remove SyntaxPriority Implementation It is too unstable in its current state. It may return in a future PR. * Disconnect Event Info from Structure Info For registration purposes, SkriptEvents are no longer tied to Structures (see StructEvent) * Allow structures to be simple or section * Improve SyntaxInfo parameter checks * Add class loading utilities * Replace ClassLoader Java 11 methods * SkriptAddon: remove unnecessary annotations * ClassLoader: add default loadClasses method * Use builder method for Expression return type * Remove license headers * Prevent SkriptImpl from exposing its addons * Use the same registry across all addons Addon-specific registries may return but will need reworked * Rename SkriptAddon#registry to SkriptAddon#syntaxRegistry * Add ViewProvider interface * Improve annotation usage and placement * Remove deprecation annotations We plan to merge this as experimental/preview API * Add listening behavior to Event SyntaxInfo Forgot about this... * Add missing experimental annotations * Rename BukkitInfos to BukkitSyntaxInfos * Add unmodifiableView for Skript With this change, I have also removed the weird NonNullPair from creating a default Skript. * Add registry storage to SkriptAddon * Add source requirement to SkriptAddon * Remove source from Localizer This can be obtained from the addon instance * Fix old addon registration * SyntaxInfo equals/hashCode improvements * Implementation optimizations * Fix ConditionType support * Return the constructed info for static registration methods * Builder interface * Remove unncessary package infos * Implement Buildable interface Allows converting SyntaxInfos back into Builders * Improve legacy collection methods * Use a map for modern addon tracking * Fix SkriptEventInfo Compatibility * Allow Skript#getAddon to return Skript's addon instance Fixes current test failures * Tweak Preconditions check * ExpressionInfo Builder: require returnType at creation * Add 'clear' methods to builders * AddonModule: add init phase * Fix incorrect Expression Builder uses --------- Co-authored-by: kiip1 <25848425+kiip1@users.noreply.github.com> Co-authored-by: LimeGlass <16087552+TheLimeGlass@users.noreply.github.com> Co-authored-by: Moderocky Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com> Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> --- src/main/java/ch/njol/skript/Skript.java | 352 +++++++++------ src/main/java/ch/njol/skript/SkriptAddon.java | 119 +++-- .../conditions/base/PropertyCondition.java | 56 ++- .../base/EventValueExpression.java | 34 ++ .../expressions/base/PropertyExpression.java | 74 +++- .../java/ch/njol/skript/lang/Condition.java | 27 +- .../ch/njol/skript/lang/ExpressionType.java | 45 +- .../java/ch/njol/skript/lang/SkriptEvent.java | 29 +- .../ch/njol/skript/lang/SkriptEventInfo.java | 135 +++++- .../njol/skript/lang/SyntaxElementInfo.java | 54 ++- .../ch/njol/skript/localization/Language.java | 133 ++++-- src/main/java/ch/njol/skript/util/Utils.java | 105 ++--- .../java/org/skriptlang/skript/Skript.java | 56 +++ .../org/skriptlang/skript/SkriptImpl.java | 263 +++++++++++ .../skriptlang/skript/addon/AddonModule.java | 36 ++ .../skriptlang/skript/addon/SkriptAddon.java | 112 +++++ .../skript/addon/SkriptAddonImpl.java | 79 ++++ .../registration/BukkitRegistryKeys.java | 22 + .../registration/BukkitSyntaxInfos.java | 353 +++++++++++++++ .../registration/BukkitSyntaxInfosImpl.java | 419 ++++++++++++++++++ .../skript/lang/structure/Structure.java | 10 +- .../skript/lang/structure/StructureInfo.java | 24 +- .../skript/localization/Localizer.java | 66 +++ .../skript/localization/LocalizerImpl.java | 74 ++++ .../registration/DefaultSyntaxInfos.java | 183 ++++++++ .../registration/DefaultSyntaxInfosImpl.java | 200 +++++++++ .../skript/registration/SyntaxInfo.java | 173 ++++++++ .../skript/registration/SyntaxInfoImpl.java | 198 +++++++++ .../skript/registration/SyntaxOrigin.java | 60 +++ .../skript/registration/SyntaxRegister.java | 41 ++ .../skript/registration/SyntaxRegistry.java | 158 +++++++ .../registration/SyntaxRegistryImpl.java | 156 +++++++ .../org/skriptlang/skript/util/Builder.java | 42 ++ .../skriptlang/skript/util/ClassLoader.java | 279 ++++++++++++ .../skriptlang/skript/util/ClassUtils.java | 19 + .../org/skriptlang/skript/util/Priority.java | 55 +++ .../skriptlang/skript/util/PriorityImpl.java | 82 ++++ .../org/skriptlang/skript/util/Registry.java | 31 ++ .../skriptlang/skript/util/ViewProvider.java | 22 + 39 files changed, 4053 insertions(+), 323 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/Skript.java create mode 100644 src/main/java/org/skriptlang/skript/SkriptImpl.java create mode 100644 src/main/java/org/skriptlang/skript/addon/AddonModule.java create mode 100644 src/main/java/org/skriptlang/skript/addon/SkriptAddon.java create mode 100644 src/main/java/org/skriptlang/skript/addon/SkriptAddonImpl.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/registration/BukkitRegistryKeys.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfos.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfosImpl.java create mode 100644 src/main/java/org/skriptlang/skript/localization/Localizer.java create mode 100644 src/main/java/org/skriptlang/skript/localization/LocalizerImpl.java create mode 100644 src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java create mode 100644 src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfosImpl.java create mode 100644 src/main/java/org/skriptlang/skript/registration/SyntaxInfo.java create mode 100644 src/main/java/org/skriptlang/skript/registration/SyntaxInfoImpl.java create mode 100644 src/main/java/org/skriptlang/skript/registration/SyntaxOrigin.java create mode 100644 src/main/java/org/skriptlang/skript/registration/SyntaxRegister.java create mode 100644 src/main/java/org/skriptlang/skript/registration/SyntaxRegistry.java create mode 100644 src/main/java/org/skriptlang/skript/registration/SyntaxRegistryImpl.java create mode 100644 src/main/java/org/skriptlang/skript/util/Builder.java create mode 100644 src/main/java/org/skriptlang/skript/util/ClassLoader.java create mode 100644 src/main/java/org/skriptlang/skript/util/ClassUtils.java create mode 100644 src/main/java/org/skriptlang/skript/util/Priority.java create mode 100644 src/main/java/org/skriptlang/skript/util/PriorityImpl.java create mode 100644 src/main/java/org/skriptlang/skript/util/Registry.java create mode 100644 src/main/java/org/skriptlang/skript/util/ViewProvider.java diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 8bcbffa039b..33adc41a12b 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -62,7 +62,6 @@ import ch.njol.skript.variables.Variables; import ch.njol.util.Closeable; import ch.njol.util.Kleenean; -import ch.njol.util.NullableChecker; import ch.njol.util.StringUtils; import ch.njol.util.coll.iterator.CheckedIterator; import ch.njol.util.coll.iterator.EnumerationIterable; @@ -87,11 +86,15 @@ import org.bukkit.plugin.Plugin; import org.bukkit.plugin.PluginDescriptionFile; import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; import org.junit.After; import org.junit.runner.JUnitCore; import org.junit.runner.Result; +import org.skriptlang.skript.bukkit.registration.BukkitRegistryKeys; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; import org.junit.runner.notification.Failure; import org.skriptlang.skript.bukkit.SkriptMetrics; import org.skriptlang.skript.bukkit.breeding.BreedingModule; @@ -109,6 +112,9 @@ import org.skriptlang.skript.lang.script.Script; import org.skriptlang.skript.lang.structure.Structure; import org.skriptlang.skript.lang.structure.StructureInfo; +import org.skriptlang.skript.registration.SyntaxOrigin; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.registration.SyntaxInfo; import java.io.File; import java.io.IOException; @@ -143,6 +149,8 @@ import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; @@ -181,14 +189,24 @@ public final class Skript extends JavaPlugin implements Listener { @Nullable private static Skript instance = null; + static org.skriptlang.skript.@UnknownNullability Skript skript = null; + private static org.skriptlang.skript.@UnknownNullability Skript unmodifiableSkript = null; + private static boolean disabled = false; private static boolean partDisabled = false; public static Skript getInstance() { - final Skript i = instance; - if (i == null) + if (instance == null) throw new IllegalStateException(); - return i; + return instance; + } + + @ApiStatus.Experimental + public static org.skriptlang.skript.Skript instance() { + if (unmodifiableSkript == null) { + throw new SkriptAPIException("Skript is still initializing"); + } + return unmodifiableSkript; } /** @@ -393,8 +411,6 @@ public void onEnable() { } catch (Exception e) { Skript.exception(e, "Update checker could not be initialized."); } - experimentRegistry = new ExperimentRegistry(this); - Feature.registerAll(getAddonInstance(), experimentRegistry); if (!getDataFolder().isDirectory()) getDataFolder().mkdirs(); @@ -469,9 +485,17 @@ public void onEnable() { } } - // initialize the Skript addon instance + // initialize the modern Skript instance + skript = org.skriptlang.skript.Skript.of(getClass(), getName()); + unmodifiableSkript = skript.unmodifiableView(); + skript.localizer().setSourceDirectories("lang", + getDataFolder().getAbsolutePath() + "lang"); + // initialize the old Skript SkriptAddon instance getAddonInstance(); + experimentRegistry = new ExperimentRegistry(this); + Feature.registerAll(getAddonInstance(), experimentRegistry); + // Load classes which are always safe to use new JavaClasses(); // These may be needed in configuration @@ -619,7 +643,6 @@ public void run() { stopAcceptingRegistrations(); - Documentation.generate(); // TODO move to test classes? // Variable loading @@ -1226,7 +1249,6 @@ private boolean isServerRunning() { private void beforeDisable() { partDisabled = true; EvtSkript.onSkriptStop(); // TODO [code style] warn user about delays in Skript stop events - ScriptLoader.unloadScripts(ScriptLoader.getLoadedScripts()); } @@ -1338,194 +1360,231 @@ public static void checkAcceptRegistrations() { private static void stopAcceptingRegistrations() { Converters.createChainedConverters(); - acceptRegistrations = false; - Classes.onRegistrationsStop(); } // ================ ADDONS ================ - private final static HashMap addons = new HashMap<>(); + @Deprecated + private static final Set addons = new HashSet<>(); /** * Registers an addon to Skript. This is currently not required for addons to work, but the returned {@link SkriptAddon} provides useful methods for registering syntax elements * and adding new strings to Skript's localization system (e.g. the required "types.[type]" strings for registered classes). - * - * @param p The plugin + * + * @param plugin The plugin */ - public static SkriptAddon registerAddon(final JavaPlugin p) { + public static SkriptAddon registerAddon(JavaPlugin plugin) { checkAcceptRegistrations(); - if (addons.containsKey(p.getName())) - throw new IllegalArgumentException("The plugin " + p.getName() + " is already registered"); - final SkriptAddon addon = new SkriptAddon(p); - addons.put(p.getName(), addon); + SkriptAddon addon = new SkriptAddon(plugin); + addons.add(addon); return addon; } - @Nullable - public static SkriptAddon getAddon(final JavaPlugin p) { - return addons.get(p.getName()); + public static @Nullable SkriptAddon getAddon(JavaPlugin plugin) { + if (plugin == Skript.getInstance()) { + return Skript.getAddonInstance(); + } + for (SkriptAddon addon : getAddons()) { + if (addon.plugin == plugin) { + return addon; + } + } + return null; } - @Nullable - public static SkriptAddon getAddon(final String name) { - return addons.get(name); + public static @Nullable SkriptAddon getAddon(String name) { + if (name.equals(Skript.getInstance().getName())) { + return Skript.getAddonInstance(); + } + for (SkriptAddon addon : getAddons()) { + if (addon.getName().equals(name)) { + return addon; + } + } + return null; } - @SuppressWarnings("null") - public static Collection getAddons() { - return Collections.unmodifiableCollection(addons.values()); + public static @Unmodifiable Collection getAddons() { + Set addons = new HashSet<>(Skript.addons); + addons.addAll(instance().addons().stream() + .filter(addon -> addons.stream().noneMatch(oldAddon -> oldAddon.name().equals(addon.name()))) + .map(SkriptAddon::fromModern) + .collect(Collectors.toSet()) + ); + return Collections.unmodifiableCollection(addons); } - @Nullable - private static SkriptAddon addon; + @Deprecated + private static @Nullable SkriptAddon addon; /** * @return A {@link SkriptAddon} representing Skript. */ public static SkriptAddon getAddonInstance() { if (addon == null) { - addon = new SkriptAddon(Skript.getInstance()); - addon.setLanguageFileDirectory("lang"); + addon = SkriptAddon.fromModern(instance()); } return addon; } // ================ CONDITIONS & EFFECTS & SECTIONS ================ - private static final List> conditions = new ArrayList<>(50); - private static final List> effects = new ArrayList<>(50); - private static final List> statements = new ArrayList<>(100); - private static final List> sections = new ArrayList<>(50); + private static final class BukkitOrigin implements SyntaxOrigin { - public static Collection> getStatements() { - return statements; - } + private final String name; - public static Collection> getEffects() { - return effects; - } + private BukkitOrigin(Plugin plugin) { + this.name = plugin.getName(); + } - public static Collection> getSections() { - return sections; - } + @Override + public String name() { + return name; + } - // ================ CONDITIONS ================ - public static Collection> getConditions() { - return conditions; } - private final static int[] conditionTypesStartIndices = new int[ConditionType.values().length]; + private static SyntaxOrigin getSyntaxOrigin(JavaPlugin plugin) { + SkriptAddon addon = getAddon(plugin); + if (addon != null) { + return SyntaxOrigin.of(addon); + } + return new BukkitOrigin(plugin); + } /** - * registers a {@link Condition}. - * - * @param condition The condition's class + * Registers a {@link Condition}. + * + * @param conditionClass The condition's class * @param patterns Skript patterns to match this condition */ - public static void registerCondition(Class condition, String... patterns) throws IllegalArgumentException { - registerCondition(condition, ConditionType.COMBINED, patterns); + public static void registerCondition(Class conditionClass, String... patterns) throws IllegalArgumentException { + registerCondition(conditionClass, ConditionType.COMBINED, patterns); } /** - * registers a {@link Condition}. - * - * @param condition The condition's class - * @param type The conditions {@link ConditionType type}. This is used to determine in which order to try to parse conditions. + * Registers a {@link Condition}. + * + * @param conditionClass The condition's class + * @param type The type of condition which affects its priority in the parsing search * @param patterns Skript patterns to match this condition */ - public static void registerCondition(Class condition, ConditionType type, String... patterns) throws IllegalArgumentException { + public static void registerCondition(Class conditionClass, ConditionType type, String... patterns) throws IllegalArgumentException { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - final SyntaxElementInfo info = new SyntaxElementInfo<>(patterns, condition, originClassPath); - conditions.add(conditionTypesStartIndices[type.ordinal()], info); - statements.add(conditionTypesStartIndices[type.ordinal()], info); - for (int i = type.ordinal(); i < ConditionType.values().length; i++) - conditionTypesStartIndices[i]++; + skript.syntaxRegistry().register(SyntaxRegistry.CONDITION, SyntaxInfo.builder(conditionClass) + .priority(type.priority()) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(conditionClass))) + .addPatterns(patterns) + .build() + ); } /** * Registers an {@link Effect}. - * - * @param effect The effect's class + * + * @param effectClass The effect's class * @param patterns Skript patterns to match this effect */ - public static void registerEffect(final Class effect, final String... patterns) throws IllegalArgumentException { + public static void registerEffect(Class effectClass, String... patterns) throws IllegalArgumentException { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - final SyntaxElementInfo info = new SyntaxElementInfo<>(patterns, effect, originClassPath); - effects.add(info); - statements.add(info); + skript.syntaxRegistry().register(SyntaxRegistry.EFFECT, SyntaxInfo.builder(effectClass) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(effectClass))) + .addPatterns(patterns) + .build() + ); } /** * Registers a {@link Section}. * - * @param section The section's class + * @param sectionClass The section's class * @param patterns Skript patterns to match this section * @see Section */ - public static void registerSection(Class section, String... patterns) throws IllegalArgumentException { + public static void registerSection(Class sectionClass, String... patterns) throws IllegalArgumentException { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - SyntaxElementInfo info = new SyntaxElementInfo<>(patterns, section, originClassPath); - sections.add(info); + skript.syntaxRegistry().register(SyntaxRegistry.SECTION, SyntaxInfo.builder(sectionClass) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(sectionClass))) + .addPatterns(patterns) + .build() + ); } - // ================ EXPRESSIONS ================ + public static @Unmodifiable Collection> getStatements() { + return instance().syntaxRegistry() + .syntaxes(SyntaxRegistry.STATEMENT).stream() + .map(SyntaxElementInfo::, Statement>fromModern) + .collect(Collectors.toUnmodifiableList()); + } + + public static @Unmodifiable Collection> getConditions() { + return instance().syntaxRegistry() + .syntaxes(SyntaxRegistry.CONDITION).stream() + .map(SyntaxElementInfo::, Condition>fromModern) + .collect(Collectors.toUnmodifiableList()); + } + + public static @Unmodifiable Collection> getEffects() { + return instance().syntaxRegistry() + .syntaxes(SyntaxRegistry.EFFECT).stream() + .map(SyntaxElementInfo::, Effect>fromModern) + .collect(Collectors.toUnmodifiableList()); + } - private final static List> expressions = new ArrayList<>(100); + public static @Unmodifiable Collection> getSections() { + return instance().syntaxRegistry() + .syntaxes(SyntaxRegistry.SECTION).stream() + .map(SyntaxElementInfo::, Section>fromModern) + .collect(Collectors.toUnmodifiableList()); + } - private final static int[] expressionTypesStartIndices = new int[ExpressionType.values().length]; + // ================ EXPRESSIONS ================ /** * Registers an expression. - * - * @param c The expression's class + * + * @param expressionType The expression's class * @param returnType The superclass of all values returned by the expression * @param type The expression's {@link ExpressionType type}. This is used to determine in which order to try to parse expressions. * @param patterns Skript patterns that match this expression * @throws IllegalArgumentException if returnType is not a normal class */ - public static , T> void registerExpression(final Class c, final Class returnType, final ExpressionType type, final String... patterns) throws IllegalArgumentException { + public static , T> void registerExpression( + Class expressionType, Class returnType, ExpressionType type, String... patterns + ) throws IllegalArgumentException { checkAcceptRegistrations(); - if (returnType.isAnnotation() || returnType.isArray() || returnType.isPrimitive()) - throw new IllegalArgumentException("returnType must be a normal type"); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - final ExpressionInfo info = new ExpressionInfo<>(patterns, returnType, c, originClassPath, type); - expressions.add(expressionTypesStartIndices[type.ordinal()], info); - for (int i = type.ordinal(); i < ExpressionType.values().length; i++) { - expressionTypesStartIndices[i]++; - } + skript.syntaxRegistry().register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(expressionType, returnType) + .priority(type.priority()) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(expressionType))) + .addPatterns(patterns) + .build() + ); } - @SuppressWarnings("null") public static Iterator> getExpressions() { - return expressions.iterator(); - } - - public static Iterator> getExpressions(final Class... returnTypes) { - return new CheckedIterator<>(getExpressions(), new NullableChecker>() { - @Override - public boolean check(final @Nullable ExpressionInfo i) { - if (i == null || i.returnType == Object.class) + List> list = new ArrayList<>(); + for (SyntaxInfo.Expression info : instance().syntaxRegistry().syntaxes(SyntaxRegistry.EXPRESSION)) + list.add((ExpressionInfo) SyntaxElementInfo.fromModern(info)); + return list.iterator(); + } + + public static Iterator> getExpressions(Class... returnTypes) { + return new CheckedIterator<>(getExpressions(), info -> { + if (info == null || info.returnType == Object.class) + return true; + for (Class returnType : returnTypes) { + assert returnType != null; + if (Converters.converterExists(info.returnType, returnType)) return true; - for (final Class returnType : returnTypes) { - assert returnType != null; - if (Converters.converterExists(i.returnType, returnType)) - return true; - } - return false; } + return false; }); } // ================ EVENTS ================ - private static final List> events = new ArrayList<>(50); - private static final List> structures = new ArrayList<>(10); - /** * Registers an event. * @@ -1545,51 +1604,66 @@ public static SkriptEventInfo registerEvent(String na * Registers an event. * * @param name The name of the event, used for error messages - * @param c The event's class + * @param eventClass The event's class * @param events The Bukkit events this event applies to * @param patterns Skript patterns to match this event * @return A SkriptEventInfo representing the registered event. Used to generate Skript's documentation. */ - public static SkriptEventInfo registerEvent(String name, Class c, Class[] events, String... patterns) { + @SuppressWarnings("ConstantConditions") // caused by bad array annotations + public static SkriptEventInfo registerEvent( + String name, Class eventClass, Class[] events, String... patterns + ) { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - - String[] transformedPatterns = new String[patterns.length]; for (int i = 0; i < patterns.length; i++) - transformedPatterns[i] = SkriptEvent.fixPattern(patterns[i]); - - SkriptEventInfo r = new SkriptEventInfo<>(name, transformedPatterns, c, originClassPath, events); - Skript.events.add(r); - return r; + patterns[i] = BukkitSyntaxInfos.fixPattern(patterns[i]); + var legacy = new SkriptEventInfo.ModernSkriptEventInfo<>(name, patterns, eventClass, "", events); + skript.syntaxRegistry().register(BukkitRegistryKeys.EVENT, legacy); + return legacy; } - public static void registerStructure(Class c, String... patterns) { + public static void registerStructure(Class structureClass, String... patterns) { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - StructureInfo structureInfo = new StructureInfo<>(patterns, c, originClassPath); - structures.add(structureInfo); + skript.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, SyntaxInfo.Structure.builder(structureClass) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(structureClass))) + .addPatterns(patterns) + .build() + ); } - public static void registerSimpleStructure(Class c, String... patterns) { + public static void registerSimpleStructure(Class structureClass, String... patterns) { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - StructureInfo structureInfo = new StructureInfo<>(patterns, c, originClassPath, true); - structures.add(structureInfo); + skript.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, SyntaxInfo.Structure.builder(structureClass) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(structureClass))) + .addPatterns(patterns) + .nodeType(SyntaxInfo.Structure.NodeType.SIMPLE) + .build() + ); } - public static void registerStructure(Class c, EntryValidator entryValidator, String... patterns) { + public static void registerStructure( + Class structureClass, EntryValidator entryValidator, String... patterns + ) { checkAcceptRegistrations(); - String originClassPath = Thread.currentThread().getStackTrace()[2].getClassName(); - StructureInfo structureInfo = new StructureInfo<>(patterns, c, originClassPath, entryValidator); - structures.add(structureInfo); - } - - public static Collection> getEvents() { - return events; - } - - public static List> getStructures() { - return structures; + skript.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, SyntaxInfo.Structure.builder(structureClass) + .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(structureClass))) + .addPatterns(patterns) + .entryValidator(entryValidator) + .build() + ); + } + + public static @Unmodifiable Collection> getEvents() { + return instance().syntaxRegistry() + .syntaxes(BukkitRegistryKeys.EVENT).stream() + .map(SyntaxElementInfo::, SkriptEvent>fromModern) + .collect(Collectors.toUnmodifiableList()); + } + + public static @Unmodifiable List> getStructures() { + return instance().syntaxRegistry() + .syntaxes(SyntaxRegistry.STRUCTURE).stream() + .map(SyntaxElementInfo::, Structure>fromModern) + .collect(Collectors.toUnmodifiableList()); } // ================ COMMANDS ================ diff --git a/src/main/java/ch/njol/skript/SkriptAddon.java b/src/main/java/ch/njol/skript/SkriptAddon.java index 5d9d8aafbd8..f09bdcf4e5d 100644 --- a/src/main/java/ch/njol/skript/SkriptAddon.java +++ b/src/main/java/ch/njol/skript/SkriptAddon.java @@ -2,49 +2,60 @@ import java.io.File; import java.io.IOException; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.bukkit.Bukkit; +import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import org.jetbrains.annotations.Nullable; -import ch.njol.skript.localization.Language; import ch.njol.skript.util.Utils; import ch.njol.skript.util.Version; +import org.jetbrains.annotations.ApiStatus; +import org.skriptlang.skript.localization.Localizer; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Registry; /** * Utility class for Skript addons. Use {@link Skript#registerAddon(JavaPlugin)} to create a SkriptAddon instance for your plugin. */ -public final class SkriptAddon { +public final class SkriptAddon implements org.skriptlang.skript.addon.SkriptAddon { public final JavaPlugin plugin; public final Version version; private final String name; + private final org.skriptlang.skript.addon.SkriptAddon addon; + /** * Package-private constructor. Use {@link Skript#registerAddon(JavaPlugin)} to get a SkriptAddon for your plugin. - * - * @param p */ - SkriptAddon(final JavaPlugin p) { - plugin = p; - name = "" + p.getName(); - Version v; + SkriptAddon(JavaPlugin plugin) { + this(plugin, Skript.skript.registerAddon(plugin.getClass(), plugin.getName())); + } + + SkriptAddon(JavaPlugin plugin, org.skriptlang.skript.addon.SkriptAddon addon) { + this.addon = addon; + this.plugin = plugin; + this.name = plugin.getName(); + Version version; try { - v = new Version("" + p.getDescription().getVersion()); - } catch (final IllegalArgumentException e) { - final Matcher m = Pattern.compile("(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?").matcher(p.getDescription().getVersion()); + version = new Version(plugin.getDescription().getVersion()); + } catch (IllegalArgumentException e) { + final Matcher m = Pattern.compile("(\\d+)(?:\\.(\\d+)(?:\\.(\\d+))?)?").matcher(plugin.getDescription().getVersion()); if (!m.find()) - throw new IllegalArgumentException("The version of the plugin " + p.getName() + " does not contain any numbers: " + p.getDescription().getVersion()); - v = new Version(Utils.parseInt("" + m.group(1)), m.group(2) == null ? 0 : Utils.parseInt("" + m.group(2)), m.group(3) == null ? 0 : Utils.parseInt("" + m.group(3))); - Skript.warning("The plugin " + p.getName() + " uses a non-standard version syntax: '" + p.getDescription().getVersion() + "'. Skript will use " + v + " instead."); + throw new IllegalArgumentException("The version of the plugin " + name + " does not contain any numbers: " + plugin.getDescription().getVersion()); + version = new Version(Utils.parseInt(m.group(1)), m.group(2) == null ? 0 : Utils.parseInt(m.group(2)), m.group(3) == null ? 0 : Utils.parseInt(m.group(3))); + Skript.warning("The plugin " + name + " uses a non-standard version syntax: '" + plugin.getDescription().getVersion() + "'. Skript will use " + version + " instead."); } - version = v; + this.version = version; } @Override public final String toString() { - return name; + return getName(); } public String getName() { @@ -65,9 +76,6 @@ public SkriptAddon loadClasses(String basePackage, String... subPackages) throws return this; } - @Nullable - private String languageFileDirectory = null; - /** * Makes Skript load language files from the specified directory, e.g. "lang" or "skript lang" if you have a lang folder yourself. Localised files will be read from the * plugin's jar and the plugin's data folder, but the default English file is only taken from the jar and must exist! @@ -76,19 +84,13 @@ public SkriptAddon loadClasses(String basePackage, String... subPackages) throws * @return This SkriptAddon */ public SkriptAddon setLanguageFileDirectory(String directory) { - if (languageFileDirectory != null) - throw new IllegalStateException(); - directory = "" + directory.replace('\\', '/'); - if (directory.endsWith("/")) - directory = "" + directory.substring(0, directory.length() - 1); - languageFileDirectory = directory; - Language.loadDefault(this); + localizer().setSourceDirectories(directory, plugin.getDataFolder().getAbsolutePath() + directory); return this; } @Nullable public String getLanguageFileDirectory() { - return languageFileDirectory; + return localizer().languageFileDirectory(); } @Nullable @@ -108,4 +110,67 @@ public File getFile() { return file; } + // + // Modern SkriptAddon Compatibility + // + + @ApiStatus.Experimental + static SkriptAddon fromModern(org.skriptlang.skript.addon.SkriptAddon addon) { + return new SkriptAddon(JavaPlugin.getProvidingPlugin(addon.source()), addon); + } + + @Override + @ApiStatus.Experimental + public Class source() { + return addon.source(); + } + + @Override + @ApiStatus.Experimental + public String name() { + return addon.name(); + } + + @Override + @ApiStatus.Experimental + public > void storeRegistry(Class registryClass, R registry) { + addon.storeRegistry(registryClass, registry); + } + + @Override + @ApiStatus.Experimental + public void removeRegistry(Class> registryClass) { + addon.removeRegistry(registryClass); + } + + @Override + @ApiStatus.Experimental + public boolean hasRegistry(Class> registryClass) { + return addon.hasRegistry(registryClass); + } + + @Override + @ApiStatus.Experimental + public > R registry(Class registryClass) { + return addon.registry(registryClass); + } + + @Override + @ApiStatus.Experimental + public > R registry(Class registryClass, Supplier putIfAbsent) { + return addon.registry(registryClass, putIfAbsent); + } + + @Override + @ApiStatus.Experimental + public SyntaxRegistry syntaxRegistry() { + return addon.syntaxRegistry(); + } + + @Override + @ApiStatus.Experimental + public Localizer localizer() { + return addon.localizer(); + } + } diff --git a/src/main/java/ch/njol/skript/conditions/base/PropertyCondition.java b/src/main/java/ch/njol/skript/conditions/base/PropertyCondition.java index b97f0d5fdf5..d539273f252 100644 --- a/src/main/java/ch/njol/skript/conditions/base/PropertyCondition.java +++ b/src/main/java/ch/njol/skript/conditions/base/PropertyCondition.java @@ -10,6 +10,10 @@ import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.util.Checker; import ch.njol.util.Kleenean; +import org.jetbrains.annotations.ApiStatus; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Priority; /** * This class can be used for an easier writing of conditions that contain only one type in the pattern, @@ -36,6 +40,14 @@ */ public abstract class PropertyCondition extends Condition implements Checker { + /** + * A priority for {@link PropertyCondition}s. + * They will be registered before {@link SyntaxInfo#PATTERN_MATCHES_EVERYTHING} expressions + * but after {@link SyntaxInfo#COMBINED} expressions. + */ + @ApiStatus.Experimental + public static final Priority DEFAULT_PRIORITY = Priority.before(SyntaxInfo.PATTERN_MATCHES_EVERYTHING); + /** * See {@link PropertyCondition} for more info */ @@ -65,7 +77,47 @@ public enum PropertyType { WILL } - private Expression expr; + /** + * @param registry The SyntaxRegistry to register with. + * @param condition The class to register + * @param property The property name, for example fly in players can fly + * @param type Must be plural, for example players in players can fly + * @param The Condition type. + * @return The registered {@link SyntaxInfo}. + */ + @ApiStatus.Experimental + public static SyntaxInfo register(SyntaxRegistry registry, Class condition, String property, String type) { + return register(registry, condition, PropertyType.BE, property, type); + } + + /** + * @param registry The SyntaxRegistry to register with. + * @param condition The class to register + * @param propertyType The property type, see {@link PropertyType} + * @param property The property name, for example fly in players can fly + * @param type Must be plural, for example players in players can fly + * @param The Condition type. + * @return The registered {@link SyntaxInfo}. + */ + @ApiStatus.Experimental + public static SyntaxInfo register(SyntaxRegistry registry, Class condition, PropertyType propertyType, String property, String type) { + if (type.contains("%")) + throw new SkriptAPIException("The type argument must not contain any '%'s"); + SyntaxInfo.Builder builder = SyntaxInfo.builder(condition).priority(DEFAULT_PRIORITY); + switch (propertyType) { + case BE -> builder.addPatterns("%" + type + "% (is|are) " + property, + "%" + type + "% (isn't|is not|aren't|are not) " + property); + case CAN -> builder.addPatterns("%" + type + "% can " + property, + "%" + type + "% (can't|cannot|can not) " + property); + case HAVE -> builder.addPatterns("%" + type + "% (has|have) " + property, + "%" + type + "% (doesn't|does not|do not|don't) have " + property); + case WILL -> builder.addPatterns("%" + type + "% will " + property, + "%" + type + "% (will (not|neither)|won't) " + property); + } + SyntaxInfo info = builder.build(); + registry.register(SyntaxRegistry.CONDITION, info); + return info; + } /** * Registers a new property condition. The property type is set to {@link PropertyType#BE}. @@ -123,6 +175,8 @@ public static String[] getPatterns(PropertyType propertyType, String property, S }; } + private Expression expr; + @Override @SuppressWarnings("unchecked") public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { diff --git a/src/main/java/ch/njol/skript/expressions/base/EventValueExpression.java b/src/main/java/ch/njol/skript/expressions/base/EventValueExpression.java index cb74c70ebe9..a421691ddf5 100644 --- a/src/main/java/ch/njol/skript/expressions/base/EventValueExpression.java +++ b/src/main/java/ch/njol/skript/expressions/base/EventValueExpression.java @@ -18,7 +18,11 @@ import ch.njol.skript.registrations.EventValues; import ch.njol.skript.util.Utils; import ch.njol.util.Kleenean; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Priority; import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.converter.Converter; @@ -47,6 +51,36 @@ */ public class EventValueExpression extends SimpleExpression implements DefaultExpression { + /** + * A priority for {@link EventValueExpression}s. + * They will be registered before {@link SyntaxInfo#COMBINED} expressions + * but after {@link SyntaxInfo#SIMPLE} expressions. + */ + @ApiStatus.Experimental + public static final Priority DEFAULT_PRIORITY = Priority.before(SyntaxInfo.COMBINED); + + /** + * Registers an event value expression with the provided pattern. + * The syntax info will be forced to use the {@link #DEFAULT_PRIORITY} priority. + * This also adds '[the]' to the start of the pattern. + * + * @param registry The SyntaxRegistry to register with. + * @param expressionClass The EventValueExpression class being registered. + * @param returnType The class representing the expression's return type. + * @param pattern The pattern to match for creating this expression. + * @param The return type. + * @param The Expression type. + * @return The registered {@link SyntaxInfo}. + */ + @ApiStatus.Experimental + public static , T> SyntaxInfo.Expression register(SyntaxRegistry registry, Class expressionClass, Class returnType, String pattern) { + SyntaxInfo.Expression info = SyntaxInfo.Expression.builder(expressionClass, returnType) + .priority(DEFAULT_PRIORITY) + .addPattern("[the] " + pattern) + .build(); + registry.register(SyntaxRegistry.EXPRESSION, info); + return info; + } /** * Registers an expression as {@link ExpressionType#EVENT} with the provided pattern. diff --git a/src/main/java/ch/njol/skript/expressions/base/PropertyExpression.java b/src/main/java/ch/njol/skript/expressions/base/PropertyExpression.java index 71f5efa9fca..29a569a4c65 100644 --- a/src/main/java/ch/njol/skript/expressions/base/PropertyExpression.java +++ b/src/main/java/ch/njol/skript/expressions/base/PropertyExpression.java @@ -8,13 +8,17 @@ import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; import com.google.common.base.Preconditions; +import java.util.Arrays; import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.UnknownNullability; import org.skriptlang.skript.lang.converter.Converter; import org.skriptlang.skript.lang.converter.Converters; - -import java.util.Arrays; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Priority; /** * Represents an expression which represents a property of another one. Remember to set the expression with {@link #setExpr(Expression)} in @@ -27,18 +31,15 @@ public abstract class PropertyExpression extends SimpleExpression { /** * A helper method to get the property patterns given a property, type, and default expression parameter. - * - * @param property the property - * @param fromType the type(s) that the property should apply to - * @param defaultExpr whether the type(s) should be optional - * + * @param property the property + * @param fromType the type(s) that the property should apply to + * @param defaultExpr whether the type(s) should be optional * @return an array of strings representing the patterns of the given property and type(s) * @throws IllegalArgumentException if property or fromType is null */ private static String[] patternsOf(String property, String fromType, boolean defaultExpr) { - if (property == null || fromType == null) - throw new IllegalArgumentException("'property' or 'fromType' was null."); - + Preconditions.checkNotNull(property, "property must be present"); + Preconditions.checkNotNull(fromType, "fromType must be present"); String types = defaultExpr ? "[of %" + fromType + "%]" : "of %" + fromType + "%"; return new String[]{"[the] " + property + " " + types, "%" + fromType + "%'[s] " + property}; } @@ -71,6 +72,36 @@ public static String[] getDefaultPatterns(String property, String fromType) { return patternsOf(property, fromType, true); } + /** + * A priority for {@link PropertyExpression}s. + * They will be registered before {@link SyntaxInfo#PATTERN_MATCHES_EVERYTHING} expressions + * but after {@link SyntaxInfo#COMBINED} expressions. + */ + @ApiStatus.Experimental + public static final Priority DEFAULT_PRIORITY = Priority.before(SyntaxInfo.PATTERN_MATCHES_EVERYTHING); + + /** + * Registers an expression with the two default property patterns "property of %types%" and "%types%'[s] property" + * + * @param registry The SyntaxRegistry to register with. + * @param expressionClass The PropertyExpression class being registered. + * @param returnType The class representing the expression's return type. + * @param property The name of the property. + * @param fromType Should be plural to support multiple objects but doesn't have to be. + * @param The return type. + * @param The Expression type. + * @return The registered {@link SyntaxInfo}. + */ + @ApiStatus.Experimental + public static , T> SyntaxInfo.Expression register(SyntaxRegistry registry, Class expressionClass, Class returnType, String property, String fromType) { + SyntaxInfo.Expression info = SyntaxInfo.Expression.builder(expressionClass, returnType) + .priority(DEFAULT_PRIORITY) + .addPatterns(getPatterns(property, fromType)) + .build(); + registry.register(SyntaxRegistry.EXPRESSION, info); + return info; + } + /** * Registers an expression as {@link ExpressionType#PROPERTY} with the two default property patterns "property of %types%" and "%types%'[s] property" * @@ -83,6 +114,29 @@ public static void register(Class> expressionClass, Skript.registerExpression(expressionClass, type, ExpressionType.PROPERTY, getPatterns(property, fromType)); } + /** + * Registers an expression with the two default property patterns "property [of %types%]" and "%types%'[s] property" + * This method also makes the expression type optional to force a default expression on the property expression. + * + * @param registry The SyntaxRegistry to register with. + * @param expressionClass The PropertyExpression class being registered. + * @param returnType The class representing the expression's return type. + * @param property The name of the property. + * @param fromType Should be plural to support multiple objects but doesn't have to be. + * @param The return type. + * @param The Expression type. + * @return The registered {@link SyntaxInfo}. + */ + @ApiStatus.Experimental + public static , T> SyntaxInfo.Expression registerDefault(SyntaxRegistry registry, Class expressionClass, Class returnType, String property, String fromType) { + SyntaxInfo.Expression info = SyntaxInfo.Expression.builder(expressionClass, returnType) + .priority(DEFAULT_PRIORITY) + .addPatterns(getDefaultPatterns(property, fromType)) + .build(); + registry.register(SyntaxRegistry.EXPRESSION, info); + return info; + } + /** * Registers an expression as {@link ExpressionType#PROPERTY} with the two default property patterns "property [of %types%]" and "%types%'[s] property" * This method also makes the expression type optional to force a default expression on the property expression. diff --git a/src/main/java/ch/njol/skript/lang/Condition.java b/src/main/java/ch/njol/skript/lang/Condition.java index ddb533a6379..31a1810d2b0 100644 --- a/src/main/java/ch/njol/skript/lang/Condition.java +++ b/src/main/java/ch/njol/skript/lang/Condition.java @@ -1,12 +1,16 @@ package ch.njol.skript.lang; import ch.njol.skript.Skript; +import ch.njol.skript.conditions.base.PropertyCondition; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Checker; import ch.njol.util.Kleenean; import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.condition.Conditional; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.util.Priority; import java.util.Iterator; @@ -23,17 +27,34 @@ public enum ConditionType { * * @see #PROPERTY */ - COMBINED, + COMBINED(SyntaxInfo.COMBINED), /** * Property conditions, e.g. "%properties% is/are data value[s]" */ - PROPERTY, + PROPERTY(PropertyCondition.DEFAULT_PRIORITY), /** * Conditions whose pattern matches (almost) everything or should be last checked. */ - PATTERN_MATCHES_EVERYTHING; + PATTERN_MATCHES_EVERYTHING(SyntaxInfo.PATTERN_MATCHES_EVERYTHING); + + @ApiStatus.Experimental + private final Priority priority; + + @ApiStatus.Experimental + ConditionType(Priority priority) { + this.priority = priority; + } + + /** + * @return The Priority equivalent of this ConditionType. + */ + @ApiStatus.Experimental + public Priority priority() { + return this.priority; + } + } private boolean negated; diff --git a/src/main/java/ch/njol/skript/lang/ExpressionType.java b/src/main/java/ch/njol/skript/lang/ExpressionType.java index 013f6ea435a..b47c538e884 100644 --- a/src/main/java/ch/njol/skript/lang/ExpressionType.java +++ b/src/main/java/ch/njol/skript/lang/ExpressionType.java @@ -2,6 +2,10 @@ import ch.njol.skript.expressions.base.EventValueExpression; import ch.njol.skript.expressions.base.PropertyExpression; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.util.Priority; /** * Used to define in which order to parse expressions. @@ -11,32 +15,63 @@ public enum ExpressionType { /** * Expressions that only match simple text, e.g. "[the] player" */ - SIMPLE, + SIMPLE(SyntaxInfo.SIMPLE), /** * Expressions that are related to the Event that are typically simple. * * @see EventValueExpression */ - EVENT, + EVENT(EventValueExpression.DEFAULT_PRIORITY), /** * Expressions that contain other expressions, e.g. "[the] distance between %location% and %location%" * * @see #PROPERTY */ - COMBINED, + COMBINED(SyntaxInfo.COMBINED), /** * Property expressions, e.g. "[the] data value[s] of %items%"/"%items%'[s] data value[s]" * * @see PropertyExpression */ - PROPERTY, + PROPERTY(PropertyExpression.DEFAULT_PRIORITY), /** * Expressions whose pattern matches (almost) everything. Typically when using regex. Example: "[the] [loop-]<.+>" */ - PATTERN_MATCHES_EVERYTHING; + PATTERN_MATCHES_EVERYTHING(SyntaxInfo.PATTERN_MATCHES_EVERYTHING); + + @ApiStatus.Experimental + private final Priority priority; + + @ApiStatus.Experimental + ExpressionType(Priority priority) { + this.priority = priority; + } + + /** + * @return The Priority equivalent of this ExpressionType. + */ + @ApiStatus.Experimental + public Priority priority() { + return priority; + } + + @ApiStatus.Experimental + public static @Nullable ExpressionType fromModern(Priority priority) { + if (priority == SyntaxInfo.SIMPLE) + return ExpressionType.SIMPLE; + if (priority == EventValueExpression.DEFAULT_PRIORITY) + return ExpressionType.EVENT; + if (priority == SyntaxInfo.COMBINED) + return ExpressionType.COMBINED; + if (priority == PropertyExpression.DEFAULT_PRIORITY) + return ExpressionType.PROPERTY; + if (priority == SyntaxInfo.PATTERN_MATCHES_EVERYTHING) + return ExpressionType.PATTERN_MATCHES_EVERYTHING; + return null; + } } diff --git a/src/main/java/ch/njol/skript/lang/SkriptEvent.java b/src/main/java/ch/njol/skript/lang/SkriptEvent.java index 4080fd923ff..c7eb245fb22 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptEvent.java +++ b/src/main/java/ch/njol/skript/lang/SkriptEvent.java @@ -8,10 +8,11 @@ import ch.njol.skript.events.EvtClick; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.structures.StructEvent.EventData; -import ch.njol.skript.util.Utils; -import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.bukkit.event.EventPriority; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; +import ch.njol.skript.util.Utils; +import org.bukkit.event.Cancellable; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.entry.EntryContainer; import org.skriptlang.skript.lang.script.Script; @@ -238,29 +239,7 @@ public boolean canExecuteAsynchronously() { * to be nullable. */ public static String fixPattern(String pattern) { - char[] chars = pattern.toCharArray(); - StringBuilder stringBuilder = new StringBuilder(); - - boolean inType = false; - for (int i = 0; i < chars.length; i++) { - char character = chars[i]; - stringBuilder.append(character); - - if (character == '%') { - // toggle inType - inType = !inType; - - // add the dash character if it's not already present - // a type specification can have two prefix characters for modification - if (inType && i + 2 < chars.length && chars[i + 1] != '-' && chars[i + 2] != '-') - stringBuilder.append('-'); - } else if (character == '\\' && i + 1 < chars.length) { - // Make sure we don't toggle inType for escape percentage signs - stringBuilder.append(chars[i + 1]); - i++; - } - } - return stringBuilder.toString(); + return BukkitSyntaxInfos.fixPattern(pattern); } @Nullable diff --git a/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java b/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java index ea593cb94ce..c083adaf7ad 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java +++ b/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java @@ -1,16 +1,28 @@ package ch.njol.skript.lang; +import ch.njol.skript.Skript; import ch.njol.skript.SkriptAPIException; import ch.njol.skript.SkriptConfig; import ch.njol.skript.lang.SkriptEvent.ListeningBehavior; +import ch.njol.skript.lang.SkriptEventInfo.ModernSkriptEventInfo; import org.bukkit.event.Event; import org.bukkit.event.player.PlayerInteractAtEntityEvent; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; import org.skriptlang.skript.lang.structure.StructureInfo; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxOrigin; +import org.skriptlang.skript.util.Priority; +import java.lang.reflect.InvocationTargetException; +import java.util.Collection; +import java.util.List; import java.util.Locale; -public final class SkriptEventInfo extends StructureInfo { +public sealed class SkriptEventInfo extends StructureInfo permits ModernSkriptEventInfo { public Class[] events; public final String name; @@ -182,4 +194,125 @@ public ListeningBehavior getListeningBehavior() { return documentationID; } + /* + * Registration API Compatibility + */ + + /** + * Internal wrapper class for providing compatibility with the new Registration API. + */ + @ApiStatus.Internal + @ApiStatus.Experimental + public static final class ModernSkriptEventInfo + extends SkriptEventInfo + implements BukkitSyntaxInfos.Event { + + private final SyntaxOrigin origin; + + public ModernSkriptEventInfo(String name, String[] patterns, Class eventClass, String originClassPath, Class[] events) { + super(name, patterns, eventClass, originClassPath, events); + origin = SyntaxOrigin.of(Skript.getAddon(JavaPlugin.getProvidingPlugin(eventClass))); + } + + @Override + public Builder, E> builder() { + return BukkitSyntaxInfos.Event.builder(type(), name()) + .origin(origin) + .addPatterns(patterns()) + .priority(priority()) + .listeningBehavior(listeningBehavior()) + .since(since()) + .documentationId(id()) + .addDescription(description()) + .addExamples(examples()) + .addKeywords(keywords()) + .addRequiredPlugins(requiredPlugins()) + .addEvents(events()); + } + + @Override + public SyntaxOrigin origin() { + return origin; + } + + @Override + public Class type() { + return getElementClass(); + } + + @Override + public E instance() { + try { + return type().getDeclaredConstructor().newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + @Override + public @Unmodifiable Collection patterns() { + return List.of(getPatterns()); + } + + @Override + public Priority priority() { + return SyntaxInfo.COMBINED; + } + + @Override + public ListeningBehavior listeningBehavior() { + return getListeningBehavior(); + } + + @Override + public String name() { + return getName(); + } + + @Override + public String id() { + return getId(); + } + + @Override + public @Nullable String since() { + return getSince(); + } + + @Override + public @Nullable String documentationId() { + return getDocumentationID(); + } + + @Override + public Collection description() { + String[] description = getDescription(); + return description != null ? List.of(description) : List.of(); + } + + @Override + public Collection examples() { + String[] examples = getExamples(); + return examples != null ? List.of(examples) : List.of(); + } + + @Override + public Collection keywords() { + String[] keywords = getKeywords(); + return keywords != null ? List.of(keywords) : List.of(); + } + + @Override + public Collection requiredPlugins() { + String[] requiredPlugins = getRequiredPlugins(); + return requiredPlugins != null ? List.of(requiredPlugins) : List.of(); + } + + @Override + public Collection> events() { + return List.of(events); + } + } + } diff --git a/src/main/java/ch/njol/skript/lang/SyntaxElementInfo.java b/src/main/java/ch/njol/skript/lang/SyntaxElementInfo.java index d639ae59e83..2724451d23d 100644 --- a/src/main/java/ch/njol/skript/lang/SyntaxElementInfo.java +++ b/src/main/java/ch/njol/skript/lang/SyntaxElementInfo.java @@ -1,12 +1,18 @@ package ch.njol.skript.lang; +import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.lang.structure.StructureInfo; + import ch.njol.skript.SkriptAPIException; import java.lang.reflect.Modifier; import java.util.Arrays; /** - * @author Peter Güttinger * @param the syntax element this info is for */ public class SyntaxElementInfo { @@ -56,4 +62,50 @@ public String[] getPatterns() { public String getOriginClassPath() { return originClassPath; } + + @Contract("_ -> new") + @ApiStatus.Experimental + @SuppressWarnings("unchecked") + public static , E extends SyntaxElement> I fromModern(SyntaxInfo info) { + if (info instanceof BukkitSyntaxInfos.Event event) { + // We must first go back to the raw input + String rawName = event.name().startsWith("On ") + ? event.name().substring(3) + : "*" + event.name(); + SkriptEventInfo eventInfo = new SkriptEventInfo<>( + rawName, event.patterns().toArray(new String[0]), + event.type(), event.origin().name(), + (Class[]) event.events().toArray(new Class[0])); + String since = event.since(); + if (since != null) + eventInfo.since(since); + String documentationId = event.documentationId(); + if (documentationId != null) + eventInfo.documentationID(documentationId); + eventInfo.listeningBehavior(event.listeningBehavior()) + .description(event.description().toArray(new String[0])) + .examples(event.examples().toArray(new String[0])) + .keywords(event.keywords().toArray(new String[0])) + .requiredPlugins(event.requiredPlugins().toArray(new String[0])); + + return (I) eventInfo; + } else if (info instanceof SyntaxInfo.Structure structure) { + return (I) new StructureInfo<>(structure.patterns().toArray(new String[0]), structure.type(), + structure.origin().name(), structure.entryValidator(), structure.nodeType()); + } else if (info instanceof SyntaxInfo.Expression expression) { + return (I) fromModernExpression(expression); + } + + return (I) new SyntaxElementInfo<>(info.patterns().toArray(new String[0]), info.type(), info.origin().name()); + } + + @Contract("_ -> new") + @ApiStatus.Experimental + private static , R> ExpressionInfo fromModernExpression(SyntaxInfo.Expression info) { + return new ExpressionInfo<>( + info.patterns().toArray(new String[0]), info.returnType(), + info.type(), info.origin().name(), ExpressionType.fromModern(info.priority()) + ); + } + } diff --git a/src/main/java/ch/njol/skript/localization/Language.java b/src/main/java/ch/njol/skript/localization/Language.java index 95b917056d4..e31567c8377 100644 --- a/src/main/java/ch/njol/skript/localization/Language.java +++ b/src/main/java/ch/njol/skript/localization/Language.java @@ -1,11 +1,12 @@ package ch.njol.skript.localization; import ch.njol.skript.Skript; -import ch.njol.skript.SkriptAddon; import ch.njol.skript.config.Config; import ch.njol.skript.util.ExceptionUtils; import ch.njol.skript.util.FileUtils; import ch.njol.skript.util.Version; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.localization.Localizer; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.Nullable; @@ -46,7 +47,7 @@ public class Language { @Nullable private static HashMap localizedLanguage = null; - private static final HashMap langVersion = new HashMap<>(); + private static final HashMap langVersion = new HashMap<>(); public static String getName() { return name; @@ -165,46 +166,80 @@ public static boolean isInitialized() { return !defaultLanguage.isEmpty(); } + @Nullable + private static String getSanitizedLanguageDirectory(SkriptAddon addon) { + Localizer localizer = addon.localizer(); + if (localizer == null) { + return null; + } + String languageFileDirectory = localizer.languageFileDirectory(); + if (languageFileDirectory == null) { + return null; + } + // sanitization + languageFileDirectory = languageFileDirectory.replace('\\', '/'); + if (languageFileDirectory.startsWith("/")) { + languageFileDirectory = languageFileDirectory.substring(1); + } + if (languageFileDirectory.endsWith("/")) { + languageFileDirectory = languageFileDirectory.substring(0, languageFileDirectory.length() - 1); + } + return languageFileDirectory; + } + public static void loadDefault(SkriptAddon addon) { - if (addon.getLanguageFileDirectory() == null) + String languageFileDirectory = getSanitizedLanguageDirectory(addon); + if (languageFileDirectory == null) { return; + } - InputStream defaultIs = addon.plugin.getResource(addon.getLanguageFileDirectory() + "/default.lang"); - InputStream englishIs = addon.plugin.getResource(addon.getLanguageFileDirectory() + "/english.lang"); + Class source = addon.source(); + assert source != null; // getSanitizedLanguageDirectory call means source should not be null + try ( + InputStream defaultIs = source.getResourceAsStream("/" + languageFileDirectory + "/default.lang"); + InputStream englishIs = source.getResourceAsStream("/" + languageFileDirectory + "/english.lang") + ) { - if (defaultIs == null) { - if (englishIs == null) { - throw new IllegalStateException(addon + " is missing the required default.lang file!"); - } else { - defaultIs = englishIs; - englishIs = null; + InputStream defaultLangIs = defaultIs; + InputStream englishLangIs = englishIs; + if (defaultLangIs == null) { + if (englishLangIs == null) { + throw new IllegalStateException(addon + " is missing the required default.lang file!"); + } else { + defaultLangIs = englishLangIs; + englishLangIs = null; + } } - } - Map def = load(defaultIs, "default", false); - Map en = load(englishIs, "english", addon == Skript.getAddonInstance()); - String v = def.get("version"); - if (v == null) - Skript.warning("Missing version in default.lang"); + Map def = load(defaultLangIs, "default", false); + Map en = load(englishLangIs, "english", addon instanceof org.skriptlang.skript.Skript); - langVersion.put(addon.plugin, v == null ? Skript.getVersion() : new Version(v)); - def.remove("version"); - defaultLanguage.putAll(def); + String v = def.get("version"); + if (v == null) + Skript.warning("Missing version in default.lang"); - if (localizedLanguage == null) - localizedLanguage = new HashMap<>(); - localizedLanguage.putAll(en); + langVersion.put(addon.name(), v == null ? Skript.getVersion() : new Version(v)); + def.remove("version"); + defaultLanguage.putAll(def); - for (LanguageChangeListener l : listeners) - l.onLanguageChange(); + if (localizedLanguage == null) + localizedLanguage = new HashMap<>(); + localizedLanguage.putAll(en); + + for (LanguageChangeListener l : listeners) + l.onLanguageChange(); + + } catch (IOException e) { + throw new RuntimeException(e); + } } public static boolean load(String name) { name = "" + name.toLowerCase(Locale.ENGLISH); localizedLanguage = new HashMap<>(); - boolean exists = load(Skript.getAddonInstance(), name, true); - for (SkriptAddon addon : Skript.getAddons()) { + boolean exists = load(Skript.instance(), name, true); + for (SkriptAddon addon : Skript.instance().addons()) { exists |= load(addon, name, false); } if (!exists) { @@ -225,20 +260,42 @@ public static boolean load(String name) { } private static boolean load(SkriptAddon addon, String name, boolean tryUpdate) { - if (addon.getLanguageFileDirectory() == null) + String languageFileDirectory = getSanitizedLanguageDirectory(addon); + if (languageFileDirectory == null) { return false; + } + + Class source = addon.source(); + // Backwards addon compatibility - if (name.equals("english") && addon.plugin.getResource(addon.getLanguageFileDirectory() + "/default.lang") == null) - return true; + if (name.equals("english")) { + try (InputStream is = source.getResourceAsStream("/" + languageFileDirectory + "/default.lang")) { + if (is == null) { + return true; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } - Map l = load(addon.plugin.getResource(addon.getLanguageFileDirectory() + "/" + name + ".lang"), name, tryUpdate); - File file = new File(addon.plugin.getDataFolder(), addon.getLanguageFileDirectory() + File.separator + name + ".lang"); - try { - if (file.exists()) - l.putAll(load(new FileInputStream(file), name, tryUpdate)); - } catch (FileNotFoundException e) { - assert false; + Map l; + try (InputStream is = source.getResourceAsStream("/" + languageFileDirectory + "/" + name + ".lang")) { + l = load(is, name, tryUpdate); + } catch (IOException e) { + throw new RuntimeException(e); } + + String dataFileDirectory = addon.localizer().dataFileDirectory(); + if (dataFileDirectory != null) { // attempt to load language files from disk + File file = new File(dataFileDirectory, File.separator + name + ".lang"); + try { + if (file.exists()) + l.putAll(load(new FileInputStream(file), name, tryUpdate)); + } catch (FileNotFoundException e) { + assert false; + } + } + if (l.isEmpty()) return false; if (!l.containsKey("version")) { @@ -246,7 +303,7 @@ private static boolean load(SkriptAddon addon, String name, boolean tryUpdate) { } else { try { Version v = new Version("" + l.get("version")); - Version lv = langVersion.get(addon.plugin); + Version lv = langVersion.get(addon.name()); assert lv != null; // set in loadDefault() if (v.isSmallerThan(lv)) Skript.warning(addon + "'s language file " + name + ".lang is outdated, some messages will be english."); diff --git a/src/main/java/ch/njol/skript/util/Utils.java b/src/main/java/ch/njol/skript/util/Utils.java index dc2b253e158..d9704829b9b 100644 --- a/src/main/java/ch/njol/skript/util/Utils.java +++ b/src/main/java/ch/njol/skript/util/Utils.java @@ -1,27 +1,5 @@ package ch.njol.skript.util; -import ch.njol.skript.Skript; -import ch.njol.skript.effects.EffTeleport; -import ch.njol.skript.localization.Language; -import ch.njol.skript.localization.LanguageChangeListener; -import ch.njol.skript.registrations.Classes; -import ch.njol.util.*; -import ch.njol.util.coll.CollectionUtils; -import ch.njol.util.coll.iterator.EnumerationIterable; -import com.google.common.collect.Iterables; -import com.google.common.io.ByteArrayDataInput; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; -import net.md_5.bungee.api.ChatColor; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.plugin.messaging.Messenger; -import org.bukkit.plugin.messaging.PluginMessageListener; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -35,6 +13,35 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.plugin.messaging.Messenger; +import org.bukkit.plugin.messaging.PluginMessageListener; + +import com.google.common.collect.Iterables; +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; + +import ch.njol.skript.Skript; +import ch.njol.skript.effects.EffTeleport; +import ch.njol.skript.localization.Language; +import ch.njol.skript.localization.LanguageChangeListener; +import ch.njol.skript.registrations.Classes; +import ch.njol.util.Callback; +import ch.njol.util.Checker; +import ch.njol.util.NonNullPair; +import ch.njol.util.Pair; +import ch.njol.util.StringUtils; +import ch.njol.util.coll.CollectionUtils; +import ch.njol.util.coll.iterator.EnumerationIterable; +import net.md_5.bungee.api.ChatColor; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; +import org.skriptlang.skript.util.ClassLoader; + /** * Utility class. * @@ -192,49 +199,25 @@ public static Pair getAmount(String s) { * as well. Use an empty array to load all subpackages of the base package. * @throws IOException If some error occurred attempting to read the plugin's jar file. * @return This SkriptAddon + * @deprecated Use {@link org.skriptlang.skript.util.ClassLoader}. */ + @Deprecated public static Class[] getClasses(Plugin plugin, String basePackage, String... subPackages) throws IOException { - assert subPackages != null; - JarFile jar = new JarFile(getFile(plugin)); - for (int i = 0; i < subPackages.length; i++) - subPackages[i] = subPackages[i].replace('.', '/') + "/"; - basePackage = basePackage.replace('.', '/') + "/"; List> classes = new ArrayList<>(); - try { - List classNames = new ArrayList<>(); - - for (JarEntry e : new EnumerationIterable<>(jar.entries())) { - if (e.getName().startsWith(basePackage) && e.getName().endsWith(".class") && !e.getName().endsWith("package-info.class")) { - boolean load = subPackages.length == 0; - for (String sub : subPackages) { - if (e.getName().startsWith(sub, basePackage.length())) { - load = true; - break; - } - } - - if (load) - classNames.add(e.getName().replace('/', '.').substring(0, e.getName().length() - ".class".length())); - } - } - - classNames.sort(String::compareToIgnoreCase); - - for (String c : classNames) { - try { - classes.add(Class.forName(c, true, plugin.getClass().getClassLoader())); - } catch (ClassNotFoundException | NoClassDefFoundError ex) { - Skript.exception(ex, "Cannot load class " + c); - } catch (ExceptionInInitializerError err) { - Skript.exception(err.getCause(), "class " + c + " generated an exception while loading"); - } - } - } finally { - try { - jar.close(); - } catch (IOException e) {} + ClassLoader loader = ClassLoader.builder() + .basePackage(basePackage) + .addSubPackages(subPackages) + .deep(true) + .initialize(true) + .forEachClass(classes::add) + .build(); + File jarFile = getFile(plugin); + if (jarFile != null) { + loader.loadClasses(plugin.getClass(), jarFile); + } else { + loader.loadClasses(plugin.getClass()); } - return classes.toArray(new Class[classes.size()]); + return classes.toArray(new Class[0]); } /** diff --git a/src/main/java/org/skriptlang/skript/Skript.java b/src/main/java/org/skriptlang/skript/Skript.java new file mode 100644 index 00000000000..25604cf5cbf --- /dev/null +++ b/src/main/java/org/skriptlang/skript/Skript.java @@ -0,0 +1,56 @@ +package org.skriptlang.skript; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.addon.SkriptAddon; + +import java.util.Collection; + +/** + * The main class for everything related to Skript. + */ +@ApiStatus.Experimental +public interface Skript extends SkriptAddon { + + /** + * Constructs a default implementation of a Skript. + * It makes use of the default implementations of required components. + * @param source The main class of the application creating this Skript. + * Typically, this can be the class invoking this method. + * @param name The name for the Skript to use. + * @return A Skript. + */ + @Contract("_, _ -> new") + static Skript of(Class source, String name) { + return new SkriptImpl(source, name); + } + + /** + * Registers the provided addon with this Skript and loads the provided modules. + * @param source The main class of the application registering this addon. + * Typically, this can be the class invoking this method. + * @param name The name of the addon to register. + */ + @Contract("_, _ -> new") + SkriptAddon registerAddon(Class source, String name); + + /** + * @return An unmodifiable snapshot of addons currently registered with this Skript. + */ + @Unmodifiable Collection addons(); + + /** + * Constructs an unmodifiable view of this Skript. + * That is, the returned Skript will be unable to register new addons + * and the individual addons from {@link #addons()} will be unmodifiable. + * Additionally, it will return unmodifiable views of its inherited {@link SkriptAddon} components. + * @return An unmodifiable view of this Skript. + */ + @Override + @Contract("-> new") + default Skript unmodifiableView() { + return new SkriptImpl.UnmodifiableSkript(this, SkriptAddon.super.unmodifiableView()); + } + +} diff --git a/src/main/java/org/skriptlang/skript/SkriptImpl.java b/src/main/java/org/skriptlang/skript/SkriptImpl.java new file mode 100644 index 00000000000..03b96f503a5 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/SkriptImpl.java @@ -0,0 +1,263 @@ +package org.skriptlang.skript; + +import ch.njol.skript.SkriptAPIException; +import com.google.common.collect.ImmutableSet; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.localization.Localizer; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Registry; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Supplier; + +final class SkriptImpl implements Skript { + + /** + * The addon instance backing this Skript. + */ + private final SkriptAddon addon; + + SkriptImpl(Class source, String name) { + addon = new SkriptAddonImpl(this, source, name, Localizer.of(this)); + storeRegistry(SyntaxRegistry.class, SyntaxRegistry.empty()); + } + + /* + * Registry Management + */ + + private static final Map, Registry> registries = new ConcurrentHashMap<>(); + + @Override + public > void storeRegistry(Class registryClass, R registry) { + registries.put(registryClass, registry); + } + + @Override + public void removeRegistry(Class> registryClass) { + registries.remove(registryClass); + } + + @Override + public boolean hasRegistry(Class> registryClass) { + return registries.containsKey(registryClass); + } + + @Override + public > R registry(Class registryClass) { + //noinspection unchecked + R registry = (R) registries.get(registryClass); + if (registry == null) + throw new NullPointerException("Registry not present for " + registryClass); + return registry; + } + + @Override + public > R registry(Class registryClass, Supplier putIfAbsent) { + //noinspection unchecked + return (R) registries.computeIfAbsent(registryClass, key -> putIfAbsent.get()); + } + + /* + * SkriptAddon Management + */ + + private static final Map addons = new HashMap<>(); + + @Override + public SkriptAddon registerAddon(Class source, String name) { + // make sure an addon is not already registered with this name + SkriptAddon existing = addons.get(name); + if (existing != null) { + throw new SkriptAPIException( + "An addon (provided by '" + existing.source().getName() + "') with the name '" + name + "' is already registered" + ); + } + + SkriptAddon addon = new SkriptAddonImpl(this, source, name, null); + addons.put(name, addon); + return addon; + } + + @Override + public @Unmodifiable Collection addons() { + return ImmutableSet.copyOf(addons.values()); + } + + /* + * SkriptAddon Implementation + */ + + @Override + public Class source() { + return addon.source(); + } + + @Override + public String name() { + return addon.name(); + } + + @Override + public SyntaxRegistry syntaxRegistry() { + return registry(SyntaxRegistry.class); + } + + @Override + public Localizer localizer() { + return addon.localizer(); + } + + @Override + public void loadModules(AddonModule... modules) { + addon.loadModules(modules); + } + + private static final class SkriptAddonImpl implements SkriptAddon { + + private final Skript skript; + private final Class source; + private final String name; + private final Localizer localizer; + + SkriptAddonImpl(Skript skript, Class source, String name, @Nullable Localizer localizer) { + this.skript = skript; + this.source = source; + this.name = name; + this.localizer = localizer == null ? Localizer.of(this) : localizer; + } + + @Override + public Class source() { + return source; + } + + @Override + public String name() { + return name; + } + + @Override + public > void storeRegistry(Class registryClass, R registry) { + skript.storeRegistry(registryClass, registry); + } + + @Override + public void removeRegistry(Class> registryClass) { + skript.removeRegistry(registryClass); + } + + @Override + public boolean hasRegistry(Class> registryClass) { + return skript.hasRegistry(registryClass); + } + + @Override + public > R registry(Class registryClass) { + return skript.registry(registryClass); + } + + @Override + public > R registry(Class registryClass, Supplier putIfAbsent) { + return skript.registry(registryClass, putIfAbsent); + } + + @Override + public SyntaxRegistry syntaxRegistry() { + return skript.syntaxRegistry(); + } + + @Override + public Localizer localizer() { + return localizer; + } + + } + + /* + * ViewProvider Implementation + */ + + static final class UnmodifiableSkript implements Skript { + + private final Skript skript; + private final SkriptAddon unmodifiableAddon; + + UnmodifiableSkript(Skript skript, SkriptAddon unmodifiableAddon) { + this.skript = skript; + this.unmodifiableAddon = unmodifiableAddon; + } + + @Override + public SkriptAddon registerAddon(Class source, String name) { + throw new UnsupportedOperationException("Cannot register addons using an unmodifiable Skript"); + } + + @Override + public @Unmodifiable Collection addons() { + ImmutableSet.Builder addons = ImmutableSet.builder(); + skript.addons().stream() + .map(SkriptAddon::unmodifiableView) + .forEach(addons::add); + return addons.build(); + } + + @Override + public Class source() { + return skript.source(); + } + + @Override + public String name() { + return skript.name(); + } + + @Override + public > void storeRegistry(Class registryClass, R registry) { + unmodifiableAddon.storeRegistry(registryClass, registry); + } + + @Override + public void removeRegistry(Class> registryClass) { + unmodifiableAddon.removeRegistry(registryClass); + } + + @Override + public boolean hasRegistry(Class> registryClass) { + return unmodifiableAddon.hasRegistry(registryClass); + } + + @Override + public > R registry(Class registryClass) { + return unmodifiableAddon.registry(registryClass); + } + + @Override + public > R registry(Class registryClass, Supplier putIfAbsent) { + return unmodifiableAddon.registry(registryClass, putIfAbsent); + } + + @Override + public SyntaxRegistry syntaxRegistry() { + return unmodifiableAddon.syntaxRegistry(); + } + + @Override + public Localizer localizer() { + return unmodifiableAddon.localizer(); + } + + @Override + public void loadModules(AddonModule... modules) { + unmodifiableAddon.loadModules(modules); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/addon/AddonModule.java b/src/main/java/org/skriptlang/skript/addon/AddonModule.java new file mode 100644 index 00000000000..243d51b730b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/addon/AddonModule.java @@ -0,0 +1,36 @@ +package org.skriptlang.skript.addon; + +import org.jetbrains.annotations.ApiStatus; +import org.skriptlang.skript.Skript; + +/** + * A module is a component of a {@link SkriptAddon} used for registering syntax and other {@link Skript} components. + *
+ * Modules have two loading phases: {@link #init(SkriptAddon)} followed by {@link #load(SkriptAddon)}. + *
+ * The init phase should be used for loading components that are needed first or that may be used by other modules, + * such as class infos (think numeric types that are used everywhere). + *
+ * The load phase should be used for loading components more specific to the module, such as syntax. + * @see SkriptAddon#loadModules(AddonModule...) + */ +@FunctionalInterface +@ApiStatus.Experimental +public interface AddonModule { + + /** + * Used for loading the components of this module that are needed first or by other modules (e.g. class infos). + * This method will always be called before {@link #load(SkriptAddon)}. + * @param addon The addon this module belongs to. + * @see #load(SkriptAddon) + */ + default void init(SkriptAddon addon) { } + + /** + * Used for loading the components (e.g. syntax) of this module. + * @param addon The addon this module belongs to. + * @see #init(SkriptAddon) + */ + void load(SkriptAddon addon); + +} diff --git a/src/main/java/org/skriptlang/skript/addon/SkriptAddon.java b/src/main/java/org/skriptlang/skript/addon/SkriptAddon.java new file mode 100644 index 00000000000..13e47180ff3 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/addon/SkriptAddon.java @@ -0,0 +1,112 @@ +package org.skriptlang.skript.addon; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.skriptlang.skript.Skript; +import org.skriptlang.skript.localization.Localizer; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Registry; +import org.skriptlang.skript.util.ViewProvider; + +import java.util.function.Supplier; + +/** + * A Skript addon is an extension to Skript that expands its features. + * Typically, an addon instance may be obtained through {@link Skript#registerAddon(Class, String)}. + */ +@ApiStatus.Experimental +public interface SkriptAddon extends ViewProvider { + + /** + * @return A class from the application that registered this addon. + * Typically, this is the main class or the specific class in which registration occurred. + */ + Class source(); + + /** + * @return The name of this addon. + */ + String name(); + + /** + * Stores a registry under registryClass. + * If a registry is already stored under registryClass, it will be replaced. + * @param registryClass The class (key) to store registry under. + * @param registry The registry to store. + * @param The type of registry. + */ + > void storeRegistry(Class registryClass, R registry); + + /** + * Removes the registry stored under registryClass. + * It is safe to call this method even if a registry is not stored under registryClass. + * @param registryClass The class (key) that the registry to remove is under. + */ + void removeRegistry(Class> registryClass); + + /** + * Determines whether a registry has been stored under registryClass. + * @param registryClass The class (key) to search for a registry under. + * @return Whether a registry is stored under registryClass. + */ + boolean hasRegistry(Class> registryClass); + + /** + * Obtains the registry stored under registryClass. + * This method will never return null, meaning it may be necessary to call {@link #hasRegistry(Class)} + * if you are not sure whether the registry you need exists. + * @param registryClass The class (key) that the registry is stored under. + * @return The registry stored under registryClass. + * @param The type of registry. + */ + > R registry(Class registryClass); + + /** + * Searches for a registry stored under registryClass. + * If the search fails, putIfAbsent will be used to get, store, and return a registry of the requested type. + * @param registryClass The class (key) to search for a registry under. + * @param putIfAbsent A supplier to use for creating an instance of the desired type of registry if one + * is not already stored under registryClass. + * @return The registry stored under registryClass or created from putIfAbsent. + * @param The type of registry. + */ + > R registry(Class registryClass, Supplier putIfAbsent); + + /** + * @return A syntax registry for this addon's syntax. + */ + SyntaxRegistry syntaxRegistry(); + + /** + * @return A localizer for this addon's localizations. + */ + Localizer localizer(); + + /** + * A helper method for loading addon modules. + * Modules will be loaded as described by {@link AddonModule}. + * @param modules The modules to load. + */ + default void loadModules(AddonModule... modules) { + for (AddonModule module : modules) { + module.init(this); + } + for (AddonModule module : modules) { + module.load(this); + } + } + + /** + * Constructs an unmodifiable view of this addon. + * That is, the returned addon will return unmodifiable views of its {@link #syntaxRegistry()} and {@link #localizer()}. + * @return An unmodifiable view of this addon. + * @see SyntaxRegistry#unmodifiableView() + * @see Localizer#unmodifiableView() + */ + @Override + @Contract("-> new") + default SkriptAddon unmodifiableView() { + return new SkriptAddonImpl.UnmodifiableAddon(this); + } + +} diff --git a/src/main/java/org/skriptlang/skript/addon/SkriptAddonImpl.java b/src/main/java/org/skriptlang/skript/addon/SkriptAddonImpl.java new file mode 100644 index 00000000000..33af5c65b74 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/addon/SkriptAddonImpl.java @@ -0,0 +1,79 @@ +package org.skriptlang.skript.addon; + +import org.skriptlang.skript.localization.Localizer; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Registry; +import org.skriptlang.skript.util.ViewProvider; + +import java.util.function.Supplier; + +class SkriptAddonImpl { + + static class UnmodifiableAddon implements SkriptAddon { + + private final SkriptAddon addon; + private final Localizer unmodifiableLocalizer; + + UnmodifiableAddon(SkriptAddon addon) { + this.addon = addon; + this.unmodifiableLocalizer = addon.localizer().unmodifiableView(); + } + + @Override + public Class source() { + return addon.source(); + } + + @Override + public String name() { + return addon.name(); + } + + @Override + public > void storeRegistry(Class registryClass, R registry) { + throw new UnsupportedOperationException("Cannot store registries on an unmodifiable addon"); + } + + @Override + public void removeRegistry(Class> registryClass) { + throw new UnsupportedOperationException("Cannot remove registries from an unmodifiable addon"); + } + + @Override + public boolean hasRegistry(Class> registryClass) { + return addon.hasRegistry(registryClass); + } + + @Override + public > R registry(Class registryClass) { + R registry = addon.registry(registryClass); + if (registry instanceof ViewProvider) { + //noinspection unchecked + registry = ((ViewProvider) registry).unmodifiableView(); + } + return registry; + } + + @Override + public > R registry(Class registryClass, Supplier putIfAbsent) { + throw new UnsupportedOperationException("Cannot store registries on an unmodifiable addon"); + } + + @Override + public SyntaxRegistry syntaxRegistry() { + return addon.syntaxRegistry().unmodifiableView(); + } + + @Override + public Localizer localizer() { + return unmodifiableLocalizer; + } + + @Override + public void loadModules(AddonModule... modules) { + throw new UnsupportedOperationException("Cannot load modules using an unmodifiable addon"); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitRegistryKeys.java b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitRegistryKeys.java new file mode 100644 index 00000000000..336b84a2361 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitRegistryKeys.java @@ -0,0 +1,22 @@ +package org.skriptlang.skript.bukkit.registration; + +import ch.njol.skript.lang.SkriptEvent; +import org.jetbrains.annotations.ApiStatus; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos.Event; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.registration.SyntaxRegistry.Key; + +/** + * A class containing {@link SyntaxRegistry} keys for Bukkit-specific syntax elements. + */ +@ApiStatus.Experimental +public final class BukkitRegistryKeys { + + private BukkitRegistryKeys() { } + + /** + * A key representing the Bukkit-specific {@link SkriptEvent} syntax element. + */ + public static final Key> EVENT = Key.of("event"); + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfos.java b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfos.java new file mode 100644 index 00000000000..f46881c2d37 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfos.java @@ -0,0 +1,353 @@ +package org.skriptlang.skript.bukkit.registration; + +import ch.njol.skript.lang.SkriptEvent; +import ch.njol.skript.lang.SkriptEvent.ListeningBehavior; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfosImpl.EventImpl; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.Collection; + +/** + * A class containing the interfaces representing Bukkit-specific SyntaxInfo implementations. + */ +@ApiStatus.Experimental +public final class BukkitSyntaxInfos { + + private BukkitSyntaxInfos() { } + + /** + * A syntax info to be used for {@link SkriptEvent}s. + * It contains additional details including the Bukkit events represented along with documentation data. + * @param The class providing the implementation of the SkriptEvent this info represents. + */ + public interface Event extends SyntaxInfo { + + /** + * @param eventClass The Structure class the info will represent. + * @param name The name of the SkriptEvent. + * @return A Structure-specific builder for creating a syntax info representing type. + */ + static Builder, E> builder( + Class eventClass, String name + ) { + return new EventImpl.BuilderImpl<>(eventClass, name); + } + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Builder, E> builder(); + + /** + * @return The listening behavior for the SkriptEvent. Determines when the event should trigger. + */ + ListeningBehavior listeningBehavior(); + + /** + * @return The name of the {@link SkriptEvent}. + */ + String name(); + + /** + * @return A documentation-friendly version of {@link #name()}. + */ + String id(); + + /** + * @return Documentation data. Represents the version of the plugin in which a syntax was added. + * @see ch.njol.skript.doc.Since + */ + @Nullable String since(); + + /** + * @return Documentation data. Used for identifying specific syntaxes in documentation. + * @see ch.njol.skript.doc.DocumentationId + */ + @Nullable String documentationId(); + + /** + * @return Documentation data. A description of a syntax. + * @see ch.njol.skript.doc.Description + */ + Collection description(); + + /** + * @return Documentation data. Examples for using a syntax. + * @see ch.njol.skript.doc.Examples + */ + Collection examples(); + + /** + * @return Documentation data. Keywords are used by the search engine to provide relevant results. + * @see ch.njol.skript.doc.Keywords + */ + Collection keywords(); + + /** + * @return Documentation data. Plugins other than Skript that are required by a syntax. + * @see ch.njol.skript.doc.RequiredPlugins + */ + Collection requiredPlugins(); + + /** + * @return A collection of the classes representing the Bukkit events the {@link SkriptEvent} listens for. + */ + Collection> events(); + + /** + * An Event-specific builder is used for constructing a new Event syntax info. + * @see #builder(Class, String) + * @param The type of builder being used. + * @param The SkriptEvent class providing the implementation of the syntax info being built. + */ + interface Builder, E extends SkriptEvent> extends SyntaxInfo.Builder { + + /** + * Sets the listening behavior the event will use. + * This determines when the event should trigger. + * By default, this is {@link ListeningBehavior#UNCANCELLED}. + * @param listeningBehavior The listening behavior to use. + * @return This builder. + * @see Event#listeningBehavior() + */ + @Contract("_ -> this") + B listeningBehavior(ListeningBehavior listeningBehavior); + + /** + * Sets the "since" value the event's documentation will use. + * @param since The "since" value to use. + * @return This builder. + * @see Event#since() + */ + @Contract("_ -> this") + B since(String since); + + /** + * Sets the documentation identifier the event's documentation will use. + * @param documentationId The documentation identifier to use. + * @return This builder. + * @see Event#documentationId() + */ + @Contract("_ -> this") + B documentationId(String documentationId); + + /** + * Adds a description line to the event's documentation. + * @param description The description line to add. + * @return This builder. + * @see Event#description() + */ + @Contract("_ -> this") + B addDescription(String description); + + /** + * Adds lines of description to the event's documentation. + * @param description The description lines to add. + * @return This builder. + * @see Event#description() + */ + @Contract("_ -> this") + B addDescription(String... description); + + /** + * Adds lines of description to the event's documentation. + * @param description The description lines to add. + * @return This builder. + * @see Event#description() + */ + @Contract("_ -> this") + B addDescription(Collection description); + + /** + * Removes all description lines from the event's documentation. + * @return This builder. + * @see Event#description() + */ + @Contract("-> this") + B clearDescription(); + + /** + * Adds an example to the event's documentation. + * @param example The example to add. + * @return This builder. + * @see Event#examples() + */ + @Contract("_ -> this") + B addExample(String example); + + /** + * Adds examples to the event's documentation. + * @param examples The examples to add. + * @return This builder. + * @see Event#examples() + */ + @Contract("_ -> this") + B addExamples(String... examples); + + /** + * Adds examples to the event's documentation. + * @param examples The examples to add. + * @return This builder. + * @see Event#examples() + */ + @Contract("_ -> this") + B addExamples(Collection examples); + + /** + * Removes all examples from the event's documentation. + * @return This builder. + * @see Event#examples() + */ + @Contract("-> this") + B clearExamples(); + + /** + * Adds a keyword to the event's documentation. + * @param keyword The keyword to add. + * @return This builder. + * @see Event#keywords() + */ + @Contract("_ -> this") + B addKeyword(String keyword); + + /** + * Adds keywords to the event's documentation. + * @param keywords The keywords to add. + * @return This builder. + * @see Event#keywords() + */ + @Contract("_ -> this") + B addKeywords(String... keywords); + + /** + * Adds keywords to the event's documentation. + * @param keywords The keywords to add. + * @return This builder. + * @see Event#keywords() + */ + @Contract("_ -> this") + B addKeywords(Collection keywords); + + /** + * Removes all keywords from the event's documentation. + * @return This builder. + * @see Event#keywords() + */ + @Contract("-> this") + B clearKeywords(); + + /** + * Adds a required plugin to event's documentation. + * @param plugin The required plugin to add. + * @return This builder. + * @see Event#requiredPlugins() + */ + @Contract("_ -> this") + B addRequiredPlugin(String plugin); + + /** + * Adds required plugins to the event's documentation. + * @param plugins The required plugins to add. + * @return This builder. + * @see Event#requiredPlugins() + */ + @Contract("_ -> this") + B addRequiredPlugins(String... plugins); + + /** + * Adds required plugins to the event's documentation. + * @param plugins The required plugins to add. + * @return This builder. + * @see Event#requiredPlugins() + */ + @Contract("_ -> this") + B addRequiredPlugins(Collection plugins); + + /** + * Removes all required plugins from the event's documentation. + * @return This builder. + * @see Event#requiredPlugins() + */ + B clearRequiredPlugins(); + + /** + * Adds an event to the event's documentation. + * @param event The event to add. + * @return This builder. + * @see Event#events() + */ + @Contract("_ -> this") + B addEvent(Class event); + + /** + * Adds events to the event's documentation. + * @param events The events to add. + * @return This builder. + * @see Event#events() + */ + @Contract("_ -> this") + B addEvents(Class... events); + + /** + * Adds events to the event's documentation. + * @param events The events to add. + * @return This builder. + * @see Event#events() + */ + @Contract("_ -> this") + B addEvents(Collection> events); + + /** + * Removes all events from the event's documentation. + * @return This builder. + * @see Event#events() + */ + @Contract("-> this") + B clearEvents(); + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Event build(); + + } + + } + + /** + * Fixes patterns in event by modifying every {@link ch.njol.skript.patterns.TypePatternElement} to be nullable. + */ + public static String fixPattern(String pattern) { + char[] chars = pattern.toCharArray(); + StringBuilder stringBuilder = new StringBuilder(); + + boolean inType = false; + for (int i = 0; i < chars.length; i++) { + char character = chars[i]; + stringBuilder.append(character); + + if (character == '%') { + // toggle inType + inType = !inType; + + // add the dash character if it's not already present + // a type specification can have two prefix characters for modification + if (inType && i + 2 < chars.length && chars[i + 1] != '-' && chars[i + 2] != '-') + stringBuilder.append('-'); + } else if (character == '\\' && i + 1 < chars.length) { + // Make sure we don't toggle inType for escape percentage signs + stringBuilder.append(chars[i + 1]); + i++; + } + } + return stringBuilder.toString(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfosImpl.java b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfosImpl.java new file mode 100644 index 00000000000..c9b2105aedf --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfosImpl.java @@ -0,0 +1,419 @@ +package org.skriptlang.skript.bukkit.registration; + +import ch.njol.skript.lang.SkriptEvent; +import ch.njol.skript.lang.SkriptEvent.ListeningBehavior; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos.Event; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxOrigin; +import org.skriptlang.skript.util.Priority; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.function.Supplier; + +final class BukkitSyntaxInfosImpl { + + static final class EventImpl implements Event { + + private final SyntaxInfo defaultInfo; + private final ListeningBehavior listeningBehavior; + private final String name; + private final String id; + private final @Nullable String since; + private final @Nullable String documentationId; + private final Collection description; + private final Collection examples; + private final Collection keywords; + private final Collection requiredPlugins; + private final Collection> events; + + EventImpl( + SyntaxInfo defaultInfo, ListeningBehavior listeningBehavior, String name, + @Nullable String since, @Nullable String documentationId, Collection description, Collection examples, + Collection keywords, Collection requiredPlugins, Collection> events + ) { + this.defaultInfo = defaultInfo; + this.listeningBehavior = listeningBehavior; + this.name = name.startsWith("*") ? name.substring(1) : "On " + name; + this.id = name.toLowerCase(Locale.ENGLISH) + .replaceAll("[#'\"<>/&]", "") + .replaceAll("\\s+", "_"); + this.since = since; + this.documentationId = documentationId; + this.description = ImmutableList.copyOf(description); + this.examples = ImmutableList.copyOf(examples); + this.keywords = ImmutableList.copyOf(keywords); + this.requiredPlugins = ImmutableList.copyOf(requiredPlugins); + this.events = ImmutableList.copyOf(events); + } + + @Override + public Builder, E> builder() { + var builder = new BuilderImpl<>(type(), name); + defaultInfo.builder().applyTo(builder); + builder.listeningBehavior(listeningBehavior); + builder.documentationId(id); + if (since != null) { + builder.since(since); + } + if (documentationId != null) { + builder.documentationId(documentationId); + } + builder.addDescription(description); + builder.addExamples(examples); + builder.addKeywords(keywords); + builder.addRequiredPlugins(requiredPlugins); + builder.addEvents(events); + return builder; + } + + @Override + public ListeningBehavior listeningBehavior() { + return listeningBehavior; + } + + @Override + public String name() { + return name; + } + + @Override + public String id() { + return id; + } + + @Override + @Nullable + public String since() { + return since; + } + + @Override + @Nullable + public String documentationId() { + return documentationId; + } + + @Override + public Collection description() { + return description; + } + + @Override + public Collection examples() { + return examples; + } + + @Override + public Collection keywords() { + return keywords; + } + + @Override + public Collection requiredPlugins() { + return requiredPlugins; + } + + @Override + public Collection> events() { + return events; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + return (other instanceof Event event) && + Objects.equals(defaultInfo, other) && + Objects.equals(name(), event.name()) && + Objects.equals(events(), event.events()); + } + + @Override + public int hashCode() { + return Objects.hash(defaultInfo, name(), events()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("origin", origin()) + .add("type", type()) + .add("patterns", patterns()) + .add("priority", priority()) + .add("name", name()) + .add("events", events()) + .toString(); + } + + // + // default methods + // + + @Override + public SyntaxOrigin origin() { + return defaultInfo.origin(); + } + + @Override + public Class type() { + return defaultInfo.type(); + } + + @Override + public E instance() { + return defaultInfo.instance(); + } + + @Override + @Unmodifiable + public Collection patterns() { + return defaultInfo.patterns(); + } + + @Override + public Priority priority() { + return defaultInfo.priority(); + } + + @SuppressWarnings("unchecked") + static final class BuilderImpl, E extends SkriptEvent> implements Event.Builder { + + private final SyntaxInfo.Builder defaultBuilder; + private ListeningBehavior listeningBehavior = ListeningBehavior.UNCANCELLED; + private final String name; + private @Nullable String since; + private @Nullable String documentationId; + private final List description = new ArrayList<>(); + private final List examples = new ArrayList<>(); + private final List keywords = new ArrayList<>(); + private final List requiredPlugins = new ArrayList<>(); + private final List> events = new ArrayList<>(); + + BuilderImpl(Class type, String name) { + this.defaultBuilder = SyntaxInfo.builder(type); + this.name = name; + } + + @Override + public B listeningBehavior(ListeningBehavior listeningBehavior) { + this.listeningBehavior = listeningBehavior; + return (B) this; + } + + @Override + public B since(String since) { + this.since = since; + return (B) this; + } + + @Override + public B documentationId(String documentationId) { + this.documentationId = documentationId; + return (B) this; + } + + @Override + public B addDescription(String description) { + this.description.add(description); + return (B) this; + } + + @Override + public B addDescription(String... description) { + Collections.addAll(this.description, description); + return (B) this; + } + + @Override + public B addDescription(Collection description) { + this.description.addAll(description); + return (B) this; + } + + @Override + public B clearDescription() { + this.description.clear(); + return (B) this; + } + + @Override + public B addExample(String example) { + this.examples.add(example); + return (B) this; + } + + @Override + public B addExamples(String... examples) { + Collections.addAll(this.examples, examples); + return (B) this; + } + + @Override + public B addExamples(Collection examples) { + this.examples.addAll(examples); + return (B) this; + } + + @Override + public B clearExamples() { + this.examples.clear(); + return (B) this; + } + + @Override + public B addKeyword(String keyword) { + this.keywords.add(keyword); + return (B) this; + } + + @Override + public B addKeywords(String... keywords) { + Collections.addAll(this.keywords, keywords); + return (B) this; + } + + @Override + public B addKeywords(Collection keywords) { + this.keywords.addAll(keywords); + return (B) this; + } + + @Override + public B clearKeywords() { + this.keywords.clear(); + return (B) this; + } + + @Override + public B addRequiredPlugin(String plugin) { + this.requiredPlugins.add(plugin); + return (B) this; + } + + @Override + public B addRequiredPlugins(String... plugins) { + Collections.addAll(this.requiredPlugins, plugins); + return (B) this; + } + + @Override + public B addRequiredPlugins(Collection plugins) { + this.requiredPlugins.addAll(plugins); + return (B) this; + } + + @Override + public B clearRequiredPlugins() { + this.requiredPlugins.clear(); + return (B) this; + } + + @Override + public B addEvent(Class event) { + this.events.add(event); + return (B) this; + } + + @Override + public B addEvents(Class... events) { + Collections.addAll(this.events, events); + return (B) this; + } + + @Override + public B addEvents(Collection> events) { + this.events.addAll(events); + return (B) this; + } + + @Override + public B clearEvents() { + this.events.clear(); + return (B) this; + } + + @Override + public B origin(SyntaxOrigin origin) { + defaultBuilder.origin(origin); + return (B) this; + } + + @Override + public B supplier(Supplier supplier) { + defaultBuilder.supplier(supplier); + return (B) this; + } + + @Override + public B addPattern(String pattern) { + defaultBuilder.addPattern(pattern); + return (B) this; + } + + @Override + public B addPatterns(String... patterns) { + defaultBuilder.addPatterns(patterns); + return (B) this; + } + + @Override + public B addPatterns(Collection patterns) { + defaultBuilder.addPatterns(patterns); + return (B) this; + } + + @Override + public B clearPatterns() { + defaultBuilder.clearPatterns(); + return (B) this; + } + + @Override + public B priority(Priority priority) { + defaultBuilder.priority(priority); + return (B) this; + } + + @Override + public Event build() { + return new EventImpl<>( + defaultBuilder.build(), listeningBehavior, name, + since, documentationId, description, examples, keywords, requiredPlugins, events + ); + } + + @Override + public void applyTo(SyntaxInfo.Builder builder) { + defaultBuilder.applyTo(builder); + //noinspection rawtypes - Should be safe, generics will not influence this + if (builder instanceof Event.Builder eventBuilder) { + eventBuilder.listeningBehavior(listeningBehavior); + if (since != null) { + eventBuilder.since(since); + } + if (documentationId != null) { + eventBuilder.documentationId(documentationId); + } + eventBuilder.addDescription(description); + eventBuilder.addExamples(examples); + eventBuilder.addKeywords(keywords); + eventBuilder.addRequiredPlugins(requiredPlugins); + eventBuilder.addEvents(events); + } + } + + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/structure/Structure.java b/src/main/java/org/skriptlang/skript/lang/structure/Structure.java index e7aad777bbd..145d99ecad1 100644 --- a/src/main/java/org/skriptlang/skript/lang/structure/Structure.java +++ b/src/main/java/org/skriptlang/skript/lang/structure/Structure.java @@ -95,7 +95,7 @@ public final boolean init(Expression[] expressions, int matchedPattern, Kleen StructureInfo structureInfo = structureData.structureInfo; assert structureInfo != null; - if (structureInfo.simple) { // simple structures do not have validators + if (structureData.node instanceof SimpleNode) { // simple structures do not have validators return init(literals, matchedPattern, parseResult, null); } @@ -188,10 +188,10 @@ public static Structure parse(String expr, Node node, @Nullable String defaultEr throw new IllegalArgumentException("only simple or section nodes may be parsed as a structure"); ParserInstance.get().getData(StructureData.class).node = node; - if (node instanceof SimpleNode) { // only allow simple structures for simple nodes - iterator = new CheckedIterator<>(iterator, item -> item != null && item.simple); - } else { // only allow non-simple structures for section nodes - iterator = new CheckedIterator<>(iterator, item -> item != null && !item.simple); + if (node instanceof SimpleNode) { // filter out section only structures + iterator = new CheckedIterator<>(iterator, item -> item != null && item.nodeType.canBeSimple()); + } else { // filter out simple only structures + iterator = new CheckedIterator<>(iterator, item -> item != null && item.nodeType.canBeSection()); } iterator = new ConsumingIterator<>(iterator, elementInfo -> ParserInstance.get().getData(StructureData.class).structureInfo = elementInfo); diff --git a/src/main/java/org/skriptlang/skript/lang/structure/StructureInfo.java b/src/main/java/org/skriptlang/skript/lang/structure/StructureInfo.java index 89e181de993..21c7e354d16 100644 --- a/src/main/java/org/skriptlang/skript/lang/structure/StructureInfo.java +++ b/src/main/java/org/skriptlang/skript/lang/structure/StructureInfo.java @@ -1,8 +1,10 @@ package org.skriptlang.skript.lang.structure; import ch.njol.skript.lang.SyntaxElementInfo; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.registration.SyntaxInfo; /** * Special {@link SyntaxElementInfo} for {@link Structure}s that may contain information such as the {@link EntryValidator}. @@ -17,20 +19,28 @@ public class StructureInfo extends SyntaxElementInfo { */ public final boolean simple; + @ApiStatus.Experimental + public final SyntaxInfo.Structure.NodeType nodeType; + public StructureInfo(String[] patterns, Class c, String originClassPath) throws IllegalArgumentException { this(patterns, c, originClassPath, false); } - public StructureInfo(String[] patterns, Class c, String originClassPath, boolean simple) throws IllegalArgumentException { - super(patterns, c, originClassPath); - this.entryValidator = null; - this.simple = simple; + public StructureInfo(String[] patterns, Class elementClass, String originClassPath, boolean simple) throws IllegalArgumentException { + this(patterns, elementClass, originClassPath, null, simple ? SyntaxInfo.Structure.NodeType.SIMPLE : SyntaxInfo.Structure.NodeType.SECTION); + } + + public StructureInfo(String[] patterns, Class elementClass, String originClassPath, @Nullable EntryValidator entryValidator) throws IllegalArgumentException { + this(patterns, elementClass, originClassPath, entryValidator, SyntaxInfo.Structure.NodeType.SECTION); } - public StructureInfo(String[] patterns, Class c, String originClassPath, EntryValidator entryValidator) throws IllegalArgumentException { - super(patterns, c, originClassPath); + @ApiStatus.Experimental + public StructureInfo(String[] patterns, Class elementClass, String originClassPath, + @Nullable EntryValidator entryValidator, SyntaxInfo.Structure.NodeType nodeType) throws IllegalArgumentException { + super(patterns, elementClass, originClassPath); this.entryValidator = entryValidator; - this.simple = false; + this.nodeType = nodeType; + this.simple = nodeType.canBeSimple(); } } diff --git a/src/main/java/org/skriptlang/skript/localization/Localizer.java b/src/main/java/org/skriptlang/skript/localization/Localizer.java new file mode 100644 index 00000000000..c237dd20a83 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/localization/Localizer.java @@ -0,0 +1,66 @@ +package org.skriptlang.skript.localization; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.util.ViewProvider; + +/** + * A Localizer is used for the localization of translatable strings. + * + * This API is highly experimental and will be subject to change due to pending localization reworks. + * In its current state, it acts as a bridge between old and new API. + * + * @see ch.njol.skript.localization.Language + */ +@ApiStatus.Experimental +public interface Localizer extends ViewProvider { + + /** + * @param addon The addon this localizer is localizing for. + * @return A localizer with no default translations. + */ + @Contract("_ -> new") + static Localizer of(SkriptAddon addon) { + return new LocalizerImpl(addon); + } + + /** + * Sets the language file directories for this localizer. + * This method will initiate a loading of any language files in the provided directories. + * @param languageFileDirectory The path to the directory on the jar containing language files. + * @param dataFileDirectory The path to the directory on the disk containing language files. + * For example, this may include language files that have been saved to enable user customization. + */ + void setSourceDirectories(String languageFileDirectory, @Nullable String dataFileDirectory); + + /** + * @return The path to the directory on the jar containing language files. + */ + @Nullable String languageFileDirectory(); + + /** + * @return The path to the directory on the disk containing language files. + */ + @Nullable String dataFileDirectory(); + + /** + * Used for obtaining the translation of a language key. + * @param key The key of the translation to obtain. + * @return The translation represented by the provided key, or null if no translation exists. + */ + @Nullable String translate(String key); + + /** + * Constructs an unmodifiable view of this localizer. + * That is, no new translations may be added. + * @return An unmodifiable view of this localizer. + */ + @Override + @Contract("-> new") + default Localizer unmodifiableView() { + return new LocalizerImpl.UnmodifiableLocalizer(this); + } + +} diff --git a/src/main/java/org/skriptlang/skript/localization/LocalizerImpl.java b/src/main/java/org/skriptlang/skript/localization/LocalizerImpl.java new file mode 100644 index 00000000000..f35a9ee8eef --- /dev/null +++ b/src/main/java/org/skriptlang/skript/localization/LocalizerImpl.java @@ -0,0 +1,74 @@ +package org.skriptlang.skript.localization; + +import ch.njol.skript.SkriptAPIException; +import ch.njol.skript.localization.Language; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.SkriptAddon; + +final class LocalizerImpl implements Localizer { + + private final SkriptAddon addon; + + LocalizerImpl(SkriptAddon addon) { + this.addon = addon; + } + + private String languageFileDirectory; + private String dataFileDirectory; + + @Override + public void setSourceDirectories(String languageFileDirectory, @Nullable String dataFileDirectory) { + if (this.languageFileDirectory != null) { + throw new SkriptAPIException("A localizer's source directories may only be set once."); + } + this.languageFileDirectory = languageFileDirectory; + this.dataFileDirectory = dataFileDirectory; + Language.loadDefault(addon); + } + + @Override + public @Nullable String languageFileDirectory() { + return languageFileDirectory; + } + + @Override + public @Nullable String dataFileDirectory() { + return dataFileDirectory; + } + + @Override + public @Nullable String translate(String key) { + return Language.get_(key); + } + + static final class UnmodifiableLocalizer implements Localizer { + + private final Localizer localizer; + + UnmodifiableLocalizer(Localizer localizer) { + this.localizer = localizer; + } + + @Override + public void setSourceDirectories(String languageFileDirectory, @Nullable String dataFileDirectory) { + throw new UnsupportedOperationException("Cannot set the source directories of an unmodifiable Localizer."); + } + + @Override + public @Nullable String languageFileDirectory() { + return localizer.languageFileDirectory(); + } + + @Override + public @Nullable String dataFileDirectory() { + return localizer.dataFileDirectory(); + } + + @Override + public @Nullable String translate(String key) { + return localizer.translate(key); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java new file mode 100644 index 00000000000..49eefd4afba --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java @@ -0,0 +1,183 @@ +package org.skriptlang.skript.registration; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.registration.DefaultSyntaxInfosImpl.ExpressionImpl; +import org.skriptlang.skript.registration.DefaultSyntaxInfosImpl.StructureImpl; + +/** + * This class is not safe to be directly referenced. + * Use {@link SyntaxInfo} instead. + */ +@ApiStatus.Internal +@ApiStatus.Experimental +public interface DefaultSyntaxInfos { + + /** + * A syntax info to be used for {@link ch.njol.skript.lang.Expression}s. + * It differs from a typical info in that it also has a return type. + * @param The class providing the implementation of the Expression this info represents. + * @param The type of the return type of the Expression. + */ + @ApiStatus.Experimental + interface Expression, R> extends SyntaxInfo { + + /** + * Constructs a builder for an expression syntax info. + * @param expressionClass The Expression class the info will represent. + * @param returnType The class representing the supertype of all values the Expression may return. + * @return An Expression-specific builder for creating a syntax info representing expressionClass. + * @param The class providing the implementation of the Expression this info represents. + * @param The supertype of all values the Expression may return. + */ + @Contract("_, _ -> new") + static , R> Builder, E, R> builder( + Class expressionClass, Class returnType) { + return new ExpressionImpl.BuilderImpl<>(expressionClass, returnType); + } + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Builder, E, R> builder(); + + /** + * @return The class representing the supertype of all values the Expression may return. + */ + Class returnType(); + + /** + * An Expression-specific builder is used for constructing a new Expression syntax info. + * @see #builder(Class) + * @param The type of builder being used. + * @param The Expression class providing the implementation of the syntax info being built. + * @param The type of the return type of the Expression. + */ + interface Builder, E extends ch.njol.skript.lang.Expression, R> extends SyntaxInfo.Builder { + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Expression build(); + + } + + } + + /** + * A syntax info to be used for {@link org.skriptlang.skript.lang.structure.Structure}s. + * It contains additional details including the {@link EntryValidator} to use, if any. + * @param The class providing the implementation of the Structure this info represents. + */ + @ApiStatus.Experimental + interface Structure extends SyntaxInfo { + + /** + * Represents type of {@link ch.njol.skript.config.Node}s that can represent a Structure. + */ + enum NodeType { + + /** + * For Structures that can be represented using a {@link ch.njol.skript.config.SimpleNode}. + */ + SIMPLE, + + /** + * For Structures that can be represented using a {@link ch.njol.skript.config.SectionNode}. + */ + SECTION, + + /** + * For Structures that can be represented using a + * {@link ch.njol.skript.config.SimpleNode} or {@link ch.njol.skript.config.SectionNode}. + */ + BOTH; + + /** + * @return Whether a Structure of this type can be represented using a {@link ch.njol.skript.config.SimpleNode}. + */ + public boolean canBeSimple() { + return this != SECTION; + } + + /** + * @return Whether a Structure of this type can be represented using a {@link ch.njol.skript.config.SectionNode}. + */ + public boolean canBeSection() { + return this != SIMPLE; + } + + } + + /** + * Constructs a builder for a structure syntax info. + * @param structureClass The Structure class the info will represent. + * @return A Structure-specific builder for creating a syntax info representing structureClass. + * By default, the {@link #nodeType()} of the builder is {@link NodeType#SECTION}. + * @param The class providing the implementation of the Structure this info represents. + */ + @Contract("_ -> new") + static Builder, E> builder(Class structureClass) { + return new StructureImpl.BuilderImpl<>(structureClass); + } + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Builder, E> builder(); + + /** + * @return The entry validator to use for handling the Structure's entries. + * If null, the Structure is expected to manually handle any entries. + */ + @Nullable EntryValidator entryValidator(); + + /** + * @return The type of {@link ch.njol.skript.config.Node}s that can represent the Structure. + */ + NodeType nodeType(); + + /** + * A Structure-specific builder is used for constructing a new Structure syntax info. + * @see #builder(Class) + * @param The type of builder being used. + * @param The Structure class providing the implementation of the syntax info being built. + */ + interface Builder, E extends org.skriptlang.skript.lang.structure.Structure> extends SyntaxInfo.Builder { + + /** + * Sets the entry validator the Structure will use for handling entries. + * @param entryValidator The entry validator to use. + * @return This builder. + * @see Structure#entryValidator() + */ + @Contract("_ -> this") + B entryValidator(EntryValidator entryValidator); + + /** + * Sets the type of {@link ch.njol.skript.config.Node}s that can represent the Structure. + * @return This builder. + * @see Structure#type() + */ + B nodeType(NodeType type); + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Structure build(); + + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfosImpl.java b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfosImpl.java new file mode 100644 index 00000000000..19be1dbc3ef --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfosImpl.java @@ -0,0 +1,200 @@ +package org.skriptlang.skript.registration; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.util.Priority; + +import java.util.Collection; +import java.util.Objects; +import java.util.function.Supplier; + +final class DefaultSyntaxInfosImpl { + + /** + * {@inheritDoc} + */ + static class ExpressionImpl, R> + extends SyntaxInfoImpl implements DefaultSyntaxInfos.Expression { + + private final Class returnType; + + ExpressionImpl( + SyntaxOrigin origin, Class type, @Nullable Supplier supplier, + Collection patterns, Priority priority, @Nullable Class returnType + ) { + super(origin, type, supplier, patterns, priority); + Preconditions.checkNotNull(returnType, "An expression syntax info must have a return type."); + this.returnType = returnType; + } + + @Override + public Expression.Builder, E, R> builder() { + var builder = new BuilderImpl<>(type(), returnType); + super.builder().applyTo(builder); + return builder; + } + + @Override + public Class returnType() { + return returnType; + } + + @Override + public boolean equals(Object other) { + return other instanceof Expression expression && + super.equals(other) && + returnType() == expression.returnType(); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), returnType()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("origin", origin()) + .add("type", type()) + .add("patterns", patterns()) + .add("priority", priority()) + .add("returnType", returnType()) + .toString(); + } + + /** + * {@inheritDoc} + */ + static final class BuilderImpl, E extends ch.njol.skript.lang.Expression, R> + extends SyntaxInfoImpl.BuilderImpl + implements Expression.Builder { + + private final Class returnType; + + BuilderImpl(Class expressionClass, Class returnType) { + super(expressionClass); + this.returnType = returnType; + } + + public Expression build() { + return new ExpressionImpl<>(origin, type, supplier, patterns, priority, returnType); + } + } + + } + + /** + * {@inheritDoc} + */ + static class StructureImpl + extends SyntaxInfoImpl implements DefaultSyntaxInfos.Structure { + + private final @Nullable EntryValidator entryValidator; + private final NodeType nodeType; + + StructureImpl( + SyntaxOrigin origin, Class type, @Nullable Supplier supplier, + Collection patterns, Priority priority, + @Nullable EntryValidator entryValidator, NodeType nodeType + ) { + super(origin, type, supplier, patterns, priority); + if (!nodeType.canBeSection() && entryValidator != null) + throw new IllegalArgumentException("Simple Structures cannot have an EntryValidator"); + this.entryValidator = entryValidator; + this.nodeType = nodeType; + } + + @Override + public Structure.Builder, E> builder() { + var builder = new BuilderImpl<>(type()); + super.builder().applyTo(builder); + if (entryValidator != null) { + builder.entryValidator(entryValidator); + } + builder.nodeType(nodeType); + return builder; + } + + @Override + public @Nullable EntryValidator entryValidator() { + return entryValidator; + } + + @Override + public NodeType nodeType() { + return nodeType; + } + + @Override + public boolean equals(Object other) { + return other instanceof Structure structure && + super.equals(other) && + Objects.equals(entryValidator(), structure.entryValidator()) && + Objects.equals(nodeType(), structure.nodeType()); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), entryValidator(), nodeType()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("origin", origin()) + .add("type", type()) + .add("patterns", patterns()) + .add("priority", priority()) + .add("entryValidator", entryValidator()) + .toString(); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + static final class BuilderImpl, E extends org.skriptlang.skript.lang.structure.Structure> + extends SyntaxInfoImpl.BuilderImpl + implements Structure.Builder { + + private @Nullable EntryValidator entryValidator; + private NodeType nodeType = NodeType.SECTION; + + BuilderImpl(Class structureClass) { + super(structureClass); + } + + @Override + public B entryValidator(EntryValidator entryValidator) { + this.entryValidator = entryValidator; + return (B) this; + } + + @Override + public B nodeType(NodeType nodeType) { + this.nodeType = nodeType; + return (B) this; + } + + public Structure build() { + return new StructureImpl<>(origin, type, supplier, patterns, priority, entryValidator, nodeType); + } + + @Override + public void applyTo(SyntaxInfo.Builder builder) { + super.applyTo(builder); + //noinspection rawtypes - Should be safe, generics will not influence this + if (builder instanceof Structure.Builder structureBuilder) { + if (entryValidator != null) { + structureBuilder.entryValidator(entryValidator); + structureBuilder.nodeType(nodeType); + } + } + } + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxInfo.java b/src/main/java/org/skriptlang/skript/registration/SyntaxInfo.java new file mode 100644 index 00000000000..f1a70785580 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxInfo.java @@ -0,0 +1,173 @@ +package org.skriptlang.skript.registration; + +import ch.njol.skript.lang.SyntaxElement; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.registration.SyntaxInfoImpl.BuilderImpl; +import org.skriptlang.skript.util.Builder.Buildable; +import org.skriptlang.skript.util.Priority; + +import java.util.Collection; +import java.util.function.Supplier; + +/** + * A syntax info contains the details of a syntax, including its origin and patterns. + * @param The class providing the implementation of the syntax this info represents. + */ +@ApiStatus.Experimental +public interface SyntaxInfo extends Buildable, SyntaxInfo>, DefaultSyntaxInfos { + + /** + * A priority for infos with patterns that only match simple text (they do not have any {@link Expression}s). + * Example: "[the] console" + */ + Priority SIMPLE = Priority.base(); + + /** + * A priority for infos with patterns that contain at least one {@link Expression}. + * This is typically the default priority of an info. + * Example: "[the] first %number% characters of %strings%" + */ + Priority COMBINED = Priority.after(SIMPLE); + + /** + * A priority for infos with patterns that can match almost anything. + * This is likely the case when using regex or multiple expressions next to each other in a pattern. + * Example: "[the] [loop-]<.+>" + */ + Priority PATTERN_MATCHES_EVERYTHING = Priority.after(COMBINED); + + /** + * Constructs a builder for a syntax info. + * @param type The syntax class the info will represent. + * @return A builder for creating a syntax info representing type. + */ + @Contract("_ -> new") + static Builder, E> builder(Class type) { + return new BuilderImpl<>(type); + } + + /** + * {@inheritDoc} + */ + @Override + @Contract("-> new") + Builder, E> builder(); + + /** + * @return The origin of this syntax. + */ + SyntaxOrigin origin(); + + /** + * @return The class providing the implementation of this syntax. + */ + Class type(); + + /** + * @return A new instance of the class providing the implementation of this syntax. + */ + @Contract("-> new") + E instance(); + + /** + * @return The patterns of this syntax. + */ + @Unmodifiable Collection patterns(); + + /** + * @return The priority of this syntax, which dictates its position for matching during parsing. + */ + Priority priority(); + + /** + * A builder is used for constructing a new syntax info. + * @see #builder(Class) + * @param The type of builder being used. + * @param The class providing the implementation of the syntax info being built. + */ + interface Builder, E extends SyntaxElement> extends org.skriptlang.skript.util.Builder, SyntaxInfo> { + + /** + * Sets the origin the syntax info will use. + * @param origin The origin to use. + * @return This builder. + * @see SyntaxInfo#origin() + */ + @Contract("_ -> this") + B origin(SyntaxOrigin origin); + + /** + * Sets the supplier the syntax info will use to create new instances of the implementing class. + * @param supplier The supplier to use. + * @return This builder. + * @see SyntaxInfo#instance() + */ + @Contract("_ -> this") + B supplier(Supplier supplier); + + /** + * Adds a new pattern to the syntax info. + * @param pattern The pattern to add. + * @return This builder. + * @see SyntaxInfo#patterns() + */ + @Contract("_ -> this") + B addPattern(String pattern); + + /** + * Adds new patterns to the syntax info. + * @param patterns The patterns to add. + * @return This builder. + * @see SyntaxInfo#patterns() + */ + @Contract("_ -> this") + B addPatterns(String... patterns); + + /** + * Adds new patterns to the syntax info. + * @param patterns The patterns to add. + * @return This builder. + * @see SyntaxInfo#patterns() + */ + @Contract("_ -> this") + B addPatterns(Collection patterns); + + /** + * Removes all patterns from the syntax info. + * @return This builder. + * @see SyntaxInfo#patterns() + */ + @Contract("-> this") + B clearPatterns(); + + /** + * Sets the priority the syntax info will use, which dictates its position for matching during parsing. + * @param priority The priority to use. + * @return This builder. + */ + @Contract("_ -> this") + B priority(Priority priority); + + /** + * Builds a new syntax info from the set details. + * @return A syntax info representing the class providing the syntax's implementation. + */ + @Contract("-> new") + SyntaxInfo build(); + + /** + * Applies the values of this builder onto builder. + * When using this method, it is possible that some values are not safe to copy over. + * For example, when applying a SyntaxInfo for some type to a SyntaxInfo of another type, + * it is *not* safe to copy over {@link #supplier(Supplier)}, but that operation will occur anyway. + * In cases like this, you are expected to correct the values. + * @param builder The builder to apply values onto. + */ + @Override + void applyTo(Builder builder); + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxInfoImpl.java b/src/main/java/org/skriptlang/skript/registration/SyntaxInfoImpl.java new file mode 100644 index 00000000000..9b1de224549 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxInfoImpl.java @@ -0,0 +1,198 @@ +package org.skriptlang.skript.registration; + +import ch.njol.skript.lang.SyntaxElement; +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.util.ClassUtils; +import org.skriptlang.skript.util.Priority; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +class SyntaxInfoImpl implements SyntaxInfo { + + private final SyntaxOrigin origin; + private final Class type; + private final @Nullable Supplier supplier; + private final Collection patterns; + private final Priority priority; + + protected SyntaxInfoImpl( + SyntaxOrigin origin, Class type, @Nullable Supplier supplier, + Collection patterns, Priority priority + ) { + Preconditions.checkArgument(supplier != null || ClassUtils.isNormalClass(type), + "Failed to register a syntax info for '" + type.getName() + "'." + + " Element classes must be a normal type unless a supplier is provided."); + Preconditions.checkArgument(!patterns.isEmpty(), + "Failed to register a syntax info for '" + type.getName() + "'." + + " There must be at least one pattern."); + this.origin = origin; + this.type = type; + this.supplier = supplier; + this.patterns = ImmutableList.copyOf(patterns); + this.priority = priority; + } + + @Override + public Builder, T> builder() { + var builder = new BuilderImpl<>(type); + builder.origin(origin); + if (supplier != null) { + builder.supplier(supplier); + } + builder.addPatterns(patterns); + builder.priority(priority); + return builder; + } + + @Override + public SyntaxOrigin origin() { + return origin; + } + + @Override + public Class type() { + return type; + } + + @Override + public T instance() { + try { + return supplier == null ? type.getDeclaredConstructor().newInstance() : supplier.get(); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + @Override + public @Unmodifiable Collection patterns() { + return patterns; + } + + @Override + public Priority priority() { + return priority; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + return other instanceof SyntaxInfo info && + Objects.equals(origin(), info.origin()) && + Objects.equals(type(), info.type()) && + Objects.equals(patterns(), info.patterns()) && + Objects.equals(priority(), info.priority()); + } + + @Override + public int hashCode() { + return Objects.hash(origin(), type(), patterns(), priority()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("origin", origin()) + .add("type", type()) + .add("patterns", patterns()) + .add("priority", priority()) + .toString(); + } + + @SuppressWarnings("unchecked") + static class BuilderImpl, E extends SyntaxElement> implements Builder { + + /** + * A default origin that describes the class of a syntax. + */ + private static final class ClassOrigin implements SyntaxOrigin { + + private final String name; + + ClassOrigin(Class clazz) { + this.name = clazz.getName(); + } + + @Override + public String name() { + return name; + } + + } + + final Class type; + SyntaxOrigin origin; + @Nullable Supplier supplier; + final List patterns = new ArrayList<>(); + Priority priority = SyntaxInfo.COMBINED; + + BuilderImpl(Class type) { + this.type = type; + origin = new ClassOrigin(type); + } + + public B origin(SyntaxOrigin origin) { + this.origin = origin; + return (B) this; + } + + public B supplier(Supplier supplier) { + this.supplier = supplier; + return (B) this; + } + + public B addPattern(String pattern) { + this.patterns.add(pattern); + return (B) this; + } + + public B addPatterns(String... patterns) { + Collections.addAll(this.patterns, patterns); + return (B) this; + } + + public B addPatterns(Collection patterns) { + this.patterns.addAll(patterns); + return (B) this; + } + + @Override + public B clearPatterns() { + this.patterns.clear(); + return (B) this; + } + + @Override + public B priority(Priority priority) { + this.priority = priority; + return (B) this; + } + + public SyntaxInfo build() { + return new SyntaxInfoImpl<>(origin, type, supplier, patterns, priority); + } + + @Override + public void applyTo(Builder builder) { + builder.origin(origin); + if (supplier != null) { + //noinspection rawtypes - Let's hope the user knows what they are doing... + builder.supplier((Supplier) supplier); + } + builder.addPatterns(patterns); + builder.priority(priority); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxOrigin.java b/src/main/java/org/skriptlang/skript/registration/SyntaxOrigin.java new file mode 100644 index 00000000000..b5e8aa9d552 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxOrigin.java @@ -0,0 +1,60 @@ +package org.skriptlang.skript.registration; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.skriptlang.skript.addon.SkriptAddon; + +/** + * The origin of a syntax, currently only used for documentation purposes. + */ +@FunctionalInterface +@ApiStatus.Experimental +public interface SyntaxOrigin { + + /** + * Constructs a syntax origin from an addon. + * @param addon The addon to construct this origin from. + * @return An origin pointing to the provided addon. + */ + @Contract("_ -> new") + static SyntaxOrigin of(SkriptAddon addon) { + return new AddonOrigin(addon); + } + + /** + * A basic origin describing the addon a syntax has originated from. + * @see SyntaxOrigin#of(SkriptAddon) + */ + final class AddonOrigin implements SyntaxOrigin { + + private final SkriptAddon addon; + + private AddonOrigin(SkriptAddon addon) { + this.addon = addon.unmodifiableView(); + } + + /** + * @return A string representing the name of the addon this origin describes. + * Equivalent to {@link SkriptAddon#name()}. + */ + @Override + public String name() { + return addon.name(); + } + + /** + * @return An unmodifiable view of the addon this origin describes. + * @see SkriptAddon#unmodifiableView() + */ + public SkriptAddon addon() { + return addon; + } + + } + + /** + * @return A string representing this origin. + */ + String name(); + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxRegister.java b/src/main/java/org/skriptlang/skript/registration/SyntaxRegister.java new file mode 100644 index 00000000000..e8ab2bb3b0b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxRegister.java @@ -0,0 +1,41 @@ +package org.skriptlang.skript.registration; + +import com.google.common.collect.ImmutableSet; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Set; +import java.util.concurrent.ConcurrentSkipListSet; + +/** + * A syntax register is a collection of registered {@link SyntaxInfo}s of a common type. + * @param The type of syntax in this register. + */ +final class SyntaxRegister> { + + private static final Comparator> SET_COMPARATOR = (a,b) -> { + if (a == b) { // only considered equal if registering the same infos + return 0; + } + int result = a.priority().compareTo(b.priority()); + // when elements have the same priority, the oldest element comes first + return result != 0 ? result : 1; + }; + + final Set syntaxes = new ConcurrentSkipListSet<>(SET_COMPARATOR); + + public Collection syntaxes() { + synchronized (syntaxes) { + return ImmutableSet.copyOf(syntaxes); + } + } + + public void add(I info) { + syntaxes.add(info); + } + + public void remove(I info) { + syntaxes.remove(info); + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxRegistry.java b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistry.java new file mode 100644 index 00000000000..acde00a843f --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistry.java @@ -0,0 +1,158 @@ +package org.skriptlang.skript.registration; + +import ch.njol.skript.lang.Condition; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.Section; +import ch.njol.skript.lang.Statement; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.lang.structure.Structure; +import org.skriptlang.skript.util.Registry; +import org.skriptlang.skript.util.ViewProvider; + +import java.util.Collection; + +/** + * A syntax registry manages all {@link SyntaxRegister}s for syntax registration. + */ +@ApiStatus.Experimental +public interface SyntaxRegistry extends ViewProvider, Registry> { + + /** + * A key representing the built-in {@link Structure} syntax element. + */ + Key> STRUCTURE = Key.of("structure"); + + /** + * A key representing the built-in {@link Section} syntax element. + */ + Key> SECTION = Key.of("section"); + + /** + * A key representing all {@link Statement} syntax elements. + * By default, this includes {@link #EFFECT} and {@link #CONDITION}. + */ + Key> STATEMENT = Key.of("statement"); + + /** + * A key representing the built-in {@link Effect} syntax element. + */ + Key> EFFECT = ChildKey.of(STATEMENT, "effect"); + + /** + * A key representing the built-in {@link Condition} syntax element. + */ + Key> CONDITION = ChildKey.of(STATEMENT, "condition"); + + /** + * A key representing the built-in {@link Expression} syntax element. + */ + Key> EXPRESSION = Key.of("expression"); + + /** + * Constructs a default implementation of a syntax registry. + * This implementation is practically a wrapper around {@code Map, SyntaxRegistry>}. + * @return A syntax registry containing no elements. + */ + @Contract("-> new") + static SyntaxRegistry empty() { + return new SyntaxRegistryImpl(); + } + + /** + * A method to obtain all syntaxes registered under a certain key. + * @param key The key to obtain syntaxes from. + * @return An unmodifiable snapshot of all syntaxes registered under key. + * @param The syntax type. + */ + > @Unmodifiable Collection syntaxes(Key key); + + /** + * Registers a new syntax under a provided key. + * @param key The key to register info under. + * @param info The syntax info to register. + * @param The syntax type. + */ + > void register(Key key, I info); + + /** + * Unregisters a syntax registered under a provided key. + * @param key The key the info is registered under. + * @param info The syntax info to unregister. + * @param The syntax type. + */ + > void unregister(Key key, I info); + + /** + * Constructs an unmodifiable view of this syntax registry. + * That is, the returned registry will not allow registrations. + * @return An unmodifiable view of this syntax registry. + */ + @Override + @Contract("-> new") + default SyntaxRegistry unmodifiableView() { + return new SyntaxRegistryImpl.UnmodifiableRegistry(this); + } + + /** + * {@inheritDoc} + * There are no guarantees on the ordering of the returned collection. + * @return An unmodifiable snapshot of all syntaxes registered. + */ + @Override + @Unmodifiable Collection> elements(); + + /** + * Represents a syntax element type. + * @param The syntax type. + */ + @ApiStatus.Experimental + interface Key> { + + /** + * @param name The name of this key. + * @return A default key implementation. + * @param The syntax type. + */ + @Contract("_ -> new") + static > Key of(String name) { + return new SyntaxRegistryImpl.KeyImpl<>(name); + } + + /** + * @return The name of the syntax element this key represents. + */ + String name(); + + } + + /** + * Like a {@link Key}, but it has a parent which causes elements to be registered to itself and its parent. + * @param The child key's syntax type. + * @param

The parent key's syntax type. + */ + @ApiStatus.Experimental + interface ChildKey> extends Key { + + /** + * @param parent The parent of this key. + * @param name The name of this key. + * @return A default child key implementation. + * @param The child key's syntax type. + * @param

The parent key's syntax type. + */ + @Contract("_, _ -> new") + static > Key of(Key

parent, String name) { + return new SyntaxRegistryImpl.ChildKeyImpl<>(parent, name); + } + + /** + * @return The parent key of this child key. + */ + Key

parent(); + + } + +} diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxRegistryImpl.java b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistryImpl.java new file mode 100644 index 00000000000..843a1061697 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistryImpl.java @@ -0,0 +1,156 @@ +package org.skriptlang.skript.registration; + +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableSet; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +final class SyntaxRegistryImpl implements SyntaxRegistry { + + private final Map, SyntaxRegister> registers = new ConcurrentHashMap<>(); + + @Override + @Unmodifiable + public > Collection syntaxes(Key key) { + return register(key).syntaxes(); + } + + @Override + public > void register(Key key, I info) { + register(key).add(info); + if (key instanceof ChildKey) { + register(((ChildKey) key).parent(), info); + } + } + + @Override + public > void unregister(Key key, I info) { + register(key).remove(info); + if (key instanceof ChildKey) { + unregister(((ChildKey) key).parent(), info); + } + } + + @SuppressWarnings("unchecked") + private > SyntaxRegister register(Key key) { + return (SyntaxRegister) registers.computeIfAbsent(key, k -> new SyntaxRegister<>()); + } + + @Override + public Collection> elements() { + ImmutableSet.Builder> builder = ImmutableSet.builder(); + registers.values().forEach(register -> { + synchronized (register.syntaxes) { + builder.addAll(register.syntaxes); + } + }); + return builder.build(); + } + + static final class UnmodifiableRegistry implements SyntaxRegistry { + + private final SyntaxRegistry registry; + + UnmodifiableRegistry(SyntaxRegistry registry) { + this.registry = registry; + } + + @Override + public @Unmodifiable Collection> elements() { + return registry.elements(); + } + + @Override + public @Unmodifiable > Collection syntaxes(Key key) { + return registry.syntaxes(key); + } + + @Override + public > void register(Key key, I info) { + throw new UnsupportedOperationException("Cannot register syntax infos with an unmodifiable syntax registry."); + } + + @Override + public > void unregister(Key key, I info) { + throw new UnsupportedOperationException("Cannot unregister syntax infos from an unmodifiable syntax registry."); + } + + } + + static class KeyImpl> implements Key { + + protected final String name; + + KeyImpl(String name) { + this.name = name; + } + + @Override + public String name() { + return name; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + return other instanceof Key key && + name().equals(key.name()); + } + + @Override + public int hashCode() { + return name().hashCode(); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name()) + .toString(); + } + + } + + static final class ChildKeyImpl> extends KeyImpl implements ChildKey { + + private final Key

parent; + + ChildKeyImpl(Key

parent, String name) { + super(name); + this.parent = parent; + } + + @Override + public Key

parent() { + return parent; + } + + @Override + public boolean equals(Object other) { + return other instanceof ChildKey key && + super.equals(other) && + parent().equals(key.parent()); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), parent()); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name()) + .add("parent", parent()) + .toString(); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/util/Builder.java b/src/main/java/org/skriptlang/skript/util/Builder.java new file mode 100644 index 00000000000..e3280fbdf8e --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/Builder.java @@ -0,0 +1,42 @@ +package org.skriptlang.skript.util; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; + +/** + * An interface providing common methods to be implemented for any builder. + * + * @param The type of builder being used. + * @param The type of object being built. + */ +@ApiStatus.Experimental +public interface Builder, T> { + + /** + * Represents an object that can be converted back into a builder. + * @param The type of builder being used. + * @param The type of object being built. + */ + interface Buildable, T> { + + /** + * @return A builder representing this object. + */ + @Contract("-> new") + B builder(); + + } + + /** + * @return An object of T built from the values specified by this builder. + */ + @Contract("-> new") + T build(); + + /** + * Applies the values of this builder onto builder. + * @param builder The builder to apply values onto. + */ + void applyTo(B builder); + +} diff --git a/src/main/java/org/skriptlang/skript/util/ClassLoader.java b/src/main/java/org/skriptlang/skript/util/ClassLoader.java new file mode 100644 index 00000000000..030f6538e37 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/ClassLoader.java @@ -0,0 +1,279 @@ +package org.skriptlang.skript.util; + +import ch.njol.skript.Skript; +import ch.njol.util.StringUtils; +import com.google.common.reflect.ClassPath; +import com.google.common.reflect.ClassPath.ResourceInfo; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.TreeSet; +import java.util.function.Consumer; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +/** + * A utility class for loading classes contained in specific packages. + */ +public class ClassLoader { + + /** + * @return A builder for creating a loader. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A utility method for loading and initializing all classes within the base package/subpackages. + * This method will perform a deep search, meaning classes within subpackages of subpackages will be initialized too. + * @param source A class within the resource classes should be loaded from. + * @param jarFile A file representing the jar to search for classes. + * @param basePackage The package within the jar to load classes from. + * @param subPackages The specific subpackages within basePackage to load. + * If not provided, classes within basePackage and all of its subpackages will be loaded. + */ + public static void loadClasses(Class source, File jarFile, String basePackage, String... subPackages) { + builder() + .basePackage(basePackage) + .addSubPackages(subPackages) + .initialize(true) + .deep(true) + .build() + .loadClasses(source, jarFile); + } + + private final String basePackage; + private final Collection subPackages; + private final boolean initialize; + private final boolean deep; + private final @Nullable Consumer> forEachClass; + + private ClassLoader(String basePackage, Collection subPackages, boolean initialize, + boolean deep, @Nullable Consumer> forEachClass) { + if (basePackage.isEmpty()) { + throw new IllegalArgumentException("The base package must be set"); + } + this.basePackage = basePackage.replace('.', '/') + "/"; + this.subPackages = subPackages.stream() + .map(subPackage -> subPackage.replace('.', '/') + "/") + .collect(Collectors.toSet()); + this.initialize = initialize; + this.deep = deep; + this.forEachClass = forEachClass; + } + + /** + * Loads all classes (from the provided source) meeting the criteria set by this loader. + * It is recommended to use one of the methods that also accept a [jar] file + * ({@link #loadClasses(Class, File)} and {@link #loadClasses(Class, JarFile)}) for increased reliability. + * @param source A class within the resource classes should be loaded from. + */ + public void loadClasses(Class source) { + loadClasses(source, (JarFile) null); + } + + /** + * Loads all classes (from the provided source) meeting the criteria set by this loader. + * @param source A class within the resource classes should be loaded from. + * @param jarFile A file representing the jar to search for classes. While it is possible to load the classes without a jar, + * it is recommended to provide one for reliability. + * @see #loadClasses(Class, JarFile) + */ + public void loadClasses(Class source, File jarFile) { + try (JarFile jar = new JarFile(jarFile)) { + loadClasses(source, jar); + } catch (IOException e) { + // TODO better logging + Skript.warning("Failed to access jar file: " + e); + loadClasses(source); // try to load using just the source class + } + } + + /** + * Loads all classes (from the provided source) meeting the criteria set by this loader. + * @param source A class within the resource classes should be loaded from. + * @param jar A jar to search for classes. While it is possible to load the classes without this jar, + * it is recommended to provide one for reliability. + * @see #loadClasses(Class, File) + */ + public void loadClasses(Class source, @Nullable JarFile jar) { + final Collection classPaths; + try { + if (jar != null) { // load from jar if available + classPaths = jar.stream() + .map(JarEntry::getName) + .collect(Collectors.toSet()); + } else { + classPaths = ClassPath.from(source.getClassLoader()).getResources().stream() + .map(ResourceInfo::getResourceName) + .collect(Collectors.toSet()); + } + } catch (IOException e) { + throw new RuntimeException("Failed to load classes: " + e); + } + + // Used for tracking valid classes if a non-recursive search is done + // Depth is the measure of how "deep" from the head package of 'basePackage' a class is + final int expectedDepth = !this.deep ? StringUtils.count(this.basePackage, '/') : 0; + final int offset = this.basePackage.length(); + + // classes will be loaded in alphabetical order + Collection classNames = new TreeSet<>(String::compareToIgnoreCase); + for (String name : classPaths) { + if (!name.startsWith(this.basePackage) || !name.endsWith(".class") || name.endsWith("package-info.class")) + continue; + boolean load; + if (this.subPackages.isEmpty()) { + // loaded only if within base package when deep searches are forbidden + load = this.deep || StringUtils.count(name, '/') == expectedDepth; + } else { + load = false; + for (String subPackage : this.subPackages) { + // if the entry is within the subpackage, ensure it is not any deeper if not permitted + if (name.startsWith(subPackage, offset) + && (this.deep || StringUtils.count(name, '/') == expectedDepth + StringUtils.count(subPackage, '/'))) { + load = true; + break; + } + } + } + + if (load) { + // replace separators and .class extension + classNames.add(name.replace('/', '.').substring(0, name.length() - 6)); + } + } + + java.lang.ClassLoader loader = source.getClassLoader(); + for (String className : classNames) { + try { + Class clazz = Class.forName(className, this.initialize, loader); + if (this.forEachClass != null) + this.forEachClass.accept(clazz); + } catch (ClassNotFoundException ex) { + throw new RuntimeException("Failed to load class: " + className, ex); + } catch (ExceptionInInitializerError err) { + throw new RuntimeException(className + " generated an exception while loading", err.getCause()); + } + } + } + + /** + * A builder for constructing a {@link ClassLoader}. + */ + public static final class Builder { + + private String basePackage = ""; + private final Collection subPackages = new HashSet<>(); + private boolean initialize; + private boolean deep; + private @Nullable Consumer> forEachClass; + + private Builder() { } + + /** + * Sets the package the loader should start loading classes from. + * This is required. + * @param basePackage A string representing package to start loading classes from. + * @return This builder. + */ + @Contract("_ -> this") + public Builder basePackage(String basePackage) { + this.basePackage = basePackage; + return this; + } + + /** + * Adds a subpackage the loader should start loading classes from. + * This is useful for when you may want to load from some, but not all, of the subpackages of the base package. + * @param subPackage A string representing a subpackage to load from. + * @return This builder. + * @see #addSubPackages(String...) + * @see #addSubPackages(Collection) + */ + @Contract("_ -> this") + public Builder addSubPackage(String subPackage) { + this.subPackages.add(subPackage); + return this; + } + + /** + * Adds subpackages the loader should start loading classes from. + * This is useful for when you may want to load from some, but not all, of the subpackages of the base package. + * @param subPackages Strings representing subpackages to load from. + * @return This builder. + * @see #addSubPackage(String) + * @see #addSubPackages(Collection) + */ + @Contract("_ -> this") + public Builder addSubPackages(String... subPackages) { + Collections.addAll(this.subPackages, subPackages); + return this; + } + + /** + * Adds subpackages the loader should start loading classes from. + * This is useful for when you may want to load from some, but not all, of the subpackages of the base package. + * @param subPackages Strings representing subpackages to load from. + * @return This builder. + * @see #addSubPackage(String) + * @see #addSubPackages(String...) + */ + @Contract("_ -> this") + public Builder addSubPackages(Collection subPackages) { + this.subPackages.addAll(subPackages); + return this; + } + + /** + * Sets whether the loader will initialize found classes. + * @param initialize Whether classes should be initialized when found. + * @return This builder. + */ + @Contract("_ -> this") + public Builder initialize(boolean initialize) { + this.initialize = initialize; + return this; + } + + /** + * Sets whether the loader will perform a deep search. + * @param deep Whether subpackages of the provided base package (or subpackages) should be searched. + * @return This builder. + */ + @Contract("_ -> this") + public Builder deep(boolean deep) { + this.deep = deep; + return this; + } + + /** + * Sets a consumer to be run for each found class. + * @param forEachClass A consumer to run for each found class. + * @return This builder. + */ + @Contract("_ -> this") + public Builder forEachClass(Consumer> forEachClass) { + this.forEachClass = forEachClass; + return this; + } + + /** + * Builds a new loader from the set details. + * @return A loader for loading classes through the manner outlined by this builder. + */ + @Contract("-> new") + public ClassLoader build() { + return new ClassLoader(basePackage, subPackages, initialize, deep, forEachClass); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/util/ClassUtils.java b/src/main/java/org/skriptlang/skript/util/ClassUtils.java new file mode 100644 index 00000000000..9315f9b14b3 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/ClassUtils.java @@ -0,0 +1,19 @@ +package org.skriptlang.skript.util; + +import java.lang.reflect.Modifier; + +/** + * Utilities for interacting with classes. + */ +public final class ClassUtils { + + /** + * @param clazz The class to check. + * @return True if clazz does not represent an annotation, array, primitive, interface, or abstract class. + */ + public static boolean isNormalClass(Class clazz) { + return !clazz.isAnnotation() && !clazz.isArray() && !clazz.isPrimitive() + && !clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()); + } + +} diff --git a/src/main/java/org/skriptlang/skript/util/Priority.java b/src/main/java/org/skriptlang/skript/util/Priority.java new file mode 100644 index 00000000000..a5f71a5c932 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/Priority.java @@ -0,0 +1,55 @@ +package org.skriptlang.skript.util; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Collection; + +/** + * Priorities are used for things like ordering syntax and loading structures in a specific order. + */ +@ApiStatus.Experimental +public interface Priority extends Comparable { + + /** + * @return A base priority for other priorities to build relationships off of. + */ + @Contract("-> new") + static Priority base() { + return new PriorityImpl(); + } + + /** + * Constructs a new priority that is before priority. + * Note that this method will not make any changes to the {@link #after()} of priority. + * @param priority The priority that will be after the returned priority. + * @return A priority that is before priority. + */ + @Contract("_ -> new") + static Priority before(Priority priority) { + return new PriorityImpl(priority, true); + } + + /** + * Constructs a new priority that is after priority. + * Note that this method will not make any changes to the {@link #before()} of priority. + * @param priority The priority that will be before the returned priority. + * @return A priority that is after priority. + */ + @Contract("_ -> new") + static Priority after(Priority priority) { + return new PriorityImpl(priority, false); + } + + /** + * @return A collection of all priorities this priority is known to be after. + */ + @Unmodifiable Collection after(); + + /** + * @return A collection of all priorities this priority is known to be before. + */ + @Unmodifiable Collection before(); + +} diff --git a/src/main/java/org/skriptlang/skript/util/PriorityImpl.java b/src/main/java/org/skriptlang/skript/util/PriorityImpl.java new file mode 100644 index 00000000000..b342cf1e982 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/PriorityImpl.java @@ -0,0 +1,82 @@ +package org.skriptlang.skript.util; + +import com.google.common.collect.ImmutableSet; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +class PriorityImpl implements Priority { + + private final Set after; + + private final Set before; + + PriorityImpl() { + this.after = ImmutableSet.of(); + this.before = ImmutableSet.of(); + } + + PriorityImpl(Priority priority, boolean isBefore) { + Set after = new HashSet<>(); + Set before = new HashSet<>(); + if (isBefore) { + before.add(priority); + } else { + after.add(priority); + } + after.addAll(priority.after()); + before.addAll(priority.before()); + + this.after = ImmutableSet.copyOf(after); + this.before = ImmutableSet.copyOf(before); + } + + @Override + public int compareTo(Priority other) { + if (this == other) { + return 0; + } + + Collection ourBefore = this.before(); + Collection otherAfter = other.after(); + + // check whether this is known to be before other and whether other is known to be after this + if (ourBefore.contains(other) || otherAfter.contains(this)) { + return -1; + } + + Collection ourAfter = this.after(); + Collection otherBefore = other.before(); + + // check whether this is known to be after other and whether other is known to be before this + if (ourAfter.contains(other) || otherBefore.contains(this)) { + return 1; + } + + // check whether the set of items we are before has common elements with the set of items other is after + if (ourBefore.stream().anyMatch(otherAfter::contains)) { + return -1; + } + + // check whether the set of items we are after has common elements with the set of items other is before + if (ourAfter.stream().anyMatch(otherBefore::contains)) { + return 1; + } + + // there is no meaningful relationship, we consider ourselves the same + // however, in cases of a custom implementation, we defer to them to determine the relationship + return (other instanceof PriorityImpl) ? 0 : (other.compareTo(this) * -1); + } + + @Override + public Collection after() { + return after; + } + + @Override + public Collection before() { + return before; + } + +} diff --git a/src/main/java/org/skriptlang/skript/util/Registry.java b/src/main/java/org/skriptlang/skript/util/Registry.java new file mode 100644 index 00000000000..46e4f859785 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/Registry.java @@ -0,0 +1,31 @@ +package org.skriptlang.skript.util; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.Collection; +import java.util.Iterator; + +/** + * A registry maintains a collection of elements. + * It is up to individual implementations as to how they may be modified. + * @param The type of elements stored in a registry. + */ +@ApiStatus.Experimental +public interface Registry extends Iterable { + + /** + * @return A collection of all elements in this registry. + */ + Collection elements(); + + /** + * By default, this is a wrapper for elements().iterator(). + * @return An iterator over all elements in this registry. + * @see Collection#iterator() + */ + @Override + default Iterator iterator() { + return elements().iterator(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/util/ViewProvider.java b/src/main/java/org/skriptlang/skript/util/ViewProvider.java new file mode 100644 index 00000000000..7a9748ad1bb --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/ViewProvider.java @@ -0,0 +1,22 @@ +package org.skriptlang.skript.util; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Contract; + +/** + * For objects that can provide an unmodifiable view of themselves. + * An unmodifiable view means that the object may only be used in a read-only manner (its values may not be changed). + * Since it is a view, it will reflect any changes made to the object it was created from. + * @param The type being viewed. + */ +@ApiStatus.Experimental +public interface ViewProvider { + + /** + * Constructs an unmodifiable view of this. + * @return An unmodifiable view of this. + */ + @Contract("-> new") + T unmodifiableView(); + +} From a18c5ae87ff41e7ce6cd1ea3f14d4a066d7ec73b Mon Sep 17 00:00:00 2001 From: Moderocky Date: Sun, 29 Dec 2024 21:58:28 +0000 Subject: [PATCH 10/19] Any X (#6728) * Add any-type & named. * Basic test for ExprName. * Make existing types any-types. * Register special any-info (for checking purposes). * Register any named type. * Add converters for everything to any-named. * Basic changes to ExprName. * Add more tests. * Add any-named lang entry. * Add any-sized type. * Support current types as amount. * New amount support + test. * Change tests so they pass :) * Fix world converter for 1.13. * Support containers. * Remove converter method I didn't use. * Clean up code for Java 17. * Apply suggestions from code review Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com> * Do change requests. * Patterns. * Add docs. * Fix. * Support numbered in empty. * Switch numbers for walrus. * Fix spacing. * Add user pattern. * Fix imports. * Apply suggestions from code review Co-authored-by: Patrick Miller * Fix switch. * Change method in slot. * Remove method for walrus. * Remove throw. * ExprName Fixes * Apply suggestions from code review Co-authored-by: Patrick Miller * Fix incompatibility in dependency. * Patch entity comparison thingy. * Do requested changes. * Some changes. --------- Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com> Co-authored-by: Patrick Miller Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> --- .../java/ch/njol/skript/aliases/ItemType.java | 39 ++++- .../java/ch/njol/skript/classes/AnyInfo.java | 40 +++++ .../ch/njol/skript/classes/ClassInfo.java | 135 ++++++++-------- .../classes/data/DefaultConverters.java | 139 +++++++++++++---- .../skript/classes/data/SkriptClasses.java | 82 ++++++---- .../java/ch/njol/skript/command/Commands.java | 2 +- .../njol/skript/conditions/CondContains.java | 103 +++++++------ .../njol/skript/conditions/CondIsEmpty.java | 43 +++--- .../njol/skript/expressions/ExprAmount.java | 73 +++++++-- .../skript/expressions/ExprItemAmount.java | 2 +- .../ch/njol/skript/expressions/ExprName.java | 145 +++++++----------- .../skript/lang/util/common/AnyAmount.java | 49 ++++++ .../skript/lang/util/common/AnyContains.java | 48 ++++++ .../skript/lang/util/common/AnyNamed.java | 42 +++++ .../skript/lang/util/common/AnyProvider.java | 28 ++++ .../java/ch/njol/skript/util/slot/Slot.java | 73 ++++++++- .../skript/lang/converter/Converter.java | 21 +++ .../skript/lang/converter/Converters.java | 16 ++ src/main/resources/lang/default.lang | 3 + .../tests/syntaxes/expressions/ExprAmount.sk | 12 ++ .../tests/syntaxes/expressions/ExprName.sk | 26 ++++ .../tests/syntaxes/sections/EffSecSpawn.sk | 10 +- 22 files changed, 826 insertions(+), 305 deletions(-) create mode 100644 src/main/java/ch/njol/skript/classes/AnyInfo.java create mode 100644 src/main/java/ch/njol/skript/lang/util/common/AnyAmount.java create mode 100644 src/main/java/ch/njol/skript/lang/util/common/AnyContains.java create mode 100644 src/main/java/ch/njol/skript/lang/util/common/AnyNamed.java create mode 100644 src/main/java/ch/njol/skript/lang/util/common/AnyProvider.java create mode 100644 src/test/skript/tests/syntaxes/expressions/ExprAmount.sk create mode 100644 src/test/skript/tests/syntaxes/expressions/ExprName.sk diff --git a/src/main/java/ch/njol/skript/aliases/ItemType.java b/src/main/java/ch/njol/skript/aliases/ItemType.java index 39ce6d21962..4cbea58df59 100644 --- a/src/main/java/ch/njol/skript/aliases/ItemType.java +++ b/src/main/java/ch/njol/skript/aliases/ItemType.java @@ -4,6 +4,8 @@ import ch.njol.skript.bukkitutil.BukkitUnsafe; import ch.njol.skript.bukkitutil.ItemUtils; import ch.njol.skript.lang.Unit; +import ch.njol.skript.lang.util.common.AnyAmount; +import ch.njol.skript.lang.util.common.AnyNamed; import ch.njol.skript.localization.Adjective; import ch.njol.skript.localization.GeneralWords; import ch.njol.skript.localization.Language; @@ -35,6 +37,7 @@ import org.bukkit.inventory.PlayerInventory; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SkullMeta; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.NotSerializableException; @@ -55,7 +58,8 @@ import java.util.stream.Collectors; @ContainerType(ItemStack.class) -public class ItemType implements Unit, Iterable, Container, YggdrasilExtendedSerializable { +public class ItemType implements Unit, Iterable, Container, YggdrasilExtendedSerializable, + AnyNamed, AnyAmount { static { // This handles updating ItemType and ItemData variable records @@ -1444,4 +1448,37 @@ public ItemType getBaseType() { return copy; } + @Override + public @Nullable String name() { + ItemMeta meta = this.getItemMeta(); + return meta.hasDisplayName() ? meta.getDisplayName() : null; + } + + @Override + public boolean supportsNameChange() { + return true; + } + + @Override + public void setName(String name) { + ItemMeta meta = this.getItemMeta(); + meta.setDisplayName(name); + this.setItemMeta(meta); + } + + @Override + public @NotNull Number amount() { + return this.getAmount(); + } + + @Override + public boolean supportsAmountChange() { + return true; + } + + @Override + public void setAmount(@Nullable Number amount) throws UnsupportedOperationException { + this.setAmount(amount != null ? amount.intValue() : 0); + } + } diff --git a/src/main/java/ch/njol/skript/classes/AnyInfo.java b/src/main/java/ch/njol/skript/classes/AnyInfo.java new file mode 100644 index 00000000000..283e0c5189b --- /dev/null +++ b/src/main/java/ch/njol/skript/classes/AnyInfo.java @@ -0,0 +1,40 @@ +package ch.njol.skript.classes; + +import ch.njol.skript.lang.util.common.AnyProvider; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * A special kind of {@link ClassInfo} for dealing with 'any'-accepting types. + * These auto-generate their user patterns (e.g. {@code named} -> {@code any named thing}). + * + * @see AnyProvider + */ +public class AnyInfo extends ClassInfo { + + /** + * @param c The class + * @param codeName The name used in patterns + */ + public AnyInfo(Class c, String codeName) { + super(c, codeName); + this.user("(any )?" + codeName + " (thing|object)s?"); + } + + @Override + public ClassInfo user(String... userInputPatterns) throws PatternSyntaxException { + if (this.userInputPatterns == null) + return super.user(userInputPatterns); + // Allow appending the patterns. + List list = new ArrayList<>(List.of(this.userInputPatterns)); + for (String pattern : userInputPatterns) { + list.add(Pattern.compile(pattern)); + } + this.userInputPatterns = list.toArray(new Pattern[0]); + return this; + } + +} diff --git a/src/main/java/ch/njol/skript/classes/ClassInfo.java b/src/main/java/ch/njol/skript/classes/ClassInfo.java index 56b24d4dceb..3c96028c58a 100644 --- a/src/main/java/ch/njol/skript/classes/ClassInfo.java +++ b/src/main/java/ch/njol/skript/classes/ClassInfo.java @@ -28,23 +28,22 @@ */ @SuppressFBWarnings("DM_STRING_VOID_CTOR") public class ClassInfo implements Debuggable { - + private final Class c; private final String codeName; private final Noun name; - + @Nullable private DefaultExpression defaultExpression = null; - + @Nullable private Parser parser = null; - + @Nullable private Cloner cloner = null; - - @Nullable - private Pattern[] userInputPatterns = null; - + + @Nullable Pattern[] userInputPatterns = null; + @Nullable private Changer changer = null; @@ -55,12 +54,12 @@ public class ClassInfo implements Debuggable { private Serializer serializer = null; @Nullable private Class serializeAs = null; - + @Nullable private Arithmetic math = null; @Nullable private Class mathRelativeType = null; - + @Nullable private String docName = null; @Nullable @@ -73,13 +72,13 @@ public class ClassInfo implements Debuggable { private String since = null; @Nullable private String[] requiredPlugins = null; - + /** * Overrides documentation id assigned from class name. */ @Nullable private String documentationId = null; - + /** * @param c The class * @param codeName The name used in patterns @@ -91,7 +90,7 @@ public ClassInfo(final Class c, final String codeName) { this.codeName = codeName; name = new Noun("types." + codeName); } - + /** * Incorrect spelling in method name. This will be removed in the future. */ @@ -99,13 +98,13 @@ public ClassInfo(final Class c, final String codeName) { public static boolean isVaildCodeName(final String name) { return isValidCodeName(name); } - + public static boolean isValidCodeName(final String name) { - return name.matches("[a-z0-9]+"); + return name.matches("(?:any-)?[a-z0-9]+"); } - + // === FACTORY METHODS === - + /** * @param parser A parser to parse values of this class or null if not applicable */ @@ -114,7 +113,7 @@ public ClassInfo parser(final Parser parser) { this.parser = parser; return this; } - + /** * @param cloner A {@link Cloner} to clone values when setting variables * or passing function arguments. @@ -124,7 +123,7 @@ public ClassInfo cloner(Cloner cloner) { this.cloner = cloner; return this; } - + /** * @param userInputPatterns Regex patterns to match this class, e.g. in the expressions loop-[type], random [type] out of ..., or as command arguments. These patterns * must be english and match singular and plural. @@ -139,7 +138,7 @@ public ClassInfo user(final String... userInputPatterns) throws PatternSyntax } return this; } - + /** * @param defaultExpression The default (event) value of this class or null if not applicable * @see EventValueExpression @@ -187,7 +186,7 @@ public ClassInfo serializer(final Serializer serializer) { serializer.register(this); return this; } - + public ClassInfo serializeAs(final Class serializeAs) { assert this.serializeAs == null; if (serializer != null) @@ -195,12 +194,12 @@ public ClassInfo serializeAs(final Class serializeAs) { this.serializeAs = serializeAs; return this; } - + @Deprecated public ClassInfo changer(final SerializableChanger changer) { return changer((Changer) changer); } - + public ClassInfo changer(final Changer changer) { assert this.changer == null; this.changer = changer; @@ -221,15 +220,15 @@ public ClassInfo math(final Class relativeType, final Arithmetic name(final String name) { this.docName = name; return this; } - + /** * Only used for Skript's documentation. - * + * * @param description * @return This ClassInfo object */ @@ -250,10 +249,10 @@ public ClassInfo description(final String... description) { this.description = description; return this; } - + /** * Only used for Skript's documentation. - * + * * @param usage * @return This ClassInfo object */ @@ -262,10 +261,10 @@ public ClassInfo usage(final String... usage) { this.usage = usage; return this; } - + /** * Only used for Skript's documentation. - * + * * @param examples * @return This ClassInfo object */ @@ -274,10 +273,10 @@ public ClassInfo examples(final String... examples) { this.examples = examples; return this; } - + /** * Only used for Skript's documentation. - * + * * @param since * @return This ClassInfo object */ @@ -286,7 +285,7 @@ public ClassInfo since(final String since) { this.since = since; return this; } - + /** * Other plugin dependencies for this ClassInfo. * @@ -300,7 +299,7 @@ public ClassInfo requiredPlugins(final String... pluginNames) { this.requiredPlugins = pluginNames; return this; } - + /** * Overrides default documentation id, which is assigned from class name. * This is especially useful for inner classes whose names are useless without @@ -313,36 +312,36 @@ public ClassInfo documentationId(String id) { this.documentationId = id; return this; } - + // === GETTERS === - + public Class getC() { return c; } - + public Noun getName() { return name; } - + public String getCodeName() { return codeName; } - + @Nullable public DefaultExpression getDefaultExpression() { return defaultExpression; } - + @Nullable public Parser getParser() { return parser; } - + @Nullable public Cloner getCloner() { return cloner; } - + /** * Clones the given object using {@link ClassInfo#cloner}, * returning the given object if no {@link Cloner} is registered. @@ -350,12 +349,12 @@ public Cloner getCloner() { public T clone(T t) { return cloner == null ? t : cloner.clone(t); } - + @Nullable public Pattern[] getUserInputPatterns() { return userInputPatterns; } - + @Nullable public Changer getChanger() { return changer; @@ -372,12 +371,12 @@ public Supplier> getSupplier() { public Serializer getSerializer() { return serializer; } - + @Nullable public Class getSerializeAs() { return serializeAs; } - + @Nullable @Deprecated public Arithmetic getMath() { @@ -389,33 +388,33 @@ public Class getSerializeAs() { public Arithmetic getRelativeMath() { return (Arithmetic) math; } - + @Nullable @Deprecated public Class getMathRelativeType() { return mathRelativeType; } - + @Nullable public String[] getDescription() { return description; } - + @Nullable public String[] getUsage() { return usage; } - + @Nullable public String[] getExamples() { return examples; } - + @Nullable public String getSince() { return since; } - + @Nullable public String getDocName() { return docName; @@ -425,7 +424,7 @@ public String getDocName() { public String[] getRequiredPlugins() { return requiredPlugins; } - + /** * Gets overridden documentation id of this this type. If no override has * been set, null is returned and the caller may try to derive this from @@ -440,13 +439,13 @@ public String getDocumentationID() { public boolean hasDocs() { return getDocName() != null && !ClassInfo.NO_DOC.equals(getDocName()); } - + // === ORDERING === - + @Nullable private Set before; private final Set after = new HashSet<>(); - + /** * Sets one or more classes that this class should occur before in the class info list. This only affects the order in which classes are parsed if it's unknown of which type * the parsed string is. @@ -454,7 +453,7 @@ public boolean hasDocs() { * Please note that subclasses will always be registered before superclasses, no matter what is defined here or in {@link #after(String...)}. *

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

* This list can safely contain classes that may not exist. - * + * * @param after * @return this ClassInfo */ @@ -479,7 +478,7 @@ public ClassInfo after(final String... after) { this.after.addAll(Arrays.asList(after)); return this; } - + /** * @return Set of classes that should be after this one. May return null. */ @@ -487,26 +486,26 @@ public ClassInfo after(final String... after) { public Set before() { return before; } - + /** * @return Set of classes that should be before this one. Never returns null. */ public Set after() { return after; } - + // === GENERAL === - + @Override @NotNull public String toString() { return getName().getSingular(); } - + public String toString(final int flags) { return getName().toString(flags); } - + @Override @NotNull public String toString(final @Nullable Event event, final boolean debug) { @@ -514,5 +513,5 @@ public String toString(final @Nullable Event event, final boolean debug) { return codeName + " (" + c.getCanonicalName() + ")"; return getName().getSingular(); } - + } diff --git a/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java b/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java index 9f0731eaa7c..a73fece5a1f 100644 --- a/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java +++ b/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java @@ -1,21 +1,20 @@ package ch.njol.skript.classes.data; +import ch.njol.skript.Skript; import ch.njol.skript.aliases.ItemType; import ch.njol.skript.command.Commands; import ch.njol.skript.entity.EntityData; import ch.njol.skript.entity.EntityType; import ch.njol.skript.entity.XpOrbData; +import ch.njol.skript.lang.util.common.AnyAmount; +import ch.njol.skript.lang.util.common.AnyNamed; import ch.njol.skript.util.BlockInventoryHolder; import ch.njol.skript.util.BlockUtils; import ch.njol.skript.util.Direction; import ch.njol.skript.util.EnchantmentType; import ch.njol.skript.util.Experience; import ch.njol.skript.util.slot.Slot; -import org.bukkit.Bukkit; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.OfflinePlayer; -import org.bukkit.World; +import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.DoubleChest; @@ -30,14 +29,19 @@ import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory; +import org.bukkit.plugin.Plugin; +import org.bukkit.scoreboard.Objective; +import org.bukkit.scoreboard.Team; import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnknownNullability; import org.skriptlang.skript.lang.converter.Converter; import org.skriptlang.skript.lang.converter.Converters; public class DefaultConverters { - + public DefaultConverters() {} - + static { // Number to subtypes converters Converters.registerConverter(Number.class, Byte.class, Number::byteValue); @@ -82,27 +86,27 @@ public DefaultConverters() {} return (LivingEntity) e; return null; }); - + // Block - Inventory Converters.registerConverter(Block.class, Inventory.class, b -> { if (b.getState() instanceof InventoryHolder) return ((InventoryHolder) b.getState()).getInventory(); return null; }, Commands.CONVERTER_NO_COMMAND_ARGUMENTS); - + // Entity - Inventory Converters.registerConverter(Entity.class, Inventory.class, e -> { if (e instanceof InventoryHolder) return ((InventoryHolder) e).getInventory(); return null; }, Commands.CONVERTER_NO_COMMAND_ARGUMENTS); - + // Block - ItemType Converters.registerConverter(Block.class, ItemType.class, ItemType::new, Converter.NO_LEFT_CHAINING | Commands.CONVERTER_NO_COMMAND_ARGUMENTS); // Block - Location Converters.registerConverter(Block.class, Location.class, BlockUtils::getLocation, Commands.CONVERTER_NO_COMMAND_ARGUMENTS); - + // Entity - Location Converters.registerConverter(Entity.class, Location.class, Entity::getLocation, Commands.CONVERTER_NO_COMMAND_ARGUMENTS); @@ -111,21 +115,21 @@ public DefaultConverters() {} // EntityData - EntityType Converters.registerConverter(EntityData.class, EntityType.class, data -> new EntityType(data, -1)); - + // ItemType - ItemStack Converters.registerConverter(ItemType.class, ItemStack.class, ItemType::getRandom); Converters.registerConverter(ItemStack.class, ItemType.class, ItemType::new); - + // Experience - XpOrbData Converters.registerConverter(Experience.class, XpOrbData.class, e -> new XpOrbData(e.getXP())); Converters.registerConverter(XpOrbData.class, Experience.class, e -> new Experience(e.getExperience())); - + // Slot - ItemType Converters.registerConverter(Slot.class, ItemType.class, s -> { ItemStack i = s.getItem(); return new ItemType(i != null ? i : new ItemStack(Material.AIR, 1)); }); - + // Block - InventoryHolder Converters.registerConverter(Block.class, InventoryHolder.class, b -> { BlockState s = b.getState(); @@ -144,25 +148,108 @@ public DefaultConverters() {} // InventoryHolder - Entity Converters.registerConverter(InventoryHolder.class, Entity.class, holder -> { - if (holder instanceof Entity) - return (Entity) holder; + if (holder instanceof Entity entity) + return entity; return null; }, Converter.NO_CHAINING); + // Anything with a name -> AnyNamed + Converters.registerConverter(OfflinePlayer.class, AnyNamed.class, player -> player::getName, Converter.NO_RIGHT_CHAINING); + if (Skript.classExists("org.bukkit.generator.WorldInfo")) + Converters.registerConverter(World.class, AnyNamed.class, world -> world::getName, Converter.NO_RIGHT_CHAINING); + else //noinspection RedundantCast getName method is on World itself in older versions + Converters.registerConverter(World.class, AnyNamed.class, world -> () -> ((World) world).getName(), Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(GameRule.class, AnyNamed.class, rule -> rule::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Server.class, AnyNamed.class, server -> server::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Plugin.class, AnyNamed.class, plugin -> plugin::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(WorldType.class, AnyNamed.class, type -> type::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Team.class, AnyNamed.class, team -> team::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Objective.class, AnyNamed.class, objective -> objective::getName, Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Nameable.class, AnyNamed.class, // + nameable -> new AnyNamed() { + @Override + public @UnknownNullability String name() { + //noinspection deprecation + return nameable.getCustomName(); + } + + @Override + public boolean supportsNameChange() { + return true; + } + + @Override + public void setName(String name) { + //noinspection deprecation + nameable.setCustomName(name); + } + }, + // + Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(Block.class, AnyNamed.class, // + block -> new AnyNamed() { + @Override + public @UnknownNullability String name() { + BlockState state = block.getState(); + if (state instanceof Nameable nameable) + //noinspection deprecation + return nameable.getCustomName(); + return null; + } + + @Override + public boolean supportsNameChange() { + return true; + } + + @Override + public void setName(String name) { + BlockState state = block.getState(); + if (state instanceof Nameable nameable) + //noinspection deprecation + nameable.setCustomName(name); + } + }, + // + Converter.NO_RIGHT_CHAINING); + Converters.registerConverter(CommandSender.class, AnyNamed.class, thing -> thing::getName, Converter.NO_RIGHT_CHAINING); + // Command senders should be done last because there might be a better alternative above + + // Anything with an amount -> AnyAmount + Converters.registerConverter(ItemStack.class, AnyAmount.class, // + item -> new AnyAmount() { + + @Override + public @NotNull Number amount() { + return item.getAmount(); + } + + @Override + public boolean supportsAmountChange() { + return true; + } + + @Override + public void setAmount(Number amount) { + item.setAmount(amount != null ? amount.intValue() : 0); + } + }, + // + Converter.NO_RIGHT_CHAINING); + // InventoryHolder - Location // since the individual ones can't be trusted to chain. Converters.registerConverter(InventoryHolder.class, Location.class, holder -> { - if (holder instanceof Entity) - return ((Entity) holder).getLocation(); - if (holder instanceof Block) - return ((Block) holder).getLocation(); - if (holder instanceof BlockState) - return BlockUtils.getLocation(((BlockState) holder).getBlock()); - if (holder instanceof DoubleChest) { - DoubleChest doubleChest = (DoubleChest) holder; + if (holder instanceof Entity entity) + return entity.getLocation(); + if (holder instanceof Block block) + return block.getLocation(); + if (holder instanceof BlockState state) + return BlockUtils.getLocation(state.getBlock()); + if (holder instanceof DoubleChest doubleChest) { if (doubleChest.getLeftSide() != null) { return BlockUtils.getLocation(((BlockState) doubleChest.getLeftSide()).getBlock()); - } else if (((DoubleChest) holder).getRightSide() != null) { + } else if (doubleChest.getRightSide() != null) { return BlockUtils.getLocation(((BlockState) doubleChest.getRightSide()).getBlock()); } } diff --git a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java index 57704e760ee..f3e02109153 100644 --- a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java @@ -1,17 +1,20 @@ package ch.njol.skript.classes.data; +import ch.njol.skript.classes.*; +import ch.njol.skript.lang.util.common.AnyAmount; +import ch.njol.skript.lang.util.common.AnyContains; +import ch.njol.skript.lang.util.common.AnyNamed; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; + import ch.njol.skript.Skript; import ch.njol.skript.aliases.Aliases; import ch.njol.skript.aliases.ItemData; import ch.njol.skript.aliases.ItemType; import ch.njol.skript.bukkitutil.EnchantmentUtils; import ch.njol.skript.bukkitutil.ItemUtils; -import ch.njol.skript.classes.Changer; -import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.classes.EnumSerializer; -import ch.njol.skript.classes.Parser; -import ch.njol.skript.classes.Serializer; -import ch.njol.skript.classes.YggdrasilSerializer; import ch.njol.skript.expressions.base.EventValueExpression; import ch.njol.skript.lang.ParseContext; import ch.njol.skript.lang.util.SimpleLiteral; @@ -36,21 +39,17 @@ import ch.njol.skript.util.visual.VisualEffect; import ch.njol.skript.util.visual.VisualEffects; import ch.njol.yggdrasil.Fields; -import org.bukkit.Material; -import org.bukkit.enchantments.Enchantment; -import org.bukkit.inventory.ItemStack; -import org.jetbrains.annotations.Nullable; import java.io.StreamCorruptedException; -import java.util.Arrays; import java.util.Iterator; import java.util.Locale; import java.util.regex.Pattern; +import java.util.Arrays; @SuppressWarnings("rawtypes") public class SkriptClasses { public SkriptClasses() {} - + static { //noinspection unchecked Classes.registerClass(new ClassInfo<>(ClassInfo.class, "classinfo") @@ -73,17 +72,17 @@ public SkriptClasses() {} public ClassInfo parse(final String s, final ParseContext context) { return Classes.getClassInfoFromUserInput(Noun.stripIndefiniteArticle(s)); } - + @Override public String toString(final ClassInfo c, final int flags) { return c.toString(flags); } - + @Override public String toVariableNameString(final ClassInfo c) { return c.getCodeName(); } - + @Override public String getDebugMessage(final ClassInfo c) { return c.getCodeName(); @@ -97,17 +96,17 @@ public Fields serialize(final ClassInfo c) { f.putObject("codeName", c.getCodeName()); return f; } - + @Override public boolean canBeInstantiated() { return false; } - + @Override public void deserialize(final ClassInfo o, final Fields f) throws StreamCorruptedException { assert false; } - + @Override protected ClassInfo deserialize(final Fields fields) throws StreamCorruptedException { final String codeName = fields.getObject("codeName", String.class); @@ -118,20 +117,20 @@ protected ClassInfo deserialize(final Fields fields) throws StreamCorruptedExcep throw new StreamCorruptedException("Invalid ClassInfo " + codeName); return ci; } - + // return c.getCodeName(); @Override @Nullable public ClassInfo deserialize(final String s) { return Classes.getClassInfoNoError(s); } - + @Override public boolean mustSyncDeserialization() { return false; } })); - + Classes.registerClass(new ClassInfo<>(WeatherType.class, "weathertype") .user("weather ?types?", "weather conditions?", "weathers?") .name("Weather Type") @@ -148,12 +147,12 @@ public boolean mustSyncDeserialization() { public WeatherType parse(final String s, final ParseContext context) { return WeatherType.parse(s); } - + @Override public String toString(final WeatherType o, final int flags) { return o.toString(flags); } - + @Override public String toVariableNameString(final WeatherType o) { return "" + o.name().toLowerCase(Locale.ENGLISH); @@ -161,7 +160,7 @@ public String toVariableNameString(final WeatherType o) { }) .serializer(new EnumSerializer<>(WeatherType.class))); - + Classes.registerClass(new ClassInfo<>(ItemType.class, "itemtype") .user("item ?types?", "materials?") .name("Item Type") @@ -603,7 +602,7 @@ public String toVariableNameString(final EnchantmentType o) { .since("2.0") .parser(new Parser() { private final RegexMessage pattern = new RegexMessage("types.experience.pattern", Pattern.CASE_INSENSITIVE); - + @Override @Nullable public Experience parse(String s, final ParseContext context) { @@ -616,12 +615,12 @@ public Experience parse(String s, final ParseContext context) { return new Experience(xp); return null; } - + @Override public String toString(final Experience xp, final int flags) { return xp.toString(); } - + @Override public String toVariableNameString(final Experience xp) { return "" + xp.getXP(); @@ -658,7 +657,7 @@ public String toVariableNameString(VisualEffect e) { }) .serializer(new YggdrasilSerializer<>())); - + Classes.registerClass(new ClassInfo<>(GameruleValue.class, "gamerulevalue") .user("gamerule values?") .name("Gamerule Value") @@ -668,6 +667,31 @@ public String toVariableNameString(VisualEffect e) { .since("2.5") .serializer(new YggdrasilSerializer()) ); + + Classes.registerClass(new AnyInfo<>(AnyNamed.class, "named") + .name("Any Named Thing") + .description("Something that has a name (e.g. an item).") + .usage("") + .examples("{thing}'s name") + .since("INSERT VERSION") + ); + + Classes.registerClass(new AnyInfo<>(AnyAmount.class, "numbered") + .name("Any Numbered/Sized Thing") + .description("Something that has an amount or size.") + .usage("") + .examples("the size of {thing}", "the amount of {thing}") + .since("INSERT VERSION") + ); + + Classes.registerClass(new AnyInfo<>(AnyContains.class, "containing") + .user("any container") + .name("Anything with Contents") + .description("Something that contains other things.") + .usage("") + .examples("{a} contains {b}") + .since("INSERT VERSION") + ); } - + } diff --git a/src/main/java/ch/njol/skript/command/Commands.java b/src/main/java/ch/njol/skript/command/Commands.java index 97f67d69f26..e8c778bf559 100644 --- a/src/main/java/ch/njol/skript/command/Commands.java +++ b/src/main/java/ch/njol/skript/command/Commands.java @@ -59,7 +59,7 @@ public abstract class Commands { /** * A Converter flag declaring that a Converter cannot be used for parsing command arguments. */ - public static final int CONVERTER_NO_COMMAND_ARGUMENTS = 4; + public static final int CONVERTER_NO_COMMAND_ARGUMENTS = 8; private final static Map commands = new HashMap<>(); diff --git a/src/main/java/ch/njol/skript/conditions/CondContains.java b/src/main/java/ch/njol/skript/conditions/CondContains.java index 6babfffc289..366141ae0b3 100644 --- a/src/main/java/ch/njol/skript/conditions/CondContains.java +++ b/src/main/java/ch/njol/skript/conditions/CondContains.java @@ -3,6 +3,7 @@ import ch.njol.skript.Skript; import ch.njol.skript.SkriptConfig; import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.lang.util.common.AnyContains; import org.skriptlang.skript.lang.comparator.Relation; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; @@ -18,14 +19,16 @@ import org.bukkit.event.Event; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.ItemStack; +import org.skriptlang.skript.lang.converter.Converters; import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.Objects; @Name("Contains") -@Description("Checks whether an inventory contains an item, a text contains another piece of text, " + - "or a list (e.g. {list variable::*} or 'drops') contains another object.") +@Description("Checks whether an inventory contains an item, a text contains another piece of text, " + + "a container contains something, " + + "or a list (e.g. {list variable::*} or 'drops') contains another object.") @Examples({"block contains 20 cobblestone", "player has 4 flint and 2 iron ingots", "{list::*} contains 5"}) @@ -45,16 +48,13 @@ public class CondContains extends Condition { * The type of check to perform */ private enum CheckType { - STRING, INVENTORY, OBJECTS, UNKNOWN + STRING, INVENTORY, OBJECTS, UNKNOWN, CONTAINER } - @SuppressWarnings("NotNullFieldNotInitialized") private Expression containers; - @SuppressWarnings("NotNullFieldNotInitialized") private Expression items; private boolean explicitSingle; - @SuppressWarnings("NotNullFieldNotInitialized") private CheckType checkType; @Override @@ -70,15 +70,15 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye checkType = CheckType.UNKNOWN; } - setNegated(matchedPattern % 2 == 1); + this.setNegated(matchedPattern % 2 == 1); return true; } @Override - public boolean check(Event e) { + public boolean check(Event event) { CheckType checkType = this.checkType; - Object[] containerValues = containers.getAll(e); + Object[] containerValues = containers.getAll(event); if (containerValues.length == 0) return isNegated(); @@ -88,6 +88,11 @@ public boolean check(Event e) { if (Arrays.stream(containerValues) .allMatch(Inventory.class::isInstance)) { checkType = CheckType.INVENTORY; + } else if (explicitSingle + && Arrays.stream(containerValues) + .allMatch(object -> object instanceof AnyContains + || Converters.converterExists(object.getClass(), AnyContains.class))) { + checkType = CheckType.CONTAINER; } else if (explicitSingle && Arrays.stream(containerValues) .allMatch(String.class::isInstance)) { @@ -97,48 +102,60 @@ public boolean check(Event e) { } } - if (checkType == CheckType.INVENTORY) { - return SimpleExpression.check(containerValues, o -> { + return switch (checkType) { + case INVENTORY -> SimpleExpression.check(containerValues, o -> { Inventory inventory = (Inventory) o; - return items.check(e, o1 -> { - if (o1 instanceof ItemType) - return ((ItemType) o1).isContainedIn(inventory); - else if (o1 instanceof ItemStack) - return inventory.containsAtLeast((ItemStack) o1, ((ItemStack) o1).getAmount()); - else if (o1 instanceof Inventory) + return items.check(event, o1 -> { + if (o1 instanceof ItemType type) { + return type.isContainedIn(inventory); + } else if (o1 instanceof ItemStack stack) { + return inventory.containsAtLeast(stack, stack.getAmount()); + } else if (o1 instanceof Inventory) { return Objects.equals(inventory, o1); - else - return false; - }); - }, isNegated(), containers.getAnd()); - } else if (checkType == CheckType.STRING) { - boolean caseSensitive = SkriptConfig.caseSensitive.value(); - - return SimpleExpression.check(containerValues, o -> { - String string = (String) o; - - return items.check(e, o1 -> { - if (o1 instanceof String) { - return StringUtils.contains(string, (String) o1, caseSensitive); - } else { - return false; } + return false; }); }, isNegated(), containers.getAnd()); - } else { - assert checkType == CheckType.OBJECTS; - - return items.check(e, o1 -> { - for (Object o2 : containerValues) { - if (Comparators.compare(o1, o2) == Relation.EQUAL) - return true; + case STRING -> { + boolean caseSensitive = SkriptConfig.caseSensitive.value(); + + yield SimpleExpression.check(containerValues, o -> { + String string = (String) o; + + return items.check(event, o1 -> { + if (o1 instanceof String text) { + return StringUtils.contains(string, text, caseSensitive); + } else { + return false; + } + }); + }, isNegated(), containers.getAnd()); + } + case CONTAINER -> SimpleExpression.check(containerValues, object -> { + AnyContains container; + if (object instanceof AnyContains) { + container = (AnyContains) object; + } else { + container = Converters.convert(object, AnyContains.class); } - return false; - }, isNegated()); - } + if (container == null) + return false; + return items.check(event, container::checkSafely); + }, isNegated(), containers.getAnd()); + default -> { + assert checkType == CheckType.OBJECTS; + yield items.check(event, o1 -> { + for (Object o2 : containerValues) { + if (Comparators.compare(o1, o2) == Relation.EQUAL) + return true; + } + return false; + }, isNegated()); + } + }; } - + @Override public String toString(@Nullable Event e, boolean debug) { return containers.toString(e, debug) + (isNegated() ? " doesn't contain " : " contains ") + items.toString(e, debug); diff --git a/src/main/java/ch/njol/skript/conditions/CondIsEmpty.java b/src/main/java/ch/njol/skript/conditions/CondIsEmpty.java index cf2623d88f0..d3eb1825c3e 100644 --- a/src/main/java/ch/njol/skript/conditions/CondIsEmpty.java +++ b/src/main/java/ch/njol/skript/conditions/CondIsEmpty.java @@ -1,52 +1,51 @@ package ch.njol.skript.conditions; -import org.bukkit.Material; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; - import ch.njol.skript.conditions.base.PropertyCondition; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.util.common.AnyAmount; import ch.njol.skript.util.slot.Slot; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; -/** - * @author Peter Güttinger - */ @Name("Is Empty") @Description("Checks whether an inventory, an inventory slot, or a text is empty.") @Examples("player's inventory is empty") @Since("unknown (before 2.1)") public class CondIsEmpty extends PropertyCondition { - + static { - register(CondIsEmpty.class, "empty", "inventories/slots/strings"); + register(CondIsEmpty.class, "empty", "inventories/slots/strings/numbered"); } - + @Override - public boolean check(final Object o) { - if (o instanceof String) - return ((String) o).isEmpty(); - if (o instanceof Inventory) { - for (ItemStack s : ((Inventory) o).getContents()) { - if (s != null && s.getType() != Material.AIR) + public boolean check(Object object) { + if (object instanceof String string) + return string.isEmpty(); + if (object instanceof Inventory inventory) { + for (ItemStack item : inventory.getContents()) { + if (item != null && item.getType() != Material.AIR) return false; // There is an item here! } return true; } - if (o instanceof Slot) { - final Slot s = (Slot) o; - final ItemStack i = s.getItem(); - return i == null || i.getType() == Material.AIR; + if (object instanceof Slot slot) { + final ItemStack item = slot.getItem(); + return item == null || item.getType() == Material.AIR; + } + if (object instanceof AnyAmount numbered) { + return numbered.isEmpty(); } assert false; return false; } - + @Override protected String getPropertyName() { return "empty"; } - + } diff --git a/src/main/java/ch/njol/skript/expressions/ExprAmount.java b/src/main/java/ch/njol/skript/expressions/ExprAmount.java index b9898f2f677..06abd492dc2 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprAmount.java +++ b/src/main/java/ch/njol/skript/expressions/ExprAmount.java @@ -1,6 +1,7 @@ package ch.njol.skript.expressions; import ch.njol.skript.Skript; +import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; @@ -12,18 +13,20 @@ import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.Variable; import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.lang.util.common.AnyAmount; import ch.njol.util.Kleenean; +import ch.njol.util.coll.CollectionUtils; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import java.util.Map; /** - * + * * @author Peter Güttinger */ @Name("Amount") -@Description({"The amount of something.", +@Description({"The amount or size of something.", "Please note that amount of %items% will not return the number of items, but the number of stacks, e.g. 1 for a stack of 64 torches. To get the amount of items in a stack, see the item amount expression.", "", "Also, you can get the recursive size of a list, which will return the recursive size of the list with sublists included, e.g.", @@ -42,23 +45,30 @@ "Please note that getting a list's recursive size can cause lag if the list is large, so only use this expression if you need to!"}) @Examples({"message \"There are %number of all players% players online!\""}) @Since("1.0") -public class ExprAmount extends SimpleExpression { +public class ExprAmount extends SimpleExpression { static { - Skript.registerExpression(ExprAmount.class, Long.class, ExpressionType.PROPERTY, + Skript.registerExpression(ExprAmount.class, Number.class, ExpressionType.PROPERTY, + "[the] (amount|number|size) of %numbered%", "[the] (amount|number|size) of %objects%", "[the] recursive (amount|number|size) of %objects%"); } @SuppressWarnings("null") private ExpressionList exprs; + private @Nullable Expression any; private boolean recursive; @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + if (matchedPattern == 0) { + //noinspection unchecked + this.any = (Expression) exprs[0]; + return true; + } this.exprs = exprs[0] instanceof ExpressionList ? (ExpressionList) exprs[0] : new ExpressionList<>(new Expression[]{exprs[0]}, Object.class, false); - this.recursive = matchedPattern == 1; + this.recursive = matchedPattern == 2; for (Expression expr : this.exprs.getExpressions()) { if (expr instanceof Literal) { return false; @@ -77,7 +87,9 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye @Override @SuppressWarnings("unchecked") - protected Long[] get(Event e) { + protected Number[] get(Event e) { + if (any != null) + return new Number[] {any.getOptionalSingle(e).orElse(() -> 0).amount()}; if (recursive) { int currentSize = 0; for (Expression expr : exprs.getExpressions()) { @@ -91,6 +103,45 @@ protected Long[] get(Event e) { return new Long[]{(long) exprs.getArray(e).length}; } + @Override + public @Nullable Class[] acceptChange(ChangeMode mode) { + if (any != null) { + return switch (mode) { + case SET, ADD, RESET, DELETE, REMOVE -> CollectionUtils.array(Number.class); + default -> null; + }; + } + return super.acceptChange(mode); + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + if (any == null) { + super.change(event, delta, mode); + return; + } + double amount = delta != null ? ((Number) delta[0]).doubleValue() : 1; + // It's okay to treat it as a double even if it's a whole number because there's no case in + // the set of real numbers where (x->double + y->double)->long != (x+y) + switch (mode) { + case REMOVE: + amount = -amount; + //$FALL-THROUGH$ + case ADD: + for (AnyAmount obj : any.getArray(event)) { + if (obj.supportsAmountChange()) + obj.setAmount(obj.amount().doubleValue() + amount); + } + break; + case RESET, DELETE, SET: + for (AnyAmount any : any.getArray(event)) { + if (any.supportsAmountChange()) + any.setAmount(amount); + } + break; + } + } + @SuppressWarnings("unchecked") private static int getRecursiveSize(Map map) { int count = 0; @@ -110,13 +161,15 @@ public boolean isSingle() { } @Override - public Class getReturnType() { - return Long.class; + public Class getReturnType() { + return any != null ? Number.class : Long.class; } @Override - public String toString(@Nullable Event e, boolean debug) { - return (recursive ? "recursive size of " : "amount of ") + exprs.toString(e, debug); + public String toString(@Nullable Event event, boolean debug) { + if (any != null) + return "amount of " + any.toString(event, debug); + return (recursive ? "recursive size of " : "amount of ") + exprs.toString(event, debug); } } diff --git a/src/main/java/ch/njol/skript/expressions/ExprItemAmount.java b/src/main/java/ch/njol/skript/expressions/ExprItemAmount.java index 23b3fe5eaef..be39645cd7f 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprItemAmount.java +++ b/src/main/java/ch/njol/skript/expressions/ExprItemAmount.java @@ -92,6 +92,6 @@ public Class getReturnType() { @Override protected String getPropertyName() { - return "item[[ ]stack] (amount|size|number)"; + return "item amount"; } } diff --git a/src/main/java/ch/njol/skript/expressions/ExprName.java b/src/main/java/ch/njol/skript/expressions/ExprName.java index 6149bfe7667..ae6b295e670 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprName.java +++ b/src/main/java/ch/njol/skript/expressions/ExprName.java @@ -4,13 +4,14 @@ import java.util.List; import ch.njol.skript.bukkitutil.InventoryUtils; -import ch.njol.skript.bukkitutil.ItemUtils; +import ch.njol.skript.lang.util.common.AnyNamed; import org.bukkit.Bukkit; import org.bukkit.GameRule; import org.bukkit.Nameable; import org.bukkit.OfflinePlayer; import org.bukkit.block.Block; import org.bukkit.block.BlockState; +import org.bukkit.command.CommandSender; import org.bukkit.entity.Entity; import org.bukkit.entity.HumanEntity; import org.bukkit.entity.LivingEntity; @@ -99,17 +100,16 @@ public class ExprName extends SimplePropertyExpression { @Nullable private static BungeeComponentSerializer serializer; - static final boolean HAS_GAMERULES; static { // Check for Adventure API if (Skript.classExists("net.kyori.adventure.text.Component") && Skript.methodExists(Bukkit.class, "createInventory", InventoryHolder.class, int.class, Component.class)) serializer = BungeeComponentSerializer.get(); - HAS_GAMERULES = Skript.classExists("org.bukkit.GameRule"); - register(ExprName.class, String.class, "(1¦name[s]|2¦(display|nick|chat|custom)[ ]name[s])", "offlineplayers/entities/blocks/itemtypes/inventories/slots/worlds" - + (HAS_GAMERULES ? "/gamerules" : "")); - register(ExprName.class, String.class, "(3¦(player|tab)[ ]list name[s])", "players"); + register(ExprName.class, String.class, "(1:name[s])", "offlineplayers/entities/inventories/named"); + register(ExprName.class, String.class, "(2:(display|nick|chat|custom)[ ]name[s])", "offlineplayers/entities/inventories/named"); + register(ExprName.class, String.class, "(3:(player|tab)[ ]list name[s])", "players"); + // we keep the entity input because we want to do something special with entities } /* @@ -123,54 +123,36 @@ public class ExprName extends SimplePropertyExpression { public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { mark = parseResult.mark; setExpr(exprs[0]); - if (mark != 1 && World.class.isAssignableFrom(getExpr().getReturnType())) { - Skript.error("Can't use 'display name' with worlds. Use 'name' instead."); - return false; - } return true; } @Override - @Nullable - public String convert(Object object) { - if (object instanceof OfflinePlayer && ((OfflinePlayer) object).isOnline()) - object = ((OfflinePlayer) object).getPlayer(); - - if (object instanceof Player) { - switch (mark) { - case 1: - return ((Player) object).getName(); - case 2: - return ((Player) object).getDisplayName(); - case 3: - return ((Player) object).getPlayerListName(); + public @Nullable String convert(Object object) { + if (object instanceof OfflinePlayer offlinePlayer) { + if (offlinePlayer.isOnline()) { // Defer to player check below + object = offlinePlayer.getPlayer(); + } else { // We can only support "name" + return mark == 1 ? offlinePlayer.getName() : null; } - } else if (object instanceof OfflinePlayer) { - return mark == 1 ? ((OfflinePlayer) object).getName() : null; - } else if (object instanceof Entity) { - return ((Entity) object).getCustomName(); - } else if (object instanceof Block) { - BlockState state = ((Block) object).getState(); - if (state instanceof Nameable) - return ((Nameable) state).getCustomName(); - } else if (object instanceof ItemType) { - ItemMeta m = ((ItemType) object).getItemMeta(); - return m.hasDisplayName() ? m.getDisplayName() : null; - } else if (object instanceof Inventory) { - Inventory inventory = (Inventory) object; + } + + if (object instanceof Player player) { + return switch (mark) { + case 1 -> player.getName(); + case 2 -> player.getDisplayName(); + case 3 -> player.getPlayerListName(); + default -> throw new IllegalStateException("Unexpected value: " + mark); + }; + } else if (object instanceof Nameable nameable) { + if (mark == 1 && nameable instanceof CommandSender sender) + return sender.getName(); + return nameable.getCustomName(); + } else if (object instanceof Inventory inventory) { if (inventory.getViewers().isEmpty()) return null; return InventoryUtils.getTitle(inventory.getViewers().get(0).getOpenInventory()); - } else if (object instanceof Slot) { - ItemStack is = ((Slot) object).getItem(); - if (is != null && is.hasItemMeta()) { - ItemMeta m = is.getItemMeta(); - return m.hasDisplayName() ? m.getDisplayName() : null; - } - } else if (object instanceof World) { - return ((World) object).getName(); - } else if (HAS_GAMERULES && object instanceof GameRule) { - return ((GameRule) object).getName(); + } else if (object instanceof AnyNamed named) { + return named.name(); } return null; } @@ -196,41 +178,32 @@ public Class[] acceptChange(ChangeMode mode) { public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { String name = delta != null ? (String) delta[0] : null; for (Object object : getExpr().getArray(event)) { - if (object instanceof Player) { + if (object instanceof Player player) { switch (mark) { case 2: - ((Player) object).setDisplayName(name != null ? name + ChatColor.RESET : ((Player) object).getName()); + player.setDisplayName(name != null ? name + ChatColor.RESET : ((Player) object).getName()); break; case 3: // Null check not necessary. This method will use the player's name if 'name' is null. - ((Player) object).setPlayerListName(name); + player.setPlayerListName(name); break; } - } else if (object instanceof Entity) { - ((Entity) object).setCustomName(name); + } else if (object instanceof Entity entity) { + entity.setCustomName(name); if (mark == 2 || mode == ChangeMode.RESET) // Using "display name" - ((Entity) object).setCustomNameVisible(name != null); - if (object instanceof LivingEntity) - ((LivingEntity) object).setRemoveWhenFarAway(name == null); - } else if (object instanceof Block) { - BlockState state = ((Block) object).getState(); - if (state instanceof Nameable) { - ((Nameable) state).setCustomName(name); - state.update(); - } - } else if (object instanceof ItemType) { - ItemType i = (ItemType) object; - ItemMeta m = i.getItemMeta(); - m.setDisplayName(name); - i.setItemMeta(m); - } else if (object instanceof Inventory) { - Inventory inv = (Inventory) object; - - if (inv.getViewers().isEmpty()) + entity.setCustomNameVisible(name != null); + if (object instanceof LivingEntity living) + living.setRemoveWhenFarAway(name == null); + } else if (object instanceof AnyNamed named) { + if (named.supportsNameChange()) + named.setName(name); + } else if (object instanceof Inventory inventory) { + + if (inventory.getViewers().isEmpty()) return; // Create a clone to avoid a ConcurrentModificationException - List viewers = new ArrayList<>(inv.getViewers()); + List viewers = new ArrayList<>(inventory.getViewers()); - InventoryType type = inv.getType(); + InventoryType type = inventory.getType(); if (!type.isCreatable()) return; @@ -239,9 +212,9 @@ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { if (name == null) name = type.getDefaultTitle(); if (type == InventoryType.CHEST) { - copy = Bukkit.createInventory(inv.getHolder(), inv.getSize(), name); + copy = Bukkit.createInventory(inventory.getHolder(), inventory.getSize(), name); } else { - copy = Bukkit.createInventory(inv.getHolder(), type, name); + copy = Bukkit.createInventory(inventory.getHolder(), type, name); } } else { Component component = type.defaultTitle(); @@ -250,22 +223,13 @@ public void change(Event event, @Nullable Object[] delta, ChangeMode mode) { component = serializer.deserialize(components); } if (type == InventoryType.CHEST) { - copy = Bukkit.createInventory(inv.getHolder(), inv.getSize(), component); + copy = Bukkit.createInventory(inventory.getHolder(), inventory.getSize(), component); } else { - copy = Bukkit.createInventory(inv.getHolder(), type, component); + copy = Bukkit.createInventory(inventory.getHolder(), type, component); } } - copy.setContents(inv.getContents()); + copy.setContents(inventory.getContents()); viewers.forEach(viewer -> viewer.openInventory(copy)); - } else if (object instanceof Slot) { - Slot s = (Slot) object; - ItemStack is = s.getItem(); - if (is != null && !ItemUtils.isAir(is.getType())) { - ItemMeta m = is.hasItemMeta() ? is.getItemMeta() : Bukkit.getItemFactory().getItemMeta(is.getType()); - m.setDisplayName(name); - is.setItemMeta(m); - s.setItem(is); - } } } } @@ -277,12 +241,11 @@ public Class getReturnType() { @Override protected String getPropertyName() { - switch (mark) { - case 1: return "name"; - case 2: return "display name"; - case 3: return "tablist name"; - default: return "name"; - } + return switch (mark) { + case 2 -> "display name"; + case 3 -> "tablist name"; + default -> "name"; + }; } } diff --git a/src/main/java/ch/njol/skript/lang/util/common/AnyAmount.java b/src/main/java/ch/njol/skript/lang/util/common/AnyAmount.java new file mode 100644 index 00000000000..9232f45826e --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/util/common/AnyAmount.java @@ -0,0 +1,49 @@ +package ch.njol.skript.lang.util.common; + +import org.jetbrains.annotations.NotNull; + +/** + * A provider for anything with a (number) amount/size. + * Anything implementing this (or convertible to this) can be used by the {@link ch.njol.skript.expressions.ExprAmount} + * property expression. + * + * @see AnyProvider + */ +@FunctionalInterface +public interface AnyAmount extends AnyProvider { + + /** + * @return This thing's amount/size + */ + @NotNull Number amount(); + + /** + * This is called before {@link #setAmount(Number)}. + * If the result is false, setting the name will never be attempted. + * + * @return Whether this supports being set + */ + default boolean supportsAmountChange() { + return false; + } + + /** + * The behaviour for changing this thing's name, if possible. + * If not possible, then {@link #supportsAmountChange()} should return false and this + * may throw an error. + * + * @param amount The name to change + * @throws UnsupportedOperationException If this is impossible + */ + default void setAmount(Number amount) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * @return Whether the amount of this is zero, i.e. empty + */ + default boolean isEmpty() { + return this.amount().intValue() == 0; + } + +} diff --git a/src/main/java/ch/njol/skript/lang/util/common/AnyContains.java b/src/main/java/ch/njol/skript/lang/util/common/AnyContains.java new file mode 100644 index 00000000000..c56472e9b55 --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/util/common/AnyContains.java @@ -0,0 +1,48 @@ +package ch.njol.skript.lang.util.common; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.UnknownNullability; + +/** + * A provider for anything that contains other things. + * Anything implementing this (or convertible to this) can be used by the {@link ch.njol.skript.conditions.CondContains} + * conditions. + * + * @see AnyProvider + */ +@FunctionalInterface +public interface AnyContains extends AnyProvider { + + /** + * If {@link #isSafeToCheck(Object)} returns false, values will not be passed to this + * method and will instead return false. + *
+ * The null-ness of the parameter depends on whether {@link #isSafeToCheck(Object)} permits null values. + * + * @param value The value to test + * @return Whether this contains {@code value} + */ + boolean contains(@UnknownNullability Type value); + + /** + * Objects are checked versus this before being cast for {@link #contains(Object)}. + * If your contains method doesn't accept all objects (e.g. for a {@link java.util.List#contains(Object)} call) + * then it can exclude unwanted types (or null values) here. + * + * @param value The value to check + * @return Whether the value is safe to call {@link #contains(Object)} with + */ + default boolean isSafeToCheck(Object value) { + return true; + } + + /** + * The internal method used to verify an object and then check its container. + */ + @ApiStatus.Internal + default boolean checkSafely(Object value) { + //noinspection unchecked + return this.isSafeToCheck(value) && this.contains((Type) value); + } + +} diff --git a/src/main/java/ch/njol/skript/lang/util/common/AnyNamed.java b/src/main/java/ch/njol/skript/lang/util/common/AnyNamed.java new file mode 100644 index 00000000000..4da763d229d --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/util/common/AnyNamed.java @@ -0,0 +1,42 @@ +package ch.njol.skript.lang.util.common; + +import org.jetbrains.annotations.UnknownNullability; + +/** + * A provider for anything with a (text) name. + * Anything implementing this (or convertible to this) can be used by the {@link ch.njol.skript.expressions.ExprName} + * property expression. + * + * @see AnyProvider + */ +@FunctionalInterface +public interface AnyNamed extends AnyProvider { + + /** + * @return This thing's name + */ + @UnknownNullability String name(); + + /** + * This is called before {@link #setName(String)}. + * If the result is false, setting the name will never be attempted. + * + * @return Whether this supports being set + */ + default boolean supportsNameChange() { + return false; + } + + /** + * The behaviour for changing this thing's name, if possible. + * If not possible, then {@link #supportsNameChange()} should return false and this + * may throw an error. + * + * @param name The name to change + * @throws UnsupportedOperationException If this is impossible + */ + default void setName(String name) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/ch/njol/skript/lang/util/common/AnyProvider.java b/src/main/java/ch/njol/skript/lang/util/common/AnyProvider.java new file mode 100644 index 00000000000..110a96ce810 --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/util/common/AnyProvider.java @@ -0,0 +1,28 @@ +package ch.njol.skript.lang.util.common; + +/** + * 'AnyProvider' types are holders for common properties (e.g. name, size) where + * it is highly likely that things other than Skript may wish to register + * exponents of the property. + *
+ *
+ * If possible, types should implement an {@link AnyProvider} subtype directly for + * the best possible parsing efficiency. + * However, implementing the interface may not be possible if: + *
    + *
  • registering an existing class from a third-party library
  • + *
  • the subtype getter method conflicts with the type's own methods + * or erasure
  • + *
  • the presence of the supertype might confuse the class's design
  • + *
+ * In these cases, a converter from the class to the AnyX type can be registered. + * The converter should not permit right-chaining or unsafe casts. + *
+ *
+ * The root provider supertype cannot include its own common methods, since these + * may conflict between things that provide two values (e.g. something declaring + * both a name and a size) + */ +public interface AnyProvider { + +} diff --git a/src/main/java/ch/njol/skript/util/slot/Slot.java b/src/main/java/ch/njol/skript/util/slot/Slot.java index 821dac0d750..515cdabfa46 100644 --- a/src/main/java/ch/njol/skript/util/slot/Slot.java +++ b/src/main/java/ch/njol/skript/util/slot/Slot.java @@ -1,32 +1,41 @@ package ch.njol.skript.util.slot; +import ch.njol.skript.aliases.Aliases; +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.bukkitutil.ItemUtils; +import ch.njol.skript.lang.util.common.AnyAmount; +import ch.njol.skript.lang.util.common.AnyNamed; +import org.bukkit.Bukkit; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; import org.jetbrains.annotations.Nullable; import ch.njol.skript.lang.Debuggable; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnknownNullability; /** * Represents a container for a single item. It could be an ordinary inventory * slot or perhaps an item frame. */ -public abstract class Slot implements Debuggable { - +public abstract class Slot implements Debuggable, AnyNamed, AnyAmount { + protected Slot() {} - + @Nullable public abstract ItemStack getItem(); - + public abstract void setItem(final @Nullable ItemStack item); - + public abstract int getAmount(); - + public abstract void setAmount(int amount); - + @Override public final String toString() { return toString(null, false); } - + /** * Checks if given slot is in same position with this. * Ignores slot contents. @@ -34,4 +43,52 @@ public final String toString() { * @return True if positions equal, false otherwise. */ public abstract boolean isSameSlot(Slot o); + + /** + * @return The name of the item in this slot + */ + @Override + public @UnknownNullability String name() { + ItemStack stack = this.getItem(); + if (stack != null && stack.hasItemMeta()) { + ItemMeta meta = stack.getItemMeta(); + return meta.hasDisplayName() ? meta.getDisplayName() : null; + } + return null; + } + + @Override + public boolean supportsNameChange() { + return true; + } + + /** + * @param name The name to change + */ + @Override + public void setName(String name) { + ItemStack stack = this.getItem(); + if (stack != null && !ItemUtils.isAir(stack.getType())) { + ItemMeta meta = stack.hasItemMeta() ? stack.getItemMeta() : Bukkit.getItemFactory().getItemMeta(stack.getType()); + meta.setDisplayName(name); + stack.setItemMeta(meta); + this.setItem(stack); + } + } + + @Override + public @NotNull Number amount() { + return this.getAmount(); + } + + @Override + public boolean supportsAmountChange() { + return true; + } + + @Override + public void setAmount(@Nullable Number amount) { + this.setAmount(amount != null ? amount.intValue() : 0); + } + } diff --git a/src/main/java/org/skriptlang/skript/lang/converter/Converter.java b/src/main/java/org/skriptlang/skript/lang/converter/Converter.java index cb7bc8d411e..6c7df376f45 100644 --- a/src/main/java/org/skriptlang/skript/lang/converter/Converter.java +++ b/src/main/java/org/skriptlang/skript/lang/converter/Converter.java @@ -19,15 +19,36 @@ public interface Converter { /** * A Converter flag declaring this Converter cannot be chained to another Converter. * This means that this Converter must be the beginning of a chain. + *
+ * Note: unchecked casts are not permitted before this converter + * (e.g. {@code Object} to {@param }). */ int NO_LEFT_CHAINING = 1; /** * A Converter flag declaring that another Converter cannot be chained to this Converter. * This means that this Converter must be the end of a chain. + *
+ * Note: unchecked casts are not permitted after this converter + * (e.g. {@param } to {@param }). */ int NO_RIGHT_CHAINING = 2; + /** + * A Converter flag declaring that the input/output of this can use an unchecked cast, + * when combined with {@link #NO_LEFT_CHAINING} or {@link #NO_RIGHT_CHAINING}. + *
+ * An unchecked cast would be {@code Number -> Integer}. (Not all numbers are integers, some are floats!) + *
+ *
+ * When combined with {@link #NO_RIGHT_CHAINING} the output can be conformed with an unchecked cast, + * e.g. {@code String -> Number (-> cast Integer)}. + *
+ * When combined with {@link #NO_RIGHT_CHAINING} the output can be conformed with an unchecked cast, + * e.g. {@code (cast Object ->) Integer -> String}. + */ + int ALLOW_UNSAFE_CASTS = 4; + /** * A Converter flag declaring that this Converter cannot be a part of a chain. */ diff --git a/src/main/java/org/skriptlang/skript/lang/converter/Converters.java b/src/main/java/org/skriptlang/skript/lang/converter/Converters.java index 1471d78580e..1efeb559676 100644 --- a/src/main/java/org/skriptlang/skript/lang/converter/Converters.java +++ b/src/main/java/org/skriptlang/skript/lang/converter/Converters.java @@ -286,8 +286,13 @@ private static Converte // Attempt to find converters that have either 'from' OR 'to' not exactly matching for (ConverterInfo unknownInfo : CONVERTERS) { + int flags = unknownInfo.getFlags(); if (unknownInfo.getFrom().isAssignableFrom(fromType) && unknownInfo.getTo().isAssignableFrom(toType)) { ConverterInfo info = (ConverterInfo) unknownInfo; + if ((flags & Converter.ALLOW_UNSAFE_CASTS) == 0) { + if ((flags & Converter.NO_RIGHT_CHAINING) == Converter.NO_RIGHT_CHAINING) + continue; + } // 'to' doesn't exactly match and needs to be filtered // Basically, this converter might convert 'F' into something that's shares a parent with 'T' @@ -301,6 +306,10 @@ private static Converte } else if (fromType.isAssignableFrom(unknownInfo.getFrom()) && toType.isAssignableFrom(unknownInfo.getTo())) { ConverterInfo info = (ConverterInfo) unknownInfo; + if ((flags & Converter.ALLOW_UNSAFE_CASTS) == 0) { + if ((flags & Converter.NO_LEFT_CHAINING) == Converter.NO_LEFT_CHAINING) + continue; + } // 'from' doesn't exactly match and needs to be filtered // Basically, this converter will only convert certain 'F' objects @@ -318,6 +327,13 @@ private static Converte for (ConverterInfo unknownInfo : CONVERTERS) { if (fromType.isAssignableFrom(unknownInfo.getFrom()) && unknownInfo.getTo().isAssignableFrom(toType)) { ConverterInfo info = (ConverterInfo) unknownInfo; + int flags = unknownInfo.getFlags(); + if ((flags & Converter.ALLOW_UNSAFE_CASTS) == 0) { + if ((flags & Converter.NO_LEFT_CHAINING) == Converter.NO_LEFT_CHAINING) + continue; + if ((flags & Converter.NO_RIGHT_CHAINING) == Converter.NO_RIGHT_CHAINING) + continue; + } // 'from' and 'to' both don't exactly match and need to be filtered // Basically, this converter will only convert certain 'F' objects diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index c35f69dfefc..28d608aa7a9 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2594,6 +2594,9 @@ types: experience.pattern: (e?xp|experience( points?)?) classinfo: type¦s @a visualeffect: visual effect¦s @a + named: named thing¦s @a + numbered: numbered thing¦s @a + containing: container¦s @a # Hooks money: money diff --git a/src/test/skript/tests/syntaxes/expressions/ExprAmount.sk b/src/test/skript/tests/syntaxes/expressions/ExprAmount.sk new file mode 100644 index 00000000000..d4cc336b031 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprAmount.sk @@ -0,0 +1,12 @@ +test "amount of objects": + set {_objects::*} to (1 and 2) + set {_amount} to amount of {_objects::*} + assert {_amount} is 2 with "was wrong" + set {_objects::*} to ("hello", "there" and 1) + set {_amount} to amount of {_objects::*} + assert {_amount} is 3 with "was wrong" + +test "amount of items": + assert amount of (3 of stone) is 3 with "was wrong" + set {_item} to 3 of stone + assert amount of {_item} is 3 with "was wrong" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprName.sk b/src/test/skript/tests/syntaxes/expressions/ExprName.sk new file mode 100644 index 00000000000..142e727de34 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprName.sk @@ -0,0 +1,26 @@ +test "name of world": + set {_thing} to the world "world" + assert name of {_thing} is "world" with "name was wrong" + set the name of {_thing} to "blob" + assert name of {_thing} is "world" with "world name changed" + +test "name of entity": + set {_before} to 5 + spawn a pig at spawn of "world": + assert event-entity is a pig with "entity not a pig" + set {_test} to event-entity + set event-entity's name to "foo" + assert {_test} is set with "entity not set" + assert {_test} is a pig with "entity variable not a pig" + assert event-entity's name is "foo" with "name didn't change" + assert {_test} exists with "entity didn't carry out" + assert {_test}'s name is "foo" with "name didn't carry out" + set {_test}'s name to "bar" + assert {_test}'s name is "bar" with "name didn't change" + delete the last spawned pig + +test "name of item": + set {_thing} to 3 of stone + assert name of {_thing} does not exist with "name was set" + set the name of {_thing} to "blob" + assert name of {_thing} is "blob" with "item name didn't change" diff --git a/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk b/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk index 30d4fc0cbf0..f19fdcd50c2 100644 --- a/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk +++ b/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk @@ -54,13 +54,13 @@ test "spawn salmon by variant" when running minecraft "1.21.2": set {_l} to test-location spawn 5 small salmon at {_l} assert size of all small salmons = 5 with "Size of all small salmons is not 5" - assert size of all salmons = 5 with "Size of all salmons is not 5" + assert size of all entities of type salmon = 5 with "Size of all salmons is not 5" spawn 3 medium salmon at {_l} assert size of all medium salmons = 3 with "Size of all medium salmons is not 3" - assert size of all salmons = 8 with "Size of all salmons is not 8" + assert size of all entities of type salmon = 8 with "Size of all salmons is not 8" spawn 2 large salmon at {_l} assert size of all large salmons = 2 with "Size of all large salmon is not 2" - assert size of all salmons = 10 with "Size of all salmon is not 10" + assert size of all entities of type salmon = 10 with "Size of all salmon is not 10" delete all large salmons assert size of all large salmons = 0 with "Large salmons did not get cleared" delete all medium salmons @@ -68,9 +68,9 @@ test "spawn salmon by variant" when running minecraft "1.21.2": delete all small salmons assert size of all small salmons = 0 with "Small salmons did not get cleared" spawn 15 of any salmon at {_l} - assert size of all salmons = 15 with "Size of all salmons is not 15" + assert size of all entities of type salmon = 15 with "Size of all salmons is not 15" clear all salmons - assert size of all salmons = 0 with "All salmons did not get cleared" + assert size of all entities of type salmon = 0 with "All salmons did not get cleared" test "spawn entities": set {_entities::*} to "allay", "axolotl", "bat", "bee", "blaze", "cat", "cave spider", "chicken" and "cod" From bc0c5eea6f14154aa489e1f94d2c598d696437d3 Mon Sep 17 00:00:00 2001 From: Shane Bee Date: Sun, 29 Dec 2024 14:03:25 -0800 Subject: [PATCH 11/19] 'sk test' command upgrades (#7308) * SkriptCommand - add tab completer for test scripts * SkriptCommand - add "all" option * SkriptCommand - fix issue with not grabbing from sub directories * SkriptCommand - fix a directory issue * SkriptCommand - change var name * SkriptCommand - cleaned this up * SkriptCommand - log results to file * SkriptCommand - Use suggested method * Skript - easier to read output for tests * SkriptCommand - fix unicode characters * SkriptCommand - adjust how we're grabbing test scripts * TestResults - color updates * TestResults - remove double append * SkriptCommand - requested changes --------- Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> Co-authored-by: cheeezburga <47320303+cheeezburga@users.noreply.github.com> --- src/main/java/ch/njol/skript/Skript.java | 6 ++- .../java/ch/njol/skript/SkriptCommand.java | 49 ++++++++++++++----- .../skript/SkriptCommandTabCompleter.java | 4 +- .../njol/skript/test/utils/TestResults.java | 6 +-- 4 files changed, 46 insertions(+), 19 deletions(-) diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 33adc41a12b..e425cb19c4e 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -69,6 +69,7 @@ import com.google.common.collect.Lists; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import org.bstats.bukkit.Metrics; import org.bstats.charts.SimplePie; import org.bukkit.*; @@ -795,7 +796,10 @@ protected void afterErrors() { EffObjectives.fail(); info("Collecting results to " + TestMode.RESULTS_FILE); - String results = new Gson().toJson(TestTracker.collectResults()); + String results = new GsonBuilder() + .setPrettyPrinting() // Easier to read lines + .disableHtmlEscaping() // Fixes issue with "'" character in test strings going unicode + .create().toJson(TestTracker.collectResults()); try { Files.write(TestMode.RESULTS_FILE, results.getBytes(StandardCharsets.UTF_8)); } catch (IOException e) { diff --git a/src/main/java/ch/njol/skript/SkriptCommand.java b/src/main/java/ch/njol/skript/SkriptCommand.java index 8be93a2de46..10470f67e06 100644 --- a/src/main/java/ch/njol/skript/SkriptCommand.java +++ b/src/main/java/ch/njol/skript/SkriptCommand.java @@ -14,12 +14,14 @@ import ch.njol.skript.test.runner.SkriptTestEvent; import ch.njol.skript.test.runner.TestMode; import ch.njol.skript.test.runner.TestTracker; +import ch.njol.skript.test.utils.TestResults; import ch.njol.skript.util.ExceptionUtils; import ch.njol.skript.util.FileUtils; import ch.njol.skript.util.SkriptColor; import ch.njol.skript.util.Utils; import ch.njol.util.OpenCloseable; import ch.njol.util.StringUtils; +import com.google.gson.GsonBuilder; import org.bukkit.Bukkit; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -32,7 +34,7 @@ import java.io.File; import java.io.FileFilter; import java.io.IOException; -import java.nio.file.Path; +import java.nio.file.Files; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -406,13 +408,15 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } } else { - scriptFile = TestMode.TEST_DIR.resolve( - Arrays.stream(args).skip(1).collect(Collectors.joining(" ")) + ".sk" - ).toFile(); - TestMode.lastTestFile = scriptFile; + if (args[1].equalsIgnoreCase("all")) { + scriptFile = TestMode.TEST_DIR.toFile(); + } else { + scriptFile = getScriptFromArgs(sender, args, TestMode.TEST_DIR.toFile()); + TestMode.lastTestFile = scriptFile; + } } - if (!scriptFile.exists()) { + if (scriptFile == null || !scriptFile.exists()) { Skript.error(sender, "Test script doesn't exist!"); return true; } @@ -425,10 +429,23 @@ public boolean onCommand(CommandSender sender, Command command, String label, St ScriptLoader.unloadScripts(ScriptLoader.getLoadedScripts()); // Get results and show them - String[] lines = TestTracker.collectResults().createReport().split("\n"); + TestResults testResults = TestTracker.collectResults(); + String[] lines = testResults.createReport().split("\n"); for (String line : lines) { Skript.info(sender, line); } + + // Log results to file + Skript.info(sender, "Collecting results to " + TestMode.RESULTS_FILE); + String results = new GsonBuilder() + .setPrettyPrinting() // Easier to read lines + .disableHtmlEscaping() // Fixes issue with "'" character in test strings going unicode + .create().toJson(testResults); + try { + Files.writeString(TestMode.RESULTS_FILE, results); + } catch (IOException e) { + Skript.exception(e, "Failed to write test results."); + } }) ); } else if (args[0].equalsIgnoreCase("list") || args[0].equalsIgnoreCase("show")) { @@ -462,10 +479,13 @@ public boolean onCommand(CommandSender sender, Command command, String label, St private static final ArgsMessage m_invalid_script = new ArgsMessage(CONFIG_NODE + ".invalid script"); private static final ArgsMessage m_invalid_folder = new ArgsMessage(CONFIG_NODE + ".invalid folder"); - @Nullable - private static File getScriptFromArgs(CommandSender sender, String[] args) { + private static @Nullable File getScriptFromArgs(CommandSender sender, String[] args) { + return getScriptFromArgs(sender, args, Skript.getInstance().getScriptsFolder()); + } + + private static @Nullable File getScriptFromArgs(CommandSender sender, String[] args, File directoryFile) { String script = StringUtils.join(args, " ", 1, args.length); - File f = getScriptFromName(script); + File f = getScriptFromName(script, directoryFile); if (f == null) { // Always allow '/' and '\' regardless of OS boolean directory = script.endsWith("/") || script.endsWith("\\") || script.endsWith(File.separator); @@ -475,8 +495,11 @@ private static File getScriptFromArgs(CommandSender sender, String[] args) { return f; } - @Nullable - public static File getScriptFromName(String script) { + public static @Nullable File getScriptFromName(String script) { + return getScriptFromName(script, Skript.getInstance().getScriptsFolder()); + } + + public static @Nullable File getScriptFromName(String script, File directoryFile) { if (script.endsWith("/") || script.endsWith("\\")) { // Always allow '/' and '\' regardless of OS script = script.replace('/', File.separatorChar).replace('\\', File.separatorChar); } else if (!StringUtils.endsWithIgnoreCase(script, ".sk")) { @@ -489,7 +512,7 @@ public static File getScriptFromName(String script) { if (script.startsWith(ScriptLoader.DISABLED_SCRIPT_PREFIX)) script = script.substring(ScriptLoader.DISABLED_SCRIPT_PREFIX_LENGTH); - File scriptFile = new File(Skript.getInstance().getScriptsFolder(), script); + File scriptFile = new File(directoryFile, script); if (!scriptFile.exists()) { scriptFile = new File(scriptFile.getParentFile(), ScriptLoader.DISABLED_SCRIPT_PREFIX + scriptFile.getName()); if (!scriptFile.exists()) { diff --git a/src/main/java/ch/njol/skript/SkriptCommandTabCompleter.java b/src/main/java/ch/njol/skript/SkriptCommandTabCompleter.java index 2fa8d5f0b4c..009a0c76f10 100644 --- a/src/main/java/ch/njol/skript/SkriptCommandTabCompleter.java +++ b/src/main/java/ch/njol/skript/SkriptCommandTabCompleter.java @@ -28,8 +28,8 @@ public List onTabComplete(CommandSender sender, Command command, String if (args[0].equalsIgnoreCase("update") && args.length == 2) { options.add("check"); options.add("changes"); - } else if (args[0].matches("(?i)(reload|disable|enable)") && args.length >= 2) { - File scripts = Skript.getInstance().getScriptsFolder(); + } else if (args[0].matches("(?i)(reload|disable|enable|test)") && args.length >= 2) { + File scripts = TestMode.DEV_MODE ? TestMode.TEST_DIR.toFile() : Skript.getInstance().getScriptsFolder(); String scriptsPathString = scripts.toPath().toString(); int scriptsPathLength = scriptsPathString.length(); diff --git a/src/main/java/ch/njol/skript/test/utils/TestResults.java b/src/main/java/ch/njol/skript/test/utils/TestResults.java index 0d82cb1ceda..b6246148f50 100644 --- a/src/main/java/ch/njol/skript/test/utils/TestResults.java +++ b/src/main/java/ch/njol/skript/test/utils/TestResults.java @@ -45,10 +45,10 @@ public boolean docsFailed() { public String createReport() { StringBuilder sb = new StringBuilder("Succeeded:\n"); for (String test : succeeded) - sb.append(test).append('\n'); - sb.append("Failed:\n"); + sb.append("").append(test).append('\n'); + sb.append("Failed:\n"); for (Map.Entry entry : failed.entrySet()) - sb.append(entry.getKey()).append(": ").append(entry.getValue()).append('\n'); + sb.append("").append(entry.getKey()).append(": ").append(entry.getValue()).append('\n'); return sb.toString(); } From e033942186a407b2bf2f4727e4c251c43b1f94fd Mon Sep 17 00:00:00 2001 From: Shane Bee Date: Sun, 29 Dec 2024 15:24:05 -0800 Subject: [PATCH 12/19] SkriptCommand - fix sending hidden files (#7315) * SkriptCommand - fix sending hidden files * SkriptCommand - fix directory issue * SkriptCommand - only show scripts * SkriptCommand - fix spacing --------- Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com> --- src/main/java/ch/njol/skript/SkriptCommand.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/njol/skript/SkriptCommand.java b/src/main/java/ch/njol/skript/SkriptCommand.java index 10470f67e06..99d43150869 100644 --- a/src/main/java/ch/njol/skript/SkriptCommand.java +++ b/src/main/java/ch/njol/skript/SkriptCommand.java @@ -35,6 +35,7 @@ import java.io.FileFilter; import java.io.IOException; import java.nio.file.Files; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -457,7 +458,7 @@ public boolean onCommand(CommandSender sender, Command command, String label, St ScriptLoader.getDisabledScripts().stream() .flatMap(file -> { if (file.isDirectory()) { - return Arrays.stream(file.listFiles()); + return getSubFiles(file).stream(); } return Arrays.stream(new File[]{file}); }) @@ -479,6 +480,20 @@ public boolean onCommand(CommandSender sender, Command command, String label, St private static final ArgsMessage m_invalid_script = new ArgsMessage(CONFIG_NODE + ".invalid script"); private static final ArgsMessage m_invalid_folder = new ArgsMessage(CONFIG_NODE + ".invalid folder"); + private static List getSubFiles(File file) { + List files = new ArrayList<>(); + if (file.isDirectory()) { + for (File listFile : file.listFiles(f -> !f.isHidden())) { + if (listFile.isDirectory()) { + files.addAll(getSubFiles(listFile)); + } else if (listFile.getName().endsWith(".sk")) { + files.add(listFile); + } + } + } + return files; + } + private static @Nullable File getScriptFromArgs(CommandSender sender, String[] args) { return getScriptFromArgs(sender, args, Skript.getInstance().getScriptsFolder()); } From b7bf6465b75b0de8370e15dab23d6963b7a56e21 Mon Sep 17 00:00:00 2001 From: Shane Bee Date: Sun, 29 Dec 2024 15:51:09 -0800 Subject: [PATCH 13/19] ExprVillagerLevel - add experience (#7318) * ExprVillagerLevel - forgot to add experience * ExprVillagerLevel.sk - add more experience tests * ExprVillagerLevel - requested changes * ExprVillagerLevel - requested changes * ExprVillagerLevel - remove unneeded comma * ExprVillagerLevel - update link --------- Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> --- .../skript/expressions/ExprVillagerLevel.java | 62 ++++++++++++++----- .../syntaxes/expressions/ExprVillagerLevel.sk | 15 +++++ 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/main/java/ch/njol/skript/expressions/ExprVillagerLevel.java b/src/main/java/ch/njol/skript/expressions/ExprVillagerLevel.java index d57be3c4fc5..5878d82f868 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprVillagerLevel.java +++ b/src/main/java/ch/njol/skript/expressions/ExprVillagerLevel.java @@ -7,6 +7,9 @@ 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.Math2; import ch.njol.util.coll.CollectionUtils; import org.bukkit.entity.LivingEntity; @@ -14,17 +17,22 @@ import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; -@Name("Villager Level") +@Name("Villager Level/Experience") @Description({ - "Represents the level of a villager.", - "Level must be between 1 and 5, with 1 being the default level.", - "Do note when a villager's level is 1, they may lose their profession."}) + "Represents the level/experience of a villager.", + "The level will determine which trades are available to players (value between 1 and 5, defaults to 1).", + "When a villager's level is 1, they may lose their profession if they don't have a workstation.", + "Experience works along with the leveling system, determining which level the villager will move to.", + "Experience must be greater than or equal to 0.", + "Learn more about villager levels on Minecraft Wiki" +}) @Examples({ "set {_level} to villager level of {_villager}", "set villager level of last spawned villager to 2", "add 1 to villager level of target entity", "remove 1 from villager level of event-entity", - "reset villager level of event-entity" + "reset villager level of event-entity", + "set villager experience of last spawned entity to 100" }) @Since("INSERT VERSION") public class ExprVillagerLevel extends SimplePropertyExpression { @@ -32,21 +40,28 @@ public class ExprVillagerLevel extends SimplePropertyExpression[] expressions, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + this.experience = parseResult.hasTag("experience"); + return super.init(expressions, matchedPattern, isDelayed, parseResult); } @Override public @Nullable Number convert(LivingEntity from) { if (from instanceof Villager villager) - return villager.getVillagerLevel(); + return experience ? villager.getVillagerExperience() : villager.getVillagerLevel(); return null; } @Override public Class @Nullable [] acceptChange(ChangeMode mode) { return switch (mode) { - case SET, ADD, REMOVE, RESET -> - CollectionUtils.array(Number.class); + case SET, ADD, REMOVE, RESET -> CollectionUtils.array(Number.class); default -> null; }; } @@ -59,16 +74,29 @@ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { for (LivingEntity livingEntity : getExpr().getArray(event)) { if (!(livingEntity instanceof Villager villager)) continue; - int previousLevel = villager.getVillagerLevel(); + int minLevel; + int maxLevel; + int previousAmount; + if (experience) { + minLevel = 0; + maxLevel = Integer.MAX_VALUE; + previousAmount = villager.getVillagerExperience(); + } else { + minLevel = 1; + maxLevel = 5; + previousAmount = villager.getVillagerLevel(); + } int newLevel = switch (mode) { case SET -> changeValue; - case ADD -> previousLevel + changeValue; - case REMOVE -> previousLevel - changeValue; - default -> 1; + case ADD -> previousAmount + changeValue; + case REMOVE -> previousAmount - changeValue; + default -> minLevel; }; - newLevel = Math2.fit(1, newLevel, 5); - if (newLevel > previousLevel && HAS_INCREASE_METHOD) { - int increase = Math2.fit(1, newLevel - previousLevel, 5); + newLevel = Math2.fit(minLevel, newLevel, maxLevel); + if (experience) { + villager.setVillagerExperience(newLevel); + } else if (newLevel > previousAmount && HAS_INCREASE_METHOD) { + int increase = Math2.fit(minLevel, newLevel - previousAmount, maxLevel); // According to the docs for this method: // Increases the level of this villager. // The villager will also unlock new recipes unlike the raw 'setVillagerLevel' method @@ -81,7 +109,7 @@ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { @Override protected String getPropertyName() { - return "villager level"; + return "villager " + (experience ? "experience" : "level"); } @Override diff --git a/src/test/skript/tests/syntaxes/expressions/ExprVillagerLevel.sk b/src/test/skript/tests/syntaxes/expressions/ExprVillagerLevel.sk index 2884356cfe0..fd1060c0c5d 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprVillagerLevel.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprVillagerLevel.sk @@ -2,6 +2,7 @@ test "villager level expression": spawn a villager at event-location: set {_e} to entity + # Level assert villager level of {_e} = 1 with "Villager should start out with a level of 1" set villager level of {_e} to 2 assert villager level of {_e} = 2 with "Villager level should now be 2" @@ -14,5 +15,19 @@ test "villager level expression": reset villager level of {_e} assert villager level of {_e} = 1 with "Villager level should reset back to 1" + # Experience + set villager experience of {_e} to 0 + assert villager experience of {_e} = 0 with "Villager experience should be 0" + set villager experience of {_e} to 100 + assert villager experience of {_e} = 100 with "Villager experience should be 100 after setting" + remove 1000 from villager experience of {_e} + assert villager experience of {_e} = 0 with "Villager experience should be 0 after removing" + add 100 to villager experience of {_e} + assert villager experience of {_e} = 100 with "Villager experience should be 100 after adding" + remove 20 from villager experience of {_e} + assert villager experience of {_e} = 80 with "Villager experience should be 80 after removing" + reset villager experience of {_e} + assert villager experience of {_e} = 0 with "Villager experience should have been reset to 0" + # Thank you for your service delete entity within {_e} From b56d33d3c636190d695ca59bdc866ef9f8cec5bc Mon Sep 17 00:00:00 2001 From: SirSmurfy2 <82696841+TheAbsolutionism@users.noreply.github.com> Date: Sun, 29 Dec 2024 19:08:41 -0500 Subject: [PATCH 14/19] Github Issue Update (#7313) Initial Commit Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 08a6df7b665..677d0ce8a8b 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -7,6 +7,8 @@ body: ## Guidelines Please make sure you are running the latest version of Skript on a supported server platform and version. Try to make sure there are no issues of this same problem currently open either. + As of the release of Skript 2.10, the oldest supported version has been raised to 1.19.4. + Any issues created for versions older than 1.19.4 will not be looked into or fixed unless the issue persists on supported versions. - type: textarea attributes: label: Skript/Server Version From cca446dd040f5f60ac3cb1517d5aec9ce23bf37f Mon Sep 17 00:00:00 2001 From: _tud <98935832+UnderscoreTud@users.noreply.github.com> Date: Mon, 30 Dec 2024 09:24:05 +0300 Subject: [PATCH 15/19] Fix expression conversion (#7165) * Fix expression conversion * Extract duplicate code into a separate helper method * improve conversion strategy * Add .sk to test file * Simplify conversion usage We need to use conversion whenever there are multiple return types. If the expression does not accept our supertype, then we can attempt to convert it, which will already handle safety checks for multiple return types * SimpleExpression: fix possible return type conversion This fixes SimpleExpression not converting possible return types that are not contained in the desired types array. For example, if an expression can return a Number or a String, and we want an Expression that is a Number or an World, it will now include converters for String->Number and String->World * Use safety checks of ConvertedExpression * Remove incorrect converter remake * Move logic from SimpleExpression to ConvertedExpression --------- Co-authored-by: APickledWalrus Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> Co-authored-by: Moderocky --- .../ch/njol/skript/lang/SkriptParser.java | 10 ++--- .../skript/lang/util/ConvertedExpression.java | 40 +++++++++++-------- .../skript/lang/util/SimpleExpression.java | 30 -------------- ...7164-expressions sometimes dont convert.sk | 13 ++++++ 4 files changed, 42 insertions(+), 51 deletions(-) create mode 100644 src/test/skript/tests/regressions/7164-expressions sometimes dont convert.sk diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index cefa92b2549..6cffb2a30fd 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -326,7 +326,6 @@ private static Variable parseVariable(String expr, Class[] r } } - @Nullable @SuppressWarnings({"unchecked", "rawtypes"}) private Expression parseSingleExpr(boolean allowUnparsedLiteral, @Nullable LogEntry error, Class... types) { @@ -368,9 +367,9 @@ private Expression parseSingleExpr(boolean allowUnparsedLiteral if ((flags & PARSE_EXPRESSIONS) != 0) { Expression parsedExpression = parseExpression(types, expr); if (parsedExpression != null) { // Expression/VariableString parsing success + Class parsedReturnType = parsedExpression.getReturnType(); for (Class type : types) { - // Check return type against everything that expression accepts - if (parsedExpression.canReturn(type)) { + if (type.isAssignableFrom(parsedReturnType)) { log.printLog(); return (Expression) parsedExpression; } @@ -540,13 +539,14 @@ private Expression parseSingleExpr(boolean allowUnparsedLiteral, @Nullable Lo if ((flags & PARSE_EXPRESSIONS) != 0) { Expression parsedExpression = parseExpression(types, expr); if (parsedExpression != null) { // Expression/VariableString parsing success + Class parsedReturnType = parsedExpression.getReturnType(); for (int i = 0; i < types.length; i++) { Class type = types[i]; if (type == null) // Ignore invalid (null) types continue; - // Check return type against everything that expression accepts - if (parsedExpression.canReturn(type)) { + // Check return type against the expression's return type + if (type.isAssignableFrom(parsedReturnType)) { if (!exprInfo.isPlural[i] && !parsedExpression.isSingle()) { // Wrong number of arguments if (context == ParseContext.COMMAND) { Skript.error(Commands.m_too_many_arguments.toString(exprInfo.classes[i].getName().getIndefiniteArticle(), exprInfo.classes[i].getName().toString()), ErrorQuality.SEMANTIC_ERROR); diff --git a/src/main/java/ch/njol/skript/lang/util/ConvertedExpression.java b/src/main/java/ch/njol/skript/lang/util/ConvertedExpression.java index 4d6228112f0..e6150fda79c 100644 --- a/src/main/java/ch/njol/skript/lang/util/ConvertedExpression.java +++ b/src/main/java/ch/njol/skript/lang/util/ConvertedExpression.java @@ -24,6 +24,7 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.Utils; import ch.njol.util.Checker; import ch.njol.util.Kleenean; import ch.njol.util.coll.CollectionUtils; @@ -96,24 +97,31 @@ public ConvertedExpression(Expression source, Class to, Collecti @Nullable public static ConvertedExpression newInstance(Expression from, Class... to) { assert !CollectionUtils.containsSuperclass(to, from.getReturnType()); - // we track a list of converters that may work - List> converters = new ArrayList<>(); - for (Class type : to) { // REMIND try more converters? -> also change WrapperExpression (and maybe ExprLoopValue) - assert type != null; - // casting to is wrong, but since the converter is only used for values returned by the expression - // (which are instances of "") this won't result in any ClassCastExceptions. - for (Class checking : from.possibleReturnTypes()) { - //noinspection unchecked - ConverterInfo converter = (ConverterInfo) Converters.getConverterInfo(checking, type); - if (converter != null) - converters.add(converter); + + // we might be able to cast some (or all) of the possible return types to T + // for possible return types that can't be directly cast, regular converters will be used + List> infos = new ArrayList<>(); + for (Class type : from.possibleReturnTypes()) { + if (CollectionUtils.containsSuperclass(to, type)) { // this type is of T, build a converter simply casting + // noinspection unchecked - 'type' is a desired type in 'to' + Class toType = (Class) type; + infos.add(new ConverterInfo<>(type, toType, toType::cast, 0)); + } else { // this possible return type is not included in 'to' + // build all converters for converting the possible return type into any of the types of 'to' + for (Class toType : to) { + ConverterInfo converter = Converters.getConverterInfo(type, toType); + if (converter != null) + infos.add(converter); + } } - int size = converters.size(); - if (size == 1) // if there is only one info, there is no need to wrap it in a list - return new ConvertedExpression<>(from, type, converters.get(0)); - if (size > 1) - return new ConvertedExpression<>(from, type, converters, true); } + if (!infos.isEmpty()) { // there are converters for (at least some of) the return types + // a note: casting to is wrong, but since the converter is used only for values + // returned by the expression (which are instances of ), this won't result in any CCEs + // noinspection rawtypes, unchecked + return new ConvertedExpression(from, Utils.getSuperType(infos.stream().map(ConverterInfo::getTo).toArray(Class[]::new)), infos, true); + } + return null; } diff --git a/src/main/java/ch/njol/skript/lang/util/SimpleExpression.java b/src/main/java/ch/njol/skript/lang/util/SimpleExpression.java index c576cd8d774..eccb3c73ba5 100644 --- a/src/main/java/ch/njol/skript/lang/util/SimpleExpression.java +++ b/src/main/java/ch/njol/skript/lang/util/SimpleExpression.java @@ -35,13 +35,10 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.converter.Converter; -import org.skriptlang.skript.lang.converter.ConverterInfo; import java.lang.reflect.Array; -import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; -import java.util.List; /** * An implementation of the {@link Expression} interface. You should usually extend this class to make a new expression. @@ -206,33 +203,6 @@ public Expression getConvertedExpression(Class... to) { // check whether this expression is already of type R if (CollectionUtils.containsSuperclass(to, getReturnType())) return (Expression) this; - - // we might be to cast some of the possible return types to R - List> infos = new ArrayList<>(); - for (Class type : this.possibleReturnTypes()) { - if (CollectionUtils.containsSuperclass(to, type)) { // this type is of R - // build a converter that for casting to R - // safety check is present in the event that we do not get this type at runtime - final Class toType = (Class) type; - infos.add(new ConverterInfo<>(getReturnType(), toType, fromObject -> { - if (toType.isInstance(fromObject)) - return (R) fromObject; - return null; - }, 0)); - } - } - int size = infos.size(); - if (size == 1) { // if there is only one info, there is no need to wrap it in a list - ConverterInfo info = infos.get(0); - //noinspection rawtypes - return new ConvertedExpression(this, info.getTo(), info); - } - if (size > 1) { - //noinspection rawtypes - return new ConvertedExpression(this, Utils.getSuperType(infos.stream().map(ConverterInfo::getTo).toArray(Class[]::new)), infos, false); - } - - // attempt traditional conversion with proper converters return this.getConvertedExpr(to); } diff --git a/src/test/skript/tests/regressions/7164-expressions sometimes dont convert.sk b/src/test/skript/tests/regressions/7164-expressions sometimes dont convert.sk new file mode 100644 index 00000000000..f7954b3a642 --- /dev/null +++ b/src/test/skript/tests/regressions/7164-expressions sometimes dont convert.sk @@ -0,0 +1,13 @@ +test "expressions sometimes dont convert": + assert formatted ({_foo} + {_bar}) is not set with "formatting nothing shouldn't throw an error" + + set {_foo} to "test" + assert formatted ({_foo} + {_bar}) is not set with "formatting string + none shouldn't throw an error" + + set {_foo} to 1 + set {_bar} to 2 + assert formatted ({_foo} + {_bar}) is not set with "formatting number + number shouldn't throw an error" + + set {_foo} to "foo" + set {_bar} to "bar" + assert formatted ({_foo} + {_bar}) is "foobar" with "formatting strings doesn't work" From 86c4fcd83e6436f55f818050a477ba45ebb38037 Mon Sep 17 00:00:00 2001 From: SirSmurfy2 <82696841+TheAbsolutionism@users.noreply.github.com> Date: Mon, 30 Dec 2024 02:17:47 -0500 Subject: [PATCH 16/19] Banner Patterns (#7216) * Starter Commit * Fix Indentation * Fix Java17 * Requested Changes * Requested Changes * Fix ClassInfo * Fix older versions * Hopefully Fixed * Requested Changes * Requested Changes * Final Changes? * Fix toString * Literal toString * Literal#isSingle * Fix Test * Requested Changes --- .../skript/classes/data/BukkitClasses.java | 34 +++ .../skript/expressions/ExprBannerItem.java | 146 +++++++++ .../expressions/ExprBannerPatterns.java | 281 ++++++++++++++++++ .../njol/skript/expressions/ExprColorOf.java | 156 +++++----- .../expressions/ExprNewBannerPattern.java | 83 ++++++ src/main/resources/lang/default.lang | 55 ++++ .../syntaxes/expressions/ExprBannerItem.sk | 7 + .../expressions/ExprNewBannerPattern.sk | 38 +++ 8 files changed, 732 insertions(+), 68 deletions(-) create mode 100644 src/main/java/ch/njol/skript/expressions/ExprBannerItem.java create mode 100644 src/main/java/ch/njol/skript/expressions/ExprBannerPatterns.java create mode 100644 src/main/java/ch/njol/skript/expressions/ExprNewBannerPattern.java create mode 100644 src/test/skript/tests/syntaxes/expressions/ExprBannerItem.sk create mode 100644 src/test/skript/tests/syntaxes/expressions/ExprNewBannerPattern.sk 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 6acf2d9be75..a97c3fc14bc 100644 --- a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java @@ -45,6 +45,7 @@ import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.block.DoubleChest; +import org.bukkit.block.banner.PatternType; import org.bukkit.block.data.BlockData; import org.bukkit.command.CommandSender; import org.bukkit.enchantments.Enchantment; @@ -1568,6 +1569,39 @@ public String toVariableNameString(EnchantmentOffer eo) { ); } + Classes.registerClass(new ClassInfo<>(org.bukkit.block.banner.Pattern.class, "bannerpattern") + .user("banner ?patterns?") + .name("Banner Pattern") + .description("Represents a banner pattern.") + .since("INSERT VERSION") + ); + + ClassInfo patternTypeInfo; + Registry patternRegistry = Bukkit.getRegistry(PatternType.class); + if (patternRegistry != null) { + patternTypeInfo = new RegistryClassInfo<>(PatternType.class, patternRegistry, "bannerpatterntype", "banner pattern types"); + } else { + try { + Class patternClass = Class.forName("org.bukkit.block.banner.PatternType"); + if (patternClass.isEnum()) { + //noinspection unchecked,rawtypes + Class enumClass = (Class) patternClass; + //noinspection rawtypes,unchecked + patternTypeInfo = new EnumClassInfo<>(enumClass, "bannerpatterntype", "banner pattern types"); + } else { + throw new IllegalStateException("PatternType is neither an enum nor a valid registry."); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + Classes.registerClass(patternTypeInfo + .user("banner ?pattern ?types?") + .name("Banner Pattern Type") + .description("Represents the various banner patterns that can be applied to a banner.") + .since("INSERT VERSION") + ); + } } diff --git a/src/main/java/ch/njol/skript/expressions/ExprBannerItem.java b/src/main/java/ch/njol/skript/expressions/ExprBannerItem.java new file mode 100644 index 00000000000..cbbdce5ac3e --- /dev/null +++ b/src/main/java/ch/njol/skript/expressions/ExprBannerItem.java @@ -0,0 +1,146 @@ +package ch.njol.skript.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Registry; +import org.bukkit.block.banner.PatternType; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.InvocationTargetException; +import java.util.*; + +@Name("Banner Pattern Item") +@Description({ + "Gets the item from a banner pattern type.", + "Note that not all banner pattern types have an item.", +}) +@Examples({ + "set {_item} to creeper charged banner pattern item", + "set {_item} to snout banner pattern item", + "set {_item} to thing banner pattern item" +}) +@Since("INSERT VERSION") +public class ExprBannerItem extends SimpleExpression { + + private static final Map bannerMaterials = new HashMap<>(); + private static boolean PATTERN_TYPE_IS_REGISTRY = false; + + static { + Registry patternRegistry = Bukkit.getRegistry(PatternType.class); + Object[] bannerPatterns; + if (patternRegistry != null) { + bannerPatterns = patternRegistry.stream().toArray(); + PATTERN_TYPE_IS_REGISTRY = true; + } else { + try { + Class patternClass = Class.forName("org.bukkit.block.banner.PatternType"); + if (patternClass.isEnum()) { + //noinspection unchecked,rawtypes + Class enumClass = (Class) patternClass; + bannerPatterns = enumClass.getEnumConstants(); + } else { + throw new IllegalStateException("PatternType is neither an enum nor a valid registry."); + } + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + if (bannerPatterns != null) { + for (Object object : bannerPatterns) { + Material material = getMaterial(object); + if (material != null) + bannerMaterials.put(object, material); + } + Skript.registerExpression(ExprBannerItem.class, ItemType.class, ExpressionType.COMBINED, + "[a[n]] %*bannerpatterntypes% item[s]"); + } + } + + private PatternType[] patternTypes; + private Literal literalPattern; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + //noinspection unchecked + literalPattern = (Literal) exprs[0]; + patternTypes = literalPattern.getArray(); + for (PatternType type : patternTypes) { + if (!bannerMaterials.containsKey(type)) { + Skript.error("There is no item for the banner pattern type '" + type + "'."); + return false; + } + } + return true; + } + + @Override + protected ItemType @Nullable [] get(Event event) { + List itemTypes = new ArrayList<>(); + for (PatternType type : patternTypes) { + Material material = bannerMaterials.get(type); + ItemType itemType = new ItemType(material); + itemTypes.add(itemType); + } + return itemTypes.toArray(new ItemType[0]); + } + + @Override + public boolean isSingle() { + return literalPattern.isSingle(); + } + + @Override + public Class getReturnType() { + return ItemType.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return literalPattern.toString(event, debug) + " items"; + } + + private static @Nullable Material getMaterial(Object object) { + if (!(object instanceof PatternType patternType)) + return null; + String key = null; + try { + if (PATTERN_TYPE_IS_REGISTRY) { + NamespacedKey namespacedKey = (NamespacedKey) PatternType.class.getMethod("getKey").invoke(patternType); + if (namespacedKey != null) + key = namespacedKey.getKey().toUpperCase(Locale.ENGLISH); + } else { + key = (String) PatternType.class.getMethod("toString").invoke(patternType); + } + } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + if (key == null) + return null; + Material material = Material.getMaterial(key + "_BANNER_PATTERN"); + return material != null ? material : checkAlias(patternType); + } + + private static @Nullable Material checkAlias(PatternType patternType) { + if (patternType == PatternType.BRICKS && Material.getMaterial("FIELD_MASONED_BANNER_PATTERN") != null) { + return Material.FIELD_MASONED_BANNER_PATTERN; + } else if (patternType == PatternType.BORDER && Material.getMaterial("BORDURE_INDENTED_BANNER_PATTER") != null) { + return Material.BORDURE_INDENTED_BANNER_PATTERN; + } + return null; + } + +} diff --git a/src/main/java/ch/njol/skript/expressions/ExprBannerPatterns.java b/src/main/java/ch/njol/skript/expressions/ExprBannerPatterns.java new file mode 100644 index 00000000000..cdd45e189ef --- /dev/null +++ b/src/main/java/ch/njol/skript/expressions/ExprBannerPatterns.java @@ -0,0 +1,281 @@ +package ch.njol.skript.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.bukkitutil.ItemUtils; +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.PropertyExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; +import ch.njol.skript.util.slot.Slot; +import ch.njol.util.Kleenean; +import ch.njol.util.coll.CollectionUtils; +import org.bukkit.DyeColor; +import org.bukkit.block.Banner; +import org.bukkit.block.Block; +import org.bukkit.block.banner.Pattern; +import org.bukkit.block.banner.PatternType; +import org.bukkit.event.Event; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BannerMeta; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +@Name("Banner Patterns") +@Description({ + "Gets or sets the banner patterns of a banner.", + "In order to set a specific position of a banner, there needs to be that many patterns already on the banner.", + "This expression will add filler patterns to the banner to allow the specified position to be set.", + "For Example, setting the 3rd banner pattern of a banner that has no patterns on it, will internally add 3 base patterns, " + + "allowing the 3rd banner pattern to be set." +}) +@Examples({ + "broadcast banner patterns of {_banneritem}", + "broadcast 1st banner pattern of block at location(0,0,0)", + "clear banner patterns of {_banneritem}" +}) +@Since("INSERT VERSION") +public class ExprBannerPatterns extends PropertyExpression { + + static { + Skript.registerExpression(ExprBannerPatterns.class, Pattern.class, ExpressionType.PROPERTY, + "[all [[of] the]|the] banner pattern[s] of %itemstacks/itemtypes/slots/blocks%", + "%itemstacks/itemtypes/slots/blocks%'[s] banner pattern[s]", + "[the] %integer%[st|nd|rd|th] [banner] pattern of %itemstacks/itemtypes/slots/blocks%", + "%itemstacks/itemtypes/slots/blocks%'[s] %integer%[st|nd|rd|th] [banner] pattern" + ); + } + + private Expression objects; + private Expression patternNumber = null; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + if (matchedPattern <= 1) { + objects = exprs[0]; + } else if (matchedPattern == 2) { + //noinspection unchecked + patternNumber = (Expression) exprs[0]; + objects = exprs[1]; + } else { + //noinspection unchecked + patternNumber = (Expression) exprs[1]; + objects = exprs[0]; + } + setExpr(objects); + return true; + } + + @Override + protected Pattern @Nullable [] get(Event event, Object[] source) { + List patterns = new ArrayList<>(); + Integer placement = patternNumber != null ? patternNumber.getSingle(event) : null; + for (Object object : objects.getArray(event)) { + if (object instanceof Block block) { + if (!(block.getState() instanceof Banner banner)) + continue; + if (placement != null && banner.numberOfPatterns() >= placement) { + patterns.add(banner.getPattern(placement - 1)); + } else if (placement == null) { + patterns.addAll(banner.getPatterns()); + } + } else { + ItemStack itemStack = ItemUtils.asItemStack(object); + if (itemStack == null || !(itemStack.getItemMeta() instanceof BannerMeta bannerMeta)) + continue; + if (placement != null && bannerMeta.numberOfPatterns() >= placement) { + patterns.add(bannerMeta.getPattern(placement - 1)); + } else if (placement == null) { + patterns.addAll(bannerMeta.getPatterns()); + } + } + } + return patterns.toArray(new Pattern[0]); + } + + /** + * Gets the appropriate {@link Consumer} to be used within {@link #change(Event, Object[], ChangeMode)}. + * @param mode The {@link ChangeMode} to get the consumer matching the behavior + * @param placement The specific pattern to set {@code pattern} + * @param pattern The pattern to be applied + * @return {@link Consumer} to be applied to objects within {@link #change(Event, Object[], ChangeMode)} + */ + private Consumer getPlacementMetaChanger(ChangeMode mode, int placement, @Nullable Pattern pattern) { + return switch (mode) { + case SET -> bannerMeta -> { + if (bannerMeta.numberOfPatterns() < placement) { + int toAdd = placement - bannerMeta.numberOfPatterns(); + for (int i = 0; i < toAdd; i++) { + bannerMeta.addPattern(new Pattern(DyeColor.WHITE, PatternType.BASE)); + } + } + bannerMeta.setPattern(placement - 1, pattern); + }; + case DELETE -> bannerMeta -> { + if (bannerMeta.numberOfPatterns() >= placement) + bannerMeta.removePattern(placement - 1); + }; + default -> bannerMeta -> {}; + }; + } + + /** + * Gets the appropriate {@link Consumer} to be used within {@link #change(Event, Object[], ChangeMode)}. + * @param mode The {@link ChangeMode} to get the consumer matching the behavior + * @param placement The specific pattern to set {@code pattern} + * @param pattern The pattern to be applied + * @return {@link Consumer} to be applied to objects within {@link #change(Event, Object[], ChangeMode)} + */ + private Consumer getPlacementBlockChanger(ChangeMode mode, int placement, @Nullable Pattern pattern) { + return switch (mode) { + case SET -> banner -> { + if (banner.numberOfPatterns() < placement) { + int toAdd = placement - banner.numberOfPatterns(); + for (int i = 0; i < toAdd; i++) { + banner.addPattern(new Pattern(DyeColor.GRAY, PatternType.BASE)); + } + } + banner.setPattern(placement - 1, pattern); + }; + case DELETE -> banner -> { + if (banner.numberOfPatterns() >= placement) + banner.removePattern(placement - 1); + }; + default -> banner -> {}; + }; + } + + /** + * Gets the appropriate {@link Consumer} to be used within {@link #change(Event, Object[], ChangeMode)}. + * @param mode The {@link ChangeMode} to get the consumer matching the behavior + * @param patterns Patterns to be added, removed, or set to corresponding with the {@code mode} + * @return {@link Consumer} to be applied to objects within {@link #change(Event, Object[], ChangeMode)} + */ + private Consumer getAllMetaChanger(ChangeMode mode, List patterns) { + return switch (mode) { + case SET -> bannerMeta -> { + bannerMeta.setPatterns(patterns); + }; + case DELETE -> bannerMeta -> { + bannerMeta.setPatterns(new ArrayList<>()); + }; + case ADD -> bannerMeta -> { + patterns.forEach(bannerMeta::addPattern); + }; + case REMOVE -> bannerMeta -> { + List current = bannerMeta.getPatterns(); + current.removeAll(patterns); + bannerMeta.setPatterns(current); + }; + default -> bannerMeta -> {}; + }; + } + + /** + * Gets the appropriate {@link Consumer} to be used within {@link #change(Event, Object[], ChangeMode)}. + * @param mode The {@link ChangeMode} to get the consumer matching the behavior + * @param patterns Patterns to be added, removed, or set to corresponding with the {@code mode} + * @return {@link Consumer} to be applied to objects within {@link #change(Event, Object[], ChangeMode)} + */ + private Consumer getAllBlockChanger(ChangeMode mode, List patterns) { + return switch (mode) { + case SET -> banner -> { + banner.setPatterns(patterns); + }; + case DELETE -> banner -> { + banner.setPatterns(new ArrayList<>()); + }; + case ADD -> banner -> { + patterns.forEach(banner::addPattern); + }; + case REMOVE -> banner -> { + List current = banner.getPatterns(); + current.removeAll(patterns); + banner.setPatterns(current); + }; + default -> banner -> {}; + }; + } + + @Override + public Class @Nullable [] acceptChange(ChangeMode mode) { + return switch (mode) { + case SET, DELETE -> (patternNumber != null) ? CollectionUtils.array(Pattern.class) : CollectionUtils.array(Pattern[].class); + case REMOVE, ADD -> (patternNumber != null) ? null : CollectionUtils.array(Pattern[].class); + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + Pattern[] patterns = (Pattern[]) delta; + int placement = patternNumber == null ? 0 : patternNumber.getSingle(event); + List patternList = patterns != null ? Arrays.stream(patterns).toList() : new ArrayList<>(); + + Consumer metaChanger = null; + Consumer blockChanger = null; + + if (placement >= 1) { + Pattern pattern = patternList.size() == 1 ? patternList.get(0) : null; + metaChanger = getPlacementMetaChanger(mode, placement, pattern); + blockChanger = getPlacementBlockChanger(mode, placement, pattern); + } else { + metaChanger = getAllMetaChanger(mode, patternList); + blockChanger = getAllBlockChanger(mode, patternList); + } + + for (Object object : objects.getArray(event)) { + if (object instanceof Block block && block.getState() instanceof Banner banner) { + blockChanger.accept(banner); + banner.update(true, false); + } else { + ItemStack itemStack = ItemUtils.asItemStack(object); + if (itemStack == null || !(itemStack.getItemMeta() instanceof BannerMeta bannerMeta)) + continue; + metaChanger.accept(bannerMeta); + itemStack.setItemMeta(bannerMeta); + if (object instanceof Slot slot) { + slot.setItem(itemStack); + } else if (object instanceof ItemType itemType) { + itemType.setItemMeta(bannerMeta); + } else if (object instanceof ItemStack itemStack1) { + itemStack1.setItemMeta(bannerMeta); + } + } + } + + } + + @Override + public boolean isSingle() { + return patternNumber != null && getExpr().isSingle(); + } + + @Override + public Class getReturnType() { + return Pattern.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + SyntaxStringBuilder builder = new SyntaxStringBuilder(event, debug); + if (patternNumber != null) { + builder.append("banner pattern", patternNumber); + } else { + builder.append("banner patterns"); + } + builder.append("of", objects); + return builder.toString(); + } + +} diff --git a/src/main/java/ch/njol/skript/expressions/ExprColorOf.java b/src/main/java/ch/njol/skript/expressions/ExprColorOf.java index 282f7fdb27c..e0239495883 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprColorOf.java +++ b/src/main/java/ch/njol/skript/expressions/ExprColorOf.java @@ -7,7 +7,6 @@ import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; -import org.skriptlang.skript.bukkit.displays.DisplayData; import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; @@ -18,6 +17,7 @@ import ch.njol.util.coll.CollectionUtils; import org.bukkit.DyeColor; import org.bukkit.FireworkEffect; +import org.bukkit.block.Banner; import org.bukkit.block.Block; import org.bukkit.block.BlockState; import org.bukkit.entity.Display; @@ -29,9 +29,11 @@ import org.bukkit.material.Colorable; import org.bukkit.material.MaterialData; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.displays.DisplayData; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; @Name("Color of") @Description({ @@ -83,13 +85,7 @@ protected Color[] get(Event event, Object[] source) { return null; return ColorRGB.fromBukkitColor(bukkitColor); } - Colorable colorable = getColorable(object); - if (colorable == null) - return null; - DyeColor dyeColor = colorable.getColor(); - if (dyeColor == null) - return null; - return SkriptColor.fromDyeColor(dyeColor); + return getColor(object); }); } @@ -121,76 +117,46 @@ protected Color[] get(Event event, Object[] source) { @Override public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { - Color color = null; - if (delta != null) - color = (Color) delta[0]; + Color[] colors = delta != null ? (Color[]) delta : null; + Consumer displayChanger = getDisplayChanger(mode, colors); + Consumer fireworkChanger = getFireworkChanger(mode, colors); for (Object object : getExpr().getArray(event)) { - if (object instanceof Item || object instanceof ItemType) { - if (mode != ChangeMode.SET) - return; - assert color != null; - ItemStack stack = object instanceof Item ? ((Item) object).getItemStack() : ((ItemType) object).getRandom(); - if (stack == null) - continue; - - MaterialData data = stack.getData(); - if (!(data instanceof Colorable)) - continue; - - ((Colorable) data).setColor(color.asDyeColor()); - stack.setData(data); - - if (object instanceof Item item) - item.setItemStack(stack); - } else if (object instanceof Block || object instanceof Colorable) { - if (mode != ChangeMode.SET) - return; + if (object instanceof TextDisplay display) { + displayChanger.accept(display); + } else if (object instanceof FireworkEffect effect) { + fireworkChanger.accept(effect); + } else if (mode == ChangeMode.SET && (object instanceof Block || object instanceof Colorable)) { + assert colors[0] != null; Colorable colorable = getColorable(object); - assert color != null; - if (colorable != null) { try { - colorable.setColor(color.asDyeColor()); + colorable.setColor(colors[0].asDyeColor()); } catch (UnsupportedOperationException ex) { // https://github.com/SkriptLang/Skript/issues/2931 Skript.error("Tried setting the color of a bed, but this isn't possible in your Minecraft version, " + "since different colored beds are different materials. " + "Instead, set the block to right material, such as a blue bed."); // Let's just assume it's a bed } - } - } else if (object instanceof TextDisplay display) { - switch (mode) { - case RESET -> display.setDefaultBackground(true); - case SET -> { - assert color != null; - if (display.isDefaultBackground()) - display.setDefaultBackground(false); - display.setBackgroundColor(color.asBukkitColor()); + } else { + if (object instanceof Block block) { + if (block.getState() instanceof Banner banner) + banner.setBaseColor(colors[0].asDyeColor()); } } - } else if (object instanceof FireworkEffect effect) { - Color[] input = (Color[]) delta; - switch (mode) { - case ADD: - for (Color c : input) - effect.getColors().add(c.asBukkitColor()); - break; - case REMOVE: - case REMOVE_ALL: - for (Color c : input) - effect.getColors().remove(c.asBukkitColor()); - break; - case DELETE: - case RESET: - effect.getColors().clear(); - break; - case SET: - effect.getColors().clear(); - for (Color c : input) - effect.getColors().add(c.asBukkitColor()); - break; - default: - break; + } else if (mode == ChangeMode.SET && (object instanceof Item || object instanceof ItemType)) { + assert colors[0] != null; + ItemStack stack = object instanceof Item ? ((Item) object).getItemStack() : ((ItemType) object).getRandom(); + if (stack == null) + continue; + //noinspection removal + MaterialData data = stack.getData(); + if (!(data instanceof Colorable colorable)) + continue; + colorable.setColor(colors[0].asDyeColor()); + //noinspection removal + stack.setData(data); + if (object instanceof Item item) { + item.setItemStack(stack); } } } @@ -206,14 +172,53 @@ public String toString(@Nullable Event event, boolean debug) { return "color of " + getExpr().toString(event, debug); } - @Nullable - private Colorable getColorable(Object colorable) { + private Consumer getDisplayChanger(ChangeMode mode, Color @Nullable [] colors) { + Color color = (colors != null && colors.length == 1) ? colors[0] : null; + return switch (mode) { + case RESET -> display -> { + display.setDefaultBackground(true); + }; + case SET -> display -> { + if (color != null) { + if (display.isDefaultBackground()) + display.setDefaultBackground(false); + display.setBackgroundColor(color.asBukkitColor()); + } + }; + default -> display -> {}; + }; + } + + private Consumer getFireworkChanger(ChangeMode mode, Color @Nullable [] colors) { + return switch (mode) { + case ADD -> effect -> { + for (Color color : colors) + effect.getColors().add(color.asBukkitColor()); + }; + case REMOVE, REMOVE_ALL -> effect -> { + for (Color color : colors) + effect.getColors().remove(color.asBukkitColor()); + }; + case DELETE, RESET -> effect -> { + effect.getColors().clear(); + }; + case SET -> effect -> { + effect.getColors().clear(); + for (Color color : colors) + effect.getColors().add(color.asBukkitColor()); + }; + default -> effect -> {}; + }; + } + + private @Nullable Colorable getColorable(Object colorable) { if (colorable instanceof Item || colorable instanceof ItemType) { ItemStack item = colorable instanceof Item ? ((Item) colorable).getItemStack() : ((ItemType) colorable).getRandom(); if (item == null) return null; + //noinspection removal MaterialData data = item.getData(); if (data instanceof Colorable) return (Colorable) data; @@ -227,4 +232,19 @@ private Colorable getColorable(Object colorable) { return null; } + private @Nullable Color getColor(Object object) { + Colorable colorable = getColorable(object); + if (colorable != null) { + DyeColor dyeColor = colorable.getColor(); + if (dyeColor == null) + return null; + return SkriptColor.fromDyeColor(dyeColor); + } + if (object instanceof Block block) { + if (block.getState() instanceof Banner banner) + return SkriptColor.fromDyeColor(banner.getBaseColor()); + } + return null; + } + } diff --git a/src/main/java/ch/njol/skript/expressions/ExprNewBannerPattern.java b/src/main/java/ch/njol/skript/expressions/ExprNewBannerPattern.java new file mode 100644 index 00000000000..f781e4a1451 --- /dev/null +++ b/src/main/java/ch/njol/skript/expressions/ExprNewBannerPattern.java @@ -0,0 +1,83 @@ +package ch.njol.skript.expressions; + +import ch.njol.skript.Skript; +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.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.util.Color; +import ch.njol.util.Kleenean; +import org.bukkit.block.banner.Pattern; +import org.bukkit.block.banner.PatternType; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +@Name("Banner Pattern") +@Description("Creates a new banner pattern.") +@Examples({ + "set {_pattern} to a creeper banner pattern colored red", + "add {_pattern} to banner patterns of {_banneritem}", + "remove {_pattern} from banner patterns of {_banneritem}", + "set the 1st banner pattern of block at location(0,0,0) to {_pattern}", + "clear the 1st banner pattern of block at location(0,0,0)", + "", + "set {_pattern} to a red mojang banner pattern" +}) +@Since("INSERT VERSION") +public class ExprNewBannerPattern extends SimpleExpression { + + static { + Skript.registerExpression(ExprNewBannerPattern.class, Pattern.class, ExpressionType.PATTERN_MATCHES_EVERYTHING, + "[a] %bannerpatterntype% colo[u]red %color%", + "[a] %*color% %bannerpatterntype%"); + } + + private Expression selectedPattern; + private Expression selectedColor; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + if (matchedPattern == 0) { + //noinspection unchecked + selectedPattern = (Expression) exprs[0]; + //noinspection unchecked + selectedColor = (Expression) exprs[1]; + } else { + //noinspection unchecked + selectedPattern = (Expression) exprs[1]; + //noinspection unchecked + selectedColor = (Expression) exprs[0]; + } + return true; + } + + @Override + protected Pattern @Nullable [] get(Event event) { + Color color = selectedColor.getSingle(event); + PatternType patternType = selectedPattern.getSingle(event); + if (color == null || color.asDyeColor() == null || patternType == null) + return null; + + return new Pattern[]{new Pattern(color.asDyeColor(), patternType)}; + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return Pattern.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "a " + selectedPattern.toString(event, debug) + " colored " + selectedColor.toString(event, debug); + } + +} diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index 28d608aa7a9..480ec5bc88c 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2472,6 +2472,59 @@ experience cooldown change reasons: plugin: plugin pickup_orb: orb pickup, pickup orb +# -- Banner Pattern Types -- +banner pattern types: + base: base banner pattern + border: border banner pattern, bordure banner pattern, bordure indented banner pattern + bricks: bricks banner pattern, field masoned banner pattern + circle: circle banner pattern + creeper: creeper banner pattern, creeper charged banner pattern + cross: cross banner pattern + curly_border: curly border banner pattern + diagonal_left: left diagonal banner pattern + diagonal_right: right diagonal banner pattern + diagonal_up_left: diagonal up left banner pattern + diagonal_up_right: diagonal up right banner pattern + flow: flow banner pattern + flower: flower banner pattern + globe: globe banner pattern + gradient: gradient banner pattern + gradient_up: gradient up banner pattern + guster: guster banner pattern + half_horizontal: horizontal half banner pattern + half_horizontal_bottom: horizontal bottom half banner pattern + half_vertical: vertical half banner pattern + half_vertical_right: right vertical half banner pattern + mojang: mojang banner pattern, thing banner pattern + piglin: piglin banner pattern, snout banner pattern + rhombus: rhombus banner pattern + skull: skull banner pattern + small_stripes: small stripes banner pattern + square_bottom_left: bottom left square banner pattern + square_bottom_right: bottom right square banner pattern + square_top_left: top left square banner pattern + square_top_right: top right square banner pattern + straight_cross: straight cross banner pattern + stripe_bottom: bottom stripe banner pattern + stripe_center: center stripe banner pattern + stripe_downleft: down left stripe banner pattern + stripe_downright: down right stripe banner pattern + stripe_left: left stripe banner pattern + stripe_middle: middle stripe banner pattern + stripe_right: right stripe banner pattern + stripe_top: top stripe banner pattern + triangle_bottom: bottom triangle banner pattern + triangle_top: top triangle banner pattern + triangles_bottom: tbottom triangles banner pattern + triangles_top: top triangles banner pattern + stripe_small: small stripe banner pattern + diagonal_left_mirror: left diagonal mirror banner pattern + diagonal_right_mirror: right diagonal mirror banner pattern + circle_middle: middle circle banner pattern + rhombus_middle: middle rhombus banner pattern + half_vertical_mirror: vertical half mirror banner pattern + half_horizontal_mirror: horizontal half mirror banner pattern + # -- Input Keys -- input keys: forward: forward movement key, forward key @@ -2575,6 +2628,8 @@ types: entitysnapshot: entity snapshot¦s @an loottable: loot table¦s @a lootcontext: loot context¦s @a + bannerpatterntype: banner pattern type¦s @a + bannerpattern: banner pattern¦s @a # Skript weathertype: weather type¦s @a diff --git a/src/test/skript/tests/syntaxes/expressions/ExprBannerItem.sk b/src/test/skript/tests/syntaxes/expressions/ExprBannerItem.sk new file mode 100644 index 00000000000..7df18be4a5a --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprBannerItem.sk @@ -0,0 +1,7 @@ +test "banner item": + set {_pattern} to a creeper banner pattern item + assert {_pattern} is set with "Creeper banner pattern should return an item" + + parse: + set {_invalid} to a base banner pattern item + assert {_invalid} is not set with "Base banner pattern type should not get an item (unless updated)" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprNewBannerPattern.sk b/src/test/skript/tests/syntaxes/expressions/ExprNewBannerPattern.sk new file mode 100644 index 00000000000..f40b844a867 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprNewBannerPattern.sk @@ -0,0 +1,38 @@ +test "new banner pattern": + set {_pattern} to a creeper banner pattern colored red + # Will fail due to the "banner pattern" item alias in skript-aliases + # assert {_pattern} is a banner pattern with "New banner pattern is not a banner pattern type" + set {_item} to a banner + add {_pattern} to the banner patterns of {_item} + assert the banner patterns of {_item} contains {_pattern} with "Banner pattern not applied to item" + remove {_pattern} from the banner patterns of {_item} + assert the banner patterns of {_item} does not contain {_pattern} with "Banner pattern was not removed from item" + set the 1st banner pattern of {_item} to {_pattern} + assert the 1st banner pattern of {_item} is {_pattern} with "Banner pattern not applied to 1st banner pattern of item" + clear the 1st banner pattern of {_item} + assert the 1st banner pattern of {_item} is not set with "1st banner pattern of item was not cleared" + + set {_old} to block at test-location + set block at test-location to a banner + assert the block at test-location is a banner with "Banner block was not set" + add {_pattern} to the banner patterns of (block at test-location) + assert the banner patterns of (block at test-location) contains {_pattern} with "Banner pattern not applied to block" + remove {_pattern} from the banner patterns of (block at test-location) + assert the banner patterns of (block at test-location) does not contain {_pattern} with "Banner pattern was not removed from block" + set the 1st banner pattern of (block at test-location) to {_pattern} + assert the 1st banner pattern of (block at test-location) is {_pattern} with "Banner pattern not applied to 1st banner pattern of block" + clear the 1st banner pattern of (block at test-location) + assert the 1st banner pattern of (block at test-location) is not set with "1st banner pattern of block was not cleared" + set block at test-location to {_old} + +test "invalid banner usage": + set {_pattern} to a border banner pattern colored gray + set {_item} to a diamond chestplate + set the banner patterns of {_item} to {_pattern} + assert the banner patterns of {_item} is not set with "Non-Banner item should not have banner patterns" + + set the block at test-location to air + set the banner patterns of block at test-location to {_pattern} + assert the banner patterns of block at test-location is not set with "Non-Banner block should not have banner patterns" + + assert a creeper banner pattern colored rgb(255, 0, 0) is not set with "RGB colors should not return a banner pattern" From 48d0bbe8fca5d6535125ff013dcefdabc4f417c4 Mon Sep 17 00:00:00 2001 From: Moderocky Date: Mon, 30 Dec 2024 07:57:10 +0000 Subject: [PATCH 17/19] Use tabs consistently with all other files. (#7330) * Use tabs consistently with all other files. * Two spaces? In the four space factory? * Finally, some good whitespace. --- .../ch/njol/skript/doc/DocumentationId.java | 5 +- .../ch/njol/skript/doc/RequiredPlugins.java | 7 +- .../ch/njol/skript/update/GithubChecker.java | 100 ++-- src/main/java/ch/njol/util/Math2.java | 66 +-- .../java/ch/njol/util/NotifyingReference.java | 78 ++- .../ch/njol/util/SynchronizedReference.java | 53 +- .../bukkit/loottables/LootContextWrapper.java | 2 +- src/test/skript/junit/-BellEventsPaper.sk | 4 +- src/test/skript/junit/-BellEventsSpigot.sk | 10 +- src/test/skript/junit/BellEvents.sk | 40 +- .../4988-function uuid multiple parameters.sk | 2 +- .../6288-event data priority pollution.sk | 4 +- .../6290-item-inventory-conflict.sk | 10 +- .../7279-create-uncreatable-inventory.sk | 2 +- .../regressions/pull-6361-function-param.sk | 14 +- .../tests/syntaxes/conditions/CondIsOfType.sk | 36 +- .../skript/tests/syntaxes/effects/EffDoIf.sk | 8 +- .../skript/tests/syntaxes/effects/EffPvP.sk | 10 +- .../syntaxes/expressions/ExprArithmetic.sk | 462 +++++++++--------- .../syntaxes/expressions/ExprInventory.sk | 18 +- .../tests/syntaxes/sections/EffSecSpawn.sk | 2 +- .../tests/syntaxes/sections/SecParse.sk | 14 +- .../tests/syntaxes/structures/StructParse.sk | 16 +- 23 files changed, 478 insertions(+), 485 deletions(-) diff --git a/src/main/java/ch/njol/skript/doc/DocumentationId.java b/src/main/java/ch/njol/skript/doc/DocumentationId.java index b425e60ee20..2929a1fe757 100644 --- a/src/main/java/ch/njol/skript/doc/DocumentationId.java +++ b/src/main/java/ch/njol/skript/doc/DocumentationId.java @@ -14,6 +14,7 @@ @Retention(RetentionPolicy.RUNTIME) @Documented public @interface DocumentationId { - - public String value(); + + public String value(); + } diff --git a/src/main/java/ch/njol/skript/doc/RequiredPlugins.java b/src/main/java/ch/njol/skript/doc/RequiredPlugins.java index 074a4e6bfd2..ce36a2639c1 100644 --- a/src/main/java/ch/njol/skript/doc/RequiredPlugins.java +++ b/src/main/java/ch/njol/skript/doc/RequiredPlugins.java @@ -10,12 +10,13 @@ * Provides a list of plugins other than Skript that the annotated * element requires to be used. Non-Spigot server software can be considered * to be plugins. - * + * */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RequiredPlugins { - - String[] value(); + + String[] value(); + } diff --git a/src/main/java/ch/njol/skript/update/GithubChecker.java b/src/main/java/ch/njol/skript/update/GithubChecker.java index de844d455e1..49bbc96fbb1 100644 --- a/src/main/java/ch/njol/skript/update/GithubChecker.java +++ b/src/main/java/ch/njol/skript/update/GithubChecker.java @@ -15,63 +15,67 @@ * Uses Github API to check for updates. */ public class GithubChecker implements UpdateChecker { - + /** * Github API response for GSON deserialization. */ public static class ResponseEntry { + public String url; - public String assets_url; - public String upload_url; - public String html_url; - public int id; - public String tag_name; - public String target_commitish; - public String name; - public boolean draft; - - public boolean prerelease; - public String created_at; - public String published_at; - - public static class AssetsEntry { - public int size; - public int download_count; - public String browser_download_url; - } - - public List assets; - public String body; // Description of release - - @Override - public String toString() { - return tag_name; - } - - public static class Author { - public String login; - public int id; - } - - public Author author; + public String assets_url; + public String upload_url; + public String html_url; + public int id; + public String tag_name; + public String target_commitish; + public String name; + public boolean draft; + + public boolean prerelease; + public String created_at; + public String published_at; + + public static class AssetsEntry { + + public int size; + public int download_count; + public String browser_download_url; + + } + + public List assets; + public String body; // Description of release + + @Override + public String toString() { + return tag_name; + } + + public static class Author { + + public String login; + public int id; + + } + + public Author author; } - + /** * Used for deserializing Github API output. */ private final Gson gson; - + public GithubChecker() { this.gson = new Gson(); } - + private List deserialize(String str) { assert str != null : "Cannot deserialize null string"; - @SuppressWarnings("serial") Type listType = new TypeToken>() {}.getType(); List responses = gson.fromJson(str, listType); assert responses != null; - + return responses; } @@ -97,7 +101,7 @@ public CompletableFuture check(ReleaseManifest manifest, Release * Latest release in the channel we're using. */ ResponseEntry latest = null; - + /** * Current release, if found. */ @@ -107,32 +111,32 @@ public CompletableFuture check(ReleaseManifest manifest, Release for (ResponseEntry release : releases) { String name = release.tag_name; assert name != null; - + // Check if this is a suitable latest release if (latest == null && channel.check(name)) { latest = release; } - + // Check whether this is a current release if (manifest.id.equals(name)) { current = release; break; // Update can't be older than current release } } - + if (latest == null) { return null; // No updates for this channel available } - + if (current != null && latest.id == current.id) { return null; // Already running latest in this channel } - + // Validate the latest release if (latest.assets.isEmpty()) { return null; // Update not (yet?) downloadable } - + try { String name = latest.tag_name; assert name != null; @@ -146,7 +150,7 @@ public CompletableFuture check(ReleaseManifest manifest, Release } else { download = new URL(latest.assets.get(0).browser_download_url); } - + return new UpdateManifest(name, createdAt, patchNotes, download); } catch (MalformedURLException e) { throw new RuntimeException(e); @@ -155,5 +159,5 @@ public CompletableFuture check(ReleaseManifest manifest, Release assert future != null; return future; } - + } diff --git a/src/main/java/ch/njol/util/Math2.java b/src/main/java/ch/njol/util/Math2.java index 04e222f7698..40073193d86 100644 --- a/src/main/java/ch/njol/util/Math2.java +++ b/src/main/java/ch/njol/util/Math2.java @@ -14,9 +14,9 @@ */ @ApiStatus.Internal public final class Math2 { - + private Math2() {} - + /** * Fits an int into the given interval. The method's behaviour when min > max is unspecified. * @@ -26,7 +26,7 @@ public static int fit(int min, int value, int max) { assert min <= max : min + "," + value + "," + max; return Math.min(Math.max(value, min), max); } - + /** * Fits a long into the given interval. The method's behaviour when min > max is unspecified. * @@ -46,17 +46,17 @@ public static float fit(float min, float value, float max) { assert min <= max : min + "," + value + "," + max; return Math.min(Math.max(value, min), max); } - + /** * Fits a double into the given interval. The method's behaviour when min > max is unspecified. - * + * * @return A double in between min and max */ public static double fit(double min, double value, double max) { assert min <= max : min + "," + value + "," + max; return Math.min(Math.max(value, min), max); } - + /** * Modulo that returns positive values even for negative arguments. * @@ -65,7 +65,7 @@ public static double fit(double min, double value, double max) { public static int mod(int value, int mod) { return (value % mod + mod) % mod; } - + /** * Modulo that returns positive values even for negative arguments. * @@ -74,7 +74,7 @@ public static int mod(int value, int mod) { public static long mod(long value, long mod) { return (value % mod + mod) % mod; } - + /** * Modulo that returns positive values even for negative arguments. * @@ -83,7 +83,7 @@ public static long mod(long value, long mod) { public static float mod(float value, float mod) { return (value % mod + mod) % mod; } - + /** * Modulo that returns positive values even for negative arguments. * @@ -92,42 +92,42 @@ public static float mod(float value, float mod) { public static double mod(double value, double mod) { return (value % mod + mod) % mod; } - + /** * Ceils the given float and returns the result as an int. */ public static int ceil(float value) { return (int) Math.ceil(value - Skript.EPSILON); } - + /** * Rounds the given float (where .5 is rounded up) and returns the result as an int. */ public static int round(float value) { return (int) Math.round(value + Skript.EPSILON); } - + /** * Floors the given double and returns the result as a long. */ public static long floor(double value) { return (long) Math.floor(value + Skript.EPSILON); } - + /** * Ceils the given double and returns the result as a long. */ public static long ceil(double value) { return (long) Math.ceil(value - Skript.EPSILON); } - + /** * Rounds the given double (where .5 is rounded up) and returns the result as a long. */ public static long round(double value) { return Math.round(value + Skript.EPSILON); } - + /** * Guarantees a float is neither NaN nor infinite. * Useful for situations when safe floats are required. @@ -151,100 +151,100 @@ public static long addClamped(long x, long y) { return Long.MAX_VALUE; return result; } - + public static long multiplyClamped(long x, long y) { long result = x * y; long ax = Math.abs(x); long ay = Math.abs(y); // Logic extracted from Math#multiplyExact to avoid having to catch an expensive exception - if (((ax | ay) >>> 31 != 0) && (((y != 0) && (result / y != x)) || (x == Long.MIN_VALUE && y == -1))) + if (((ax | ay) >>> 31 != 0) && (((y != 0) && (result / y != x)) || (x == Long.MIN_VALUE && y == -1))) // If either x or y is negative return the min value, otherwise return the max value - return x < 0 == y < 0 ? Long.MAX_VALUE : Long.MIN_VALUE; + return x < 0 == y < 0 ? Long.MAX_VALUE : Long.MIN_VALUE; return result; } - + @Deprecated @ScheduledForRemoval public static int floorI(double value) { return (int) Math.floor(value + Skript.EPSILON); } - + @Deprecated @ScheduledForRemoval public static int ceilI(double value) { return (int) Math.ceil(value - Skript.EPSILON); } - + // Change signature to return int instead of removing. @Deprecated @ScheduledForRemoval public static long floor(float value) { return (long) Math.floor(value + Skript.EPSILON); } - + @Deprecated @ScheduledForRemoval public static int min(int a, int b, int c) { return Math.min(a, Math.min(b, c)); } - + @Deprecated @ScheduledForRemoval public static int min(int... numbers) { if (numbers.length == 0) return 0; - + return Arrays.stream(numbers) .min() .getAsInt(); } - + @Deprecated @ScheduledForRemoval public static int max(int a, int b, int c) { return Math.max(a, Math.max(b, c)); } - + @Deprecated @ScheduledForRemoval public static int max(int... numbers) { if (numbers.length == 0) return 0; - + return Arrays.stream(numbers) .max() .getAsInt(); } - + @Deprecated @ScheduledForRemoval public static double min(double a, double b, double c) { return Math.min(a, Math.min(b, c)); } - + @Deprecated @ScheduledForRemoval public static double min(double... numbers) { if (numbers.length == 0) return Double.NaN; - + return Arrays.stream(numbers) .min() .getAsDouble(); } - + @Deprecated @ScheduledForRemoval public static double max(double a, double b, double c) { return Math.max(a, Math.max(b, c)); } - + @Deprecated @ScheduledForRemoval public static double max(double... numbers) { if (numbers.length == 0) return Double.NaN; - + return Arrays.stream(numbers) .max() .getAsDouble(); diff --git a/src/main/java/ch/njol/util/NotifyingReference.java b/src/main/java/ch/njol/util/NotifyingReference.java index 127a72d39c2..bca7b91b509 100644 --- a/src/main/java/ch/njol/util/NotifyingReference.java +++ b/src/main/java/ch/njol/util/NotifyingReference.java @@ -2,47 +2,39 @@ import org.jetbrains.annotations.Nullable; -/** - * @author Peter G�ttinger - * - */ -public class NotifyingReference -{ - @Nullable - private volatile V value; - private final boolean notifyAll; - - public NotifyingReference(@Nullable V value, boolean notifyAll) - { - this.value = value; - this.notifyAll = notifyAll; - } - - public NotifyingReference(@Nullable V value) - { - this.value = value; - this.notifyAll = true; - } - - public NotifyingReference() - { - this.value = null; - this.notifyAll = true; - } - - @Nullable - public synchronized V get() - { - return this.value; - } - - public synchronized void set(@Nullable V newValue) - { - this.value = newValue; - if (this.notifyAll) { - notifyAll(); - } else { - notify(); - } - } +public class NotifyingReference { + + @Nullable + private volatile V value; + private final boolean notifyAll; + + public NotifyingReference(@Nullable V value, boolean notifyAll) { + this.value = value; + this.notifyAll = notifyAll; + } + + public NotifyingReference(@Nullable V value) { + this.value = value; + this.notifyAll = true; + } + + public NotifyingReference() { + this.value = null; + this.notifyAll = true; + } + + @Nullable + public synchronized V get() { + return this.value; + } + + public synchronized void set(@Nullable V newValue) { + this.value = newValue; + if (this.notifyAll) { + notifyAll(); + } else { + notify(); + } + } + } diff --git a/src/main/java/ch/njol/util/SynchronizedReference.java b/src/main/java/ch/njol/util/SynchronizedReference.java index 6786a0dac45..b21484e86c1 100644 --- a/src/main/java/ch/njol/util/SynchronizedReference.java +++ b/src/main/java/ch/njol/util/SynchronizedReference.java @@ -2,32 +2,27 @@ import org.jetbrains.annotations.Nullable; -/** - * @author Peter G�ttinger - * - */ -public class SynchronizedReference -{ - @Nullable - private volatile V value; - - public SynchronizedReference(@Nullable V initialValue) - { - this.value = initialValue; - } - - public SynchronizedReference() {} - - @Nullable - public final V get() - { - assert (Thread.holdsLock(this)); - return this.value; - } - - public final void set(@Nullable V newValue) - { - assert (Thread.holdsLock(this)); - this.value = newValue; - } -} \ No newline at end of file +public class SynchronizedReference { + + @Nullable + private volatile V value; + + public SynchronizedReference(@Nullable V initialValue) { + this.value = initialValue; + } + + public SynchronizedReference() { + } + + @Nullable + public final V get() { + assert (Thread.holdsLock(this)); + return this.value; + } + + public final void set(@Nullable V newValue) { + assert (Thread.holdsLock(this)); + this.value = newValue; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/loottables/LootContextWrapper.java b/src/main/java/org/skriptlang/skript/bukkit/loottables/LootContextWrapper.java index e2afc7ed2c3..35aa234d49d 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/loottables/LootContextWrapper.java +++ b/src/main/java/org/skriptlang/skript/bukkit/loottables/LootContextWrapper.java @@ -23,7 +23,7 @@ public class LootContextWrapper { * @param location the location of the LootContext. */ public LootContextWrapper(@NotNull Location location) { - this.location = location; + this.location = location; } /** diff --git a/src/test/skript/junit/-BellEventsPaper.sk b/src/test/skript/junit/-BellEventsPaper.sk index 61afbd566f5..bc36739064b 100644 --- a/src/test/skript/junit/-BellEventsPaper.sk +++ b/src/test/skript/junit/-BellEventsPaper.sk @@ -1,5 +1,5 @@ # This file contains events we can run on Paper 1.16.5+ on bell ringing: - junit test is "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" - complete objective "bell rings" for junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" + junit test is "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" + complete objective "bell rings" for junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" diff --git a/src/test/skript/junit/-BellEventsSpigot.sk b/src/test/skript/junit/-BellEventsSpigot.sk index cf33b09aff5..65ba0e256f7 100644 --- a/src/test/skript/junit/-BellEventsSpigot.sk +++ b/src/test/skript/junit/-BellEventsSpigot.sk @@ -1,10 +1,10 @@ # This file contains tests we can run on Spigot 1.19.4+ on bell ringing: - junit test is "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" - complete objective "bell rings" for junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" + junit test is "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" + complete objective "bell rings" for junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" on bell resonating: - junit test is "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" - complete objective "bell resonates" for junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" - assert size of event-entities is greater than or equal to 1 with "bell did not reveal the pillager" + junit test is "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" + complete objective "bell resonates" for junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" + assert size of event-entities is greater than or equal to 1 with "bell did not reveal the pillager" diff --git a/src/test/skript/junit/BellEvents.sk b/src/test/skript/junit/BellEvents.sk index 2d7597ef4b2..29c9ad47738 100644 --- a/src/test/skript/junit/BellEvents.sk +++ b/src/test/skript/junit/BellEvents.sk @@ -1,25 +1,25 @@ test "BellEventsTest" when running JUnit: - set {_slashIndex} to last index of "/" in "%script%.sk" - if {_slashIndex} is -1: # try \ separator (Windows) - set {_slashIndex} to last index of "\" in "%script%.sk" - set {_parent} to substring of "%script%.sk" from 0 to {_slashIndex} + set {_slashIndex} to last index of "/" in "%script%.sk" + if {_slashIndex} is -1: # try \ separator (Windows) + set {_slashIndex} to last index of "\" in "%script%.sk" + set {_parent} to substring of "%script%.sk" from 0 to {_slashIndex} - if running below minecraft "1.19.4": - complete objective "bell resonates" for junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" - if running below minecraft "1.16.5": - complete objective "bell rings" for junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" - else: - load script "%{_parent}%-BellEventsPaper.sk" - else: - load script "%{_parent}%-BellEventsSpigot.sk" + if running below minecraft "1.19.4": + complete objective "bell resonates" for junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" + if running below minecraft "1.16.5": + complete objective "bell rings" for junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" + else: + load script "%{_parent}%-BellEventsPaper.sk" + else: + load script "%{_parent}%-BellEventsSpigot.sk" - ensure junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" completes "bell resonates" - ensure junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" completes "bell rings" + ensure junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" completes "bell resonates" + ensure junit test "org.skriptlang.skript.test.tests.syntaxes.events.BellEventsTest" completes "bell rings" on script unload: - set {_slashIndex} to last index of "/" in "%script%.sk" - if {_slashIndex} is -1: # try \ separator (Windows) - set {_slashIndex} to last index of "\" in "%script%.sk" - set {_parent} to substring of "%script%.sk" from 0 to {_slashIndex} - disable script "%{_parent}%BellEventsSpigot.sk" - disable script "%{_parent}%BellEventsPaper.sk" + set {_slashIndex} to last index of "/" in "%script%.sk" + if {_slashIndex} is -1: # try \ separator (Windows) + set {_slashIndex} to last index of "\" in "%script%.sk" + set {_parent} to substring of "%script%.sk" from 0 to {_slashIndex} + disable script "%{_parent}%BellEventsSpigot.sk" + disable script "%{_parent}%BellEventsPaper.sk" diff --git a/src/test/skript/tests/regressions/4988-function uuid multiple parameters.sk b/src/test/skript/tests/regressions/4988-function uuid multiple parameters.sk index 891acea46c5..8689089c36c 100644 --- a/src/test/skript/tests/regressions/4988-function uuid multiple parameters.sk +++ b/src/test/skript/tests/regressions/4988-function uuid multiple parameters.sk @@ -2,7 +2,7 @@ # The exact function from the issue local function test(uuid: string, s: string) :: boolean: if {_uuid} is set: - if {_s} is set: + if {_s} is set: return true return false diff --git a/src/test/skript/tests/regressions/6288-event data priority pollution.sk b/src/test/skript/tests/regressions/6288-event data priority pollution.sk index 3de238ca724..2da0df2c53d 100644 --- a/src/test/skript/tests/regressions/6288-event data priority pollution.sk +++ b/src/test/skript/tests/regressions/6288-event data priority pollution.sk @@ -1,5 +1,5 @@ on join with priority lowest: - stop + stop every 10 seconds: - stop + stop diff --git a/src/test/skript/tests/regressions/6290-item-inventory-conflict.sk b/src/test/skript/tests/regressions/6290-item-inventory-conflict.sk index 38b2d328cb9..9d3a50794f1 100644 --- a/src/test/skript/tests/regressions/6290-item-inventory-conflict.sk +++ b/src/test/skript/tests/regressions/6290-item-inventory-conflict.sk @@ -1,6 +1,6 @@ test "item inventory conflict": - parse: - loop all players: - loop all items in the loop-player's inventory: - set {_item} to loop-item - assert parse logs are not set with "failed to parse loop" + parse: + loop all players: + loop all items in the loop-player's inventory: + set {_item} to loop-item + assert parse logs are not set with "failed to parse loop" diff --git a/src/test/skript/tests/regressions/7279-create-uncreatable-inventory.sk b/src/test/skript/tests/regressions/7279-create-uncreatable-inventory.sk index 623669f4ed3..b39fca8fbb1 100644 --- a/src/test/skript/tests/regressions/7279-create-uncreatable-inventory.sk +++ b/src/test/skript/tests/regressions/7279-create-uncreatable-inventory.sk @@ -4,7 +4,7 @@ test "create un-creatable inventory": assert composter inventory named "BOB" is set to fail with "This should fail, cannot create a player inventory" # Test creating inventory types which can be created - assert chest inventory named "BOB" is set with "Should be able to create a named chest inventory" + assert chest inventory named "BOB" is set with "Should be able to create a named chest inventory" assert hopper inventory named "Mr Hopper" is set with "Should be able to create a named hopper inventory" # Test opening an inventory type which cannot be created diff --git a/src/test/skript/tests/regressions/pull-6361-function-param.sk b/src/test/skript/tests/regressions/pull-6361-function-param.sk index 81ef44a8d98..f0bc5bfd4ef 100644 --- a/src/test/skript/tests/regressions/pull-6361-function-param.sk +++ b/src/test/skript/tests/regressions/pull-6361-function-param.sk @@ -20,7 +20,7 @@ function test_English(test: text) :: text: - return {_test} + return {_test} function test_Arabic(تجربة: text) :: text: return {_تجربة} @@ -38,9 +38,9 @@ function test_German(prüfen: text) :: text: return {_prüfen} test "function parameter names": - assert test_English("text") = "text" with "Function 'test_English' failed function parameter name test" - assert test_Arabic("text") = "text" with "Function 'test_Arabic' failed function parameter name test" - assert test_Japanese("text") = "text" with "Function 'test_Japanese' failed function parameter name test" - assert test_Greek("text") = "text" with "Function 'test_Greek' failed function parameter name test" - assert test_Thai("text") = "text" with "Function 'test_Thai' failed function parameter name test" - assert test_German("text") = "text" with "Function 'test_German' failed function parameter name test" \ No newline at end of file + assert test_English("text") = "text" with "Function 'test_English' failed function parameter name test" + assert test_Arabic("text") = "text" with "Function 'test_Arabic' failed function parameter name test" + assert test_Japanese("text") = "text" with "Function 'test_Japanese' failed function parameter name test" + assert test_Greek("text") = "text" with "Function 'test_Greek' failed function parameter name test" + assert test_Thai("text") = "text" with "Function 'test_Thai' failed function parameter name test" + assert test_German("text") = "text" with "Function 'test_German' failed function parameter name test" diff --git a/src/test/skript/tests/syntaxes/conditions/CondIsOfType.sk b/src/test/skript/tests/syntaxes/conditions/CondIsOfType.sk index c4a7ce6e5e1..5a0d2e2c1a1 100644 --- a/src/test/skript/tests/syntaxes/conditions/CondIsOfType.sk +++ b/src/test/skript/tests/syntaxes/conditions/CondIsOfType.sk @@ -17,23 +17,23 @@ test "item is of type condition": assert ender pearl is of type plain ender pearl with "ender pearl isn't of type plain ender pearl" test "entity is of type condition": - spawn zombie at spawn of "world" - assert last spawned zombie is of type zombie with "zombie isn't of type zombie" - assert last spawned zombie is not of type ghast with "zombie isn't of type ghast" - set {_zombie} to last spawned zombie - assert {_zombie} is of type zombie with "zombie in a variable isn't of type zombie" - delete last spawned zombie + spawn zombie at spawn of "world" + assert last spawned zombie is of type zombie with "zombie isn't of type zombie" + assert last spawned zombie is not of type ghast with "zombie isn't of type ghast" + set {_zombie} to last spawned zombie + assert {_zombie} is of type zombie with "zombie in a variable isn't of type zombie" + delete last spawned zombie - spawn armor stand at spawn of "world" - assert last spawned armor stand is of type armor stand with "armor stand isn't of type armor stand" - assert last spawned armor stand is not of type ghast with "armor stand isn't of type ghast" - set {_armor-stand} to last spawned armor stand - assert {_armor-stand} is of type armor stand with "armor stand in a variable isn't of type armor stand" - delete last spawned armor stand + spawn armor stand at spawn of "world" + assert last spawned armor stand is of type armor stand with "armor stand isn't of type armor stand" + assert last spawned armor stand is not of type ghast with "armor stand isn't of type ghast" + set {_armor-stand} to last spawned armor stand + assert {_armor-stand} is of type armor stand with "armor stand in a variable isn't of type armor stand" + delete last spawned armor stand - spawn enderpearl at spawn of "world" - assert last spawned enderpearl is of type ender pearl with "enderpearl isn't of type enderpearl" - assert last spawned enderpearl is not of type ghast with "enderpearl isn't of type ghast" - set {_enderpearl} to last spawned enderpearl - assert {_enderpearl} is of type ender pearl with "enderpearl in a variable isn't of type enderpearl" - delete last spawned enderpearl + spawn enderpearl at spawn of "world" + assert last spawned enderpearl is of type ender pearl with "enderpearl isn't of type enderpearl" + assert last spawned enderpearl is not of type ghast with "enderpearl isn't of type ghast" + set {_enderpearl} to last spawned enderpearl + assert {_enderpearl} is of type ender pearl with "enderpearl in a variable isn't of type enderpearl" + delete last spawned enderpearl diff --git a/src/test/skript/tests/syntaxes/effects/EffDoIf.sk b/src/test/skript/tests/syntaxes/effects/EffDoIf.sk index da17471d8d5..b9637ed3332 100644 --- a/src/test/skript/tests/syntaxes/effects/EffDoIf.sk +++ b/src/test/skript/tests/syntaxes/effects/EffDoIf.sk @@ -1,6 +1,6 @@ test "do if": - set {_false} to false if 1 is 1 - assert {_false} is false with "Do if didn't run when it should have" + set {_false} to false if 1 is 1 + assert {_false} is false with "Do if didn't run when it should have" - set {_unset} to true if 1 is 2 - assert {_unset} is not set with "Do if ran when it shouldn't have" + set {_unset} to true if 1 is 2 + assert {_unset} is not set with "Do if ran when it shouldn't have" diff --git a/src/test/skript/tests/syntaxes/effects/EffPvP.sk b/src/test/skript/tests/syntaxes/effects/EffPvP.sk index bb41b776670..37cee2ea12f 100644 --- a/src/test/skript/tests/syntaxes/effects/EffPvP.sk +++ b/src/test/skript/tests/syntaxes/effects/EffPvP.sk @@ -1,6 +1,6 @@ test "pvp effect": - set {_world} to first element out of all worlds - disable pvp in {_world} - assert pvp is disabled in {_world} with "PvP was not disabled" - enable pvp in {_world} - assert pvp is enabled in {_world} with "PvP was not enabled" + set {_world} to first element out of all worlds + disable pvp in {_world} + assert pvp is disabled in {_world} with "PvP was not disabled" + enable pvp in {_world} + assert pvp is enabled in {_world} with "PvP was not enabled" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprArithmetic.sk b/src/test/skript/tests/syntaxes/expressions/ExprArithmetic.sk index ef17ec0d979..5a81450c29d 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprArithmetic.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprArithmetic.sk @@ -1,247 +1,247 @@ test "number - number operations": - # --Addition/Subtraction-- - - assert (1 + 1) is (2) with "1 + 1 is not 2" - assert (2 + 2) is (4) with "2 + 2 is not 4" - assert (3 - 3) is (0) with "3 - 3 is not 0" - assert (4 - 2) is (2) with "4 - 2 is not 2" - assert (5 + 1) is (6) with "5 + 1 is not 6" - assert (6 - -2) is (8) with "6 - -2 is not 8" - assert (4-(40)) is (-36) with "4 - 40 is not -36" - - # --Multiplication-- - - assert (1*5) is (5) with "1 * 5 is not 5" - assert (2*5) is (10) with "2 * 5 is not 10" - assert (3*-5) is (-15) with "3 * -5 is not 15" - assert (4*0) is (0) with "4 * 0 is not 0" - assert (5*infinity value) is (infinity value) with "5 * infinity is not infinity" - - # --Division-- - - assert (1/5) is (0.2) with "1 / 5 is not 0.2" - assert (2/5) is (0.4) with "2 / 5 is not 0.4" - assert (3/-5) is (-0.6) with "3 / -5 is not -0.6" - assert (4/0) is (infinity value) with "4 / 0 is not infinity" - assert (5/0) is (infinity value) with "5 / 0 is not infinity" - assert isNaN(0/0) is true with "0 / 0 is not NaN" - assert (5/2.5) is (2) with "5 / 2.5 is not 2" - - # --Exponents-- - - assert (1^5) is (1) with "1 ^ 5 is not 1" - assert (2^5) is (32) with "2 ^ 5 is not 32" - assert (3^-5) is (0.00411522633) with "3 ^ -5 is not 0.00411522633" - assert (4^0) is (1) with "4 ^ 0 is not 1" - assert (5^0) is (1) with "5 ^ 0 is not 1" - assert (0^0) is (1) with "0 ^ 0 is not 1" - assert (5^2.5) - 55.901 < 0.001 with "5 ^ 2.5 is not 55.9" - - # --Order of Operations-- - - assert (1 + 2 - 1 * 3) is (0) with "1 + 2 - 1 * 3 is not 0" - assert (1 + 2 - 1 / 2 * 3 ) is (1.5) with "1 + 2 - 1 / 2 * 3 is not 1.5" - assert (1 + (2) - 1 / (2) * 3) is (1.5) with "1 + (2) - 1 / (2) * 3 is not 1.5" - assert (1 + (2 - 1 / 2) * 3) is (5.5) with "1 + (2 - 1 / 2) * 3 is not 5.5" - assert (1 + 2 - 1 / 2 * 3 + 4 - 5 * 6 / 2 ) is (-9.5) with "1 + 2 - 1 / 2 * 3 + 4 - 5 * 6 / 2 is not -9.5" - assert (1 + (1 - 2) * 3) is (-2) with "1 + (1 - 2) * 3 is not -2" - - # --Edge Cases-- - - assert (0^99999) is (0) with "0^99999 is not 0" - assert (1 + 2.3 + 3.01) is (6.31) with "1 + 2.3 + 3.01 is not 6.31" - assert (0.1 + 0.1 + 0.1) is (0.30000000000000004) with "0.1 + 0.1 + 0.1 is not 0.30000000000000004" - assert (1/-infinity value) is (-0) with "1/-infinity value is not -0" - - - # --Standard Operations-- - - set {_x} to 1 - set {_y} to 2 - set {_z} to 3 - assert ({_x}) is (1) with "{_x} is not 1" - assert ({_y}) is (2) with "{_y} is not 2" - assert ({_z}) is (3) with "{_z} is not 3" - assert ({_x} + {_y} + {_z}) is (6) with "1 + 2 + 3 is not 6" - assert ({_x} + {_y} - {_z}) is (0) with "1 + 2 - 3 is not 0" - assert ({_x} + {_y} * {_z}) is (7) with "1 + 2 * 3 is not 7" - assert ({_x} + {_y} / {_z}) is (1.6666666666666667) with "1 + 2 / 3 is not 1.6666666666666667" - assert ({_x} + {_y} ^ {_z}) is (9) with "1 + 2 ^ 3 is not 9" - assert ({_x} - {_y} + {_z}) is (2) with "1 - 2 + 3 is not 2" - assert ({_x} - {_y} - {_z}) is (-4) with "1 - 2 - 3 is not -4" - assert ({_x} - {_y} * {_z}) is (-5) with "1 - 2 * 3 is not -5" - assert ({_x} - {_y} / {_z}) is (0.3333333333333333) with "1 - 2 / 3 is not 0.3333333333333333" - assert ({_x} - {_y} ^ {_z}) is (-7) with "1 - 2 ^ 3 is not -7" - assert ({_x} * {_y} + {_z}) is (5) with "1 * 2 + 3 is not 5" - assert ({_x} * {_y} - {_z}) is (-1) with "1 * 2 - 3 is not -1" - assert ({_x} * {_y} * {_z}) is (6) with "1 * 2 * 3 is not 6" - assert ({_x} * {_y} / {_z}) is (0.6666666666666666) with "1 * 2 / 3 is not 0.6666666666666666" - assert ({_x} * {_y} ^ {_z}) is (8) with "1 * 2 ^ 3 is not 8" - assert ({_x} / {_y} + {_z}) is (3.5) with "1 / 2 + 3 is not 3.5" - assert ({_x} / {_y} - {_z}) is (-2.5) with "1 / 2 - 3 is not -2.5" - assert ({_x} / {_y} * {_z}) is (1.5) with "1 / 2 * 3 is not 1.5" - assert ({_x} / {_y} / {_z}) is (0.16666666666666666) with "1 / 2 / 3 is not 0.16666666666666666" - assert ({_x} / {_y} ^ {_z}) is (0.125) with "1 / 2 ^ 3 is not 0.125" - assert ({_x} ^ {_y} + {_z}) is (4) with "1 ^ 2 + 3 is not 4" - assert ({_x} ^ {_y} - {_z}) is (-2) with "1 ^ 2 - 3 is not -2" - assert ({_x} ^ {_y} * {_z}) is (3) with "1 ^ 2 * 3 is not 3" - assert ({_x} ^ {_y} / {_z}) is (0.3333333333333333) with "1 ^ 2 / 3 is not 0.3333333333333333" - assert ({_x} ^ {_y} ^ {_z}) is (1) with "1 ^ 2 ^ 3 is not 1" - - # --Non-Numbers-- - - set {_x} to 1 - set {_y} to {_} - set {_z} to 3 - - assert ({_y} / 3) is (0) with " / 3 is not 0" - assert ({_x}) is (1) with "{_x} is not 1" - assert ({_y} + 1 + 1) is (2) with " + 1 + 1 is not 2" - assert (1 + {_y} + 1) is (2) with "1 + + 1 is not 2" - assert ({_z}) is (3) with "{_z} is not 3" - assert ({_x} + {_y} + {_z}) is (4) with "1 + + 3 is not 4" - assert ({_x} + {_y} - {_z}) is (-2) with "1 + - 3 is not -2" - assert ({_x} + {_y} * {_z}) is (1) with "1 + * 3 is not 1" - assert ({_x} + {_y} / {_z}) is (1) with "1 + / 3 is not 1" - assert ({_x} + {_y} ^ {_z}) is (1) with "1 + ^ 3 is not 1" - assert ({_x} - {_y} + {_z}) is (4) with "1 - + 3 is not 4" - assert ({_x} - {_y} - {_z}) is (-2) with "1 - - 3 is not -2" - assert ({_x} - {_y} * {_z}) is (1) with "1 - * 3 is not 1" - assert ({_x} - {_y} / {_z}) is (1) with "1 - / 3 is not 1" - assert ({_x} - {_y} ^ {_z}) is (1) with "1 - ^ 3 is not 1" - assert ({_x} * {_y} + {_z}) is (3) with "1 * + 3 is not 3" - assert ({_x} * {_y} - {_z}) is (-3) with "1 * - 3 is not -3" - assert ({_x} * {_y} * {_z}) is (0) with "1 * * 3 is not 0" - assert ({_x} * {_y} / {_z}) is (0) with "1 * / 3 is not 0" - assert ({_x} * {_y} ^ {_z}) is (0) with "1 * ^ 3 is not 0" - assert ({_x} / {_y} + {_z}) is (infinity value) with "1 / + 3 is not infinity" - assert ({_x} / {_y} - {_z}) is (infinity value) with "1 / - 3 is not infinity" - assert ({_x} / {_y} * {_z}) is (infinity value) with "1 / * 3 is not infinity" - assert ({_x} / {_y} / {_z}) is (infinity value) with "1 / / 3 is not infinity" - assert ({_x} / {_y} ^ {_z}) is (infinity value) with "1 / ^ 3 is not infinity" - assert ({_x} ^ {_y} + {_z}) is (4) with "1 ^ + 3 is not 4" - assert ({_x} ^ {_y} - {_z}) is (-2) with "1 ^ - 3 is not -2" - assert ({_x} ^ {_y} * {_z}) is (3) with "1 ^ * 3 is not 3" - assert ({_x} ^ {_y} / {_z}) is (0.333333333333) with "1 ^ / 3 is not 0.333333333333" - assert ({_x} ^ {_y} ^ {_z}) is (1) with "1 ^ ^ 3 is not 1" - - # --Difference-- - assert (difference between 1 and 2) is (1) with "difference between 1 and 2 is not 1" - assert (difference between 2 and 1) is (1) with "difference between 2 and 1 is not 1" - assert (difference between 1 and 1) is (0) with "difference between 1 and 1 is not 0" - assert (difference between 1 and 0) is (1) with "difference between 1 and 0 is not 1" - assert (difference between 0 and 1) is (1) with "difference between 0 and 1 is not 1" - assert (difference between 0 and 0) is (0) with "difference between 0 and 0 is not 0" - assert (difference between 1 and infinity value) is (infinity value) with "difference between 1 and infinity is not infinity" - assert (difference between infinity value and 1) is (infinity value) with "difference between infinity and 1 is not infinity" - assert isNaN(difference between infinity value and infinity value) is true with "difference between infinity and infinity is not NaN" - assert isNaN(difference between 1 and NaN value) is true with "difference between 1 and NaN is not NaN" - assert isNaN(difference between NaN value and 1) is true with "difference between NaN and 1 is not NaN" - assert isNaN(difference between NaN value and NaN value) is true with "difference between NaN and NaN is not NaN" + # --Addition/Subtraction-- + + assert (1 + 1) is (2) with "1 + 1 is not 2" + assert (2 + 2) is (4) with "2 + 2 is not 4" + assert (3 - 3) is (0) with "3 - 3 is not 0" + assert (4 - 2) is (2) with "4 - 2 is not 2" + assert (5 + 1) is (6) with "5 + 1 is not 6" + assert (6 - -2) is (8) with "6 - -2 is not 8" + assert (4-(40)) is (-36) with "4 - 40 is not -36" + + # --Multiplication-- + + assert (1*5) is (5) with "1 * 5 is not 5" + assert (2*5) is (10) with "2 * 5 is not 10" + assert (3*-5) is (-15) with "3 * -5 is not 15" + assert (4*0) is (0) with "4 * 0 is not 0" + assert (5*infinity value) is (infinity value) with "5 * infinity is not infinity" + + # --Division-- + + assert (1/5) is (0.2) with "1 / 5 is not 0.2" + assert (2/5) is (0.4) with "2 / 5 is not 0.4" + assert (3/-5) is (-0.6) with "3 / -5 is not -0.6" + assert (4/0) is (infinity value) with "4 / 0 is not infinity" + assert (5/0) is (infinity value) with "5 / 0 is not infinity" + assert isNaN(0/0) is true with "0 / 0 is not NaN" + assert (5/2.5) is (2) with "5 / 2.5 is not 2" + + # --Exponents-- + + assert (1^5) is (1) with "1 ^ 5 is not 1" + assert (2^5) is (32) with "2 ^ 5 is not 32" + assert (3^-5) is (0.00411522633) with "3 ^ -5 is not 0.00411522633" + assert (4^0) is (1) with "4 ^ 0 is not 1" + assert (5^0) is (1) with "5 ^ 0 is not 1" + assert (0^0) is (1) with "0 ^ 0 is not 1" + assert (5^2.5) - 55.901 < 0.001 with "5 ^ 2.5 is not 55.9" + + # --Order of Operations-- + + assert (1 + 2 - 1 * 3) is (0) with "1 + 2 - 1 * 3 is not 0" + assert (1 + 2 - 1 / 2 * 3 ) is (1.5) with "1 + 2 - 1 / 2 * 3 is not 1.5" + assert (1 + (2) - 1 / (2) * 3) is (1.5) with "1 + (2) - 1 / (2) * 3 is not 1.5" + assert (1 + (2 - 1 / 2) * 3) is (5.5) with "1 + (2 - 1 / 2) * 3 is not 5.5" + assert (1 + 2 - 1 / 2 * 3 + 4 - 5 * 6 / 2 ) is (-9.5) with "1 + 2 - 1 / 2 * 3 + 4 - 5 * 6 / 2 is not -9.5" + assert (1 + (1 - 2) * 3) is (-2) with "1 + (1 - 2) * 3 is not -2" + + # --Edge Cases-- + + assert (0^99999) is (0) with "0^99999 is not 0" + assert (1 + 2.3 + 3.01) is (6.31) with "1 + 2.3 + 3.01 is not 6.31" + assert (0.1 + 0.1 + 0.1) is (0.30000000000000004) with "0.1 + 0.1 + 0.1 is not 0.30000000000000004" + assert (1/-infinity value) is (-0) with "1/-infinity value is not -0" + + + # --Standard Operations-- + + set {_x} to 1 + set {_y} to 2 + set {_z} to 3 + assert ({_x}) is (1) with "{_x} is not 1" + assert ({_y}) is (2) with "{_y} is not 2" + assert ({_z}) is (3) with "{_z} is not 3" + assert ({_x} + {_y} + {_z}) is (6) with "1 + 2 + 3 is not 6" + assert ({_x} + {_y} - {_z}) is (0) with "1 + 2 - 3 is not 0" + assert ({_x} + {_y} * {_z}) is (7) with "1 + 2 * 3 is not 7" + assert ({_x} + {_y} / {_z}) is (1.6666666666666667) with "1 + 2 / 3 is not 1.6666666666666667" + assert ({_x} + {_y} ^ {_z}) is (9) with "1 + 2 ^ 3 is not 9" + assert ({_x} - {_y} + {_z}) is (2) with "1 - 2 + 3 is not 2" + assert ({_x} - {_y} - {_z}) is (-4) with "1 - 2 - 3 is not -4" + assert ({_x} - {_y} * {_z}) is (-5) with "1 - 2 * 3 is not -5" + assert ({_x} - {_y} / {_z}) is (0.3333333333333333) with "1 - 2 / 3 is not 0.3333333333333333" + assert ({_x} - {_y} ^ {_z}) is (-7) with "1 - 2 ^ 3 is not -7" + assert ({_x} * {_y} + {_z}) is (5) with "1 * 2 + 3 is not 5" + assert ({_x} * {_y} - {_z}) is (-1) with "1 * 2 - 3 is not -1" + assert ({_x} * {_y} * {_z}) is (6) with "1 * 2 * 3 is not 6" + assert ({_x} * {_y} / {_z}) is (0.6666666666666666) with "1 * 2 / 3 is not 0.6666666666666666" + assert ({_x} * {_y} ^ {_z}) is (8) with "1 * 2 ^ 3 is not 8" + assert ({_x} / {_y} + {_z}) is (3.5) with "1 / 2 + 3 is not 3.5" + assert ({_x} / {_y} - {_z}) is (-2.5) with "1 / 2 - 3 is not -2.5" + assert ({_x} / {_y} * {_z}) is (1.5) with "1 / 2 * 3 is not 1.5" + assert ({_x} / {_y} / {_z}) is (0.16666666666666666) with "1 / 2 / 3 is not 0.16666666666666666" + assert ({_x} / {_y} ^ {_z}) is (0.125) with "1 / 2 ^ 3 is not 0.125" + assert ({_x} ^ {_y} + {_z}) is (4) with "1 ^ 2 + 3 is not 4" + assert ({_x} ^ {_y} - {_z}) is (-2) with "1 ^ 2 - 3 is not -2" + assert ({_x} ^ {_y} * {_z}) is (3) with "1 ^ 2 * 3 is not 3" + assert ({_x} ^ {_y} / {_z}) is (0.3333333333333333) with "1 ^ 2 / 3 is not 0.3333333333333333" + assert ({_x} ^ {_y} ^ {_z}) is (1) with "1 ^ 2 ^ 3 is not 1" + + # --Non-Numbers-- + + set {_x} to 1 + set {_y} to {_} + set {_z} to 3 + + assert ({_y} / 3) is (0) with " / 3 is not 0" + assert ({_x}) is (1) with "{_x} is not 1" + assert ({_y} + 1 + 1) is (2) with " + 1 + 1 is not 2" + assert (1 + {_y} + 1) is (2) with "1 + + 1 is not 2" + assert ({_z}) is (3) with "{_z} is not 3" + assert ({_x} + {_y} + {_z}) is (4) with "1 + + 3 is not 4" + assert ({_x} + {_y} - {_z}) is (-2) with "1 + - 3 is not -2" + assert ({_x} + {_y} * {_z}) is (1) with "1 + * 3 is not 1" + assert ({_x} + {_y} / {_z}) is (1) with "1 + / 3 is not 1" + assert ({_x} + {_y} ^ {_z}) is (1) with "1 + ^ 3 is not 1" + assert ({_x} - {_y} + {_z}) is (4) with "1 - + 3 is not 4" + assert ({_x} - {_y} - {_z}) is (-2) with "1 - - 3 is not -2" + assert ({_x} - {_y} * {_z}) is (1) with "1 - * 3 is not 1" + assert ({_x} - {_y} / {_z}) is (1) with "1 - / 3 is not 1" + assert ({_x} - {_y} ^ {_z}) is (1) with "1 - ^ 3 is not 1" + assert ({_x} * {_y} + {_z}) is (3) with "1 * + 3 is not 3" + assert ({_x} * {_y} - {_z}) is (-3) with "1 * - 3 is not -3" + assert ({_x} * {_y} * {_z}) is (0) with "1 * * 3 is not 0" + assert ({_x} * {_y} / {_z}) is (0) with "1 * / 3 is not 0" + assert ({_x} * {_y} ^ {_z}) is (0) with "1 * ^ 3 is not 0" + assert ({_x} / {_y} + {_z}) is (infinity value) with "1 / + 3 is not infinity" + assert ({_x} / {_y} - {_z}) is (infinity value) with "1 / - 3 is not infinity" + assert ({_x} / {_y} * {_z}) is (infinity value) with "1 / * 3 is not infinity" + assert ({_x} / {_y} / {_z}) is (infinity value) with "1 / / 3 is not infinity" + assert ({_x} / {_y} ^ {_z}) is (infinity value) with "1 / ^ 3 is not infinity" + assert ({_x} ^ {_y} + {_z}) is (4) with "1 ^ + 3 is not 4" + assert ({_x} ^ {_y} - {_z}) is (-2) with "1 ^ - 3 is not -2" + assert ({_x} ^ {_y} * {_z}) is (3) with "1 ^ * 3 is not 3" + assert ({_x} ^ {_y} / {_z}) is (0.333333333333) with "1 ^ / 3 is not 0.333333333333" + assert ({_x} ^ {_y} ^ {_z}) is (1) with "1 ^ ^ 3 is not 1" + + # --Difference-- + assert (difference between 1 and 2) is (1) with "difference between 1 and 2 is not 1" + assert (difference between 2 and 1) is (1) with "difference between 2 and 1 is not 1" + assert (difference between 1 and 1) is (0) with "difference between 1 and 1 is not 0" + assert (difference between 1 and 0) is (1) with "difference between 1 and 0 is not 1" + assert (difference between 0 and 1) is (1) with "difference between 0 and 1 is not 1" + assert (difference between 0 and 0) is (0) with "difference between 0 and 0 is not 0" + assert (difference between 1 and infinity value) is (infinity value) with "difference between 1 and infinity is not infinity" + assert (difference between infinity value and 1) is (infinity value) with "difference between infinity and 1 is not infinity" + assert isNaN(difference between infinity value and infinity value) is true with "difference between infinity and infinity is not NaN" + assert isNaN(difference between 1 and NaN value) is true with "difference between 1 and NaN is not NaN" + assert isNaN(difference between NaN value and 1) is true with "difference between NaN and 1 is not NaN" + assert isNaN(difference between NaN value and NaN value) is true with "difference between NaN and NaN is not NaN" test "vector - vector operations": - # --Addition/Subtraction-- - set {_v1} to vector(1, 2, 3) - set {_v2} to vector(4, 5, 6) - - assert ({_v1} + {_v2}) is (vector(5, 7, 9)) with "{_v1} + {_v2} is not vector(5, 7, 9)" - assert ({_v1} - {_v2}) is (vector(-3, -3, -3)) with "{_v1} - {_v2} is not vector(-3, -3, -3)" - assert ({_v1} * {_v2}) is (vector(4, 10, 18)) with "{_v1} * {_v2} is not vector(4, 10, 18)" - assert ({_v1} / {_v2}) is (vector(0.25, 0.4, 0.5)) with "{_v1} / {_v2} is not vector(0.25, 0.4, 0.5)" - - # --Zero Vectors-- - - set {_v1} to vector(0,0,0) - set {_v2} to vector(1,2,3) - assert ({_v1} + {_v2}) is (vector(1, 2, 3)) with "vector(0,0,0) + vector(1,2,3) is not vector(1, 2, 3)" - assert ({_v1} - {_v2}) is (vector(-1, -2, -3)) with "vector(0,0,0) - vector(1,2,3) is not vector(-1, -2, -3)" - assert ({_v1} * {_v2}) is (vector(0, 0, 0)) with "vector(0,0,0) * vector(1,2,3) is not vector(0, 0, 0)" - assert ({_v1} / {_v2}) is (vector(0, 0, 0)) with "vector(0,0,0) / vector(1,2,3) is not vector(0, 0, 0)" - assert ({_v2} + {_v1}) is (vector(1, 2, 3)) with "vector(1,2,3) + vector(0,0,0) is not vector(1, 2, 3)" - assert ({_v2} - {_v1}) is (vector(1, 2, 3)) with "vector(1,2,3) - vector(0,0,0) is not vector(1, 2, 3)" - assert ({_v2} * {_v1}) is (vector(0, 0, 0)) with "vector(1,2,3) * vector(0,0,0) is not vector(0, 0, 0)" - assert vector_equals(({_v2} / {_v1}), infinity value, infinity value, infinity value) is true with "vector(1,2,3) / vector(0,0,0) is not vector(infinity, infinity, infinity)" - - # --Non-Vectors-- - set {_v1} to vector(1, 2, 3) - set {_v2} to "test" - assert ({_v1} + {_v2}) is not set with "vector plus string is set" - assert ({_v1} - {_v2}) is not set with "vector minus string is set" - assert ({_v1} * {_v2}) is not set with "vector multiplied by string is set" - assert ({_v1} / {_v2}) is not set with "vector divided by string is set" - assert ({_v2} + {_v1}) is not set with "string plus vector is set" - assert ({_v2} - {_v1}) is not set with "string minus vector is set" - assert ({_v2} * {_v1}) is not set with "string multiplied by vector is set" - assert ({_v2} / {_v1}) is not set with "string divided by vector is set" - - # --Edge Cases-- - set {_v1} to vector(1, infinity value, 3) - set {_v2} to vector(4, 5, NaN value) - assert vector_equals(({_v1} + {_v2}), 5, infinity value, NaN value) is true with "vector(1, infinity, 3) + vector(4, 5, NaN) is not vector(5, infinity, NaN)" - assert vector_equals(({_v1} - {_v2}), -3, infinity value, NaN value) is true with "vector(1, infinity, 3) - vector(4, 5, NaN) is not vector(-3, infinity, NaN)" - assert vector_equals(({_v1} * {_v2}), 4, infinity value, NaN value) is true with "vector(1, infinity, 3) * vector(4, 5, NaN) is not vector(4, infinity, NaN)" - assert vector_equals(({_v1} / {_v2}), 0.25, infinity value, NaN value) is true with "vector(1, infinity, 3) / vector(4, 5, NaN) is not vector(0.25, infinity, NaN)" - assert vector_equals(({_v2} + {_v1}), 5, infinity value, NaN value) is true with "vector(4, 5, NaN) + vector(1, infinity, 3) is not vector(5, infinity, NaN)" - assert vector_equals(({_v2} - {_v1}), 3, -infinity value, NaN value) is true with "vector(4, 5, NaN) - vector(1, infinity, 3) is not vector(3, -infinity, NaN)" - assert vector_equals(({_v2} * {_v1}), 4, infinity value, NaN value) is true with "vector(4, 5, NaN) * vector(1, infinity, 3) is not vector(4, infinity, NaN)" - assert vector_equals(({_v2} / {_v1}), 4, 0, NaN value) is true with "vector(4, 5, NaN) / vector(1, infinity, 3) is not vector(4, 0, NaN)" - - # --Difference-- - - set {_v1} to vector(1, 2, 3) - set {_v2} to vector(4, 5, 6) - assert (difference between {_v1} and {_v2}) is (vector(3, 3, 3)) with "difference between vector(1,2,3) and vector(4,5,6) is not vector(3, 3, 3)" - assert (difference between {_v2} and {_v1}) is (vector(3, 3, 3)) with "difference between vector(4,5,6) and vector(1,2,3) is not vector(3, 3, 3)" - assert (difference between {_v1} and {_v1}) is (vector(0, 0, 0)) with "difference between vector(1,2,3) and vector(1,2,3) is not vector(0, 0, 0)" - assert (difference between {_v1} and vector(0,0,0)) is (vector(1, 2, 3)) with "difference between vector(1,2,3) and vector(0,0,0) is not vector(1, 2, 3)" - assert (difference between vector(0,0,0) and {_v1}) is (vector(1, 2, 3)) with "difference between vector(0,0,0) and {_v1} is not vector(1, 2, 3)" - assert vector_equals(difference between {_v1} and vector(infinity value, -infinity value, NaN value), infinity value, infinity value, NaN value) is true with "difference between vector(1,2,3) and vector(infinity, -infinity, NaN) is not vector(infinity, infinity, NaN)" + # --Addition/Subtraction-- + set {_v1} to vector(1, 2, 3) + set {_v2} to vector(4, 5, 6) + + assert ({_v1} + {_v2}) is (vector(5, 7, 9)) with "{_v1} + {_v2} is not vector(5, 7, 9)" + assert ({_v1} - {_v2}) is (vector(-3, -3, -3)) with "{_v1} - {_v2} is not vector(-3, -3, -3)" + assert ({_v1} * {_v2}) is (vector(4, 10, 18)) with "{_v1} * {_v2} is not vector(4, 10, 18)" + assert ({_v1} / {_v2}) is (vector(0.25, 0.4, 0.5)) with "{_v1} / {_v2} is not vector(0.25, 0.4, 0.5)" + + # --Zero Vectors-- + + set {_v1} to vector(0,0,0) + set {_v2} to vector(1,2,3) + assert ({_v1} + {_v2}) is (vector(1, 2, 3)) with "vector(0,0,0) + vector(1,2,3) is not vector(1, 2, 3)" + assert ({_v1} - {_v2}) is (vector(-1, -2, -3)) with "vector(0,0,0) - vector(1,2,3) is not vector(-1, -2, -3)" + assert ({_v1} * {_v2}) is (vector(0, 0, 0)) with "vector(0,0,0) * vector(1,2,3) is not vector(0, 0, 0)" + assert ({_v1} / {_v2}) is (vector(0, 0, 0)) with "vector(0,0,0) / vector(1,2,3) is not vector(0, 0, 0)" + assert ({_v2} + {_v1}) is (vector(1, 2, 3)) with "vector(1,2,3) + vector(0,0,0) is not vector(1, 2, 3)" + assert ({_v2} - {_v1}) is (vector(1, 2, 3)) with "vector(1,2,3) - vector(0,0,0) is not vector(1, 2, 3)" + assert ({_v2} * {_v1}) is (vector(0, 0, 0)) with "vector(1,2,3) * vector(0,0,0) is not vector(0, 0, 0)" + assert vector_equals(({_v2} / {_v1}), infinity value, infinity value, infinity value) is true with "vector(1,2,3) / vector(0,0,0) is not vector(infinity, infinity, infinity)" + + # --Non-Vectors-- + set {_v1} to vector(1, 2, 3) + set {_v2} to "test" + assert ({_v1} + {_v2}) is not set with "vector plus string is set" + assert ({_v1} - {_v2}) is not set with "vector minus string is set" + assert ({_v1} * {_v2}) is not set with "vector multiplied by string is set" + assert ({_v1} / {_v2}) is not set with "vector divided by string is set" + assert ({_v2} + {_v1}) is not set with "string plus vector is set" + assert ({_v2} - {_v1}) is not set with "string minus vector is set" + assert ({_v2} * {_v1}) is not set with "string multiplied by vector is set" + assert ({_v2} / {_v1}) is not set with "string divided by vector is set" + + # --Edge Cases-- + set {_v1} to vector(1, infinity value, 3) + set {_v2} to vector(4, 5, NaN value) + assert vector_equals(({_v1} + {_v2}), 5, infinity value, NaN value) is true with "vector(1, infinity, 3) + vector(4, 5, NaN) is not vector(5, infinity, NaN)" + assert vector_equals(({_v1} - {_v2}), -3, infinity value, NaN value) is true with "vector(1, infinity, 3) - vector(4, 5, NaN) is not vector(-3, infinity, NaN)" + assert vector_equals(({_v1} * {_v2}), 4, infinity value, NaN value) is true with "vector(1, infinity, 3) * vector(4, 5, NaN) is not vector(4, infinity, NaN)" + assert vector_equals(({_v1} / {_v2}), 0.25, infinity value, NaN value) is true with "vector(1, infinity, 3) / vector(4, 5, NaN) is not vector(0.25, infinity, NaN)" + assert vector_equals(({_v2} + {_v1}), 5, infinity value, NaN value) is true with "vector(4, 5, NaN) + vector(1, infinity, 3) is not vector(5, infinity, NaN)" + assert vector_equals(({_v2} - {_v1}), 3, -infinity value, NaN value) is true with "vector(4, 5, NaN) - vector(1, infinity, 3) is not vector(3, -infinity, NaN)" + assert vector_equals(({_v2} * {_v1}), 4, infinity value, NaN value) is true with "vector(4, 5, NaN) * vector(1, infinity, 3) is not vector(4, infinity, NaN)" + assert vector_equals(({_v2} / {_v1}), 4, 0, NaN value) is true with "vector(4, 5, NaN) / vector(1, infinity, 3) is not vector(4, 0, NaN)" + + # --Difference-- + + set {_v1} to vector(1, 2, 3) + set {_v2} to vector(4, 5, 6) + assert (difference between {_v1} and {_v2}) is (vector(3, 3, 3)) with "difference between vector(1,2,3) and vector(4,5,6) is not vector(3, 3, 3)" + assert (difference between {_v2} and {_v1}) is (vector(3, 3, 3)) with "difference between vector(4,5,6) and vector(1,2,3) is not vector(3, 3, 3)" + assert (difference between {_v1} and {_v1}) is (vector(0, 0, 0)) with "difference between vector(1,2,3) and vector(1,2,3) is not vector(0, 0, 0)" + assert (difference between {_v1} and vector(0,0,0)) is (vector(1, 2, 3)) with "difference between vector(1,2,3) and vector(0,0,0) is not vector(1, 2, 3)" + assert (difference between vector(0,0,0) and {_v1}) is (vector(1, 2, 3)) with "difference between vector(0,0,0) and {_v1} is not vector(1, 2, 3)" + assert vector_equals(difference between {_v1} and vector(infinity value, -infinity value, NaN value), infinity value, infinity value, NaN value) is true with "difference between vector(1,2,3) and vector(infinity, -infinity, NaN) is not vector(infinity, infinity, NaN)" test "number - vector operations": - set {_v1} to vector(1, 2, 3) - set {_v2} to 2 - assert ({_v1} + 2) is not set with "vector plus number is set" - assert ({_v1} - 2) is not set with "vector minus number is set" - assert ({_v1} * 2) is (vector(2, 4, 6)) with "vector(1,2,3) * 2 is not vector(2, 4, 6)" - assert ({_v1} / 2) is (vector(0.5, 1, 1.5)) with "vector(1,2,3) / 2 is not vector(0.5, 1, 1.5)" - assert ({_v1} ^ 2) is not set with "vector to the power of a number is set" - assert ({_v1} * 0) is (vector(0, 0, 0)) with "vector(1,2,3) * 0 is not vector(0, 0, 0)" - assert vector_equals(({_v1} / 0), infinity value, infinity value, infinity value) is true with "vector(1,2,3) / 0 is not vector(infinity, infinity, infinity)" - assert vector_equals(({_v1} * -infinity value), -infinity value, -infinity value, -infinity value) is true with "vector(1,2,3) * -infinity is not vector(-infinity, -infinity, -infinity)" - assert vector_equals(({_v1} / -infinity value), -0, -0, -0) is true with "vector(1,2,3) / -infinity value is not vector(-0, -0, -0)" - assert vector_equals(({_v1} * NaN value), NaN value, NaN value, NaN value) is true with "vector(1,2,3) * NaN is not vector(NaN, NaN, NaN)" - assert vector_equals(({_v1} / NaN value), NaN value, NaN value, NaN value) is true with "vector(1,2,3) / NaN is not vector(NaN, NaN, NaN)" + set {_v1} to vector(1, 2, 3) + set {_v2} to 2 + assert ({_v1} + 2) is not set with "vector plus number is set" + assert ({_v1} - 2) is not set with "vector minus number is set" + assert ({_v1} * 2) is (vector(2, 4, 6)) with "vector(1,2,3) * 2 is not vector(2, 4, 6)" + assert ({_v1} / 2) is (vector(0.5, 1, 1.5)) with "vector(1,2,3) / 2 is not vector(0.5, 1, 1.5)" + assert ({_v1} ^ 2) is not set with "vector to the power of a number is set" + assert ({_v1} * 0) is (vector(0, 0, 0)) with "vector(1,2,3) * 0 is not vector(0, 0, 0)" + assert vector_equals(({_v1} / 0), infinity value, infinity value, infinity value) is true with "vector(1,2,3) / 0 is not vector(infinity, infinity, infinity)" + assert vector_equals(({_v1} * -infinity value), -infinity value, -infinity value, -infinity value) is true with "vector(1,2,3) * -infinity is not vector(-infinity, -infinity, -infinity)" + assert vector_equals(({_v1} / -infinity value), -0, -0, -0) is true with "vector(1,2,3) / -infinity value is not vector(-0, -0, -0)" + assert vector_equals(({_v1} * NaN value), NaN value, NaN value, NaN value) is true with "vector(1,2,3) * NaN is not vector(NaN, NaN, NaN)" + assert vector_equals(({_v1} / NaN value), NaN value, NaN value, NaN value) is true with "vector(1,2,3) / NaN is not vector(NaN, NaN, NaN)" test "timespan arithmetic": - set {_t1} to 1 second - set {_t2} to 2 seconds - assert ({_t1} + {_t2}) is (3 seconds) with "1 second + 2 seconds is not 3 seconds" - assert ({_t1} - {_t2}) is (0 seconds) with "1 second - 2 seconds is not 0 seconds" - assert ({_t1} * 2) is (2 seconds) with "1 second * 2 is not 2 seconds" - assert ({_t1} / 2) is (0.5 seconds) with "1 second / 2 is not 0.5 seconds" + set {_t1} to 1 second + set {_t2} to 2 seconds + assert ({_t1} + {_t2}) is (3 seconds) with "1 second + 2 seconds is not 3 seconds" + assert ({_t1} - {_t2}) is (0 seconds) with "1 second - 2 seconds is not 0 seconds" + assert ({_t1} * 2) is (2 seconds) with "1 second * 2 is not 2 seconds" + assert ({_t1} / 2) is (0.5 seconds) with "1 second / 2 is not 0.5 seconds" - assert (2 * {_t1}) is (2 seconds) with "2 * 1 second is not 2 seconds" - assert (2 / {_t1}) is not set with "number divided by timespan is set" + assert (2 * {_t1}) is (2 seconds) with "2 * 1 second is not 2 seconds" + assert (2 / {_t1}) is not set with "number divided by timespan is set" - assert ({_t1} + 2) is not set with "timespan plus number is set" + assert ({_t1} + 2) is not set with "timespan plus number is set" - assert {_t1} / {_t2} is 0.5 with "timespan / timespan failed" - assert {_t1} / 1 tick is 20 with "timespan / timespan of different units failed" - assert 0 seconds / {_t2} is 0 with "0 timespan / timespan failed" - assert {_t1} / 0 seconds is infinity value with "timespan / 0 timespan failed" - assert isNaN(0 seconds / 0 ticks) is true with "0 timespan / 0 timespan failed", expected NaN value, got (0 seconds / 0 ticks) + assert {_t1} / {_t2} is 0.5 with "timespan / timespan failed" + assert {_t1} / 1 tick is 20 with "timespan / timespan of different units failed" + assert 0 seconds / {_t2} is 0 with "0 timespan / timespan failed" + assert {_t1} / 0 seconds is infinity value with "timespan / 0 timespan failed" + assert isNaN(0 seconds / 0 ticks) is true with "0 timespan / 0 timespan failed", expected NaN value, got (0 seconds / 0 ticks) test "date arithmetic": - set {_d1} to now - set {_d2} to 1 day before {_d1} - assert ({_d1} + {_d2}) is not set with "" - - assert ({_d1} + 1 day) is (1 day after {_d1}) with "now + 1 day is not 1 day from now" - assert ({_d1} - 1 day) is (1 day before {_d1}) with "now - 1 day is not 1 day ago" - assert ({_d1} + 1 week) is (1 week after {_d1}) with "now + 1 week is not 1 week from now" - assert ({_d1} - 1 week) is (1 week before {_d1}) with "now - 1 week is not 1 week ago" - assert ({_d1} + 1) is not set with "" + set {_d1} to now + set {_d2} to 1 day before {_d1} + assert ({_d1} + {_d2}) is not set with "" + + assert ({_d1} + 1 day) is (1 day after {_d1}) with "now + 1 day is not 1 day from now" + assert ({_d1} - 1 day) is (1 day before {_d1}) with "now - 1 day is not 1 day ago" + assert ({_d1} + 1 week) is (1 week after {_d1}) with "now + 1 week is not 1 week from now" + assert ({_d1} - 1 week) is (1 week before {_d1}) with "now - 1 week is not 1 week ago" + assert ({_d1} + 1) is not set with "" local function vector_equals(vec: vector, x: number, y: number, z: number) :: boolean: return false if component_equals((x of {_vec}), {_x}) is false diff --git a/src/test/skript/tests/syntaxes/expressions/ExprInventory.sk b/src/test/skript/tests/syntaxes/expressions/ExprInventory.sk index b4165f8eddd..038c41fac5b 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprInventory.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprInventory.sk @@ -1,12 +1,12 @@ test "item inventory": - set {_shulker} to any shulker box - assert inventory of {_shulker} is set with "Failed to get shulker inventory" - set slot 1 of inventory of {_shulker} to dirt - assert slot 1 of inventory of {_shulker} is dirt with "Failed to set slot in shulker inventory" + set {_shulker} to any shulker box + assert inventory of {_shulker} is set with "Failed to get shulker inventory" + set slot 1 of inventory of {_shulker} to dirt + assert slot 1 of inventory of {_shulker} is dirt with "Failed to set slot in shulker inventory" - set {_chest} to chest - assert inventory of {_chest} is set with "Failed to get chest inventory" - set slot 1 of inventory of {_chest} to dirt - assert slot 1 of inventory of {_chest} is dirt with "Failed to set slot in chest inventory" + set {_chest} to chest + assert inventory of {_chest} is set with "Failed to get chest inventory" + set slot 1 of inventory of {_chest} to dirt + assert slot 1 of inventory of {_chest} is dirt with "Failed to set slot in chest inventory" - assert inventory of dirt is not set with "Inventory was found for an item without an inventory" + assert inventory of dirt is not set with "Inventory was found for an item without an inventory" diff --git a/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk b/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk index f19fdcd50c2..696a24ca888 100644 --- a/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk +++ b/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk @@ -88,7 +88,7 @@ test "spawn entities": add "falling powder snow" to {_entities::*} if running minecraft "1.20.0": - add "camel" and "sniffer" to {_entities::*} + add "camel" and "sniffer" to {_entities::*} if running minecraft "1.20.6": add "armadillo" to {_entities::*} if running minecraft "1.21": diff --git a/src/test/skript/tests/syntaxes/sections/SecParse.sk b/src/test/skript/tests/syntaxes/sections/SecParse.sk index 4478e781ff3..99ed6ec8041 100644 --- a/src/test/skript/tests/syntaxes/sections/SecParse.sk +++ b/src/test/skript/tests/syntaxes/sections/SecParse.sk @@ -1,8 +1,8 @@ test "parsing section": - parse: - parse: - # intentionally empty to cause an error - assert last parse logs contain "A parse section must contain code" with "Parse section didn't return expected error" - parse: - set {_x} to {_y} # valid code - assert last parse logs is not set with "Parse section contained errors when code was valid" + parse: + parse: + # intentionally empty to cause an error + assert last parse logs contain "A parse section must contain code" with "Parse section didn't return expected error" + parse: + set {_x} to {_y} # valid code + assert last parse logs is not set with "Parse section contained errors when code was valid" diff --git a/src/test/skript/tests/syntaxes/structures/StructParse.sk b/src/test/skript/tests/syntaxes/structures/StructParse.sk index 5b249535d36..33d5973a1c0 100644 --- a/src/test/skript/tests/syntaxes/structures/StructParse.sk +++ b/src/test/skript/tests/syntaxes/structures/StructParse.sk @@ -1,11 +1,11 @@ parse: - results: {StructParse::parse::*} - code: - parse: - results: "oops" - code: - on script load: - broadcast "hi" + results: {StructParse::parse::*} + code: + parse: + results: "oops" + code: + on script load: + broadcast "hi" test "StructParse": - assert {StructParse::parse::*} contains """oops"" cannot be set to strings" with "StructParse error not found" \ No newline at end of file + assert {StructParse::parse::*} contains """oops"" cannot be set to strings" with "StructParse error not found" From b78db1d861fb83a87c6a14098f858eaacacc1625 Mon Sep 17 00:00:00 2001 From: Shane Bee Date: Mon, 30 Dec 2024 08:24:05 -0800 Subject: [PATCH 18/19] ExprSets - fix return issue (#7327) * ExprSets - fix return issue * 7327 - add regression test * ExprSets - get rid of ArrayList, eww --------- Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> --- src/main/java/ch/njol/skript/expressions/ExprSets.java | 6 ++++-- .../skript/tests/regressions/7327-expr-sets-return.sk | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 src/test/skript/tests/regressions/7327-expr-sets-return.sk diff --git a/src/main/java/ch/njol/skript/expressions/ExprSets.java b/src/main/java/ch/njol/skript/expressions/ExprSets.java index 318475a390f..29e5a617f5a 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprSets.java +++ b/src/main/java/ch/njol/skript/expressions/ExprSets.java @@ -1,6 +1,8 @@ package ch.njol.skript.expressions; +import java.lang.reflect.Array; import java.util.Iterator; +import java.util.List; import java.util.function.Supplier; import org.bukkit.event.Event; @@ -61,8 +63,8 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye @Override protected Object[] get(Event event) { - Iterator iterator = supplier.get(); - return Lists.newArrayList(iterator).toArray(new Object[0]); + List objects = Lists.newArrayList(supplier.get()); + return objects.toArray((Object[]) Array.newInstance(classInfo.getC(), objects.size())); } @Override diff --git a/src/test/skript/tests/regressions/7327-expr-sets-return.sk b/src/test/skript/tests/regressions/7327-expr-sets-return.sk new file mode 100644 index 00000000000..1d4d1eebe42 --- /dev/null +++ b/src/test/skript/tests/regressions/7327-expr-sets-return.sk @@ -0,0 +1,10 @@ +test "ExprSets class return failure": + + set {_i} to diamond sword with item flag all item flags + assert {_i} is set with "Item with all item flags should be set" + + set {_i::*} to 1 of all itemtypes + assert {_i::*} is set with "X of set of all itemtypes should be set" + + # You're a lone wolf, Just want to make sure you don't error + set custom model data of all itemtypes to 10 From 03a506f8857ef02315c1834439385a532eced672 Mon Sep 17 00:00:00 2001 From: Shane Bee Date: Mon, 30 Dec 2024 08:29:06 -0800 Subject: [PATCH 19/19] gradle.properties - update testEnv (#7325) Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com> --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 8008d0748ff..a00ca65cc8d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,5 +7,5 @@ groupid=ch.njol name=skript version=2.9.5 jarName=Skript.jar -testEnv=java21/paper-1.21.3 +testEnv=java21/paper-1.21.4 testEnvJavaVersion=21