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/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 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()))); } } 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/main/java/ch/njol/skript/sections/SecFor.java b/src/main/java/ch/njol/skript/sections/SecFor.java index 575accc07b9..9fe0e2dbab5 100644 --- a/src/main/java/ch/njol/skript/sections/SecFor.java +++ b/src/main/java/ch/njol/skript/sections/SecFor.java @@ -45,7 +45,7 @@ When looping a simple (non-indexed) set of values, e.g. all players, the index w "loop key {_index} and value {_value} in {list of items::*}:", "\tbroadcast \"%{_index}% = %{_value}%\"", "", - "for each {_index} = {_value} in {my list::*}:", + "for each {_index}, {_value} in {my list::*}:", "\tbroadcast \"%{_index}% = %{_value}%\"", }) @Since("INSERT VERSION") diff --git a/src/main/java/ch/njol/skript/test/runner/EffTestPluralClassInfos.java b/src/main/java/ch/njol/skript/test/runner/EffTestPluralClassInfos.java new file mode 100644 index 00000000000..1a73ba5cdff --- /dev/null +++ b/src/main/java/ch/njol/skript/test/runner/EffTestPluralClassInfos.java @@ -0,0 +1,70 @@ +package ch.njol.skript.test.runner; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.NoDoc; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.registrations.Classes; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +/** + * This class is used to test whether class-info plurals are detected successfully. + * The syntax in it should never be parsed or used (even in test mode) + * and does nothing. + */ +@Name("Test Plural Class Infos") +@Description("Tests that plural class infos are identified correctly.") +@NoDoc +public class EffTestPluralClassInfos extends Effect { + + static { + class Example1 {} + class Example2 {} + class Example3 {} + class Example4 {} + if (TestMode.ENABLED) { + Classes.registerClass(new ClassInfo<>(Example1.class, "testgui") + .user("example1") + .name("Test -ui")); + Classes.registerClass(new ClassInfo<>(Example2.class, "exemplus") + .user("example2") + .name("Test -i")); + Classes.registerClass(new ClassInfo<>(Example3.class, "aardwolf") + .user("example3") + .name("Test -ves")); + Classes.registerClass(new ClassInfo<>(Example4.class, "hoof") + .user("example3") + .name("Test -ves 2")); + Skript.registerEffect(EffTestPluralClassInfos.class, + "classinfo test for %testgui%", + "classinfo test for %testguis%", + "classinfo test for %exemplus%", + "classinfo test for %exempli%", + "classinfo test for %aardwolf%", + "classinfo test for %aardwolves%", + "classinfo test for %hoof%", + "classinfo test for %hooves%"); + } + } + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + return false; + } + + @Override + protected void execute(Event event) { + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return ""; + } + +} diff --git a/src/main/java/ch/njol/skript/util/Utils.java b/src/main/java/ch/njol/skript/util/Utils.java index 75c1b165444..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. * @@ -52,7 +46,7 @@ public abstract class Utils { protected final static Deque plurals = new LinkedList<>(); static { - plurals.add(new WordEnding("axe", "axes")); + plurals.add(new WordEnding("axe", "axes")); // not complete since we have battleaxe, etc. plurals.add(new WordEnding("x", "xes")); plurals.add(new WordEnding("ay", "ays")); @@ -61,14 +55,20 @@ public abstract class Utils { plurals.add(new WordEnding("oy", "oys")); plurals.add(new WordEnding("uy", "uys")); plurals.add(new WordEnding("kie", "kies")); - plurals.add(new WordEnding("zombie", "zombies")); + plurals.add(new WordEnding("zombie", "zombies", true)); plurals.add(new WordEnding("y", "ies")); - plurals.add(new WordEnding("wife", "wives")); // we have to do the -ife -> ives first + plurals.add(new WordEnding("wife", "wives", true)); // we have to do the -ife -> ives first plurals.add(new WordEnding("life", "lives")); - plurals.add(new WordEnding("knife", "knives")); + plurals.add(new WordEnding("knife", "knives", true)); plurals.add(new WordEnding("ive", "ives")); - plurals.add(new WordEnding("elf", "elves")); // self shelf elf + + plurals.add(new WordEnding("lf", "lves")); // self shelf elf wolf half etc. + plurals.add(new WordEnding("thief", "thieves", true)); + plurals.add(new WordEnding("ief", "iefs")); // chiefs, fiefs, briefs + + plurals.add(new WordEnding("hoof", "hooves")); + plurals.add(new WordEnding("fe", "ves"));// most -f words' plurals can end in -fs as well as -ves plurals.add(new WordEnding("h", "hes")); @@ -79,16 +79,18 @@ public abstract class Utils { plurals.add(new WordEnding("api", "apis")); // api fix plurals.add(new WordEnding("us", "i")); - plurals.add(new WordEnding("hoe", "hoes")); - plurals.add(new WordEnding("toe", "toes")); + plurals.add(new WordEnding("hoe", "hoes", true)); + plurals.add(new WordEnding("toe", "toes", true)); + plurals.add(new WordEnding("foe", "foes", true)); + plurals.add(new WordEnding("woe", "woes", true)); plurals.add(new WordEnding("o", "oes")); - plurals.add(new WordEnding("alias", "aliases")); - plurals.add(new WordEnding("gas", "gases")); + plurals.add(new WordEnding("alias", "aliases", true)); + plurals.add(new WordEnding("gas", "gases", true)); - plurals.add(new WordEnding("child", "children")); + plurals.add(new WordEnding("child", "children")); // grandchild, etc. - plurals.add(new WordEnding("sheep", "sheep")); + plurals.add(new WordEnding("sheep", "sheep", true)); // general ending plurals.add(new WordEnding("", "s")); @@ -104,7 +106,7 @@ public static String join(final Object[] objects) { b.append(", "); b.append(Classes.toString(objects[i])); } - return "" + b.toString(); + return b.toString(); } public static String join(final Iterable objects) { @@ -118,7 +120,7 @@ public static String join(final Iterable objects) { first = false; b.append(Classes.toString(o)); } - return "" + b.toString(); + return b.toString(); } @SuppressWarnings("unchecked") @@ -134,7 +136,7 @@ public static Pair getAmount(String s) { } else if (s.matches("an? .+")) { return new Pair<>(s.split(" ", 2)[1], 1); } - return new Pair<>(s, Integer.valueOf(-1)); + return new Pair<>(s, -1); } // public final static class AmountResponse { @@ -267,14 +269,55 @@ public static File getFile(Plugin plugin) { public static NonNullPair getEnglishPlural(String word) { assert word != null; if (word.isEmpty()) - return new NonNullPair<>("", Boolean.FALSE); + return new NonNullPair<>("", false); + if (!couldBeSingular(word)) { + for (final WordEnding ending : plurals) { + if (ending.isCompleteWord()) { + // Complete words shouldn't be used as partial pieces + if (word.length() != ending.plural().length()) + continue; + } + if (word.endsWith(ending.plural())) + return new NonNullPair<>( + word.substring(0, word.length() - ending.plural().length()) + ending.singular(), + true + ); + if (word.endsWith(ending.plural().toUpperCase(Locale.ENGLISH))) + return new NonNullPair<>( + word.substring(0, word.length() - ending.plural().length()) + + ending.singular().toUpperCase(Locale.ENGLISH), + true + ); + } + } + return new NonNullPair<>(word, false); + } + + private static boolean couldBeSingular(String word) { for (final WordEnding ending : plurals) { - if (word.endsWith(ending.plural())) - return new NonNullPair<>(word.substring(0, word.length() - ending.plural().length()) + ending.singular(), Boolean.TRUE); - if (word.endsWith(ending.plural().toUpperCase(Locale.ENGLISH))) - return new NonNullPair<>(word.substring(0, word.length() - ending.plural().length()) + ending.singular().toUpperCase(Locale.ENGLISH), Boolean.TRUE); + if (ending.singular().isBlank()) + continue; + if (ending.isCompleteWord() && ending.singular().length() != word.length()) + continue; // Skip complete words + + if (word.endsWith(ending.singular()) || word.toLowerCase().endsWith(ending.singular())) { + return true; + } } - return new NonNullPair<>(word, Boolean.FALSE); + return false; + } + + /** + * Adds a singular/plural word override for the given words. + * This is inserted first in the list of words to be checked: it will always be matched + * and will override all other plurality rules. + * This will only match the word exactly, and will not apply to derivations of the word. + * + * @param singular The singular form of the word + * @param plural The plural form of the word + */ + public static void addPluralOverride(String singular, String plural) { + Utils.plurals.addFirst(new WordEnding(singular, plural, true)); } /** @@ -286,6 +329,11 @@ public static NonNullPair getEnglishPlural(String word) { public static String toEnglishPlural(String word) { assert word != null && word.length() != 0; for (WordEnding ending : plurals) { + if (ending.isCompleteWord()) { + // Complete words shouldn't be used as partial pieces + if (word.length() != ending.singular().length()) + continue; + } if (word.endsWith(ending.singular())) return word.substring(0, word.length() - ending.singular().length()) + ending.plural(); } @@ -518,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 @@ -547,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("<>", "")); } /** @@ -593,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 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}"); + 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; } } @@ -791,13 +837,10 @@ public static boolean isInteger(Number... numbers) { return true; } - protected static class WordEnding { // To be a record in 2.10 - - private final String singular, plural; + protected record WordEnding(String singular, String plural, boolean isCompleteWord) { - private WordEnding(String singular, String plural) { - this.singular = singular; - this.plural = plural; + public WordEnding(String singular, String plural) { + this(singular, plural, false); } public String singular() { @@ -811,8 +854,7 @@ public String plural() { @Override public boolean equals(Object object) { if (this == object) return true; - if (!(object instanceof WordEnding)) return false; - WordEnding ending = (WordEnding) object; + if (!(object instanceof WordEnding ending)) return false; return Objects.equals(singular, ending.singular) && Objects.equals(plural, ending.plural); } 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/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" 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"