diff --git a/gradle.properties b/gradle.properties index 5e27c501c58..8207c8da4ec 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.parallel=true groupid=ch.njol name=skript -version=2.10.0-pre1 +version=2.10.0 jarName=Skript.jar testEnv=java21/paper-1.21.4 testEnvJavaVersion=21 diff --git a/src/main/java/ch/njol/skript/ModernSkriptBridge.java b/src/main/java/ch/njol/skript/ModernSkriptBridge.java new file mode 100644 index 00000000000..194badaf0ac --- /dev/null +++ b/src/main/java/ch/njol/skript/ModernSkriptBridge.java @@ -0,0 +1,89 @@ +package ch.njol.skript; + +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.Skript; +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.function.Supplier; + +/** + * Bridge for interacting with the modern API classes from {@link org.skriptlang.skript}. + */ +final class ModernSkriptBridge { + + private ModernSkriptBridge() { } + + /** + * Similar to {@link Skript#unmodifiableView()}, but permits addon registration. + */ + public static final class SpecialUnmodifiableSkript implements Skript { + + private final Skript skript; + private final Skript unmodifiableSkript; + + public SpecialUnmodifiableSkript(Skript skript) { + this.skript = skript; + this.unmodifiableSkript = skript.unmodifiableView(); + } + + @Override + public SkriptAddon registerAddon(Class source, String name) { + return skript.registerAddon(source, name); + } + + @Override + public @Unmodifiable Collection addons() { + return unmodifiableSkript.addons(); + } + + @Override + public Class source() { + return unmodifiableSkript.source(); + } + + @Override + public String name() { + return unmodifiableSkript.name(); + } + + @Override + public > void storeRegistry(Class registryClass, R registry) { + unmodifiableSkript.storeRegistry(registryClass, registry); + } + + @Override + public void removeRegistry(Class> registryClass) { + unmodifiableSkript.removeRegistry(registryClass); + } + + @Override + public boolean hasRegistry(Class> registryClass) { + return unmodifiableSkript.hasRegistry(registryClass); + } + + @Override + public > R registry(Class registryClass) { + return unmodifiableSkript.registry(registryClass); + } + + @Override + public > R registry(Class registryClass, Supplier putIfAbsent) { + return unmodifiableSkript.registry(registryClass, putIfAbsent); + } + + @Override + public SyntaxRegistry syntaxRegistry() { + return unmodifiableSkript.syntaxRegistry(); + } + + @Override + public Localizer localizer() { + return unmodifiableSkript.localizer(); + } + } + +} diff --git a/src/main/java/ch/njol/skript/ScriptLoader.java b/src/main/java/ch/njol/skript/ScriptLoader.java index bd436928093..411323f9795 100644 --- a/src/main/java/ch/njol/skript/ScriptLoader.java +++ b/src/main/java/ch/njol/skript/ScriptLoader.java @@ -9,11 +9,11 @@ import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.Statement; import ch.njol.skript.lang.TriggerItem; -import ch.njol.skript.lang.TriggerSection; -import ch.njol.skript.lang.function.EffFunctionCall; import ch.njol.skript.lang.parser.ParserInstance; -import ch.njol.skript.log.*; -import ch.njol.skript.sections.SecLoop; +import ch.njol.skript.log.CountingLogHandler; +import ch.njol.skript.log.LogEntry; +import ch.njol.skript.log.RetainingLogHandler; +import ch.njol.skript.log.SkriptLogger; import ch.njol.skript.structures.StructOptions.OptionsData; import ch.njol.skript.test.runner.TestMode; import ch.njol.skript.util.ExceptionUtils; @@ -21,12 +21,10 @@ import ch.njol.skript.util.Task; import ch.njol.skript.util.Timespan; import ch.njol.skript.variables.TypeHints; -import ch.njol.util.Kleenean; import ch.njol.util.NonNullPair; import ch.njol.util.OpenCloseable; import ch.njol.util.StringUtils; import org.bukkit.Bukkit; -import org.bukkit.event.Event; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.script.Script; @@ -41,11 +39,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 90e860c16b3..adf004c1eaf 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -10,6 +10,16 @@ import ch.njol.skript.hooks.Hook; import ch.njol.skript.lang.*; import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionInfo; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.Section; +import ch.njol.skript.lang.SkriptEvent; +import ch.njol.skript.lang.SkriptEventInfo; +import ch.njol.skript.lang.Statement; +import ch.njol.skript.lang.SyntaxElementInfo; +import ch.njol.skript.lang.Trigger; +import ch.njol.skript.lang.TriggerItem; import ch.njol.skript.lang.Condition.ConditionType; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.skript.localization.Language; @@ -85,6 +95,7 @@ import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxOrigin; import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.ClassLoader; import java.io.File; import java.io.IOException; @@ -148,7 +159,7 @@ 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 skript = null; private static org.skriptlang.skript.@UnknownNullability Skript unmodifiableSkript = null; private static boolean disabled = false; @@ -453,7 +464,7 @@ public void onEnable() { // initialize the modern Skript instance skript = org.skriptlang.skript.Skript.of(getClass(), getName()); - unmodifiableSkript = skript.unmodifiableView(); + unmodifiableSkript = new ModernSkriptBridge.SpecialUnmodifiableSkript(skript); skript.localizer().setSourceDirectories("lang", getDataFolder().getAbsolutePath() + "lang"); // initialize the old Skript SkriptAddon instance @@ -669,10 +680,11 @@ protected void afterErrors() { debug("Early init done"); if (TestMode.ENABLED) { - if (TestMode.DEV_MODE) + if (TestMode.DEV_MODE) { runTests(); // Dev mode doesn't need a delay - else + } else { Bukkit.getWorlds().get(0).getChunkAtAsync(100, 100).thenRun(() -> runTests()); + } } Skript.metrics = new Metrics(Skript.getInstance(), 722); // 722 is our bStats plugin ID @@ -813,9 +825,14 @@ private void runTests() { TestingLogHandler errorCounter = new TestingLogHandler(Level.SEVERE); try { errorCounter.start(); - File testDir = TestMode.TEST_DIR.toFile(); - assert testDir != null; - ScriptLoader.loadScripts(testDir, errorCounter); + + // load example scripts (cleanup after) + ScriptLoader.loadScripts(new File(getScriptsFolder(), "-examples" + File.separator), errorCounter); + // unload these as to not interfere with the tests + ScriptLoader.unloadScripts(ScriptLoader.getLoadedScripts()); + + // load test directory scripts + ScriptLoader.loadScripts(TestMode.TEST_DIR.toFile(), errorCounter); } finally { errorCounter.stop(); } @@ -836,14 +853,24 @@ private void runTests() { info("Running sync JUnit tests..."); try { - List> classes = Lists.newArrayList(Utils.getClasses(Skript.getInstance(), "org.skriptlang.skript.test", "tests")); - // Don't attempt to run inner/anonymous classes as tests - classes.removeIf(Class::isAnonymousClass); - classes.removeIf(Class::isLocalClass); - // Test that requires package access. This is only present when compiling with src/test. - classes.add(Class.forName("ch.njol.skript.variables.FlatFileStorageTest")); - classes.add(Class.forName("ch.njol.skript.config.ConfigTest")); - classes.add(Class.forName("ch.njol.skript.config.NodeTest")); + // Search for all test classes + Set> classes = new HashSet<>(); + ClassLoader.builder() + .addSubPackages("org.skriptlang.skript", "ch.njol.skript") + .filter(fqn -> fqn.endsWith("Test")) + .initialize(true) + .deep(true) + .forEachClass(clazz -> { + if (clazz.isAnonymousClass() || clazz.isLocalClass()) + return; + classes.add(clazz); + }) + .build() + .loadClasses(Skript.class, getFile()); + // remove some known non-tests that get picked up + classes.remove(SkriptJUnitTest.class); + classes.remove(SkriptAsyncJUnitTest.class); + size.set(classes.size()); for (Class clazz : classes) { if (SkriptAsyncJUnitTest.class.isAssignableFrom(clazz)) { @@ -853,11 +880,6 @@ private void runTests() { runTest(clazz, shutdownDelay, tests, milliseconds, ignored, fails); } - } catch (IOException e) { - Skript.exception(e, "Failed to execute JUnit runtime tests."); - } catch (ClassNotFoundException e) { - // Should be the Skript test jar gradle task. - assert false : "Class 'ch.njol.skript.variables.FlatFileStorageTest' was not found."; } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { Skript.exception(e, "Failed to initalize test JUnit classes."); @@ -1437,7 +1459,18 @@ public String name() { } - private static SyntaxOrigin getSyntaxOrigin(JavaPlugin plugin) { + /** + * Attempts to create a SyntaxOrigin from a provided class. + */ + @ApiStatus.Internal + @ApiStatus.Experimental + public static SyntaxOrigin getSyntaxOrigin(Class source) { + JavaPlugin plugin; + try { + plugin = JavaPlugin.getProvidingPlugin(source); + } catch (IllegalArgumentException e) { // Occurs when the method fails to determine the providing plugin + return () -> source.getName(); + } SkriptAddon addon = getAddon(plugin); if (addon != null) { return SyntaxOrigin.of(addon); @@ -1466,7 +1499,7 @@ public static void registerCondition(Class conditionCla checkAcceptRegistrations(); skript.syntaxRegistry().register(SyntaxRegistry.CONDITION, SyntaxInfo.builder(conditionClass) .priority(type.priority()) - .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(conditionClass))) + .origin(getSyntaxOrigin(conditionClass)) .addPatterns(patterns) .build() ); @@ -1481,7 +1514,7 @@ public static void registerCondition(Class conditionCla public static void registerEffect(Class effectClass, String... patterns) throws IllegalArgumentException { checkAcceptRegistrations(); skript.syntaxRegistry().register(SyntaxRegistry.EFFECT, SyntaxInfo.builder(effectClass) - .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(effectClass))) + .origin(getSyntaxOrigin(effectClass)) .addPatterns(patterns) .build() ); @@ -1497,7 +1530,7 @@ public static void registerEffect(Class effectClass, Strin public static void registerSection(Class sectionClass, String... patterns) throws IllegalArgumentException { checkAcceptRegistrations(); skript.syntaxRegistry().register(SyntaxRegistry.SECTION, SyntaxInfo.builder(sectionClass) - .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(sectionClass))) + .origin(getSyntaxOrigin(sectionClass)) .addPatterns(patterns) .build() ); @@ -1548,7 +1581,7 @@ public static , T> void registerExpression( checkAcceptRegistrations(); skript.syntaxRegistry().register(SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(expressionType, returnType) .priority(type.priority()) - .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(expressionType))) + .origin(getSyntaxOrigin(expressionType)) .addPatterns(patterns) .build() ); @@ -1615,7 +1648,7 @@ public static SkriptEventInfo registerEvent( public static void registerStructure(Class structureClass, String... patterns) { checkAcceptRegistrations(); skript.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, SyntaxInfo.Structure.builder(structureClass) - .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(structureClass))) + .origin(getSyntaxOrigin(structureClass)) .addPatterns(patterns) .build() ); @@ -1624,7 +1657,7 @@ public static void registerStructure(Class structureCla public static void registerSimpleStructure(Class structureClass, String... patterns) { checkAcceptRegistrations(); skript.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, SyntaxInfo.Structure.builder(structureClass) - .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(structureClass))) + .origin(getSyntaxOrigin(structureClass)) .addPatterns(patterns) .nodeType(SyntaxInfo.Structure.NodeType.SIMPLE) .build() @@ -1636,7 +1669,7 @@ public static void registerStructure( ) { checkAcceptRegistrations(); skript.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, SyntaxInfo.Structure.builder(structureClass) - .origin(getSyntaxOrigin(JavaPlugin.getProvidingPlugin(structureClass))) + .origin(getSyntaxOrigin(structureClass)) .addPatterns(patterns) .entryValidator(entryValidator) .build() diff --git a/src/main/java/ch/njol/skript/SkriptAddon.java b/src/main/java/ch/njol/skript/SkriptAddon.java index f09bdcf4e5d..af381ff3049 100644 --- a/src/main/java/ch/njol/skript/SkriptAddon.java +++ b/src/main/java/ch/njol/skript/SkriptAddon.java @@ -33,7 +33,7 @@ public final class SkriptAddon implements org.skriptlang.skript.addon.SkriptAddo * Package-private constructor. Use {@link Skript#registerAddon(JavaPlugin)} to get a SkriptAddon for your plugin. */ SkriptAddon(JavaPlugin plugin) { - this(plugin, Skript.skript.registerAddon(plugin.getClass(), plugin.getName())); + this(plugin, Skript.instance().registerAddon(plugin.getClass(), plugin.getName())); } SkriptAddon(JavaPlugin plugin, org.skriptlang.skript.addon.SkriptAddon addon) { diff --git a/src/main/java/ch/njol/skript/classes/Changer.java b/src/main/java/ch/njol/skript/classes/Changer.java index 544433b6ee6..74b0040fe15 100644 --- a/src/main/java/ch/njol/skript/classes/Changer.java +++ b/src/main/java/ch/njol/skript/classes/Changer.java @@ -1,6 +1,7 @@ package ch.njol.skript.classes; import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import ch.njol.skript.classes.data.DefaultChangers; @@ -50,7 +51,7 @@ public boolean supportsKeyedChange() { abstract class ChangerUtils { - public static void change(Changer changer, Object[] what, Object @Nullable [] delta, ChangeMode mode) { + public static void change(@NotNull Changer changer, Object[] what, Object @Nullable [] delta, ChangeMode mode) { //noinspection unchecked changer.change((T[]) what, delta, mode); } @@ -63,19 +64,36 @@ public static void change(Changer changer, Object[] what, Object @Nullabl * @param types The types to test for * @return Whether expression.{@link Expression#change(Event, Object[], ChangeMode) change}(event, type[], mode) can be used or not. */ - public static boolean acceptsChange(final Expression expression, final ChangeMode mode, final Class... types) { - final Class[] validTypes = expression.acceptChange(mode); + public static boolean acceptsChange(@NotNull Expression expression, ChangeMode mode, Class... types) { + Class[] validTypes = expression.acceptChange(mode); if (validTypes == null) return false; - for (final Class type : types) { - for (final Class validType : validTypes) { - if (validType.isArray() ? validType.getComponentType().isAssignableFrom(type) : validType.isAssignableFrom(type)) + + for (int i = 0; i < validTypes.length; i++) { + if (validTypes[i].isArray()) + validTypes[i] = validTypes[i].getComponentType(); + } + + return acceptsChangeTypes(validTypes, types); + } + + /** + * Tests whether any of the given types is accepted by the given array of valid types. + * + * @param types The types to test for + * @param validTypes The valid types. All array classes should be unwrapped to their component type before calling. + * @return Whether any of the types is accepted by the valid types. + */ + public static boolean acceptsChangeTypes(Class[] validTypes, Class @NotNull ... types) { + for (Class type : types) { + for (Class validType : validTypes) { + if (validType.isAssignableFrom(type)) return true; } } return false; } - + } } diff --git a/src/main/java/ch/njol/skript/classes/Converter.java b/src/main/java/ch/njol/skript/classes/Converter.java new file mode 100644 index 00000000000..3bf628cf2c0 --- /dev/null +++ b/src/main/java/ch/njol/skript/classes/Converter.java @@ -0,0 +1,53 @@ +package ch.njol.skript.classes; + +import ch.njol.skript.command.Commands; +import ch.njol.skript.util.Utils; +import org.jetbrains.annotations.Nullable; + +/** + *

WARNING! This class has been removed in this update.

+ * This class stub has been left behind to prevent loading errors from outdated addons, + * but its functionality has been largely removed. + * + * @deprecated Use {@link org.skriptlang.skript.lang.converter.Converter} + */ +@Deprecated(forRemoval = true) +public interface Converter extends org.skriptlang.skript.lang.converter.Converter { + + // Interfaces don't have a so we trigger the warning notice with this + int $_WARNING = Utils.loadedRemovedClassWarning(Converter.class); + + @Deprecated(forRemoval = true) + int NO_LEFT_CHAINING = org.skriptlang.skript.lang.converter.Converter.NO_LEFT_CHAINING; + @Deprecated(forRemoval = true) + int NO_RIGHT_CHAINING = org.skriptlang.skript.lang.converter.Converter.NO_RIGHT_CHAINING; + @Deprecated(forRemoval = true) + int NO_CHAINING = NO_LEFT_CHAINING | NO_RIGHT_CHAINING; + @Deprecated(forRemoval = true) + int NO_COMMAND_ARGUMENTS = Commands.CONVERTER_NO_COMMAND_ARGUMENTS; + + @Deprecated(forRemoval = true) + @Nullable T convert(F f); + + @Deprecated(forRemoval = true) + final class ConverterUtils { + + @Deprecated(forRemoval = true) + public static Converter createInstanceofConverter(Class from, Converter conv) { + throw new UnsupportedOperationException(); + } + + @Deprecated(forRemoval = true) + public static Converter createInstanceofConverter(Converter conv, Class to) { + throw new UnsupportedOperationException(); + } + + @Deprecated(forRemoval = true) + public static Converter + createDoubleInstanceofConverter(Class from, Converter conv, Class to) { + throw new UnsupportedOperationException(); + } + + } + +} 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 f43258f2970..78e5799ef92 100644 --- a/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java +++ b/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java @@ -206,9 +206,11 @@ public boolean supportsNameChange() { @Override public void setName(String name) { BlockState state = block.getState(); - if (state instanceof Nameable nameable) + if (state instanceof Nameable nameable) { //noinspection deprecation nameable.setCustomName(name); + state.update(true, false); + } } }, // 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 9f015bdc2ff..edae0fba4ad 100644 --- a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java @@ -16,6 +16,7 @@ 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 ch.njol.skript.lang.util.common.AnyValued; import ch.njol.skript.localization.Noun; import ch.njol.skript.localization.RegexMessage; import ch.njol.skript.registrations.Classes; @@ -24,19 +25,21 @@ import ch.njol.skript.util.visual.VisualEffect; import ch.njol.skript.util.visual.VisualEffects; import ch.njol.yggdrasil.Fields; -import org.skriptlang.skript.lang.util.SkriptQueue; import org.bukkit.Material; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.script.Script; +import org.skriptlang.skript.lang.util.SkriptQueue; import org.skriptlang.skript.util.Executable; import java.io.File; +import java.io.NotSerializableException; import java.io.StreamCorruptedException; import java.nio.file.Path; import java.util.Arrays; import java.util.Iterator; +import java.util.List; import java.util.Locale; import java.util.regex.Pattern; @@ -730,7 +733,51 @@ public void change(SkriptQueue[] what, Object @Nullable [] delta, ChangeMode mod } } }) - .serializer(new YggdrasilSerializer<>()) + .parser(new Parser() { + + @Override + public boolean canParse(ParseContext context) { + return false; + } + + @Override + public String toString(SkriptQueue queue, int flags) { + return Classes.toString(queue.toArray(), flags, true); + } + + @Override + public String toVariableNameString(SkriptQueue queue) { + return this.toString(queue, 0); + } + + }) + .serializer(new Serializer() { + @Override + public Fields serialize(SkriptQueue queue) throws NotSerializableException { + Fields fields = new Fields(); + fields.putObject("contents", queue.toArray()); + return fields; + } + + @Override + public void deserialize(SkriptQueue queue, Fields fields) + throws StreamCorruptedException, NotSerializableException { + Object[] contents = fields.getObject("contents", Object[].class); + queue.clear(); + if (contents != null) + queue.addAll(List.of(contents)); + } + + @Override + public boolean mustSyncDeserialization() { + return false; + } + + @Override + protected boolean canBeInstantiated() { + return true; + } + }) ); @@ -899,6 +946,14 @@ public String toVariableNameString(DynamicFunctionReference function) { .since("2.10") ); + Classes.registerClass(new AnyInfo<>(AnyValued.class, "valued") + .name("Any Valued Thing") + .description("Something that has a value.") + .usage("") + .examples("the text of {node}") + .since("2.10") + ); + Classes.registerClass(new AnyInfo<>(AnyContains.class, "containing") .user("any container") .name("Anything with Contents") diff --git a/src/main/java/ch/njol/skript/config/EntryNode.java b/src/main/java/ch/njol/skript/config/EntryNode.java index 259c95e7ff4..67faa517d7f 100644 --- a/src/main/java/ch/njol/skript/config/EntryNode.java +++ b/src/main/java/ch/njol/skript/config/EntryNode.java @@ -4,13 +4,15 @@ import java.util.Map.Entry; import java.util.Objects; +import ch.njol.skript.lang.util.common.AnyValued; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; /** * @author Peter Güttinger */ -public class EntryNode extends Node implements Entry { +public class EntryNode extends Node implements Entry, AnyValued { private String value; @@ -35,6 +37,11 @@ public String getValue() { return value; } + @Override + public @UnknownNullability String value() { + return this.getValue(); + } + @Override public String setValue(final @Nullable String v) { if (v == null) @@ -44,6 +51,21 @@ public String setValue(final @Nullable String v) { return r; } + @Override + public void changeValue(String value) throws UnsupportedOperationException { + this.setValue(value); + } + + @Override + public Class valueType() { + return String.class; + } + + @Override + public boolean supportsValueChange() { + return false; // todo editable configs soon + } + @Override String save_i() { return key + config.getSaveSeparator() + value; diff --git a/src/main/java/ch/njol/skript/effects/EffConnect.java b/src/main/java/ch/njol/skript/effects/EffConnect.java index 53b0b20ffcc..f2c848eb4be 100644 --- a/src/main/java/ch/njol/skript/effects/EffConnect.java +++ b/src/main/java/ch/njol/skript/effects/EffConnect.java @@ -37,7 +37,8 @@ public class EffConnect extends Effect { static { Skript.registerEffect(EffConnect.class, - "(send|connect) %players% to [proxy|bungeecord] [server] %string%", + "connect %players% to [proxy|bungeecord] [server] %string%", + "send %players% to [proxy|bungeecord] server %string%", "transfer %players% to server %string% [on port %-number%]" ); } @@ -51,7 +52,7 @@ public class EffConnect extends Effect { public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { players = (Expression) exprs[0]; server = (Expression) exprs[1]; - transfer = matchedPattern == 1; + transfer = matchedPattern == 2; if (transfer) { port = (Expression) exprs[2]; diff --git a/src/main/java/ch/njol/skript/events/EvtFirework.java b/src/main/java/ch/njol/skript/events/EvtFirework.java index 7c64987f0de..412f2b246e2 100644 --- a/src/main/java/ch/njol/skript/events/EvtFirework.java +++ b/src/main/java/ch/njol/skript/events/EvtFirework.java @@ -4,10 +4,8 @@ import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptEvent; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; import ch.njol.skript.util.Color; -import java.util.Arrays; -import java.util.List; - import ch.njol.skript.util.ColorRGB; import org.bukkit.FireworkEffect; import org.bukkit.event.Event; @@ -15,28 +13,33 @@ import org.bukkit.inventory.meta.FireworkMeta; import org.jetbrains.annotations.Nullable; +import java.util.Set; +import java.util.stream.Collectors; public class EvtFirework extends SkriptEvent { static { if (Skript.classExists("org.bukkit.event.entity.FireworkExplodeEvent")) //Making the event argument type fireworkeffects, led to Skript having troubles parsing for some reason. - Skript.registerEvent("Firework Explode", EvtFirework.class, FireworkExplodeEvent.class, "[a] firework explo(d(e|ing)|sion) [colo[u]red %-colors%]") + Skript.registerEvent("Firework Explode", EvtFirework.class, FireworkExplodeEvent.class, + "[a] firework explo(d(e|ing)|sion) [colo[u]red %-colors%]") .description("Called when a firework explodes.") - .examples("on firework explode:", - "\tif event-colors contains red:", - "on firework exploding colored red, light green and black:", - "on firework explosion colored rgb 0, 255, 0:", - "\tbroadcast \"A firework colored %colors% was exploded at %location%!\"") + .examples( + "on firework explode:", + "\tif event-colors contains red:", + "on firework exploding colored red, light green and black:", + "on firework explosion colored rgb 0, 255, 0:", + "\tbroadcast \"A firework colored %colors% was exploded at %location%!\"" + ) .since("2.4"); } private @Nullable Literal colors; - - @SuppressWarnings("unchecked") + @Override public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult) { if (args[0] != null) + //noinspection unchecked colors = (Literal) args[0]; return true; } @@ -49,13 +52,14 @@ public boolean check(Event event) { if (colors == null) return true; - List colours = colors.stream(event) + Set colours = colors.stream(event) .map(color -> { if (color instanceof ColorRGB) return color.asBukkitColor(); return color.asDyeColor().getFireworkColor(); }) - .toList(); + .collect(Collectors.toSet()); + FireworkMeta meta = fireworkExplodeEvent.getEntity().getFireworkMeta(); for (FireworkEffect effect : meta.getEffects()) { if (colours.containsAll(effect.getColors())) @@ -65,8 +69,14 @@ public boolean check(Event event) { } @Override - public String toString(@Nullable Event e, boolean debug) { - return "Firework explode " + (colors != null ? " with colors " + colors.toString(e, debug) : ""); + public String toString(@Nullable Event event, boolean debug) { + SyntaxStringBuilder builder = new SyntaxStringBuilder(event, debug); + + builder.append("firework explode"); + if (colors != null) + builder.append("with colors").append(colors); + + return builder.toString(); } } diff --git a/src/main/java/ch/njol/skript/expressions/ExprName.java b/src/main/java/ch/njol/skript/expressions/ExprName.java index 4501d11ac11..32aaa7ab72f 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprName.java +++ b/src/main/java/ch/njol/skript/expressions/ExprName.java @@ -99,8 +99,8 @@ public class ExprName extends SimplePropertyExpression { serializer = BungeeComponentSerializer.get(); List patterns = new ArrayList<>(); - patterns.addAll(Arrays.asList(getPatterns("name[s]", "offlineplayers/entities/inventories/nameds"))); - patterns.addAll(Arrays.asList(getPatterns("(display|nick|chat|custom)[ ]name[s]", "offlineplayers/entities/inventories/nameds"))); + patterns.addAll(Arrays.asList(getPatterns("name[s]", "offlineplayers/entities/nameds/inventories"))); + patterns.addAll(Arrays.asList(getPatterns("(display|nick|chat|custom)[ ]name[s]", "offlineplayers/entities/nameds/inventories"))); patterns.addAll(Arrays.asList(getPatterns("(player|tab)[ ]list name[s]", "players"))); Skript.registerExpression(ExprName.class, String.class, ExpressionType.COMBINED, patterns.toArray(new String[0])); diff --git a/src/main/java/ch/njol/skript/expressions/ExprQueue.java b/src/main/java/ch/njol/skript/expressions/ExprQueue.java index 5f01ffc8a98..d3edce4e7c9 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprQueue.java +++ b/src/main/java/ch/njol/skript/expressions/ExprQueue.java @@ -47,7 +47,7 @@ public class ExprQueue extends SimpleExpression { static { Skript.registerExpression(ExprQueue.class, SkriptQueue.class, ExpressionType.COMBINED, - "[a] new queue [(of|with) %-objects%]"); + "[a] [new] queue [(of|with) %-objects%]"); } private @Nullable Expression contents; @@ -88,8 +88,8 @@ public Class getReturnType() { @Override public String toString(@Nullable Event event, boolean debug) { if (contents == null) - return "a new queue"; - return "a new queue of " + contents.toString(event, debug); + return "a queue"; + return "a queue of " + contents.toString(event, debug); } } diff --git a/src/main/java/ch/njol/skript/expressions/ExprTypeOf.java b/src/main/java/ch/njol/skript/expressions/ExprTypeOf.java index 0d17ed9e90a..d31c59e8f41 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprTypeOf.java +++ b/src/main/java/ch/njol/skript/expressions/ExprTypeOf.java @@ -8,6 +8,8 @@ import ch.njol.skript.entity.EntityData; import ch.njol.skript.expressions.base.SimplePropertyExpression; import ch.njol.skript.lang.util.ConvertedExpression; +import ch.njol.skript.util.EnchantmentType; +import org.bukkit.enchantments.Enchantment; import org.skriptlang.skript.lang.converter.Converters; import org.bukkit.block.data.BlockData; import org.bukkit.inventory.Inventory; @@ -17,19 +19,21 @@ @Name("Type of") @Description({ - "Type of a block, item, entity, inventory or potion effect.", + "Type of a block, item, entity, inventory, potion effect or enchantment type.", "Types of items, blocks and block datas are item types similar to them but have amounts", "of one, no display names and, on Minecraft 1.13 and newer versions, are undamaged.", "Types of entities and inventories are entity types and inventory types known to Skript.", - "Types of potion effects are potion effect types." + "Types of potion effects are potion effect types.", + "Types of enchantment types are enchantments." }) @Examples({"on rightclick on an entity:", "\tmessage \"This is a %type of clicked entity%!\""}) -@Since("1.4, 2.5.2 (potion effect), 2.7 (block datas)") +@Since("1.4, 2.5.2 (potion effect), 2.7 (block datas), 2.10 (enchantment type)") public class ExprTypeOf extends SimplePropertyExpression { static { - register(ExprTypeOf.class, Object.class, "type", "entitydatas/itemtypes/inventories/potioneffects/blockdatas"); + register(ExprTypeOf.class, Object.class, "type", + "entitydatas/itemtypes/inventories/potioneffects/blockdatas/enchantmenttypes"); } @Override @@ -39,17 +43,19 @@ protected String getPropertyName() { @Override @Nullable - public Object convert(Object o) { - if (o instanceof EntityData) { - return ((EntityData) o).getSuperType(); - } else if (o instanceof ItemType) { - return ((ItemType) o).getBaseType(); - } else if (o instanceof Inventory) { - return ((Inventory) o).getType(); - } else if (o instanceof PotionEffect) { - return ((PotionEffect) o).getType(); - } else if (o instanceof BlockData) { - return new ItemType(((BlockData) o).getMaterial()); + public Object convert(Object object) { + if (object instanceof EntityData entityData) { + return entityData.getSuperType(); + } else if (object instanceof ItemType itemType) { + return itemType.getBaseType(); + } else if (object instanceof Inventory inventory) { + return inventory.getType(); + } else if (object instanceof PotionEffect potionEffect) { + return potionEffect.getType(); + } else if (object instanceof BlockData blockData) { + return new ItemType(blockData.getMaterial()); + } else if (object instanceof EnchantmentType enchantmentType) { + return enchantmentType.getType(); } assert false; return null; @@ -61,6 +67,7 @@ public Class getReturnType() { return EntityData.class.isAssignableFrom(returnType) ? EntityData.class : ItemType.class.isAssignableFrom(returnType) ? ItemType.class : PotionEffectType.class.isAssignableFrom(returnType) ? PotionEffectType.class + : EnchantmentType.class.isAssignableFrom(returnType) ? Enchantment.class : BlockData.class.isAssignableFrom(returnType) ? ItemType.class : Object.class; } @@ -71,4 +78,5 @@ public Class getReturnType() { return null; return super.getConvertedExpr(to); } + } diff --git a/src/main/java/ch/njol/skript/expressions/ExprNodeValue.java b/src/main/java/ch/njol/skript/expressions/ExprValue.java similarity index 61% rename from src/main/java/ch/njol/skript/expressions/ExprNodeValue.java rename to src/main/java/ch/njol/skript/expressions/ExprValue.java index 07f0b85966d..6f71462cc69 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprNodeValue.java +++ b/src/main/java/ch/njol/skript/expressions/ExprValue.java @@ -3,8 +3,6 @@ import ch.njol.skript.Skript; import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.classes.Parser; -import ch.njol.skript.config.EntryNode; import ch.njol.skript.config.Node; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; @@ -14,20 +12,19 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.Literal; -import ch.njol.skript.lang.ParseContext; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.common.AnyValued; import ch.njol.skript.registrations.Feature; import ch.njol.util.Kleenean; -import ch.njol.util.coll.CollectionUtils; import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.lang.reflect.Array; -@Name("Value (Experimental)") +@Name("Value") @Description({ - "Returns the value of a node in a loaded config.", + "Returns the value of something that has a value, e.g. a node in a config.", "The value is automatically converted to the specified type (e.g. text, number) where possible." }) @Examples({ @@ -44,78 +41,72 @@ # timespan value of {_node} = 12 hours (duration)""", }) -@Since("2.10") -public class ExprNodeValue extends SimplePropertyExpression { +@Since("2.10 (Nodes), 2.10 (Any)") +public class ExprValue extends SimplePropertyExpression { static { - Skript.registerExpression(ExprNodeValue.class, Object.class, ExpressionType.PROPERTY, - "[the] %*classinfo% value [at] %string% (from|in) %node%", - "[the] %*classinfo% value of %node%", - "[the] %*classinfo% values of %nodes%", - "%node%'s %*classinfo% value", - "%nodes%'[s] %*classinfo% values" + Skript.registerExpression(ExprValue.class, Object.class, ExpressionType.PROPERTY, + "[the] %*classinfo% value [at] %string% (from|in) %node%", + "[the] %*classinfo% value of %valued%", + "[the] %*classinfo% values of %valueds%", + "%valued%'s %*classinfo% value", + "%valueds%'[s] %*classinfo% values" ); } private boolean isSingle; private ClassInfo classInfo; - private Parser parser; private @Nullable Expression pathExpression; @Override @SuppressWarnings("unchecked") public boolean init(Expression[] expressions, int pattern, Kleenean isDelayed, ParseResult parseResult) { - if (!this.getParser().hasExperiment(Feature.SCRIPT_REFLECTION)) - return false; @NotNull Literal> format; switch (pattern) { case 0: + if (!this.getParser().hasExperiment(Feature.SCRIPT_REFLECTION)) + return false; this.isSingle = true; format = (Literal>) expressions[0]; this.pathExpression = (Expression) expressions[1]; - this.setExpr((Expression) expressions[2]); - break; + this.setExpr(expressions[2]); + break; case 1: this.isSingle = true; case 2: format = (Literal>) expressions[0]; - this.setExpr((Expression) expressions[1]); + this.setExpr(expressions[1]); break; case 3: this.isSingle = true; default: format = (Literal>) expressions[1]; - this.setExpr((Expression) expressions[0]); + this.setExpr(expressions[0]); } this.classInfo = format.getSingle(); - if (classInfo.getC() == String.class) // don't bother with parser - return true; - this.parser = classInfo.getParser(); - if (this.parser == null || !this.parser.canParse(ParseContext.CONFIG)) { - Skript.error("The type '" + classInfo.getName() + "' cannot be used to parse config values."); - return false; - } return true; } @Override - - public @Nullable Object convert(@Nullable Node node) { - if (!(node instanceof EntryNode entryNode)) + public @Nullable Object convert(@Nullable Object object) { + if (object == null) return null; - String string = entryNode.getValue(); - if (classInfo.getC() == String.class) - return string; - return parser.parse(string, ParseContext.CONFIG); + if (object instanceof AnyValued valued) + return valued.convertedValue(classInfo); + return null; } @Override - protected Object[] get(Event event, Node[] source) { + protected Object[] get(Event event, Object[] source) { if (pathExpression != null) { + if (!(source[0] instanceof Node main)) + return (Object[]) Array.newInstance(this.getReturnType(), 0); String path = pathExpression.getSingle(event); - Node node = source[0].getNodeAt(path); + Node node = main.getNodeAt(path); Object[] array = (Object[]) Array.newInstance(this.getReturnType(), 1); - array[0] = this.convert(node); + if (!(node instanceof AnyValued valued)) + return (Object[]) Array.newInstance(this.getReturnType(), 0); + array[0] = this.convert(valued); return array; } return super.get(source, this); @@ -123,7 +114,26 @@ protected Object[] get(Event event, Node[] source) { @Override public Class @Nullable [] acceptChange(ChangeMode mode) { - return null; // todo editable configs in future + if (pathExpression != null) // todo editable configs soon + return null; + return switch (mode) { + case SET -> new Class[] {Object.class}; + case RESET, DELETE -> new Class[0]; + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + if (pathExpression != null) + return; + Object newValue = delta != null ? delta[0] : null; + for (Object object : getExpr().getArray(event)) { + if (!(object instanceof AnyValued valued)) + continue; + if (valued.supportsValueChange()) + valued.changeValueSafely(newValue); + } } @Override diff --git a/src/main/java/ch/njol/skript/lang/Expression.java b/src/main/java/ch/njol/skript/lang/Expression.java index 42653c83d04..6240e674b76 100644 --- a/src/main/java/ch/njol/skript/lang/Expression.java +++ b/src/main/java/ch/njol/skript/lang/Expression.java @@ -308,13 +308,18 @@ default Map[]> getAcceptedChangeModes() { * changing the expression. For example, {@code set vector length of {_v} to 1}, rather than * {@code set {_v} to vector(0,1,0)}. *
- * This is a 1 to 1 transformation and should not add or remove elements. - * For {@link Variable}s, this will retain indices. For non-{@link Variable}s, it will - * evaluate {@link #getArray(Event)}, apply the change function on each, and call - * {@link #change(Event, Object[], ChangeMode)} with the modified values and {@link ChangeMode#SET}. + * This is a 1 to 1 transformation and should not add elements. + * Returning null will remove the element. + * Returning a type not accepted by {@link #acceptChange(ChangeMode)} for {@link ChangeMode#SET} + * will depend on the implementer. The default implementation will remove the element. + *
+ * This expression must support {@link ChangeMode#SET} for this method to work. * * @param event The event to use for local variables and evaluation * @param changeFunction A 1-to-1 function that transforms a single input to a single output. + * Returning null will remove the element. + * Returning a type not accepted by {@link #acceptChange(ChangeMode)} for {@link ChangeMode#SET} + * will depend on the implementer. The default implementation will remove the element. * @param The output type of the change function. Must be a type returned * by {{@link #acceptChange(ChangeMode)}} for {@link ChangeMode#SET}. */ @@ -329,13 +334,18 @@ default void changeInPlace(Event event, Function changeFunction) { * changing the expression. For example, {@code set vector length of {_v} to 1}, rather than * {@code set {_v} to vector(0,1,0)}. *
- * This is a 1 to 1 transformation and should not add or remove elements. - * For {@link Variable}s, this will retain indices. For non-{@link Variable}s, it will - * evaluate the expression, apply the change function on each value, and call - * {@link #change(Event, Object[], ChangeMode)} with the modified values and {@link ChangeMode#SET}. + * This is a 1 to 1 transformation and should not add elements. + * Returning null will remove the element. + * Returning a type not accepted by {@link #acceptChange(ChangeMode)} for {@link ChangeMode#SET} + * will depend on the implementer. The default implementation will remove the element. + *
+ * This expression must support {@link ChangeMode#SET} for this method to work. * * @param event The event to use for local variables and evaluation * @param changeFunction A 1-to-1 function that transforms a single input to a single output. + * Returning null will remove the element. + * Returning a type not accepted by {@link #acceptChange(ChangeMode)} for {@link ChangeMode#SET} + * will depend on the implementer. The default implementation will remove the element. * @param getAll Whether to evaluate with {@link #getAll(Event)} or {@link #getArray(Event)}. * @param The output type of the change function. Must be a type returned * by {{@link #acceptChange(ChangeMode)}} for {@link ChangeMode#SET}. @@ -345,9 +355,17 @@ default void changeInPlace(Event event, Function changeFunction, boole T[] values = getAll ? getAll(event) : getArray(event); if (values.length == 0) return; + + @SuppressWarnings("DataFlowIssue") + Class[] validClasses = Arrays.stream(acceptChange(ChangeMode.SET)) + .map(c -> c.isArray() ? c.getComponentType() : c) + .toArray(Class[]::new); + List newValues = new ArrayList<>(); for (T value : values) { - newValues.add(changeFunction.apply(value)); + R newValue = changeFunction.apply(value); + if (newValue != null && ChangerUtils.acceptsChangeTypes(validClasses, newValue.getClass())) + newValues.add(newValue); } change(event, newValues.toArray(), ChangeMode.SET); } diff --git a/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java b/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java index c083adaf7ad..71bd8e661e9 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java +++ b/src/main/java/ch/njol/skript/lang/SkriptEventInfo.java @@ -7,7 +7,6 @@ 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; @@ -211,11 +210,11 @@ public static final class ModernSkriptEventInfo 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))); + this.origin = Skript.getSyntaxOrigin(eventClass); } @Override - public Builder, E> builder() { + public Builder, E> toBuilder() { return BukkitSyntaxInfos.Event.builder(type(), name()) .origin(origin) .addPatterns(patterns()) diff --git a/src/main/java/ch/njol/skript/lang/SkriptParser.java b/src/main/java/ch/njol/skript/lang/SkriptParser.java index a6eb399dfc8..e3460b950de 100644 --- a/src/main/java/ch/njol/skript/lang/SkriptParser.java +++ b/src/main/java/ch/njol/skript/lang/SkriptParser.java @@ -12,7 +12,9 @@ import ch.njol.skript.lang.function.ExprFunctionCall; import ch.njol.skript.lang.function.FunctionReference; import ch.njol.skript.lang.function.Functions; +import ch.njol.skript.lang.parser.ParseStackOverflowException; import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.lang.parser.ParsingStack; import ch.njol.skript.lang.util.SimpleLiteral; import ch.njol.skript.localization.Language; import ch.njol.skript.localization.Message; @@ -183,6 +185,7 @@ public boolean hasTag(String tag) { } private @Nullable T parse(Iterator> source) { + ParsingStack parsingStack = getParser().getParsingStack(); try (ParseLogHandler log = SkriptLogger.startParseLogHandler()) { while (source.hasNext()) { SyntaxElementInfo info = source.next(); @@ -193,15 +196,24 @@ public boolean hasTag(String tag) { assert pattern != null; ParseResult parseResult; try { + parsingStack.push(new ParsingStack.Element(info, patternIndex)); parseResult = parse_i(pattern); } catch (MalformedPatternException e) { String message = "pattern compiling exception, element class: " + info.getElementClass().getName(); try { JavaPlugin providingPlugin = JavaPlugin.getProvidingPlugin(info.getElementClass()); message += " (provided by " + providingPlugin.getName() + ")"; - } catch (IllegalArgumentException | IllegalStateException ignored) {} - throw new RuntimeException(message, e); + } catch (IllegalArgumentException | IllegalStateException ignored) { } + throw new RuntimeException(message, e); + } catch (StackOverflowError e) { + // Parsing caused a stack overflow, possibly due to too long lines + throw new ParseStackOverflowException(e, new ParsingStack(parsingStack)); + } finally { + // Recursive parsing call done, pop the element from the parsing stack + ParsingStack.Element stackElement = parsingStack.pop(); + + assert stackElement.syntaxElementInfo() == info && stackElement.patternIndex() == patternIndex; } if (parseResult != null) { assert parseResult.source != null; // parse results from parse_i have a source @@ -249,6 +261,8 @@ public boolean hasTag(String tag) { } } } + + // No successful syntax elements parsed, print errors and return log.printError(); return null; } diff --git a/src/main/java/ch/njol/skript/lang/parser/ParseStackOverflowException.java b/src/main/java/ch/njol/skript/lang/parser/ParseStackOverflowException.java new file mode 100644 index 00000000000..5ce8c0631ca --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/parser/ParseStackOverflowException.java @@ -0,0 +1,32 @@ +package ch.njol.skript.lang.parser; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +/** + * An exception noting that a {@link StackOverflowError} has occurred + * during Skript parsing. Contains information about the {@link ParsingStack} + * from when the stack overflow occurred. + */ +public class ParseStackOverflowException extends RuntimeException { + + protected final ParsingStack parsingStack; + + public ParseStackOverflowException(StackOverflowError cause, ParsingStack parsingStack) { + super(createMessage(parsingStack), cause); + this.parsingStack = parsingStack; + } + + /** + * Creates the exception message from the given {@link ParsingStack}. + */ + private static String createMessage(ParsingStack stack) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + PrintStream printStream = new PrintStream(stream); + stack.print(printStream); + + return stream.toString(); + } + +} diff --git a/src/main/java/ch/njol/skript/lang/parser/ParserInstance.java b/src/main/java/ch/njol/skript/lang/parser/ParserInstance.java index ddafc2e76ae..71085291b54 100644 --- a/src/main/java/ch/njol/skript/lang/parser/ParserInstance.java +++ b/src/main/java/ch/njol/skript/lang/parser/ParserInstance.java @@ -25,11 +25,7 @@ import org.skriptlang.skript.lang.structure.Structure; import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Function; public final class ParserInstance implements Experimented { @@ -228,6 +224,8 @@ public void deleteCurrentEvent() { * See also {@link #isCurrentEvent(Class[])} for checking with multiple argument classes */ public boolean isCurrentEvent(Class event) { + if (currentEvents == null) + return false; for (Class currentEvent : currentEvents) { // check that current event is same or child of event we want if (event.isAssignableFrom(currentEvent)) @@ -459,6 +457,19 @@ public String getIndentation() { return indentation; } + // Parsing stack + + private final ParsingStack parsingStack = new ParsingStack(); + + /** + * Gets the current parsing stack. + *

+ * Although the stack can be modified, doing so is not recommended. + */ + public ParsingStack getParsingStack() { + return parsingStack; + } + // Experiments API @Override @@ -622,7 +633,7 @@ public interface ScriptActivityChangeEvent extends ScriptLoader.LoaderEvent, Scr * That is, the contents of any collections will remain the same, but there is no guarantee that * the contents themselves will remain unchanged. * @see #backup() - * @see #restoreBackup(Backup) + * @see #restoreBackup(Backup) */ public static class Backup { @@ -703,8 +714,8 @@ public HashMap getCurrentOptions() { @Deprecated public @Nullable SkriptEvent getCurrentSkriptEvent() { Structure structure = getCurrentStructure(); - if (structure instanceof SkriptEvent) - return (SkriptEvent) structure; + if (structure instanceof SkriptEvent event) + return event; return null; } @@ -713,7 +724,7 @@ public HashMap getCurrentOptions() { */ @Deprecated public void setCurrentSkriptEvent(@Nullable SkriptEvent currentSkriptEvent) { - setCurrentStructure(currentSkriptEvent); + this.setCurrentStructure(currentSkriptEvent); } /** @@ -721,7 +732,7 @@ public void setCurrentSkriptEvent(@Nullable SkriptEvent currentSkriptEvent) { */ @Deprecated public void deleteCurrentSkriptEvent() { - setCurrentStructure(null); + this.setCurrentStructure(null); } /** diff --git a/src/main/java/ch/njol/skript/lang/parser/ParsingStack.java b/src/main/java/ch/njol/skript/lang/parser/ParsingStack.java new file mode 100644 index 00000000000..aeaa0ae0314 --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/parser/ParsingStack.java @@ -0,0 +1,192 @@ +package ch.njol.skript.lang.parser; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.SyntaxElement; +import ch.njol.skript.lang.SyntaxElementInfo; +import ch.njol.util.Kleenean; + +import java.io.PrintStream; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; + +/** + * A stack that keeps track of what Skript is currently parsing. + *

+ * When accessing the stack from within + * {@link SyntaxElement#init(Expression[], int, Kleenean, SkriptParser.ParseResult)}, + * the stack element corresponding to that {@link SyntaxElement} is not + * on the parsing stack. + */ +public class ParsingStack implements Iterable { + + private final LinkedList stack; + + /** + * Creates an empty parsing stack. + */ + public ParsingStack() { + this.stack = new LinkedList<>(); + } + + /** + * Creates a parsing stack containing all elements + * of another given parsing stack. + */ + public ParsingStack(ParsingStack parsingStack) { + this.stack = new LinkedList<>(parsingStack.stack); + } + + /** + * Removes and returns the top element of this stack. + * + * @throws IllegalStateException if the stack is empty. + */ + public Element pop() throws IllegalStateException { + if (stack.isEmpty()) { + throw new IllegalStateException("Stack is empty"); + } + + return stack.pop(); + } + + /** + * Returns the element at the given index in the stack, + * starting with the top element at index 0. + * + * @param index the index in stack. + * @throws IndexOutOfBoundsException if the index is not appointed + * to an element in the stack. + */ + public Element peek(int index) throws IndexOutOfBoundsException { + if (index < 0 || index >= size()) { + throw new IndexOutOfBoundsException("Index: " + index); + } + + return stack.get(index); + } + + /** + * Returns the top element of the stack. + * Equivalent to {@code peek(0)}. + * + * @throws IllegalStateException if the stack is empty. + */ + public Element peek() throws IllegalStateException { + if (stack.isEmpty()) { + throw new IllegalStateException("Stack is empty"); + } + + return stack.peek(); + } + + /** + * Adds the given element to the top of the stack. + */ + public void push(Element element) { + stack.push(element); + } + + /** + * Check if this stack is empty. + */ + public boolean isEmpty() { + return stack.isEmpty(); + } + + /** + * Gets the size of the stack. + */ + public int size() { + return stack.size(); + } + + /** + * Prints this stack to the given {@link PrintStream}. + * + * @param printStream a {@link PrintStream} to print the stack to. + */ + public void print(PrintStream printStream) { + // Synchronized to assure it'll all be printed at once, + // PrintStream uses synchronization on itself internally, justifying warning suppression + + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (printStream) { + printStream.println("Stack:"); + + if (stack.isEmpty()) { + printStream.println(""); + } else { + for (Element element : stack) { + printStream.println("\t" + element.getSyntaxElementClass().getName() + + " @ " + element.patternIndex()); + } + } + } + } + + /** + * Iterate over the stack, starting at the top. + */ + @Override + public Iterator iterator() { + return Collections.unmodifiableList(stack).iterator(); + } + + /** + * A stack element, containing details about the syntax element it is about. + */ + public record Element(SyntaxElementInfo syntaxElementInfo, int patternIndex) { + + public Element { + assert patternIndex >= 0 && patternIndex < syntaxElementInfo.getPatterns().length; + } + + /** + * Gets the raw {@link SyntaxElementInfo} of this stack element. + *

+ * For ease of use, consider using other getters of this class. + * + * @see #getSyntaxElementClass() + * @see #getPattern() + */ + @Override + public SyntaxElementInfo syntaxElementInfo() { + return syntaxElementInfo; + } + + /** + * Gets the index to the registered patterns for the syntax element + * of this stack element. + */ + @Override + public int patternIndex() { + return patternIndex; + } + + /** + * Gets the syntax element class of this stack element. + */ + public Class getSyntaxElementClass() { + return syntaxElementInfo.getElementClass(); + } + + /** + * Gets the pattern that was matched for this stack element. + */ + public String getPattern() { + return syntaxElementInfo.getPatterns()[patternIndex]; + } + + /** + * Gets all patterns registered with the syntax element + * of this stack element. + */ + public String[] getPatterns() { + return syntaxElementInfo.getPatterns(); + } + + } + +} diff --git a/src/main/java/ch/njol/skript/lang/util/common/AnyValued.java b/src/main/java/ch/njol/skript/lang/util/common/AnyValued.java new file mode 100644 index 00000000000..93c36715c19 --- /dev/null +++ b/src/main/java/ch/njol/skript/lang/util/common/AnyValued.java @@ -0,0 +1,110 @@ +package ch.njol.skript.lang.util.common; + +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.StringMode; +import com.sun.jdi.request.StepRequest; +import org.jetbrains.annotations.UnknownNullability; +import org.skriptlang.skript.lang.converter.Converters; + +/** + * A provider for anything with a value. + * Anything implementing this (or convertible to this) can be used by the {@link ch.njol.skript.expressions.ExprValue} + * property expression. + * + * @see AnyProvider + */ +public interface AnyValued extends AnyProvider { + + /** + * @return This thing's value + */ + @UnknownNullability + Type value(); + + default Converted convertedValue(ClassInfo expected) { + Type value = value(); + if (value == null) + return null; + if (expected.getC().isInstance(value)) + return expected.getC().cast(value); + + // For strings, it is probably better to use toString/Parser in either + // direction, instead of a converter + + if (expected.getC() == String.class) + //noinspection unchecked + return (Converted) Classes.toString(value, StringMode.MESSAGE); + if (value instanceof String string + && expected.getParser() != null + && expected.getParser().canParse(ParseContext.CONFIG)) { + return expected.getParser().parse(string, ParseContext.CONFIG); + } + + return Converters.convert(value, expected.getC()); + } + + /** + * This is called before {@link #changeValue(Object)}. + * If the result is false, setting the value will never be attempted. + * + * @return Whether this supports being set + */ + default boolean supportsValueChange() { + return false; + } + + /** + * The behaviour for changing this thing's value, if possible. + * If not possible, then {@link #supportsValueChange()} should return false and this + * may throw an error. + * + * @param value The new value + * @throws UnsupportedOperationException If this is impossible + */ + default void changeValue(Type value) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * + * @return The type of values this accepts (or provides) + */ + Class valueType(); + + /** + * A default implementation of 'resetting' the value (setting it to null). + * Implementations should override this if different behaviour is required. + * + * @throws UnsupportedOperationException If changing is not supported + */ + default void resetValue() throws UnsupportedOperationException { + this.changeValueSafely(null); + } + + /** + * This method can be overridden to filter out bad values (e.g. null, objects of the wrong type, etc.) + * and make sure {@link #changeValue(Object)} is not called with a bad parameter. + * + * @param value The (unchecked) new value + */ + default void changeValueSafely(Object value) throws UnsupportedOperationException { + Class typeClass = this.valueType(); + ClassInfo classInfo = Classes.getSuperClassInfo(typeClass); + if (value == null) { + this.changeValue(null); + } else if (typeClass == String.class) { + this.changeValue(typeClass.cast(Classes.toString(value, StringMode.MESSAGE))); + } else if (value instanceof String string + && classInfo.getParser() != null + && classInfo.getParser().canParse(ParseContext.CONFIG)) { + Type convert = (Type) classInfo.getParser().parse(string, ParseContext.CONFIG); + this.changeValue(convert); + } else { + Type convert = Converters.convert(value, typeClass); + this.changeValue(convert); + } + } + +} diff --git a/src/main/java/ch/njol/skript/localization/Adjective.java b/src/main/java/ch/njol/skript/localization/Adjective.java index 56c2a9f771e..bbae6c443ec 100644 --- a/src/main/java/ch/njol/skript/localization/Adjective.java +++ b/src/main/java/ch/njol/skript/localization/Adjective.java @@ -57,9 +57,7 @@ protected void onValueChange() { @Override public String toString() { validate(); - if (Skript.testing()) - Skript.warning("Invalid use of Adjective.toString()"); - return "" + def; + return def; } public String toString(int gender, int flags) { diff --git a/src/main/java/ch/njol/skript/registrations/Classes.java b/src/main/java/ch/njol/skript/registrations/Classes.java index 9db136253f6..39f0b2dc772 100644 --- a/src/main/java/ch/njol/skript/registrations/Classes.java +++ b/src/main/java/ch/njol/skript/registrations/Classes.java @@ -26,6 +26,7 @@ import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Chunk; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; import ch.njol.skript.Skript; @@ -58,16 +59,16 @@ * @author Peter Güttinger */ public abstract class Classes { - + private Classes() {} - + @Nullable private static ClassInfo[] classInfos = null; private final static List> tempClassInfos = new ArrayList<>(); private final static HashMap, ClassInfo> exactClassInfos = new HashMap<>(); private final static HashMap, ClassInfo> superClassInfos = new HashMap<>(); private final static HashMap> classInfosByCodeName = new HashMap<>(); - + /** * @param info info about the class to register */ @@ -90,11 +91,11 @@ public static void registerClass(final ClassInfo info) { throw e; } } - + public static void onRegistrationsStop() { - + sortClassInfos(); - + // validate serializeAs for (final ClassInfo ci : getClassInfos()) { if (ci.getSerializeAs() != null) { @@ -106,7 +107,7 @@ public static void onRegistrationsStop() { } } } - + // register to Yggdrasil for (final ClassInfo ci : getClassInfos()) { final Serializer s = ci.getSerializer(); @@ -116,17 +117,17 @@ public static void onRegistrationsStop() { EntityData.onRegistrationStop(); } - + /** * Sorts the class infos according to sub/superclasses and relations set with {@link ClassInfo#before(String...)} and {@link ClassInfo#after(String...)}. */ @SuppressFBWarnings("LI_LAZY_INIT_STATIC") private static void sortClassInfos() { assert classInfos == null; - + if (!Skript.testing() && SkriptConfig.addonSafetyChecks.value()) removeNullElements(); - + // merge before, after & sub/supertypes in after for (final ClassInfo ci : tempClassInfos) { final Set before = ci.before(); @@ -149,7 +150,7 @@ private static void sortClassInfos() { ci.after().add(ci2.getCodeName()); } } - + // remove unresolvable dependencies (and print a warning if testing) for (final ClassInfo ci : tempClassInfos) { final Set s = new HashSet<>(); @@ -171,9 +172,9 @@ private static void sortClassInfos() { if (!s.isEmpty() && Skript.testing()) Skript.warning(s.size() + " dependency/ies could not be resolved for " + ci + ": " + StringUtils.join(s, ", ")); } - + final List> classInfos = new ArrayList<>(tempClassInfos.size()); - + boolean changed = true; while (changed) { changed = false; @@ -189,9 +190,9 @@ private static void sortClassInfos() { } } } - + Classes.classInfos = classInfos.toArray(new ClassInfo[classInfos.size()]); - + // check for circular dependencies if (!tempClassInfos.isEmpty()) { final StringBuilder b = new StringBuilder(); @@ -202,7 +203,7 @@ private static void sortClassInfos() { } throw new IllegalStateException("ClassInfos with circular dependencies detected: " + b.toString()); } - + // debug message if (Skript.debug()) { final StringBuilder b = new StringBuilder(); @@ -213,9 +214,9 @@ private static void sortClassInfos() { } Skript.info("All registered classes in order: " + b.toString()); } - + } - + @SuppressWarnings({"null", "unused"}) private static void removeNullElements() { Iterator> it = tempClassInfos.iterator(); @@ -225,12 +226,12 @@ private static void removeNullElements() { it.remove(); } } - + private static void checkAllowClassInfoInteraction() { if (Skript.isAcceptRegistrations()) throw new IllegalStateException("Cannot use classinfos until registration is over"); } - + @SuppressWarnings("null") public static List> getClassInfos() { checkAllowClassInfoInteraction(); @@ -239,10 +240,10 @@ public static List> getClassInfos() { return Collections.emptyList(); return Collections.unmodifiableList(Arrays.asList(ci)); } - + /** * This method can be called even while Skript is loading. - * + * * @param codeName * @return The ClassInfo with the given code name * @throws SkriptAPIException If the given class was not registered @@ -253,10 +254,10 @@ public static ClassInfo getClassInfo(final String codeName) { throw new SkriptAPIException("No class info found for " + codeName); return ci; } - + /** * This method can be called even while Skript is loading. - * + * * @param codeName * @return The class info registered with the given code name or null if the code name is invalid or not yet registered */ @@ -264,12 +265,12 @@ public static ClassInfo getClassInfo(final String codeName) { public static ClassInfo getClassInfoNoError(final @Nullable String codeName) { return classInfosByCodeName.get(codeName); } - + /** * Gets the class info for the given class. *

* This method can be called even while Skript is loading. - * + * * @param c The exact class to get the class info for. * @return The class info for the given class or null if no info was found. */ @@ -278,14 +279,15 @@ public static ClassInfo getClassInfoNoError(final @Nullable String codeName) public static ClassInfo getExactClassInfo(final @Nullable Class c) { return (ClassInfo) exactClassInfos.get(c); } - + /** * Gets the class info of the given class or its closest registered superclass. This method will never return null unless c is null. - * + * * @param c * @return The closest superclass's info */ @SuppressWarnings("unchecked") + @Contract(pure = true, value = "!null -> !null") public static ClassInfo getSuperClassInfo(final Class c) { assert c != null; checkAllowClassInfoInteraction(); @@ -317,7 +319,7 @@ public static ClassInfo getSuperClassInfo(Class... classes) { /** * Gets all the class info of the given class in closest order to ending on object. This list will never be empty unless c is null. - * + * * @param c the class to check if assignable from * @return The closest list of superclass infos */ @@ -333,10 +335,10 @@ public static List> getAllSuperClassInfos(Class c) { } return list; } - + /** * Gets a class by its code name - * + * * @param codeName * @return the class with the given code name * @throws SkriptAPIException If the given class was not registered @@ -345,10 +347,10 @@ public static Class getClass(final String codeName) { checkAllowClassInfoInteraction(); return getClassInfo(codeName).getC(); } - + /** * As the name implies - * + * * @param name * @return the class info or null if the name was not recognised */ @@ -367,10 +369,10 @@ public static ClassInfo getClassInfoFromUserInput(String name) { } return null; } - + /** * As the name implies - * + * * @param name * @return the class or null if the name was not recognized */ @@ -380,10 +382,10 @@ public static Class getClassFromUserInput(final String name) { final ClassInfo ci = getClassInfoFromUserInput(name); return ci == null ? null : ci.getC(); } - + /** * Gets the default of a class - * + * * @param codeName * @return the expression holding the default value or null if this class doesn't have one * @throws SkriptAPIException If the given class was not registered @@ -393,10 +395,10 @@ public static DefaultExpression getDefaultExpression(final String codeName) { checkAllowClassInfoInteraction(); return getClassInfo(codeName).getDefaultExpression(); } - + /** * Gets the default expression of a class - * + * * @param c The class * @return The expression holding the default value or null if this class doesn't have one */ @@ -406,7 +408,7 @@ public static DefaultExpression getDefaultExpression(final Class c) { final ClassInfo ci = getExactClassInfo(c); return ci == null ? null : ci.getDefaultExpression(); } - + /** * Clones the given object by calling {@link ClassInfo#clone(Object)}, * getting the {@link ClassInfo} from the closest registered superclass @@ -428,10 +430,10 @@ public static Object clone(Object obj) { return classInfo.clone(obj); } } - + /** * Gets the name a class was registered with. - * + * * @param c The exact class * @return The name of the class or null if the given class wasn't registered. */ @@ -441,12 +443,12 @@ public static String getExactClassName(final Class c) { final ClassInfo ci = exactClassInfos.get(c); return ci == null ? null : ci.getCodeName(); } - + /** * Parses without trying to convert anything. *

* Can log an error xor other log messages. - * + * * @param s * @param c * @return The parsed object @@ -473,14 +475,14 @@ public static T parseSimple(final String s, final Class c, final ParseCon } return null; } - + /** * Parses a string to get an object of the desired type. *

* Instead of repeatedly calling this with the same class argument, you should get a parser with {@link #getParser(Class)} and use it for parsing. *

* Can log an error if it returned null. - * + * * @param s The string to parse * @param c The desired type. The returned value will be of this type or a subclass if it. * @return The parsed object @@ -516,10 +518,10 @@ public static T parse(final String s, final Class c, final ParseContext c } return null; } - + /** * Gets a parser for parsing instances of the desired type from strings. The returned parser may only be used for parsing, i.e. you must not use its toString methods. - * + * * @param to * @return A parser to parse object of the desired type */ @@ -547,12 +549,12 @@ public static Parser getParser(final Class to) { } return null; } - + /** * Gets a parser for an exactly known class. You should usually use {@link #getParser(Class)} instead of this method. *

* The main benefit of this method is that it's the only class info method of Skript that can be used while Skript is initializing and thus useful for parsing configs. - * + * * @param c * @return A parser to parse object of the desired type */ @@ -570,7 +572,7 @@ public static Parser getExactParser(final Class c) { return ci == null ? null : ci.getParser(); } } - + private static Parser createConvertedParser(final Parser parser, final Converter converter) { return new Parser() { @SuppressWarnings("unchecked") @@ -582,19 +584,19 @@ public T parse(final String s, final ParseContext context) { return null; return converter.convert((F) f); } - + @Override public String toString(final T o, final int flags) { throw new UnsupportedOperationException(); } - + @Override public String toVariableNameString(final T o) { throw new UnsupportedOperationException(); } }; } - + /** * @param o Any object, preferably not an array: use {@link Classes#toString(Object[], boolean)} instead. * @return String representation of the object (using a parser if found or {@link String#valueOf(Object)} otherwise). @@ -606,15 +608,15 @@ public String toVariableNameString(final T o) { public static String toString(final @Nullable Object o) { return toString(o, StringMode.MESSAGE, 0); } - + public static String getDebugMessage(final @Nullable Object o) { return toString(o, StringMode.DEBUG, 0); } - + public static String toString(final @Nullable T o, final StringMode mode) { return toString(o, mode, 0); } - + private static String toString(final @Nullable T o, final StringMode mode, final int flags) { assert flags == 0 || mode == StringMode.MESSAGE; if (o == null) @@ -644,23 +646,23 @@ private static String toString(final @Nullable T o, final StringMode mode, f } return mode == StringMode.VARIABLE_NAME ? "object:" + o : "" + o; } - + public static String toString(final Object[] os, final int flags, final boolean and) { return toString(os, and, null, StringMode.MESSAGE, flags); } - + public static String toString(final Object[] os, final int flags, final @Nullable ChatColor c) { return toString(os, true, c, StringMode.MESSAGE, flags); } - + public static String toString(final Object[] os, final boolean and) { return toString(os, and, null, StringMode.MESSAGE, 0); } - + public static String toString(final Object[] os, final boolean and, final StringMode mode) { return toString(os, and, null, mode, 0); } - + private static String toString(final Object[] os, final boolean and, final @Nullable ChatColor c, final StringMode mode, final int flags) { if (os.length == 0) return toString(null); @@ -680,15 +682,15 @@ private static String toString(final Object[] os, final boolean and, final @Null } return "" + b.toString(); } - + /** * consists of {@link Yggdrasil#MAGIC_NUMBER} and {@link Variables#YGGDRASIL_VERSION} */ private final static byte[] YGGDRASIL_START = {(byte) 'Y', (byte) 'g', (byte) 'g', 0, (Variables.YGGDRASIL_VERSION >>> 8) & 0xFF, Variables.YGGDRASIL_VERSION & 0xFF}; - + @SuppressWarnings("null") private final static Charset UTF_8 = Charset.forName("UTF-8"); - + private static byte[] getYggdrasilStart(final ClassInfo c) throws NotSerializableException { assert Enum.class.isAssignableFrom(Kleenean.class) && Tag.getType(Kleenean.class) == Tag.T_ENUM : Tag.getType(Kleenean.class); // TODO why is this check here? final Tag t = Tag.getType(c.getC()); @@ -707,66 +709,65 @@ private static byte[] getYggdrasilStart(final ClassInfo c) throws NotSerializ assert i == r.length; return r; } - + /** * Must be called on the appropriate thread for the given value (i.e. the main thread currently) */ - public static SerializedVariable.@Nullable Value serialize(@Nullable Object o) { - if (o == null) + public static SerializedVariable.@Nullable Value serialize(@Nullable Object object) { + if (object == null) return null; - + // temporary assert Bukkit.isPrimaryThread(); - ClassInfo ci = getSuperClassInfo(o.getClass()); - if (ci.getSerializeAs() != null) { - ci = getExactClassInfo(ci.getSerializeAs()); - if (ci == null) { - assert false : o.getClass(); + ClassInfo classInfo = getSuperClassInfo(object.getClass()); + if (classInfo.getSerializeAs() != null) { + classInfo = getExactClassInfo(classInfo.getSerializeAs()); + if (classInfo == null) { + assert false : object.getClass(); return null; } - o = Converters.convert(o, ci.getC()); - if (o == null) { - assert false : ci.getCodeName(); + object = Converters.convert(object, classInfo.getC()); + if (object == null) { + assert false : classInfo.getCodeName(); return null; } } - final Serializer s = ci.getSerializer(); - if (s == null) // value cannot be saved + Serializer serializer = classInfo.getSerializer(); + if (serializer == null) // value cannot be saved return null; - assert s.mustSyncDeserialization() ? Bukkit.isPrimaryThread() : true; + assert !serializer.mustSyncDeserialization() || Bukkit.isPrimaryThread(); try { - final ByteArrayOutputStream bout = new ByteArrayOutputStream(); - final YggdrasilOutputStream yout = Variables.yggdrasil.newOutputStream(bout); - yout.writeObject(o); - yout.flush(); - yout.close(); - final byte[] r = bout.toByteArray(); - final byte[] start = getYggdrasilStart(ci); - for (int i = 0; i < start.length; i++) - assert r[i] == start[i] : o + " (" + ci.getC().getName() + "); " + Arrays.toString(start) + ", " + Arrays.toString(r); - final byte[] r2 = new byte[r.length - start.length]; - System.arraycopy(r, start.length, r2, 0, r2.length); + ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); + YggdrasilOutputStream yggdrasilOutputStream = Variables.yggdrasil.newOutputStream(byteOutputStream); - if (o instanceof Date date) - System.out.println(date.getTime()); + yggdrasilOutputStream.writeObject(object); + yggdrasilOutputStream.flush(); + yggdrasilOutputStream.close(); - Object d = deserialize(ci, new ByteArrayInputStream(r2)); - if (d instanceof Date date) - System.out.println(date.getTime()); + byte[] byteArray = byteOutputStream.toByteArray(); + byte[] start = getYggdrasilStart(classInfo); + for (int i = 0; i < start.length; i++) + assert byteArray[i] == start[i] : object + " (" + classInfo.getC().getName() + "); " + Arrays.toString(start) + ", " + Arrays.toString(byteArray); + byte[] byteArrayCopy = new byte[byteArray.length - start.length]; + System.arraycopy(byteArray, start.length, byteArrayCopy, 0, byteArrayCopy.length); - assert equals(o, d) : o + " (" + o.getClass() + ") != " + d + " (" + (d == null ? null : d.getClass()) + "): " + Arrays.toString(r); + Object deserialized; + assert equals(object, + deserialized = deserialize(classInfo, new ByteArrayInputStream(byteArrayCopy))) + : object + " (" + object.getClass() + ") != " + deserialized + " (" + + (deserialized == null ? null : deserialized.getClass()) + "): " + Arrays.toString(byteArray); - return new SerializedVariable.Value(ci.getCodeName(), r2); - } catch (final IOException e) { // shouldn't happen - Skript.exception(e); + return new SerializedVariable.Value(classInfo.getCodeName(), byteArrayCopy); + } catch (IOException ex) { // shouldn't happen + Skript.exception(ex); return null; } } - + private static boolean equals(final @Nullable Object o, final @Nullable Object d) { if (o instanceof Chunk) { // CraftChunk does neither override equals nor is it a "coordinate-specific singleton" like Block if (!(d instanceof Chunk)) @@ -776,12 +777,12 @@ private static boolean equals(final @Nullable Object o, final @Nullable Object d } return o == null ? d == null : o.equals(d); } - + @Nullable public static Object deserialize(final ClassInfo type, final byte[] value) { return deserialize(type, new ByteArrayInputStream(value)); } - + @Nullable public static Object deserialize(final String type, final byte[] value) { final ClassInfo ci = getClassInfoNoError(type); @@ -789,7 +790,7 @@ public static Object deserialize(final String type, final byte[] value) { return null; return deserialize(ci, new ByteArrayInputStream(value)); } - + @Nullable public static Object deserialize(final ClassInfo type, InputStream value) { Serializer s; @@ -814,12 +815,12 @@ public static Object deserialize(final ClassInfo type, InputStream value) { } catch (final IOException e) {} } } - + /** * Deserialises an object. *

* This method must only be called from Bukkits main thread! - * + * * @param type * @param value * @return Deserialised value or null if the input is invalid @@ -836,5 +837,5 @@ public static Object deserialize(final String type, final String value) { return null; return s.deserialize(value); } - + } diff --git a/src/main/java/ch/njol/skript/registrations/Converters.java b/src/main/java/ch/njol/skript/registrations/Converters.java new file mode 100644 index 00000000000..183ab302295 --- /dev/null +++ b/src/main/java/ch/njol/skript/registrations/Converters.java @@ -0,0 +1,121 @@ +package ch.njol.skript.registrations; + +import ch.njol.skript.classes.Converter; +import ch.njol.skript.util.Utils; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.converter.ConverterInfo; + +import java.util.List; +import java.util.stream.Collectors; + +/** + *

WARNING! This class has been removed in this update.

+ * This class stub has been left behind to prevent loading errors from outdated addons, + * but its functionality has been largely removed. + * + * @deprecated Use {@link org.skriptlang.skript.lang.converter.Converters} + */ +@Deprecated(forRemoval = true) +@SuppressWarnings("removal") +public abstract class Converters { + + static { + Utils.loadedRemovedClassWarning(Converters.class); + } + + @SuppressWarnings("unchecked") + @Deprecated(forRemoval = true) + public static List> getConverters() { + return org.skriptlang.skript.lang.converter.Converters.getConverterInfos().stream() + .map(unknownInfo -> { + org.skriptlang.skript.lang.converter.ConverterInfo info = + (org.skriptlang.skript.lang.converter.ConverterInfo) unknownInfo; + return new ConverterInfo<>(info.getFrom(), info.getTo(), info.getConverter(), info.getFlags()); + }) + .collect(Collectors.toList()); + } + + @Deprecated(forRemoval = true) + public static void registerConverter(Class from, Class to, Converter converter) { + registerConverter(from, to, converter, 0); + } + + @Deprecated(forRemoval = true) + public static void registerConverter(Class from, Class to, Converter converter, int options) { + org.skriptlang.skript.lang.converter.Converters.registerConverter(from, to, converter::convert, options); + } + + @Deprecated(forRemoval = true) + public static T convert(@Nullable F o, Class to) { + return org.skriptlang.skript.lang.converter.Converters.convert(o, to); + } + + @Deprecated(forRemoval = true) + public static T convert(@Nullable F o, Class[] to) { + return org.skriptlang.skript.lang.converter.Converters.convert(o, to); + } + + @Deprecated(forRemoval = true) + public static T[] convertArray(@Nullable Object[] o, Class to) { + T[] converted = org.skriptlang.skript.lang.converter.Converters.convert(o, to); + if (converted.length == 0) // no longer nullable with new converter classes + return null; + return converted; + } + + @Deprecated(forRemoval = true) + public static T[] convertArray(@Nullable Object[] o, Class[] to, + Class superType) { + return org.skriptlang.skript.lang.converter.Converters.convert(o, to, superType); + } + + @Deprecated(forRemoval = true) + public static T[] convertStrictly(Object[] original, Class to) throws ClassCastException { + return org.skriptlang.skript.lang.converter.Converters.convertStrictly(original, to); + } + + @Deprecated(forRemoval = true) + public static T convertStrictly(Object original, Class to) throws ClassCastException { + return org.skriptlang.skript.lang.converter.Converters.convertStrictly(original, to); + } + + @Deprecated(forRemoval = true) + public static boolean converterExists(Class from, Class to) { + return org.skriptlang.skript.lang.converter.Converters.converterExists(from, to); + } + + @Deprecated(forRemoval = true) + public static boolean converterExists(Class from, Class... to) { + return org.skriptlang.skript.lang.converter.Converters.converterExists(from, to); + } + + @Deprecated(forRemoval = true) + public static Converter getConverter(Class from, Class to) { + org.skriptlang.skript.lang.converter.Converter converter = + org.skriptlang.skript.lang.converter.Converters.getConverter(from, to); + if (converter == null) + return null; + return (Converter) converter::convert; + } + + @Deprecated(forRemoval = true) + public static ConverterInfo getConverterInfo(Class from, Class to) { + org.skriptlang.skript.lang.converter.ConverterInfo info = + org.skriptlang.skript.lang.converter.Converters.getConverterInfo(from, to); + if (info == null) + return null; + return new ConverterInfo<>(info.getFrom(), info.getTo(), info.getConverter()::convert, info.getFlags()); + } + + @Deprecated(forRemoval = true) + public static T[] convertUnsafe(F[] from, Class to, + Converter conv) { + return org.skriptlang.skript.lang.converter.Converters.convertUnsafe(from, to, conv::convert); + } + + @Deprecated(forRemoval = true) + public static T[] convert(F[] from, Class to, Converter conv) { + return org.skriptlang.skript.lang.converter.Converters.convert(from, to, conv::convert); + } + +} diff --git a/src/main/java/ch/njol/skript/registrations/Feature.java b/src/main/java/ch/njol/skript/registrations/Feature.java index f1bb137d616..1001e767f07 100644 --- a/src/main/java/ch/njol/skript/registrations/Feature.java +++ b/src/main/java/ch/njol/skript/registrations/Feature.java @@ -11,6 +11,7 @@ * Experimental feature toggles as provided by Skript itself. */ public enum Feature implements Experiment { + EXAMPLES("examples", LifeCycle.STABLE), QUEUES("queues", LifeCycle.EXPERIMENTAL), FOR_EACH_LOOPS("for loop", LifeCycle.EXPERIMENTAL, "for [each] loop[s]"), SCRIPT_REFLECTION("reflection", LifeCycle.EXPERIMENTAL, "[script] reflection"), diff --git a/src/main/java/ch/njol/skript/sections/SecFor.java b/src/main/java/ch/njol/skript/sections/SecFor.java index a7777f3ee79..1c191360df8 100644 --- a/src/main/java/ch/njol/skript/sections/SecFor.java +++ b/src/main/java/ch/njol/skript/sections/SecFor.java @@ -107,13 +107,18 @@ public boolean init(Expression[] exprs, .getName() + " implements Container but is missing the required @ContainerType annotation"); this.expression = new ContainerExpression((Expression>) expression, type.value()); } - if (expression.isSingle()) { + if (this.getParser().hasExperiment(Feature.QUEUES) // Todo: change this if other iterable things are added + && expression.isSingle() + && (expression instanceof Variable || Iterable.class.isAssignableFrom(expression.getReturnType()))) { + // Some expressions return one thing but are potentially iterable anyway, e.g. queues + super.iterableSingle = true; + } else if (expression.isSingle()) { Skript.error("Can't loop '" + expression + "' because it's only a single value"); return false; } // this.loadOptionalCode(sectionNode); - super.setNext(this); + this.setInternalNext(this); return true; } diff --git a/src/main/java/ch/njol/skript/sections/SecLoop.java b/src/main/java/ch/njol/skript/sections/SecLoop.java index 877ca2e7152..4c620413dac 100644 --- a/src/main/java/ch/njol/skript/sections/SecLoop.java +++ b/src/main/java/ch/njol/skript/sections/SecLoop.java @@ -10,6 +10,7 @@ import ch.njol.skript.lang.*; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.util.ContainerExpression; +import ch.njol.skript.registrations.Feature; import ch.njol.skript.util.Container; import ch.njol.skript.util.Container.ContainerType; import ch.njol.skript.util.LiteralUtils; @@ -19,10 +20,7 @@ import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.WeakHashMap; +import java.util.*; @Name("Loop") @Description({ @@ -85,6 +83,7 @@ public class SecLoop extends LoopSection { private boolean guaranteedToLoop; private Object nextValue = null; private boolean loopPeeking; + protected boolean iterableSingle; @Override @SuppressWarnings("unchecked") @@ -107,7 +106,12 @@ public boolean init(Expression[] exprs, this.expression = new ContainerExpression((Expression>) expression, type.value()); } - if (expression.isSingle()) { + if (this.getParser().hasExperiment(Feature.QUEUES) // Todo: change this if other iterable things are added + && expression.isSingle() + && (expression instanceof Variable || Iterable.class.isAssignableFrom(expression.getReturnType()))) { + // Some expressions return one thing but are potentially iterable anyway, e.g. queues + this.iterableSingle = true; + } else if (expression.isSingle()) { Skript.error("Can't loop '" + expression + "' because it's only a single value"); return false; } @@ -115,7 +119,7 @@ public boolean init(Expression[] exprs, guaranteedToLoop = guaranteedToLoop(expression); loadOptionalCode(sectionNode); - super.setNext(this); + this.setInternalNext(this); return true; } @@ -124,11 +128,24 @@ public boolean init(Expression[] exprs, protected @Nullable TriggerItem walk(Event event) { Iterator iter = iteratorMap.get(event); if (iter == null) { - iter = expression instanceof Variable variable ? variable.variablesIterator(event) : expression.iterator(event); - if (iter != null && iter.hasNext()) { - iteratorMap.put(event, iter); + if (iterableSingle) { + Object value = expression.getSingle(event); + if (value instanceof Container container) { + // Container may have special behaviour over regular iterator + iter = container.containerIterator(); + } else if (value instanceof Iterable iterable) { + iter = iterable.iterator(); + } else { + iter = Collections.singleton(value).iterator(); + } } else { - iter = null; + iter = expression instanceof Variable variable ? variable.variablesIterator(event) : + expression.iterator(event); + if (iter != null && iter.hasNext()) { + iteratorMap.put(event, iter); + } else { + iter = null; + } } } @@ -193,6 +210,13 @@ public SecLoop setNext(@Nullable TriggerItem next) { return this; } + /** + * @see LoopSection#setNext(TriggerItem) + */ + protected void setInternalNext(TriggerItem item) { + super.setNext(item); + } + @Nullable @Override public TriggerItem getActualNext() { diff --git a/src/main/java/ch/njol/skript/structures/StructExample.java b/src/main/java/ch/njol/skript/structures/StructExample.java new file mode 100644 index 00000000000..c0a263f66d5 --- /dev/null +++ b/src/main/java/ch/njol/skript/structures/StructExample.java @@ -0,0 +1,74 @@ +package ch.njol.skript.structures; + +import ch.njol.skript.ScriptLoader; +import ch.njol.skript.Skript; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.doc.*; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.function.FunctionEvent; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.registrations.Feature; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.entry.EntryContainer; +import org.skriptlang.skript.lang.structure.Structure; + +@NoDoc +@Name("Example") +@Description({ + "Examples are structures that are parsed, but will never be run.", + "They are used as miniature tutorials for demonstrating code snippets in the example files.", + "Scripts containing an example are seen as 'examples' by the parser and may have special safety restrictions." +}) +@Examples({""" + example: + broadcast "hello world" + # this is never run""" +}) +@Since("2.10") +public class StructExample extends Structure { + + public static final Priority PRIORITY = new Priority(550); + + static { + Skript.registerStructure(StructExample.class, + "example" + ); + } + + private SectionNode source; + + @Override + public boolean init(Literal[] literals, int matchedPattern, ParseResult parseResult, + @Nullable EntryContainer entryContainer) { + if (!this.getParser().hasExperiment(Feature.EXAMPLES)) + return false; + assert entryContainer != null; // cannot be null for non-simple structures + this.source = entryContainer.getSource(); + return true; + } + + @Override + public boolean load() { + ParserInstance parser = this.getParser(); + // This acts like a 'function' except without some of the features (e.g. returns) + // The code is parsed and loaded, but then discarded since it will never be run + // This allows things like parse problems and errors to be detected. + parser.setCurrentEvent("example", FunctionEvent.class); + ScriptLoader.loadItems(source); + parser.deleteCurrentEvent(); + return true; + } + + @Override + public Priority getPriority() { + return PRIORITY; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "example"; + } + +} diff --git a/src/main/java/ch/njol/skript/test/runner/StructEntryContainerTest.java b/src/main/java/ch/njol/skript/test/runner/StructTestEntryContainer.java similarity index 94% rename from src/main/java/ch/njol/skript/test/runner/StructEntryContainerTest.java rename to src/main/java/ch/njol/skript/test/runner/StructTestEntryContainer.java index 14ce4770544..86d679e971b 100644 --- a/src/main/java/ch/njol/skript/test/runner/StructEntryContainerTest.java +++ b/src/main/java/ch/njol/skript/test/runner/StructTestEntryContainer.java @@ -18,7 +18,7 @@ import java.util.List; -public class StructEntryContainerTest extends Structure { +public class StructTestEntryContainer extends Structure { public static class TestEvent extends Event { @Override @@ -29,7 +29,7 @@ public static class TestEvent extends Event { static { if (TestMode.ENABLED) - Skript.registerStructure(StructEntryContainerTest.class, + Skript.registerStructure(StructTestEntryContainer.class, EntryValidator.builder() .addSection("has entry", true) .build(), diff --git a/src/main/java/ch/njol/skript/util/Utils.java b/src/main/java/ch/njol/skript/util/Utils.java index 6a2db700ae3..100c488cb95 100644 --- a/src/main/java/ch/njol/skript/util/Utils.java +++ b/src/main/java/ch/njol/skript/util/Utils.java @@ -19,6 +19,7 @@ import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.messaging.Messenger; import org.bukkit.plugin.messaging.PluginMessageListener; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -29,6 +30,8 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -94,7 +97,8 @@ public abstract class Utils { plurals.add(new WordEnding("", "s")); } - private Utils() {} + private Utils() { + } public static String join(final Object[] objects) { assert objects != null; @@ -186,10 +190,11 @@ public static Pair getAmount(String s) { * Loads classes of the plugin by package. Useful for registering many syntax elements like Skript does it. * * @param basePackage The base package to add to all sub packages, e.g. "ch.njol.skript". - * @param subPackages Which subpackages of the base package should be loaded, e.g. "expressions", "conditions", "effects". Subpackages of these packages will be loaded - * 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. + * @param subPackages Which subpackages of the base package should be loaded, e.g. "expressions", + * "conditions", "effects". Subpackages of these packages will be loaded + * as well. Use an empty array to load all subpackages of the base package. * @return This SkriptAddon + * @throws IOException If some error occurred attempting to read the plugin's jar file. * @deprecated Use {@link org.skriptlang.skript.util.ClassLoader}. */ @Deprecated @@ -212,7 +217,8 @@ public static Class[] getClasses(Plugin plugin, String basePackage, String... } /** - * The first invocation of this method uses reflection to invoke the protected method {@link JavaPlugin#getFile()} to get the plugin's jar file. + * The first invocation of this method uses reflection to invoke the protected method {@link JavaPlugin#getFile()} + * to get the plugin's jar file. * * @return The jar file of the plugin. */ @@ -288,7 +294,7 @@ private static boolean couldBeSingular(String word) { * 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 + * @param plural The plural form of the word */ public static void addPluralOverride(String singular, String plural) { Utils.plurals.addFirst(new WordEnding(singular, plural, true)); @@ -355,7 +361,7 @@ public static String A(final String s) { /** * Adds 'a' or 'an' to the given string, depending on the first character of the string. * - * @param s The string to add the article to + * @param s The string to add the article to * @param capA Whether to use a capital a or not * @return The given string with an appended a/an (or A/An if capA is true) and a space at the beginning * @see #a(String) @@ -433,12 +439,12 @@ public static double getBlockHeight(final int type, final byte data) { /** * Sends a plugin message using the first player from {@link Bukkit#getOnlinePlayers()}. - * + *

* The next plugin message to be received through {@code channel} will be assumed to be * the response. * * @param channel the channel for this plugin message - * @param data the data to add to the outgoing message + * @param data the data to add to the outgoing message * @return a completable future for the message of the responding plugin message, if there is one. * this completable future will complete exceptionally if no players are online. */ @@ -448,26 +454,27 @@ public static CompletableFuture sendPluginMessage(String cha /** * Sends a plugin message using the from {@code player}. - * + *

* The next plugin message to be received through {@code channel} will be assumed to be * the response. * - * @param player the player to send the plugin message through + * @param player the player to send the plugin message through * @param channel the channel for this plugin message - * @param data the data to add to the outgoing message + * @param data the data to add to the outgoing message * @return a completable future for the message of the responding plugin message, if there is one. * this completable future will complete exceptionally if no players are online. */ - public static CompletableFuture sendPluginMessage(Player player, String channel, String... data) { + public static CompletableFuture sendPluginMessage(Player player, String channel, + String... data) { return sendPluginMessage(player, channel, r -> true, data); } /** * Sends a plugin message using the first player from {@link Bukkit#getOnlinePlayers()}. * - * @param channel the channel for this plugin message + * @param channel the channel for this plugin message * @param messageVerifier verifies that a plugin message is the response to the sent message - * @param data the data to add to the outgoing message + * @param data the data to add to the outgoing message * @return a completable future for the message of the responding plugin message, if there is one. * this completable future will complete exceptionally if the player is null. * @throws IllegalStateException when there are no players online @@ -484,21 +491,21 @@ public static CompletableFuture sendPluginMessage( /** * Sends a plugin message. - * + *

* Example usage using the "GetServers" bungee plugin message channel via an overload: * - * Utils.sendPluginMessage("BungeeCord", r -> "GetServers".equals(r.readUTF()), "GetServers") - * .thenAccept(response -> Bukkit.broadcastMessage(response.readUTF()) // comma delimited server broadcast - * .exceptionally(ex -> { - * Skript.warning("Failed to get servers because there are no players online"); - * return null; - * }); + * Utils.sendPluginMessage("BungeeCord", r -> "GetServers".equals(r.readUTF()), "GetServers") + * .thenAccept(response -> Bukkit.broadcastMessage(response.readUTF()) // comma delimited server broadcast + * .exceptionally(ex -> { + * Skript.warning("Failed to get servers because there are no players online"); + * return null; + * }); * * - * @param player the player to send the plugin message through - * @param channel the channel for this plugin message + * @param player the player to send the plugin message through + * @param channel the channel for this plugin message * @param messageVerifier verifies that a plugin message is the response to the sent message - * @param data the data to add to the outgoing message + * @param data the data to add to the outgoing message * @return a completable future for the message of the responding plugin message, if there is one. * this completable future will complete exceptionally if the player is null. */ @@ -523,7 +530,8 @@ public static CompletableFuture sendPluginMessage( messenger.registerIncomingPluginChannel(skript, channel, listener); - completableFuture.whenComplete((r, ex) -> messenger.unregisterIncomingPluginChannel(skript, channel, listener)); + completableFuture.whenComplete((r, ex) -> messenger.unregisterIncomingPluginChannel(skript, channel, + listener)); // if we haven't gotten a response after a minute, let's just assume there wil never be one Bukkit.getScheduler().scheduleSyncDelayedTask(skript, () -> { @@ -540,7 +548,8 @@ public static CompletableFuture sendPluginMessage( return completableFuture; } - final static ChatColor[] styles = {ChatColor.BOLD, ChatColor.ITALIC, ChatColor.STRIKETHROUGH, ChatColor.UNDERLINE, ChatColor.MAGIC, ChatColor.RESET}; + final static ChatColor[] styles = {ChatColor.BOLD, ChatColor.ITALIC, ChatColor.STRIKETHROUGH, ChatColor.UNDERLINE, + ChatColor.MAGIC, ChatColor.RESET}; final static Map chat = new HashMap<>(); final static Map englishChat = new HashMap<>(); @@ -581,7 +590,8 @@ public static String getChatStyle(final String s) { } /** - * Replaces english <chat styles> in the message. This is used for messages in the language file as the language of colour codes is not well defined while the language is + * Replaces english <chat styles> in the message. This is used for messages in the language file as the + * language of colour codes is not well defined while the language is * changing, and for some hardcoded messages. * * @param message @@ -632,6 +642,7 @@ public static String getChatStyle(final String s) { /** * 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. */ @@ -651,6 +662,7 @@ public static String getChatStyle(final String s) { /** * Tries to get a {@link ChatColor} from the given string. + * * @param string The string code to parse. * @return The ChatColor, or null if it couldn't be parsed. */ @@ -700,14 +712,15 @@ public static Class getSuperType(final Class... classes) { * Note that if the "best guess" is not a real supertype, it can never be selected. * * @param bestGuess The fallback class to guess - * @param classes The types to check + * @param classes The types to check + * @param The highest common denominator found + * @param The input type spread * @return The most appropriate common class of all provided - * @param The highest common denominator found - * @param The input type spread */ @SafeVarargs @SuppressWarnings("unchecked") - public static Class highestDenominator(Class bestGuess, @NotNull Class @NotNull ... classes) { + public static Class highestDenominator(Class bestGuess, + @NotNull Class @NotNull ... classes) { assert classes.length > 0; Class chosen = classes[0]; outer: @@ -739,7 +752,8 @@ public static Class highestDenominator(Class< } /** - * Parses a number that was validated to be an integer but might still result in a {@link NumberFormatException} when parsed with {@link Integer#parseInt(String)} due to + * Parses a number that was validated to be an integer but might still result in a {@link NumberFormatException} + * when parsed with {@link Integer#parseInt(String)} due to * overflow. * This method will return {@link Integer#MIN_VALUE} or {@link Integer#MAX_VALUE} respectively if that happens. * @@ -756,7 +770,8 @@ public static int parseInt(final String s) { } /** - * Parses a number that was validated to be an integer but might still result in a {@link NumberFormatException} when parsed with {@link Long#parseLong(String)} due to + * Parses a number that was validated to be an integer but might still result in a {@link NumberFormatException} + * when parsed with {@link Long#parseLong(String)} due to * overflow. * This method will return {@link Long#MIN_VALUE} or {@link Long#MAX_VALUE} respectively if that happens. * @@ -775,6 +790,7 @@ public static long parseLong(final String s) { /** * Gets class for name. Throws RuntimeException instead of checked one. * Use this only when absolutely necessary. + * * @param name Class name. * @return The class. */ @@ -791,7 +807,7 @@ public static Class classForName(String name) { /** * Finds the index of the last in a {@link List} that matches the given {@link Predicate}. * - * @param list the {@link List} to search. + * @param list the {@link List} to search. * @param checker the {@link Predicate} to match elements against. * @return the index of the element found, or -1 if no matching element was found. */ @@ -840,4 +856,51 @@ public int hashCode() { } + /** + * Prints a warning about the loading/use of a class that has been deprecated or removed. + * This is a fairly-unsafe method and should only be used during class-loading. + * + * @param source The class about which to print the warning. This MUST be the class calling this method. + * @return 0 (for use by interfaces) + */ + @ApiStatus.Internal + public static int loadedRemovedClassWarning(Class source) { + Logger logger = Skript.getInstance().getLogger(); + Exception exception = new Exception(); + exception.fillInStackTrace(); + StackTraceElement[] stackTrace = exception.getStackTrace(); + StackTraceElement caller = stackTrace[2]; + String authors, name; + try { + Class callingClass = Class.forName(caller.getClassName()); + JavaPlugin plugin = JavaPlugin.getProvidingPlugin(callingClass); + name = plugin.getDescription().getFullName(); + authors = String.valueOf(plugin.getDescription().getAuthors()); + } catch (ClassNotFoundException | IllegalArgumentException | ClassCastException error) { + name = caller.getClassLoaderName(); + authors = "(unknown)"; + } + logger.log(Level.SEVERE, + String.format(""" + + + WARNING! + + An addon attempted to load a deprecated/outdated/removed '%s' class. + + The plugin '%s' tried to use a class that has been deprecated/removed in this version of Skript. + Please make sure you are using the latest supported version of the addon. + + If there are no supported versions, you should contact the author(s): %s, and ask them to update it. + + (This addon may not work correctly on this version of Skript.) + + """, + source.getSimpleName(), + name, + authors) + ); + return 0; + } + } diff --git a/src/main/java/org/skriptlang/skript/SkriptImpl.java b/src/main/java/org/skriptlang/skript/SkriptImpl.java index 03b96f503a5..6c98e26a52c 100644 --- a/src/main/java/org/skriptlang/skript/SkriptImpl.java +++ b/src/main/java/org/skriptlang/skript/SkriptImpl.java @@ -32,7 +32,7 @@ final class SkriptImpl implements Skript { * Registry Management */ - private static final Map, Registry> registries = new ConcurrentHashMap<>(); + private final Map, Registry> registries = new ConcurrentHashMap<>(); @Override public > void storeRegistry(Class registryClass, R registry) { @@ -68,7 +68,7 @@ public > R registry(Class registryClass, Supplier pu * SkriptAddon Management */ - private static final Map addons = new HashMap<>(); + private final Map addons = new HashMap<>(); @Override public SkriptAddon registerAddon(Class source, String name) { diff --git a/src/main/java/org/skriptlang/skript/bukkit/input/elements/conditions/CondIsPressingKey.java b/src/main/java/org/skriptlang/skript/bukkit/input/elements/conditions/CondIsPressingKey.java index 223274c0c02..98d3616df9d 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/input/elements/conditions/CondIsPressingKey.java +++ b/src/main/java/org/skriptlang/skript/bukkit/input/elements/conditions/CondIsPressingKey.java @@ -2,6 +2,7 @@ import ch.njol.skript.Skript; import ch.njol.skript.doc.*; +import ch.njol.skript.effects.Delay; import ch.njol.skript.lang.Condition; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; @@ -46,6 +47,7 @@ public class CondIsPressingKey extends Condition { private Expression players; private Expression inputKeys; private boolean past; + private boolean delayed; @Override public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { @@ -54,8 +56,14 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is //noinspection unchecked inputKeys = (Expression) expressions[1]; past = matchedPattern > 1; - if (past && !getParser().isCurrentEvent(PlayerInputEvent.class)) - Skript.warning("Checking the past state of a player's input outside the 'player input' event has no effect."); + delayed = !isDelayed.isFalse(); + if (past) { + if (!getParser().isCurrentEvent(PlayerInputEvent.class)) { + Skript.warning("Checking the past state of a player's input outside the 'player input' event has no effect."); + } else if (delayed) { + Skript.warning("Checking the past state of a player's input after the event has passed has no effect."); + } + } setNegated(matchedPattern == 1 || matchedPattern == 3); return true; } @@ -65,10 +73,11 @@ public boolean check(Event event) { Player eventPlayer = event instanceof PlayerInputEvent inputEvent ? inputEvent.getPlayer() : null; InputKey[] inputKeys = this.inputKeys.getAll(event); boolean and = this.inputKeys.getAnd(); + boolean delayed = this.delayed || Delay.isDelayed(event); return players.check(event, player -> { Input input; // If we want to get the new input of the event-player, we must get it from the event - if (!past && player.equals(eventPlayer)) { + if (!delayed && !past && player.equals(eventPlayer)) { input = ((PlayerInputEvent) event).getInput(); } else { // Otherwise, we get the current (or past in case of an event-player) input input = player.getCurrentInput(); diff --git a/src/main/java/org/skriptlang/skript/bukkit/input/elements/expressions/ExprCurrentInputKeys.java b/src/main/java/org/skriptlang/skript/bukkit/input/elements/expressions/ExprCurrentInputKeys.java index f57d735ff55..5aaa079f769 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/input/elements/expressions/ExprCurrentInputKeys.java +++ b/src/main/java/org/skriptlang/skript/bukkit/input/elements/expressions/ExprCurrentInputKeys.java @@ -2,6 +2,7 @@ import ch.njol.skript.Skript; import ch.njol.skript.doc.*; +import ch.njol.skript.effects.Delay; import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; @@ -29,9 +30,12 @@ public class ExprCurrentInputKeys extends PropertyExpression { register(ExprCurrentInputKeys.class, InputKey.class, "[current] (inputs|input keys)", "players"); } + private boolean delayed; + @Override public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { setExpr((Expression) expressions[0]); + delayed = !isDelayed.isFalse(); return true; } @@ -41,9 +45,11 @@ protected InputKey[] get(Event event, Player[] source) { if (SUPPORTS_TIME_STATES && getTime() == EventValues.TIME_NOW && event instanceof PlayerInputEvent inputEvent) eventPlayer = inputEvent.getPlayer(); + boolean delayed = this.delayed || Delay.isDelayed(event); + List inputKeys = new ArrayList<>(); for (Player player : source) { - if (player.equals(eventPlayer)) { + if (!delayed && player.equals(eventPlayer)) { inputKeys.addAll(InputKey.fromInput(((PlayerInputEvent) event).getInput())); } else { inputKeys.addAll(InputKey.fromInput(player.getCurrentInput())); diff --git a/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfos.java b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfos.java index f46881c2d37..8a29109a016 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfos.java +++ b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfos.java @@ -41,7 +41,7 @@ static Builder, E> builder( */ @Override @Contract("-> new") - Builder, E> builder(); + Builder, E> toBuilder(); /** * @return The listening behavior for the SkriptEvent. Determines when the event should trigger. @@ -291,7 +291,7 @@ interface Builder, E extends SkriptEvent> extends Syntax * @see Event#events() */ @Contract("_ -> this") - B addEvents(Class... events); + B addEvents(Class[] events); /** * Adds events to the event's documentation. diff --git a/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfosImpl.java b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfosImpl.java index c9b2105aedf..96f8f614743 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfosImpl.java +++ b/src/main/java/org/skriptlang/skript/bukkit/registration/BukkitSyntaxInfosImpl.java @@ -56,9 +56,9 @@ static final class EventImpl implements Event { } @Override - public Builder, E> builder() { + public Builder, E> toBuilder() { var builder = new BuilderImpl<>(type(), name); - defaultInfo.builder().applyTo(builder); + defaultInfo.toBuilder().applyTo(builder); builder.listeningBehavior(listeningBehavior); builder.documentationId(id); if (since != null) { diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTag.java b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTag.java index eba0506b63e..8c990e6b85e 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTag.java +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTag.java @@ -11,6 +11,7 @@ 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.ContextlessEvent; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.skript.lang.util.SimpleLiteral; import ch.njol.util.Kleenean; @@ -130,8 +131,8 @@ public String toString(@Nullable Event event, boolean debug) { @Override public Expression simplify() { if (names instanceof Literal) - return new SimpleLiteral<>(getArray(null), Tag.class, true); - return this; + return new SimpleLiteral<>(getArray(ContextlessEvent.get()), Tag.class, true); + return super.simplify(); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagContents.java b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagContents.java index e09e0df8618..422e4a2e805 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagContents.java +++ b/src/main/java/org/skriptlang/skript/bukkit/tags/elements/ExprTagContents.java @@ -8,11 +8,15 @@ import ch.njol.skript.doc.Keywords; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.entity.EntityData; 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.ContextlessEvent; import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.skript.util.Utils; +import ch.njol.skript.lang.util.SimpleLiteral; +import ch.njol.skript.registrations.Classes; import ch.njol.util.Kleenean; import org.bukkit.Material; import org.bukkit.Tag; @@ -22,7 +26,9 @@ import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.tags.TagType; +import java.lang.reflect.Array; import java.util.Objects; +import java.util.stream.Stream; @Name("Tags Contents") @Description({ @@ -45,12 +51,16 @@ public class ExprTagContents extends SimpleExpression { } private Expression> tag; - private TagType @Nullable [] tagTypes; + + private Class returnType; + private Class[] possibleReturnTypes; @Override public boolean init(Expression @NotNull [] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { //noinspection unchecked tag = (Expression>) expressions[0]; + + TagType[] tagTypes = null; if (expressions[0] instanceof ExprTag exprTag) { tagTypes = exprTag.types; } else if (expressions[0] instanceof ExprTagsOf exprTagsOf) { @@ -58,14 +68,38 @@ public boolean init(Expression @NotNull [] expressions, int matchedPattern, K } else if (expressions[0] instanceof ExprTagsOfType exprTagsOfType) { tagTypes = exprTagsOfType.types; } + if (tagTypes != null) { // try to determine the return type + possibleReturnTypes = new Class[tagTypes.length]; + for (int i = 0; i < tagTypes.length; i++) { + Class type = tagTypes[i].type(); + // map types + if (type == Material.class) { + type = ItemType.class; + } else if (type == EntityType.class) { + type = EntityData.class; + } + possibleReturnTypes[i] = type; + } + returnType = Classes.getSuperClassInfo(possibleReturnTypes).getC(); + } else { + returnType = Object.class; + possibleReturnTypes = new Class[]{returnType}; + } + return true; } @Override + @SuppressWarnings({"unchecked", "rawtypes", "RedundantCast"}) // cast to avoid type issues protected Object @Nullable [] get(Event event) { + return ((Stream) stream(event)).toArray(length -> Array.newInstance(getReturnType(), length)); + } + + @Override + public Stream stream(Event event) { Tag tag = this.tag.getSingle(event); if (tag == null) - return null; + return Stream.empty(); return tag.getValues().stream() .map(value -> { if (value instanceof Material material) { @@ -75,8 +109,7 @@ public boolean init(Expression @NotNull [] expressions, int matchedPattern, K } return null; }) - .filter(Objects::nonNull) - .toArray(); + .filter(Objects::nonNull); } @Override @@ -86,14 +119,12 @@ public boolean isSingle() { @Override public Class getReturnType() { - if (tagTypes != null) { - Class[] possibleTypes = new Class[tagTypes.length]; - for (int i = 0; i < tagTypes.length; i++) { - possibleTypes[i] = tagTypes[i].type(); - } - return Utils.getSuperType(possibleTypes); - } - return Object.class; + return returnType; + } + + @Override + public Class[] possibleReturnTypes() { + return possibleReturnTypes; } @Override @@ -101,4 +132,13 @@ public String toString(@Nullable Event event, boolean debug) { return "the tag contents of " + tag.toString(event, debug); } + @Override + public Expression simplify() { + if (tag instanceof Literal>) { + Object[] values = getArray(ContextlessEvent.get()); + return new SimpleLiteral(values, values.getClass().getComponentType(), true); + } + return super.simplify(); + } + } diff --git a/src/main/java/org/skriptlang/skript/lang/util/SkriptQueue.java b/src/main/java/org/skriptlang/skript/lang/util/SkriptQueue.java index a7fbf9be576..c05d859edf0 100644 --- a/src/main/java/org/skriptlang/skript/lang/util/SkriptQueue.java +++ b/src/main/java/org/skriptlang/skript/lang/util/SkriptQueue.java @@ -1,7 +1,7 @@ package org.skriptlang.skript.lang.util; import ch.njol.skript.lang.util.common.AnyAmount; -import ch.njol.yggdrasil.YggdrasilSerializable; +import ch.njol.skript.util.Container; import org.jetbrains.annotations.NotNull; import java.util.*; @@ -10,8 +10,9 @@ * A queue of elements. * Elements will only be added to the queue if they are not null, with nothing happening if the elements are null. */ +@Container.ContainerType(Object.class) public class SkriptQueue extends LinkedList<@NotNull Object> - implements Deque, Queue, YggdrasilSerializable, AnyAmount { + implements Deque, Queue, AnyAmount, Container { @Override public boolean add(Object element) { @@ -97,4 +98,23 @@ public Object[] removeRangeSafely(int fromIndex, int toIndex) { return this.size(); } + @Override + public Iterator containerIterator() { + return new Iterator<>() { + @Override + public boolean hasNext() { + return !SkriptQueue.this.isEmpty(); + } + + @Override + public Object next() { + return SkriptQueue.this.pollFirst(); + } + + @Override + public void remove() { + } + }; + } + } diff --git a/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java index 49eefd4afba..1e019ff4ce4 100644 --- a/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java +++ b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfos.java @@ -43,7 +43,7 @@ static , R> Builder new") - Builder, E, R> builder(); + Builder, E, R> toBuilder(); /** * @return The class representing the supertype of all values the Expression may return. @@ -132,7 +132,7 @@ static Builder new") - Builder, E> builder(); + Builder, E> toBuilder(); /** * @return The entry validator to use for handling the Structure's entries. diff --git a/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfosImpl.java b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfosImpl.java index 19be1dbc3ef..30350bf75a9 100644 --- a/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfosImpl.java +++ b/src/main/java/org/skriptlang/skript/registration/DefaultSyntaxInfosImpl.java @@ -30,9 +30,9 @@ static class ExpressionImpl, R> } @Override - public Expression.Builder, E, R> builder() { + public Expression.Builder, E, R> toBuilder() { var builder = new BuilderImpl<>(type(), returnType); - super.builder().applyTo(builder); + super.toBuilder().applyTo(builder); return builder; } @@ -107,9 +107,9 @@ static class StructureImpl, E> builder() { + public Structure.Builder, E> toBuilder() { var builder = new BuilderImpl<>(type()); - super.builder().applyTo(builder); + super.toBuilder().applyTo(builder); if (entryValidator != null) { builder.entryValidator(entryValidator); } @@ -189,8 +189,8 @@ public void applyTo(SyntaxInfo.Builder builder) { if (builder instanceof Structure.Builder structureBuilder) { if (entryValidator != null) { structureBuilder.entryValidator(entryValidator); - structureBuilder.nodeType(nodeType); } + 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 index f1a70785580..693c294d373 100644 --- a/src/main/java/org/skriptlang/skript/registration/SyntaxInfo.java +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxInfo.java @@ -5,7 +5,6 @@ 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; @@ -16,7 +15,7 @@ * @param The class providing the implementation of the syntax this info represents. */ @ApiStatus.Experimental -public interface SyntaxInfo extends Buildable, SyntaxInfo>, DefaultSyntaxInfos { +public interface SyntaxInfo extends DefaultSyntaxInfos { /** * A priority for infos with patterns that only match simple text (they do not have any {@link Expression}s). @@ -49,11 +48,10 @@ static Builder, E> builder(Cla } /** - * {@inheritDoc} + * @return A builder representing this SyntaxInfo. */ - @Override @Contract("-> new") - Builder, E> builder(); + Builder, E> toBuilder(); /** * @return The origin of this syntax. @@ -87,7 +85,7 @@ static Builder, E> builder(Cla * @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> { + interface Builder, E extends SyntaxElement> { /** * Sets the origin the syntax info will use. @@ -165,7 +163,6 @@ interface Builder, E extends SyntaxElement> extends org. * 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 index 9b1de224549..82c436f48e0 100644 --- a/src/main/java/org/skriptlang/skript/registration/SyntaxInfoImpl.java +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxInfoImpl.java @@ -42,7 +42,7 @@ protected SyntaxInfoImpl( } @Override - public Builder, T> builder() { + public Builder, T> toBuilder() { var builder = new BuilderImpl<>(type); builder.origin(origin); if (supplier != null) { diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxRegistry.java b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistry.java index acde00a843f..578bf45be27 100644 --- a/src/main/java/org/skriptlang/skript/registration/SyntaxRegistry.java +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistry.java @@ -15,7 +15,7 @@ import java.util.Collection; /** - * A syntax registry manages all {@link SyntaxRegister}s for syntax registration. + * A syntax registry is a central container for all {@link SyntaxInfo}s. */ @ApiStatus.Experimental public interface SyntaxRegistry extends ViewProvider, Registry> { @@ -77,11 +77,19 @@ static SyntaxRegistry empty() { */ > void register(Key key, I info); + /** + * Unregisters all registrations of a syntax, regardless of the {@link Key}. + * @param info The syntax info to unregister. + * @see #unregister(Key, SyntaxInfo) + */ + void unregister(SyntaxInfo 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. + * @see #unregister(SyntaxInfo) */ > void unregister(Key key, I info); @@ -144,7 +152,7 @@ interface ChildKey> extends Key { * @param

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

parent, String name) { + static > ChildKey of(Key

parent, String name) { return new SyntaxRegistryImpl.ChildKeyImpl<>(parent, name); } diff --git a/src/main/java/org/skriptlang/skript/registration/SyntaxRegistryImpl.java b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistryImpl.java index 843a1061697..8b7c17e6431 100644 --- a/src/main/java/org/skriptlang/skript/registration/SyntaxRegistryImpl.java +++ b/src/main/java/org/skriptlang/skript/registration/SyntaxRegistryImpl.java @@ -27,6 +27,14 @@ public > void register(Key key, I info) { } } + @Override + @SuppressWarnings({"unchecked", "rawtypes"}) + public void unregister(SyntaxInfo info) { + for (Key key : registers.keySet()) { + unregister(key, info); + } + } + @Override public > void unregister(Key key, I info) { register(key).remove(info); @@ -74,6 +82,11 @@ public > void register(Key key, I info) { throw new UnsupportedOperationException("Cannot register syntax infos with an unmodifiable syntax registry."); } + @Override + public void unregister(SyntaxInfo info) { + throw new UnsupportedOperationException("Cannot unregister syntax infos from an unmodifiable syntax registry."); + } + @Override public > void unregister(Key key, I info) { throw new UnsupportedOperationException("Cannot unregister syntax infos from an unmodifiable syntax registry."); diff --git a/src/main/java/org/skriptlang/skript/util/Builder.java b/src/main/java/org/skriptlang/skript/util/Builder.java deleted file mode 100644 index e3280fbdf8e..00000000000 --- a/src/main/java/org/skriptlang/skript/util/Builder.java +++ /dev/null @@ -1,42 +0,0 @@ -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 index 030f6538e37..fe3d7ace528 100644 --- a/src/main/java/org/skriptlang/skript/util/ClassLoader.java +++ b/src/main/java/org/skriptlang/skript/util/ClassLoader.java @@ -2,6 +2,7 @@ import ch.njol.skript.Skript; import ch.njol.util.StringUtils; +import com.google.common.base.MoreObjects; import com.google.common.reflect.ClassPath; import com.google.common.reflect.ClassPath.ResourceInfo; import org.jetbrains.annotations.Contract; @@ -14,6 +15,7 @@ import java.util.HashSet; import java.util.TreeSet; import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.stream.Collectors; @@ -51,19 +53,21 @@ public static void loadClasses(Class source, File jarFile, String basePackage private final String basePackage; private final Collection subPackages; + private final @Nullable Predicate filter; 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"); + private ClassLoader(String basePackage, Collection subPackages, @Nullable Predicate filter, + boolean initialize, boolean deep, @Nullable Consumer> forEachClass) { + if (!basePackage.isEmpty()) { // allow empty base package + basePackage = basePackage.replace('.', '/') + "/"; } - this.basePackage = basePackage.replace('.', '/') + "/"; + this.basePackage = basePackage; this.subPackages = subPackages.stream() .map(subPackage -> subPackage.replace('.', '/') + "/") .collect(Collectors.toSet()); + this.filter = filter; this.initialize = initialize; this.deep = deep; this.forEachClass = forEachClass; @@ -127,8 +131,9 @@ public void loadClasses(Class source, @Nullable JarFile jar) { // 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")) + 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 @@ -147,7 +152,10 @@ public void loadClasses(Class source, @Nullable JarFile jar) { if (load) { // replace separators and .class extension - classNames.add(name.replace('/', '.').substring(0, name.length() - 6)); + name = name.replace('/', '.').substring(0, name.length() - 6); + if (filter == null || filter.test(name)) { // final check for loading + classNames.add(name); + } } } @@ -155,8 +163,9 @@ public void loadClasses(Class source, @Nullable JarFile jar) { for (String className : classNames) { try { Class clazz = Class.forName(className, this.initialize, loader); - if (this.forEachClass != null) + if (this.forEachClass != null) { this.forEachClass.accept(clazz); + } } catch (ClassNotFoundException ex) { throw new RuntimeException("Failed to load class: " + className, ex); } catch (ExceptionInInitializerError err) { @@ -165,6 +174,37 @@ public void loadClasses(Class source, @Nullable JarFile jar) { } } + /** + * @return A builder representing this ClassLoader. + */ + @Contract("-> new") + public Builder toBuilder() { + Builder builder = builder() + .basePackage(this.basePackage) + .addSubPackages(this.subPackages) + .initialize(this.initialize) + .deep(this.deep); + if (filter != null) { + builder.filter(this.filter); + } + if (forEachClass != null) { + builder.forEachClass(this.forEachClass); + } + return builder; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("basePackage", basePackage) + .add("subPackages", subPackages) + .add("filter", filter) + .add("initialize", initialize) + .add("deep", deep) + .add("forEachClass", forEachClass) + .toString(); + } + /** * A builder for constructing a {@link ClassLoader}. */ @@ -172,6 +212,7 @@ public static final class Builder { private String basePackage = ""; private final Collection subPackages = new HashSet<>(); + private @Nullable Predicate filter = null; private boolean initialize; private boolean deep; private @Nullable Consumer> forEachClass; @@ -232,6 +273,18 @@ public Builder addSubPackages(Collection subPackages) { return this; } + /** + * A predicate for whether a fully qualified class name should be loaded as a {@link Class}. + * @param filter A predicate for filtering class names. + * It should return true for class names to load. + * @return This builder. + */ + @Contract("_ -> this") + public Builder filter(Predicate filter) { + this.filter = filter; + return this; + } + /** * Sets whether the loader will initialize found classes. * @param initialize Whether classes should be initialized when found. @@ -271,7 +324,7 @@ public Builder forEachClass(Consumer> forEachClass) { */ @Contract("-> new") public ClassLoader build() { - return new ClassLoader(basePackage, subPackages, initialize, deep, forEachClass); + return new ClassLoader(basePackage, subPackages, filter, initialize, deep, forEachClass); } } diff --git a/src/main/java/org/skriptlang/skript/util/PriorityImpl.java b/src/main/java/org/skriptlang/skript/util/PriorityImpl.java index b342cf1e982..3272349f865 100644 --- a/src/main/java/org/skriptlang/skript/util/PriorityImpl.java +++ b/src/main/java/org/skriptlang/skript/util/PriorityImpl.java @@ -1,9 +1,12 @@ package org.skriptlang.skript.util; +import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableSet; +import org.jetbrains.annotations.NotNull; import java.util.Collection; import java.util.HashSet; +import java.util.Objects; import java.util.Set; class PriorityImpl implements Priority { @@ -33,7 +36,7 @@ class PriorityImpl implements Priority { } @Override - public int compareTo(Priority other) { + public int compareTo(@NotNull Priority other) { if (this == other) { return 0; } @@ -79,4 +82,24 @@ public Collection before() { return before; } + @Override + public int hashCode() { + return Objects.hash(after, before); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Priority priority)) + return false; + return compareTo(priority) == 0; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("after", after) + .add("before", before) + .toString(); + } + } diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index 360d557e8cf..e380aef7bcf 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2680,6 +2680,7 @@ types: function: function¦s @a named: named thing¦s @a numbered: numbered thing¦s @a + valued: valued thing¦s @a containing: container¦s @a # Hooks diff --git a/src/main/resources/scripts/-examples/chest menus.sk b/src/main/resources/scripts/-examples/chest menus.sk index 42de70edfe3..525f7e805ab 100644 --- a/src/main/resources/scripts/-examples/chest menus.sk +++ b/src/main/resources/scripts/-examples/chest menus.sk @@ -5,11 +5,11 @@ # command /simplechest: - permission: skript.example.chest - trigger: - set {_chest} to a new chest inventory named "Simple Chest" - set slot 0 of {_chest} to apple # Slots are numbered 0, 1, 2... - open {_chest} to player + permission: skript.example.chest + trigger: + set {_chest} to a new chest inventory named "Simple Chest" + set slot 0 of {_chest} to apple # Slots are numbered 0, 1, 2... + open {_chest} to player # # An example of listening for click events in a chest inventory. @@ -17,19 +17,19 @@ command /simplechest: # command /chestmenu: - permission: skript.example.menu - trigger: - set {_menu} to a new chest inventory with 1 row named "Simple Menu" - set slot 4 of {_menu} to stone named "Button" # Slots are numbered 0, 1, 2... - open {_menu} to player + permission: skript.example.menu + trigger: + set {_menu} to a new chest inventory with 1 row named "Simple Menu" + set slot 4 of {_menu} to stone named "Button" # Slots are numbered 0, 1, 2... + open {_menu} to player on inventory click: # Listen for players clicking in an inventory. - name of event-inventory is "Simple Menu" # Make sure it's our menu. - cancel event - if index of event-slot is 4: # The button slot. - send "You clicked the button." - else: - send "You didn't click the button." + name of event-inventory is "Simple Menu" # Make sure it's our menu. + cancel event + if index of event-slot is 4: # The button slot. + send "You clicked the button." + else: + send "You didn't click the button." # # An example of making and filling a fancy inventory with a function. @@ -37,26 +37,26 @@ on inventory click: # Listen for players clicking in an inventory. # aliases: - menu items = TNT, lava bucket, string, coal, oak planks + menu items = TNT, lava bucket, string, coal, oak planks -function makeMenu(name: text, rows: number) :: inventory: - set {_gui} to a new chest inventory with {_rows} rows with name {_name} - loop {_rows} * 9 times: # Fill the inventory with random items. - set slot loop-number - 1 of {_gui} to random item out of menu items - return {_gui} +function make_menu(name: text, rows: number) :: inventory: + set {_gui} to a new chest inventory with {_rows} rows with name {_name} + loop {_rows} * 9 times: # Fill the inventory with random items. + set slot loop-number - 1 of {_gui} to random item out of menu items + return {_gui} command /fancymenu: - permission: skript.example.menu - trigger: - set {_menu} to makeMenu("hello", 4) - add {_menu} to {my inventories::*} # Prepare to listen to this inventory. - open {_menu} to player + permission: skript.example.menu + trigger: + set {_menu} to make_menu("hello", 4) + add {_menu} to {my inventories::*} # Prepare to listen to this inventory. + open {_menu} to player on inventory click: # Listen for players clicking in any inventory. - if {my inventories::*} contains event-inventory: # Make sure it's our menu. - cancel event - send "You clicked slot %index of event-slot%!" + if {my inventories::*} contains event-inventory: # Make sure it's our menu. + cancel event + send "You clicked slot %index of event-slot%!" on inventory close: # No longer need to listen to this inventory. - {my inventories::*} contains event-inventory - remove event-inventory from {my inventories::*} + {my inventories::*} contains event-inventory + remove event-inventory from {my inventories::*} diff --git a/src/main/resources/scripts/-examples/commands.sk b/src/main/resources/scripts/-examples/commands.sk index 303eb2ee70b..40162adeb4c 100644 --- a/src/main/resources/scripts/-examples/commands.sk +++ b/src/main/resources/scripts/-examples/commands.sk @@ -5,10 +5,10 @@ # command /broadcast : - permission: skript.example.broadcast - description: Broadcasts a message to everybody. - trigger: - broadcast arg-text + permission: skript.example.broadcast + description: Broadcasts a message to everybody. + trigger: + broadcast arg-text # # A simple /home command that allows players to set, remove and travel to homes. @@ -17,29 +17,29 @@ command /broadcast : # command /home []: - description: Set, delete or travel to your home. - usage: /home set/remove , /home - permission: skript.example.home - executable by: players - trigger: - if arg-1 is "set": - if arg-2 is set: - set {homes::%uuid of player%::%arg-2%} to player's location - send "Set your home %arg-2% to %location of player%" to player - else: - send "You must specify a name for this home." to player - else if arg-1 is "remove": - if arg-2 is set: - delete {homes::%uuid of player%::%arg-2%} - send "Deleted your home %arg-2%" to player - else: - send "You must specify the name of this home." to player - else if arg-2 is set: - send "Correct usage: /home set/remove " to player - else if {homes::%uuid of player%::%arg-1%} is set: - teleport player to {homes::%uuid of player%::%arg-1%} - else: - send "You have no home named %arg-1%." to player + description: Set, delete or travel to your home. + usage: /home set/remove , /home + permission: skript.example.home + executable by: players + trigger: + if arg-1 is "set": + if arg-2 is set: + set {homes::%uuid of player%::%arg-2%} to player's location + send "Set your home %arg-2% to %location of player%" to player + else: + send "You must specify a name for this home." to player + else if arg-1 is "remove": + if arg-2 is set: + delete {homes::%uuid of player%::%arg-2%} + send "Deleted your home %arg-2%" to player + else: + send "You must specify the name of this home." to player + else if arg-2 is set: + send "Correct usage: /home set/remove " to player + else if {homes::%uuid of player%::%arg-1%} is set: + teleport player to {homes::%uuid of player%::%arg-1%} + else: + send "You have no home named %arg-1%." to player # # An /item command that accepts Skript item aliases. @@ -48,24 +48,24 @@ command /home []: # aliases: - # Creates an alias `blacklisted` for this list of items. - blacklisted = TNT, bedrock, obsidian, mob spawner, lava, lava bucket + # Creates an alias `blacklisted` for this list of items. + blacklisted = TNT, bedrock, obsidian, spawner, lava, lava bucket command /item : - description: Give yourself some items. - usage: /item - aliases: /i - executable by: players - permission: skript.example.item - cooldown: 30 seconds - cooldown message: You need to wait %remaining time% to use this command again. - cooldown bypass: skript.example.cooldown - trigger: - if player has permission "skript.example.item.all": - give argument to player - else: - loop argument: - if loop-item is not blacklisted: - give loop-item to player - else: - send "%loop-item% is blacklisted and cannot be spawned." to player + description: Give yourself some items. + usage: /item + aliases: /i + executable by: players + permission: skript.example.item + cooldown: 30 seconds + cooldown message: You need to wait %remaining time% to use this command again. + cooldown bypass: skript.example.cooldown + trigger: + if player has permission "skript.example.item.all": + give argument to player + else: + loop argument: + if loop-item is not blacklisted: + give loop-item to player + else: + send "%loop-item% is blacklisted and cannot be spawned." to player diff --git a/src/main/resources/scripts/-examples/events.sk b/src/main/resources/scripts/-examples/events.sk index 45fbfb2a7d5..6d6aa6d8ad4 100644 --- a/src/main/resources/scripts/-examples/events.sk +++ b/src/main/resources/scripts/-examples/events.sk @@ -5,10 +5,10 @@ # on join: - set the join message to "Oh look, %player% joined! :)" + set the join message to "Oh look, %player% joined! :)" on quit: - set the quit message to "Oh no, %player% left! :(" + set the quit message to "Oh no, %player% left! :(" # # This example cancels damage for players if they have a specific permission. @@ -16,14 +16,14 @@ on quit: # on damage: - victim is a player - if the victim has permission "skript.example.damage": - cancel the event # Stops the default behaviour - the victim taking damage. - else: - send "Ouch! You took %damage% damage." to the victim - add damage to {damage::%uuid of victim%::taken} - if the attacker is a player: - add damage to {damage::%uuid of attacker%::dealt} + victim is a player + if the victim has permission "skript.example.damage": + cancel the event # Stops the default behaviour - the victim taking damage. + else: + send "Ouch! You took %damage% damage." to the victim + add damage to {damage::%uuid of victim%::taken} + if the attacker is a player: + add damage to {damage::%uuid of attacker%::dealt} # # This example allows players to wear specified blocks as hats. @@ -31,14 +31,14 @@ on damage: # aliases: # An alias for our allowed hat items. - custom helmets = iron block, gold block, diamond block + custom helmets = iron block, gold block, diamond block on inventory click: - event-slot is the helmet slot of player # Check that player clicked their head slot. - inventory action is place all or nothing - player has permission "skript.example.helmet" - cursor slot of player is custom helmets # Check if the item is in our custom alias. - cancel the event - set {_old helmet} to the helmet of player - set the helmet of player to the cursor slot of player - set the cursor slot of player to {_old helmet} + event-slot is the helmet slot of player # Check that player clicked their head slot. + inventory action is place all or nothing + player has permission "skript.example.helmet" + cursor slot of player is custom helmets # Check if the item is in our custom alias. + cancel the event + set {_old helmet} to the helmet of player + set the helmet of player to the cursor slot of player + set the cursor slot of player to {_old helmet} diff --git a/src/main/resources/scripts/-examples/experimental features/for loops.sk b/src/main/resources/scripts/-examples/experimental features/for loops.sk new file mode 100644 index 00000000000..a4a7f71ab5d --- /dev/null +++ b/src/main/resources/scripts/-examples/experimental features/for loops.sk @@ -0,0 +1,50 @@ +using examples + +# This flag enables the experimental 'for each loops' feature within this file +using for loops + + +example: # A simple loop + + # For-loops allow you to extract the loop value directly into a variable + for each {_player} in all players: + send "Hello!" to {_player} + + # This is exactly the same as using a regular loop and loop-value + loop all players: + send "Hello!" to loop-value + + +example: # Nested loops + + # For loops are designed to help when nesting multiple loops + for each {_word} in {words::*}: + for each {_player} in all players: + send "The word is %{_word}%" to {_player} + + # This is exactly the same as the following + loop {words::*}: + loop all players: + send "The word is %loop-value-1%" to loop-value-2 + + +example: # Keeping the last value + + # Since the loop value is extracted to a variable, + # this is still available at the end of the loop + for {_player} in all players: + send "Hello!" to {_player} + + # The final player to be looped is still in the variable + send "(You're my favourite)" to {_player} + + +example: # Index and value + + # For a list with keys, you can extract both the key and the value + for index {_number}, value {_value} in {my list::*}: + broadcast "%{_number}%. %{_value}%" + + # You can also loop only the indices + for index {_key} in {my list::*}: + broadcast {_key} diff --git a/src/main/resources/scripts/-examples/experimental features/queues.sk b/src/main/resources/scripts/-examples/experimental features/queues.sk new file mode 100644 index 00000000000..a03c8ce956a --- /dev/null +++ b/src/main/resources/scripts/-examples/experimental features/queues.sk @@ -0,0 +1,175 @@ +using examples + +# This flag enables the experimental 'queues' feature within this file +using queues + +example: # Creating queues + + # Queues are a storage container for objects. + set {queue} to a queue + + # Queues function like {lists::*} but are a single variable. + add "hello" to {queue} + remove "hello" from {queue} + clear {queue} + + # You can create a queue with some initial items in it + set {queue} to a queue of "hello" and "world" + + # You can also create a queue of things from a list + # Anything can go in a queue + set {my items::*} to potato, apple and carrot + set {queue} to a queue of {my items::*} + + +example: # Taking items from a queue + + set {queue} to a queue of "hello" and "world" + + # Asking for an item from the queue also removes it + the first element of {queue} is "hello" + # 'hello' is gone + + the first element of {queue} is "world" + # 'world' is gone + + {queue} is empty + + +example: # Taking items from the end of a queue + + set {queue} to a queue of "hello" and "world" + + the last element of {queue} is "world" + # 'world' is gone + + the last element of {queue} is "hello" + # 'hello' is gone + + {queue} is empty + + +example: # Processing a queue + + # Queues are First In, First Out (FIFO) + # This means new items are added to the END, and removed from the START + + set {queue} to a queue of "hello" + + add "world" to {queue} + # Queue is now 'hello', 'world' + broadcast the first element of {queue} + # 'hello' is gone + + add "hello" to {queue} + # Queue is now 'world', 'hello' + + +example: # Looping a queue + + # Looping items in a queue also removes them + set {queue} to a queue of all players + + loop {queue}: + send "Good morning!" to loop-value + + # All the items were processed + {queue} is empty + + # This means queues are good for pausing and resuming a task + # without losing your progress! + set {queue} to a queue of all players + set {count} to 0 + + loop {queue}: + teleport loop-value to {my cool battle arena} + # Each player is removed in this loop + add 1 to {count} + + if {count} is 10: + # We have too many players in the battle arena for now! + exit loop + + wait 1 minute + # Only the players we didn't loop before are still in the queue + loop {queue}: + teleport loop-value to {my cool battle arena} + + +example: # Turning a queue into a list + + set {queue} to a queue of "hello" and "world" + # We can copy the items in the queue into a list + set {my list::*} to dequeued {queue} + {my list::*} is "hello" and "world" + # Deconstructing the queue does not remove them from the queue + + the first 2 elements in {queue} are "hello" and "world" + # But asking for the elements does! + {queue} is empty + + +example: # Peeking at a queue without removing items + + # Sometimes, we want to check what the next item will be, without removing it + set {queue} to a queue of "hello" and "world" + # We can 'peek' at the first and last elements + the start of {queue} is "hello" + the end of {queue} is "world" + + # We can also change these values + set the end of {queue} to "there" + # The queue is now 'hello', 'there' + + # The other change effects (set, add, remove, delete) also work here + add "well," to the start of {queue} + # The queue is now 'well,', 'hello', 'there' + + delete the end of {queue} + # The queue is now 'well,', 'hello' + + +using for loops +example: # Continuous processing of a queue + + # Create a queue of all players (now) and some items + set {players} to a queue of all players + set {gifts} to a queue of potato, apple and carrot + + # This will keep cycling through the 'players' and 'gifts' queues + # until all of the initial players are no longer online + for each {_player} in {players}: + {_player} is online + set {_item} to the first element of {gifts} + give {_item} to {_player} + + # Add the player and item back to their queue + add {_player} to {players} + add {_item} to {gifts} + # As items are added to the END of a queue, this will cycle round in order + + # This will loop forever, so we want a delay + wait 30 seconds + + +# +# A queue for rotating periodical announcements +# + +on script load: + # Create an empty queue + set {advert messages} to a queue + + # Add some messages to it + add "Welcome to my server!" to {advert messages} + add "All furniture, 5%% off in store and online!" to {advert messages} + add "Discount sale on christmas trees!" to {advert messages} + add "Vote for [candidate name]!" to {advert messages} + +every 3 minutes: + # Take the first message out of the queue + set {_message} to the first element of {advert messages} + broadcast "" + {_message} + + # Add the message back to the end of the queue + add {_message} to {advert messages} diff --git a/src/main/resources/scripts/-examples/experimental features/script reflection.sk b/src/main/resources/scripts/-examples/experimental features/script reflection.sk new file mode 100644 index 00000000000..05d2fe1f4eb --- /dev/null +++ b/src/main/resources/scripts/-examples/experimental features/script reflection.sk @@ -0,0 +1,244 @@ +using examples + +# This flag enables the experimental 'script reflection' feature within this file +# Script reflection is a collection of powerful tools for scripts to inspect themselves +using script reflection + + +### +THE BASICS +===== + +'Reflection' is for something to look at itself (like in a mirror). + +Script reflection is a collection of powerful tools for scripts to inspect themselves. + +We can start with basic access to scripts. +### + + +example: # Getting a script + + # You can obtain the current script + set {_script} to the current script + {_script} exists + + # You can also find a script by name + set {_script} to the script named "my cool script.sk" + + # Even if it's in a folder + set {_script} to the script named "examples/events.sk" + + # Even if it's not enabled + set {_script} to the script named "-disabled script.sk" + + +example: # Looping scripts + + # It's possible to find multiple scripts at a time. + set {_scripts::*} to all loaded scripts + set {_scripts::*} to the scripts named "my script.sk" and "my cooler script.sk" + set {_scripts::*} to the scripts in folder "examples/" + + loop all loaded scripts: + broadcast name of loop-value + + +example: # The name and path of a script + + # Scripts have a name, which is their file name (without .sk) + set {_script} to the script named "my script.sk" + the name of {_script} is "my script" + + # This doesn't include their folder name + set {_script} to the script named "folder/my script.sk" + the name of {_script} is "my script" + + # However, if you just print out a script, it will include the whole path + set {_script} to the script named "folder/my script.sk" + "%{_script}%" is "folder/my script.sk" + + +example: # Enabling, disabling and loading scripts + + # You can enable a disabled script + set {_script} to the script named "-disabled script.sk" + # This parses and loads its code + enable {_script} + + # Now that it's been enabled, you can disable it again + set {_script} to the script named "disabled script.sk" + # This unloads its code and renames the script + disable {_script} + + # A disabled script is skipped when Skript loads + # If you want to remove it temporarily, you can `unload` it + set {_script} to the script named "my cool script.sk" + unload {_script} + + # You can then load it again, which is the same as enabling + load {_script} + + # Or just reload it + reload {_script} + + +### +CONFIGS +===== + +There are also tools provided for reading Skript's built-in configuration files. +These allow you to check values from these files. + +Configs are arranged as a tree of 'nodes', which have values: + +regular node: value +regular node: value +section node: # This node contains more nodes! + regular node: value + regular node: value + another section node: # Sections can contain sections! + regular node: value +### + + +example: # Get the Skript config + + set {_config} to the skript config + {_config} exists + + +example: # Reading a simple node value + + # Since configs are arranged in 'nodes' which have values, + # we just need to ask for these nodes, and then ask for their values + + # You can get a node value directly from a config by name + set {_value} to the boolean value at "color codes reset formatting" in the skript config + broadcast "Do colour codes reset formatting? %{_value}%" + + # You can also get a node specifically + set {_node} to the node "number accuracy" of {_config} + # And then check its value + set {_value} to the number value of {_node} + broadcast "Number accuracy: %{_value}%" + + +example: # Value conversion + + set {_node} to the node "number accuracy" of the skript config + + # By specifying the TYPE of value you want, it is automatically parsed and converted + set {_value} to the text value of {_node} + {_value} is a text + + set {_value} to the number value of {_node} + {_value} is a number + + delete {_value} + + # If it is impossible to convert or parse the value to what you asked for, + # it will be nothing + set {_value} to the player value of {_node} + # '0.1' can't be converted to a player + {_value} does not exist + + +example: # Looping nodes + + # A config can be treated as a node + set {_node} to the skript config + # The nodes inside a section (or a config) + loop the nodes of {_node}: + # Nodes have a name + broadcast the name of loop-value + + +### +MORE CONFIGS +===== + +Scripts themselves are stored as configs, where each line is a node. +This means that you are able to read the content of a script. + +The examples in this section will use a 'my script.sk' which has the following code: + +on first join: + give apple to the player + +on damage: + if the attacker exists: + teleport the attacker to the victim +### + + +example: # Reading a script + + # A script is also a config + set {_script} to the script named "my script.sk" + + # The code in each line is the node name + set {_node} to the node "on first join" in {_script} + + # This event is a section, so we can loop it + loop the nodes of {_node}: + # There is only one line (node) in the section + the name of loop-value is "give apple to the player" + + # You can traverse the node tree like this + set {_node} to the node "on damage" in {_script} + set {_node} to the node "if the attacker exists" in {_node} + # The first line inside that section + set {_node} to the first element of nodes of {_node} + + the name of {_node} is "teleport the attacker to the victim" + + + +### +FUNCTIONS +===== + +The final part of script reflection is the ability to find and run a function. +### + +example: # Finding a function + + # You can find a function by name + set {_my function} to the function "my_function" + set {_my function} to the function "my_function()" + + # You can also find a function from a particular script + set {_my function} to the function "my_function" from {_my script} + + # You can even get multiple functions at once + set {_functions::*} to all functions from the current script + + +example: # Running a function + + set {_my function} to the function "my_function()" + # You can now run this function + run {_my function} + + # You can also provide the arguments for running this function + run {_my function} with arguments "hello" and 7.5 + + +example: # The result of a function + + # Some functions return a result + set {_my function} to the function "my_function()" + # Asking for the result runs the function + set {_result} to the result of {_my function} + + # You can also ask for the result with arguments + set {_result} to the result of {_my function} with arguments "hello" and 7.5 + + +# An example command to run a function by name +command /func : + permission: skript.example.reflection + trigger: + set {_function} to the function arg-text + run {_function} diff --git a/src/main/resources/scripts/-examples/functions.sk b/src/main/resources/scripts/-examples/functions.sk index bb743a075dd..723f6d79fb0 100644 --- a/src/main/resources/scripts/-examples/functions.sk +++ b/src/main/resources/scripts/-examples/functions.sk @@ -4,52 +4,52 @@ # This demonstrates how to declare and run a simple function. # -function sayMessage(message: text): - broadcast {_message} # our message argument is available in `{_message}`. +function say_message(message: text): + broadcast {_message} # our message argument is available in `{_message}`. on first join: - wait 1 second - sayMessage("Welcome, %player%!") # Runs the `sayMessage` function. + wait 1 second + say_message("Welcome, %player%!") # Runs the `say_message` function. # # An example of a function with multiple parameters and a return type. # This demonstrates how to return a value and use it. # -function giveApple(name: text, amount: number) :: item: - set {_item} to an apple - set the name of {_item} to {_name} - set the item amount of {_item} to {_amount} - return {_item} # Gives this value to the code that called the function. +function give_apple(name: text, amount: number) :: item: + set {_item} to an apple + set the name of {_item} to {_name} + set the item amount of {_item} to {_amount} + return {_item} # Gives this value to the code that called the function. command /appleexample: - permission: skript.example.apple - trigger: - send "Giving you an apple!" - set {_item} to giveApple("Banana", 4) - give player {_item} + permission: skript.example.apple + trigger: + send "Giving you an apple!" + set {_item} to give_apple("Banana", 4) + give player {_item} # # An example of a recursive (self-calling) function that is used to repeat a complex task. # Please note that self-calling functions can loop infinitely, so use with caution. # -function destroyOre(source: block) :: blocks: - add {_source} to {_found::*} - break {_source} naturally using an iron pickaxe - loop blocks in radius 1 of {_source}: - loop-block is any ore - break loop-block naturally using an iron pickaxe - if {_found::*} does not contain loop-block: - add destroyOre(loop-block) to {_found::*} - return {_found::*} +function destroy_gold(source: block) :: blocks: + add {_source} to {_found::*} + break {_source} naturally using an iron pickaxe + loop blocks in radius 1 of {_source}: + loop-block is a gold ore block + break loop-block naturally using an iron pickaxe + if {_found::*} does not contain loop-block: + add destroy_gold(loop-block) to {_found::*} + return {_found::*} command /oreexample: - permission: skript.example.ore - trigger: - if player's target block is any ore: - send "Destroying all connected ore." - set {_found::*} to destroyOre(player's target block) - send "Destroyed %size of {_found::*}% connected ores!" - else: - send "You must be looking at an ore block!" + permission: skript.example.ore + trigger: + if the player's target block is a gold ore block: + send "Destroying all connected ore." + set {_found::*} to destroy_gold(player's target block) + send "Destroyed %size of {_found::*}% connected ores!" + else: + send "You must be looking at an ore block!" diff --git a/src/main/resources/scripts/-examples/loops.sk b/src/main/resources/scripts/-examples/loops.sk index 73765868c2c..5448907f815 100644 --- a/src/main/resources/scripts/-examples/loops.sk +++ b/src/main/resources/scripts/-examples/loops.sk @@ -5,15 +5,15 @@ # command /loopexample: - permission: skript.example.loop - trigger: - set {_number} to 5 - loop {_number} times: # Runs `{_number}` times. - send "The number is %loop-number%." + permission: skript.example.loop + trigger: + set {_number} to 5 + loop {_number} times: # Runs `{_number}` times. + send "The number is %loop-number%." - set {_list::*} to "apple", "banana" and "orange" - loop {_list::*}: # Runs for each value in the list. - send "The word is: %loop-value%" + set {_list::*} to "apple", "banana" and "orange" + loop {_list::*}: # Runs for each value in the list. + send "The word is: %loop-value%" # # Examples for while-loops, which run as long as the condition is true. @@ -21,31 +21,31 @@ command /loopexample: # command /whileexample: - permission: skript.example.while - trigger: - set {_number} to 5 - while {_number} is greater than 0: - send "The number is %{_number}%" - remove a random number between 0 and 2 from {_number} - send "Finished counting down." + permission: skript.example.while + trigger: + set {_number} to 5 + while {_number} is greater than 0: + send "The number is %{_number}%" + remove a random number between 0 and 2 from {_number} + send "Finished counting down." - while true is true: # this will run forever - add "banana" to {_list::*} - if size of {_list::*} is 10: - exit loop - send "The list has %size of {_list::*}% bananas." + while true is true: # this will run forever + add "banana" to {_list::*} + if size of {_list::*} is 10: + exit loop + send "The list has %size of {_list::*}% bananas." command /dowhileexample: - permission: skript.example.dowhile - trigger: - set {_number} to a random integer between 0 and 6 # The player will get 1 to 3 apples. - do while {_number} is greater than 3: # This will always run at least once, even if `{_number} is less than or equal to 3`. - give the player an apple - remove 1 from {_number} - send "Finished giving out apples!" + permission: skript.example.dowhile + trigger: + set {_number} to a random integer between 0 and 6 # The player will get 1 to 3 apples. + do while {_number} is greater than 3: # This will always run at least once, even if `{_number} is less than or equal to 3`. + give the player an apple + remove 1 from {_number} + send "Finished giving out apples!" - do while true is false: # This will run once - the condition is checked AFTER the code is executed. - send "I will run only once!" + do while true is false: # This will run once - the condition is checked AFTER the code is executed. + send "I will run only once!" # # Examples for looping collections of specific types, such as players, blocks and items. @@ -53,23 +53,22 @@ command /dowhileexample: # command /anotherloopexample: - permission: skript.example.loop - trigger: - send "Listing all players:" - loop all players: # Remember - player is the command sender, loop-player is the loop value. - send " - %loop-player%" - if loop-player has permission "skript.example.apple": - give loop-player an apple named "Potato" + permission: skript.example.loop + trigger: + send "Listing all players:" + loop all players: # Remember - player is the command sender, loop-player is the loop value. + send " - %loop-player%" + if loop-player has permission "skript.example.apple": + give loop-player an apple named "Potato" - set {_items::*} to stone, oak planks and an apple - loop {_items::*}: - send "%loop-index%. %loop-value%" - give loop-value to player + set {_items::*} to stone, oak planks and an apple + loop {_items::*}: + send "%loop-index%. %loop-value%" + give loop-value to player - loop blocks in radius 2 of player: - loop-block is a chest - loop items of types ore and log: # Loop-block comes from the first loop, loop-item from the second. - inventory of loop-block contains loop-item - remove loop-item from the inventory of loop-block - send "Destroyed a %loop-item%!" - exit loop # Exits the item loop. + loop blocks in radius 2 of player: + loop-block is a chest + loop items in loop-block: # Loop-block comes from the first loop, loop-item from the second. + loop-item is a dirt block # Matches any dirt block item + send "%loop-block% contains %loop-item%!" + exit loop # Exits the item loop. diff --git a/src/main/resources/scripts/-examples/options and meta.sk b/src/main/resources/scripts/-examples/options and meta.sk index 8d0e1c35955..912d7d83729 100644 --- a/src/main/resources/scripts/-examples/options and meta.sk +++ b/src/main/resources/scripts/-examples/options and meta.sk @@ -4,15 +4,15 @@ # options: - my server name: Server Name - condition: player is alive - nice message: "You're alive!" + my server name: Server Name + condition: player is alive + nice message: "You're alive!" on join: - send "Welcome to {@my server name}" - # Options don't need `%...%` since they are raw inputs. - if {@condition}: # The raw `player is alive` is copied here during parsing. - send {@nice message} + send "Welcome to {@my server name}" + # Options don't need `%...%` since they are raw inputs. + if {@condition}: # The raw `player is alive` is copied here during parsing. + send {@nice message} # # An example of custom aliases for groups of items. @@ -20,11 +20,11 @@ on join: # aliases: - pretty items = iron ingot, gold ingot, diamond + pretty items = iron ingot, gold ingot, diamond on join: - player has permission "skript.example.aliases" - give player random item out of pretty items # A random item from our alias. + player has permission "skript.example.aliases" + give player random item out of pretty items # A random item from our alias. # # An example showing how default variables can be used. @@ -33,12 +33,12 @@ on join: # variables: - score::%player% = 100 - some variable = "Hello" + {score::%player%} = 100 + {some variable} = "Hello" command /variabletest: - permission: skript.example.variables - trigger: - add 1 to {score::%player%} - send "Your score is now %{score::%player%}%." - send {some variable} + permission: skript.example.variables + trigger: + add 1 to {score::%player%} + send "Your score is now %{score::%player%}%." + send {some variable} diff --git a/src/main/resources/scripts/-examples/text formatting.sk b/src/main/resources/scripts/-examples/text formatting.sk index 5163ca116c3..d3771ccc5f4 100644 --- a/src/main/resources/scripts/-examples/text formatting.sk +++ b/src/main/resources/scripts/-examples/text formatting.sk @@ -8,11 +8,11 @@ # command /color: - permission: skript.example.color - trigger: - send "&6This message is golden." - send "This message is light red and bold." - send "<#FF0000>This message is red." + permission: skript.example.color + trigger: + send "&6This message is golden." + send "This message is light red and bold." + send "<#FF0000>This message is red." # # Other formatting options are also available. @@ -20,19 +20,19 @@ command /color: # command /forum: - permission: skript.example.link - trigger: - send "To visit the website, [click here]" + permission: skript.example.link + trigger: + send "To visit the website, [click here]" command /copy : - permission: skript.example.copy - trigger: - # Insertion: when the player shift clicks on the message, it will add the text to their text box. - # To use variables and other expressions with these tags, you have to send the text as `formatted`. - send formatted "%arg-1%" + permission: skript.example.copy + trigger: + # Insertion: when the player shift clicks on the message, it will add the text to their text box. + # To use variables and other expressions with these tags, you have to send the text as `formatted`. + send formatted "%arg-1%" command /suggest: - permission: skript.example.suggest - trigger: - send "Click here to run the command /say hi" - send "Click here to suggest the command /say hi" + permission: skript.example.suggest + trigger: + send "Click here to run the command /say hi" + send "Click here to suggest the command /say hi" diff --git a/src/main/resources/scripts/-examples/timings.sk b/src/main/resources/scripts/-examples/timings.sk index 18073095e5e..b99e4b70967 100644 --- a/src/main/resources/scripts/-examples/timings.sk +++ b/src/main/resources/scripts/-examples/timings.sk @@ -4,7 +4,7 @@ # at 18:00: - set the time to 7:00 + set the time to 7:00 # # This example schedules a repeating action. Each time the delay elapses, the trigger will be run. @@ -12,25 +12,25 @@ at 18:00: # every 5 minutes: - broadcast "Did you know that five minutes have passed?" - loop all players: - if loop-player has permission "skript.example.apple": - give loop-player an apple + broadcast "Did you know that five minutes have passed?" + loop all players: + if loop-player has permission "skript.example.apple": + give loop-player an apple # # This example shows how the `wait` effect can be used to delay code being run. # command /waitexample: - permission: skript.example.wait - trigger: - send "Waiting for two seconds..." - wait 2 seconds - send "Finished waiting!" + permission: skript.example.wait + trigger: + send "Waiting for two seconds..." + wait 2 seconds + send "Finished waiting!" - send "Counting to five." - set {_count} to 0 - while {_count} is less than 5: - wait 3 ticks # Minecraft game ticks: 20 ticks = 1 second. - add 0.5 to {_count} - send "Finished counting!" + send "Counting to five." + set {_count} to 0 + while {_count} is less than 5: + wait 3 ticks # Minecraft game ticks: 20 ticks = 1 second. + add 0.5 to {_count} + send "Finished counting!" diff --git a/src/main/resources/scripts/-examples/variables.sk b/src/main/resources/scripts/-examples/variables.sk index a39061e1230..1cd6aa4ccce 100644 --- a/src/main/resources/scripts/-examples/variables.sk +++ b/src/main/resources/scripts/-examples/variables.sk @@ -7,13 +7,13 @@ # command /timer: - permission: skript.example.timer - trigger: - if {timer} is set: - send "This command was last run %time since {timer}% ago." - else: - send "This command has never been run." - set {timer} to now + permission: skript.example.timer + trigger: + if {timer} is set: + send "This command was last run %time since {timer}% ago." + else: + send "This command has never been run." + set {timer} to now # # This example stores two items in a global list variable `{items::%uuid of player%::*}`. @@ -21,17 +21,17 @@ command /timer: # on join: - set {items::%uuid of player%::helmet} to player's helmet - set {items::%uuid of player%::boots} to player's boots - send "Stored your helmet and boots." + set {items::%uuid of player%::helmet} to player's helmet + set {items::%uuid of player%::boots} to player's boots + send "Stored your helmet and boots." command /outfit: - executable by: players - permission: skript.example.outfit - trigger: - give player {items::%uuid of player%::*} # gives the contents of the list - clear {items::%uuid of player%::*} # clears this list - send "Gave you the helmet and boots you joined with." + executable by: players + permission: skript.example.outfit + trigger: + give player {items::%uuid of player%::*} # gives the contents of the list + clear {items::%uuid of player%::*} # clears this list + send "Gave you the helmet and boots you joined with." # # An example of adding, looping and removing the contents of a list variable. @@ -39,15 +39,15 @@ command /outfit: # command /shoppinglist: - permission: skript.example.list - trigger: - add "bacon" to {_shopping list::*} - add "eggs" to {_shopping list::*} - add "oats" and "sugar" to {_shopping list::*} - send "You have %size of {_shopping list::*}% things in your shopping list:" - loop {_shopping list::*}: - send "%loop-index%. %loop-value%" - send "You bought some %{_shopping list::1}%!" - remove "bacon" from {_shopping list::*} - send "Removing bacon from your list." - send "You now have %size of {_shopping list::*}% things in your shopping list." + permission: skript.example.list + trigger: + add "bacon" to {_shopping list::*} + add "eggs" to {_shopping list::*} + add "oats" and "sugar" to {_shopping list::*} + send "You have %size of {_shopping list::*}% things in your shopping list:" + loop {_shopping list::*}: + send "%loop-index%. %loop-value%" + send "You bought some %{_shopping list::1}%!" + remove "bacon" from {_shopping list::*} + send "Removing bacon from your list." + send "You now have %size of {_shopping list::*}% things in your shopping list." diff --git a/src/test/java/org/skriptlang/skript/SkriptTest.java b/src/test/java/org/skriptlang/skript/SkriptTest.java new file mode 100644 index 00000000000..ef4168ced0f --- /dev/null +++ b/src/test/java/org/skriptlang/skript/SkriptTest.java @@ -0,0 +1,46 @@ +package org.skriptlang.skript; + +import ch.njol.skript.SkriptAPIException; +import org.junit.Test; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.addon.BaseSkriptAddonTests; + +import static org.junit.Assert.*; + +public class SkriptTest extends BaseSkriptAddonTests { + + @Override + public Skript addon() { + return Skript.of(source(), name()); + } + + @Override + public Class source() { + return SkriptTest.class; + } + + @Override + public String name() { + return "TestSkript"; + } + + @Test + public void testAddonRegistration() { + Skript skript = addon(); + Skript unmodifiable = skript.unmodifiableView(); + + // should have no addons by default + assertTrue(skript.addons().isEmpty()); + assertTrue(unmodifiable.addons().isEmpty()); + + SkriptAddon addon = skript.registerAddon(SkriptTest.class, "TestAddon"); + assertThrows(UnsupportedOperationException.class, () -> unmodifiable.registerAddon(SkriptTest.class, "TestAddon")); + assertThrows(SkriptAPIException.class, () -> skript.registerAddon(SkriptAddon.class, "TestAddon")); + + assertTrue(skript.addons().contains(addon)); + // unmodifiable addons list would contain an unmodifiable addon + assertEquals(1, skript.addons().size()); + assertEquals(1, unmodifiable.addons().size()); + } + +} diff --git a/src/test/java/org/skriptlang/skript/addon/BaseSkriptAddonTests.java b/src/test/java/org/skriptlang/skript/addon/BaseSkriptAddonTests.java new file mode 100644 index 00000000000..cf6dee49c7f --- /dev/null +++ b/src/test/java/org/skriptlang/skript/addon/BaseSkriptAddonTests.java @@ -0,0 +1,102 @@ +package org.skriptlang.skript.addon; + +import org.junit.Test; +import org.skriptlang.skript.registration.SyntaxRegistry; +import org.skriptlang.skript.util.Registry; + +import java.util.Collection; + +import static org.junit.Assert.*; + +public abstract class BaseSkriptAddonTests { + + private static class MockRegistry implements Registry { + + @Override + public Collection elements() { + throw new UnsupportedOperationException(); + } + + } + + public abstract SkriptAddon addon(); + + public abstract Class source(); + + public abstract String name(); + + @Test + public void testSource() { + final SkriptAddon addon = addon(); + + assertEquals(source(), addon.source()); + assertEquals(source(), addon.unmodifiableView().source()); + } + + @Test + public void testName() { + final SkriptAddon addon = addon(); + + assertEquals(name(), addon.name()); + assertEquals(name(), addon.unmodifiableView().name()); + } + + @Test + public void testRegistry() { + final SkriptAddon addon = addon(); + final SkriptAddon unmodifiable = addon.unmodifiableView(); + final MockRegistry registry = new MockRegistry(); + + // storing a registry + addon.storeRegistry(MockRegistry.class, registry); + assertThrows(UnsupportedOperationException.class, () -> unmodifiable.storeRegistry(MockRegistry.class, registry)); + + // get a registry + assertEquals(registry, addon.registry(MockRegistry.class)); + assertEquals(registry, unmodifiable.registry(MockRegistry.class)); + + // has a registry + assertTrue(addon.hasRegistry(MockRegistry.class)); + assertTrue(unmodifiable.hasRegistry(MockRegistry.class)); + + // remove a registry + addon.removeRegistry(MockRegistry.class); + assertThrows(UnsupportedOperationException.class, () -> unmodifiable.removeRegistry(MockRegistry.class)); + + // get a registry + assertThrows(NullPointerException.class, () -> addon.registry(MockRegistry.class)); + assertThrows(NullPointerException.class, () -> unmodifiable.registry(MockRegistry.class)); + + // has a registry + assertFalse(addon.hasRegistry(MockRegistry.class)); + assertFalse(unmodifiable.hasRegistry(MockRegistry.class)); + + // get a registry (alternate) + addon.registry(MockRegistry.class, () -> registry); + assertEquals(registry, addon.registry(MockRegistry.class)); + assertEquals(registry, unmodifiable.registry(MockRegistry.class)); + assertTrue(addon.hasRegistry(MockRegistry.class)); + assertTrue(unmodifiable.hasRegistry(MockRegistry.class)); + } + + @Test + public void testSyntaxRegistry() { + final SkriptAddon addon = addon(); + final SkriptAddon unmodifiable = addon.unmodifiableView(); + + assertNotNull(addon.syntaxRegistry()); + assertNotNull(unmodifiable.syntaxRegistry()); + // unmodifiable's syntax registry should be unmodifiable (different) + assertNotEquals(addon.syntaxRegistry(), unmodifiable.syntaxRegistry()); + assertEquals(addon.registry(SyntaxRegistry.class), addon.syntaxRegistry()); + } + + @Test + public void testLocalizer() { + final SkriptAddon addon = addon(); + + assertNotNull(addon.localizer()); + assertNotNull(addon.unmodifiableView().localizer()); + } + +} diff --git a/src/test/java/org/skriptlang/skript/addon/SkriptAddonTest.java b/src/test/java/org/skriptlang/skript/addon/SkriptAddonTest.java new file mode 100644 index 00000000000..ffd5407d855 --- /dev/null +++ b/src/test/java/org/skriptlang/skript/addon/SkriptAddonTest.java @@ -0,0 +1,23 @@ +package org.skriptlang.skript.addon; + +import org.skriptlang.skript.Skript; + +public class SkriptAddonTest extends BaseSkriptAddonTests { + + @Override + public SkriptAddon addon() { + return Skript.of(source(), name()) + .registerAddon(source(), name()); + } + + @Override + public Class source() { + return SkriptAddonTest.class; + } + + @Override + public String name() { + return "TestAddon"; + } + +} diff --git a/src/test/java/org/skriptlang/skript/bukkit/registration/EventSyntaxInfoTest.java b/src/test/java/org/skriptlang/skript/bukkit/registration/EventSyntaxInfoTest.java new file mode 100644 index 00000000000..b2575924ccb --- /dev/null +++ b/src/test/java/org/skriptlang/skript/bukkit/registration/EventSyntaxInfoTest.java @@ -0,0 +1,204 @@ +package org.skriptlang.skript.bukkit.registration; + +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptEvent; +import ch.njol.skript.lang.SkriptEvent.ListeningBehavior; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.coll.CollectionUtils; +import org.bukkit.event.Event; +import org.bukkit.event.block.BlockEvent; +import org.bukkit.event.entity.EntityEvent; +import org.bukkit.event.player.PlayerEvent; +import org.jetbrains.annotations.Nullable; +import org.junit.Test; +import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos.Event.Builder; +import org.skriptlang.skript.bukkit.registration.EventSyntaxInfoTest.MockSkriptEvent; +import org.skriptlang.skript.registration.BaseSyntaxInfoTests; + +import java.util.List; +import java.util.function.Supplier; + +import static org.junit.Assert.*; + +public class EventSyntaxInfoTest extends BaseSyntaxInfoTests> { + + public static final class MockSkriptEvent extends SkriptEvent { + + @Override + public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean check(Event event) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + throw new UnsupportedOperationException(); + } + + } + + @Override + public Builder builder(boolean addPattern) { + var info = BukkitSyntaxInfos.Event.builder(MockSkriptEvent.class, "mock event"); + if (addPattern) { + info.addPattern("default"); + } + return info; + } + + @Override + public Class type() { + return MockSkriptEvent.class; + } + + @Override + public Supplier supplier() { + return MockSkriptEvent::new; + } + + @Test + public void testListeningBehavior() { + for (final var behavior : ListeningBehavior.values()) { + var info = builder(true) + .listeningBehavior(behavior) + .build(); + assertEquals(behavior, info.listeningBehavior()); + assertEquals(behavior, info.toBuilder().build().listeningBehavior()); + + var info2 = builder(true); + info.toBuilder().applyTo(info2); + assertEquals(behavior, info2.build().listeningBehavior()); + } + } + + @Test + public void testSince() { + var info = builder(true) + .since("since") + .build(); + assertEquals("since", info.since()); + assertEquals("since", info.toBuilder().build().since()); + + var info2 = builder(true); + info.toBuilder().applyTo(info2); + assertEquals("since", info2.build().since()); + } + + @Test + public void testDocumentationId() { + var info = builder(true) + .documentationId("id") + .build(); + assertEquals("id", info.documentationId()); + assertEquals("id", info.toBuilder().build().documentationId()); + + var info2 = builder(true); + info.toBuilder().applyTo(info2); + assertEquals("id", info2.build().documentationId()); + } + + @Test + public void testDescription() { + var info = builder(true) + .addDescription("1") + .addDescription(new String[]{"2"}) + .addDescription(List.of("3")) + .build(); + assertArrayEquals(new String[]{"1", "2", "3"}, info.description().toArray()); + + var info2 = info.toBuilder() + .clearDescription() + .addDescription("4") + .build(); + assertArrayEquals(new String[]{"4"}, info2.description().toArray()); + + var info3 = info.toBuilder(); + info2.toBuilder().applyTo(info3); + assertArrayEquals(new String[]{"1", "2", "3", "4"}, info3.build().description().toArray()); + } + + @Test + public void testExamples() { + var info = builder(true) + .addExample("1") + .addExamples("2") + .addExamples(List.of("3")) + .build(); + assertArrayEquals(new String[]{"1", "2", "3"}, info.examples().toArray()); + + var info2 = info.toBuilder() + .clearExamples() + .addExample("4") + .build(); + assertArrayEquals(new String[]{"4"}, info2.examples().toArray()); + + var info3 = info.toBuilder(); + info2.toBuilder().applyTo(info3); + assertArrayEquals(new String[]{"1", "2", "3", "4"}, info3.build().examples().toArray()); + } + + @Test + public void testKeywords() { + var info = builder(true) + .addKeyword("1") + .addKeywords("2") + .addKeywords(List.of("3")) + .build(); + assertArrayEquals(new String[]{"1", "2", "3"}, info.keywords().toArray()); + + var info2 = info.toBuilder() + .clearKeywords() + .addKeyword("4") + .build(); + assertArrayEquals(new String[]{"4"}, info2.keywords().toArray()); + + var info3 = info.toBuilder(); + info2.toBuilder().applyTo(info3); + assertArrayEquals(new String[]{"1", "2", "3", "4"}, info3.build().keywords().toArray()); + } + + @Test + public void testRequiredPlugins() { + var info = builder(true) + .addRequiredPlugin("1") + .addRequiredPlugins("2") + .addRequiredPlugins(List.of("3")) + .build(); + assertArrayEquals(new String[]{"1", "2", "3"}, info.requiredPlugins().toArray()); + + var info2 = info.toBuilder() + .clearRequiredPlugins() + .addRequiredPlugin("4") + .build(); + assertArrayEquals(new String[]{"4"}, info2.requiredPlugins().toArray()); + + var info3 = info.toBuilder(); + info2.toBuilder().applyTo(info3); + assertArrayEquals(new String[]{"1", "2", "3", "4"}, info3.build().requiredPlugins().toArray()); + } + + @Test + public void testEvents() { + var info = builder(true) + .addEvent(Event.class) + .addEvents(CollectionUtils.array(PlayerEvent.class)) + .addEvents(List.of(EntityEvent.class)) + .build(); + assertArrayEquals(new Class[]{Event.class, PlayerEvent.class, EntityEvent.class}, info.events().toArray()); + + var info2 = info.toBuilder() + .clearEvents() + .addEvent(BlockEvent.class) + .build(); + assertArrayEquals(new Class[]{BlockEvent.class}, info2.events().toArray()); + + var info3 = info.toBuilder(); + info2.toBuilder().applyTo(info3); + assertArrayEquals(new Class[]{Event.class, PlayerEvent.class, EntityEvent.class, BlockEvent.class}, info3.build().events().toArray()); + } + +} diff --git a/src/test/java/org/skriptlang/skript/registration/BaseSyntaxInfoTests.java b/src/test/java/org/skriptlang/skript/registration/BaseSyntaxInfoTests.java new file mode 100644 index 00000000000..dfbcd77573d --- /dev/null +++ b/src/test/java/org/skriptlang/skript/registration/BaseSyntaxInfoTests.java @@ -0,0 +1,102 @@ +package org.skriptlang.skript.registration; + +import ch.njol.skript.lang.SyntaxElement; +import org.junit.Test; +import org.skriptlang.skript.util.Priority; + +import java.util.List; +import java.util.function.Supplier; + +import static org.junit.Assert.*; + +/** + * Contains base tests for SyntaxInfos. + */ +public abstract class BaseSyntaxInfoTests> { + + public abstract T builder(boolean addPattern); + + public abstract Class type(); + + public abstract Supplier supplier(); + + @Test + public void testOrigin() { + var info = builder(true) + .origin(() -> "Hello World!") + .build(); + assertEquals("Hello World!", info.origin().name()); + assertEquals("Hello World!", info.toBuilder().build().origin().name()); + + var info2 = builder(true); + info.toBuilder().applyTo(info2); + assertEquals("Hello World!", info2.build().origin().name()); + } + + @Test + public void testType() { + var info = builder(true) + .build(); + assertEquals(type(), info.type()); + assertEquals(type(), info.toBuilder().build().type()); + } + + @Test + public void testInstance() { + var info = builder(true) + .build(); + assertNotNull(info.instance()); + + info = builder(true) + .supplier(supplier()) + .build(); + assertNotNull(info.instance()); + + info = builder(true) + .supplier(() -> { + throw new UnsupportedOperationException(); + }) + .build(); + assertThrows(UnsupportedOperationException.class, info::instance); + + var info2 = builder(true); + info.toBuilder().applyTo(info2); + assertThrows(UnsupportedOperationException.class, () -> info2.build().instance()); + } + + @Test + public void testPatterns() { + var info = builder(false) + .addPattern("1") + .addPatterns("2") + .addPatterns(List.of("3")) + .build(); + assertArrayEquals(new String[]{"1", "2", "3"}, info.patterns().toArray()); + + var info2 = info.toBuilder() + .clearPatterns() + .addPattern("4") + .build(); + assertArrayEquals(new String[]{"4"}, info2.patterns().toArray()); + + var info3 = info.toBuilder(); + info2.toBuilder().applyTo(info3); + assertArrayEquals(new String[]{"1", "2", "3", "4"}, info3.build().patterns().toArray()); + } + + @Test + public void testPriority() { + final Priority base = Priority.base(); + + var info = builder(true) + .priority(base) + .build(); + assertEquals(base, info.priority()); + assertEquals(base, info.toBuilder().build().priority()); + + var info2 = builder(true); + info.toBuilder().applyTo(info2); + assertEquals(base, info2.build().priority()); + } + +} diff --git a/src/test/java/org/skriptlang/skript/registration/ExpressionSyntaxInfoTest.java b/src/test/java/org/skriptlang/skript/registration/ExpressionSyntaxInfoTest.java new file mode 100644 index 00000000000..0437fe7df9b --- /dev/null +++ b/src/test/java/org/skriptlang/skript/registration/ExpressionSyntaxInfoTest.java @@ -0,0 +1,152 @@ +package org.skriptlang.skript.registration; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.junit.Test; +import org.skriptlang.skript.registration.ExpressionSyntaxInfoTest.MockExpression; + +import java.util.Iterator; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import static org.junit.Assert.*; + +public class ExpressionSyntaxInfoTest extends BaseSyntaxInfoTests> { + + public static final class MockExpression implements Expression { + + @Override + public String getSingle(Event event) { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getArray(Event event) { + throw new UnsupportedOperationException(); + } + + @Override + public String[] getAll(Event event) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSingle() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean check(Event event, Predicate checker, boolean negated) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean check(Event event, Predicate checker) { + throw new UnsupportedOperationException(); + } + + @Override + @SafeVarargs + public final Expression getConvertedExpression(Class... to) { + throw new UnsupportedOperationException(); + } + + @Override + public Class getReturnType() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean getAnd() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean setTime(int time) { + throw new UnsupportedOperationException(); + } + + @Override + public int getTime() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDefault() { + throw new UnsupportedOperationException(); + } + + @Override + public Expression getSource() { + throw new UnsupportedOperationException(); + } + + @Override + public Expression simplify() { + throw new UnsupportedOperationException(); + } + + @Override + public Class[] acceptChange(ChangeMode mode) { + throw new UnsupportedOperationException(); + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + throw new UnsupportedOperationException(); + } + + @Override + public Iterator iterator(Event event) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isLoopOf(String input) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + throw new UnsupportedOperationException(); + } + + } + + @Override + public SyntaxInfo.Expression.Builder builder(boolean addPattern) { + var info = SyntaxInfo.Expression.builder(MockExpression.class, String.class); + if (addPattern) { + info.addPattern("default"); + } + return info; + } + + @Override + public Class type() { + return MockExpression.class; + } + + @Override + public Supplier supplier() { + return MockExpression::new; + } + + @Test + public void testReturnType() { + var info = builder(true) + .build(); + assertEquals(String.class, info.returnType()); + assertEquals(String.class, info.toBuilder().build().returnType()); + } + +} diff --git a/src/test/java/org/skriptlang/skript/registration/KeyTest.java b/src/test/java/org/skriptlang/skript/registration/KeyTest.java new file mode 100644 index 00000000000..702ddbf79e1 --- /dev/null +++ b/src/test/java/org/skriptlang/skript/registration/KeyTest.java @@ -0,0 +1,25 @@ +package org.skriptlang.skript.registration; + +import org.junit.Test; +import org.skriptlang.skript.registration.SyntaxRegistry.ChildKey; +import org.skriptlang.skript.registration.SyntaxRegistry.Key; + +import static org.junit.Assert.*; + +public class KeyTest { + + @Test + public void testKey() { + assertEquals("TestKey", Key.of("TestKey").name()); + } + + @Test + public void testChildKey() { + final Key key = Key.of("TestKey"); + final ChildKey child = ChildKey.of(key, "TestChildKey"); + + assertEquals(key, child.parent()); + assertEquals("TestChildKey", child.name()); + } + +} diff --git a/src/test/java/org/skriptlang/skript/registration/StructureSyntaxInfoTest.java b/src/test/java/org/skriptlang/skript/registration/StructureSyntaxInfoTest.java new file mode 100644 index 00000000000..4dacb2cd860 --- /dev/null +++ b/src/test/java/org/skriptlang/skript/registration/StructureSyntaxInfoTest.java @@ -0,0 +1,87 @@ +package org.skriptlang.skript.registration; + +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.junit.Test; +import org.skriptlang.skript.lang.entry.EntryContainer; +import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.lang.structure.Structure; +import org.skriptlang.skript.registration.StructureSyntaxInfoTest.MockStructure; + +import java.util.function.Supplier; + +import static org.junit.Assert.*; + +public class StructureSyntaxInfoTest extends BaseSyntaxInfoTests> { + + public static final class MockStructure extends Structure { + + @Override + public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult, @Nullable EntryContainer entryContainer) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean load() { + throw new UnsupportedOperationException(); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + throw new UnsupportedOperationException(); + } + + } + + @Override + public SyntaxInfo.Structure.Builder builder(boolean addPattern) { + var info = SyntaxInfo.Structure.builder(MockStructure.class); + if (addPattern) { + info.addPattern("default"); + } + return info; + } + + @Override + public Class type() { + return MockStructure.class; + } + + @Override + public Supplier supplier() { + return MockStructure::new; + } + + @Test + public void testEntryValidator() { + final EntryValidator validator = EntryValidator.builder().build(); + + var info = builder(true) + .entryValidator(validator) + .build(); + assertEquals(validator, info.entryValidator()); + assertEquals(validator, info.toBuilder().build().entryValidator()); + + var info2 = builder(true); + info.toBuilder().applyTo(info2); + assertEquals(validator, info2.build().entryValidator()); + } + + @Test + public void testNodeType() { + for (final var type : SyntaxInfo.Structure.NodeType.values()) { + var info = builder(true) + .nodeType(type) + .build(); + assertEquals(type, info.nodeType()); + assertEquals(type, info.toBuilder().build().nodeType()); + + var info2 = builder(true); + info.toBuilder().applyTo(info2); + assertEquals(type, info2.build().nodeType()); + } + } + +} diff --git a/src/test/java/org/skriptlang/skript/registration/SyntaxInfoTest.java b/src/test/java/org/skriptlang/skript/registration/SyntaxInfoTest.java new file mode 100644 index 00000000000..bf71e3df7f4 --- /dev/null +++ b/src/test/java/org/skriptlang/skript/registration/SyntaxInfoTest.java @@ -0,0 +1,48 @@ +package org.skriptlang.skript.registration; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxElement; +import ch.njol.util.Kleenean; +import org.jetbrains.annotations.NotNull; +import org.skriptlang.skript.registration.SyntaxInfo.Builder; +import org.skriptlang.skript.registration.SyntaxInfoTest.MockSyntaxElement; + +import java.util.function.Supplier; + +public class SyntaxInfoTest extends BaseSyntaxInfoTests> { + + public static final class MockSyntaxElement implements SyntaxElement { + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + throw new UnsupportedOperationException(); + } + + @Override + public @NotNull String getSyntaxTypeName() { + throw new UnsupportedOperationException(); + } + + } + + @Override + public SyntaxInfo.Builder builder(boolean addPattern) { + var info = SyntaxInfo.builder(MockSyntaxElement.class); + if (addPattern) { // sometimes required as infos must have at least one pattern + info.addPattern("default"); + } + return info; + } + + @Override + public Class type() { + return MockSyntaxElement.class; + } + + @Override + public Supplier supplier() { + return MockSyntaxElement::new; + } + +} diff --git a/src/test/java/org/skriptlang/skript/registration/SyntaxRegistryTest.java b/src/test/java/org/skriptlang/skript/registration/SyntaxRegistryTest.java new file mode 100644 index 00000000000..298a96121fd --- /dev/null +++ b/src/test/java/org/skriptlang/skript/registration/SyntaxRegistryTest.java @@ -0,0 +1,93 @@ +package org.skriptlang.skript.registration; + +import ch.njol.skript.lang.SyntaxElement; +import org.junit.Test; +import org.skriptlang.skript.registration.SyntaxRegistry.Key; +import org.skriptlang.skript.util.Priority; + +import static org.junit.Assert.*; + +public class SyntaxRegistryTest { + + private static SyntaxRegistry syntaxRegistry() { + return SyntaxRegistry.empty(); + } + + private static SyntaxInfo info() { + return SyntaxInfo.builder(SyntaxElement.class) + .supplier(() -> { + throw new UnsupportedOperationException(); + }) + .addPattern("default") + .build(); + } + + private static Key> key() { + return key("TestKey"); + } + + private static Key> key(String name) { + return Key.of(name); + } + + @Test + public void testBasic() { + final SyntaxRegistry registry = syntaxRegistry(); + final SyntaxRegistry unmodifiable = registry.unmodifiableView(); + final var info = info(); + + // test registration + registry.register(key(), info); + assertThrows(UnsupportedOperationException.class, () -> unmodifiable.register(key(), info)); + assertArrayEquals(new SyntaxInfo[]{info}, registry.syntaxes(key()).toArray()); + assertArrayEquals(new SyntaxInfo[]{info}, unmodifiable.syntaxes(key()).toArray()); + assertArrayEquals(new SyntaxInfo[]{info}, registry.elements().toArray()); + assertArrayEquals(new SyntaxInfo[]{info}, unmodifiable.elements().toArray()); + + // test unregistration + registry.unregister(key(), info); + assertThrows(UnsupportedOperationException.class, () -> unmodifiable.unregister(key(), info)); + assertTrue(registry.syntaxes(key()).isEmpty()); + assertTrue(unmodifiable.syntaxes(key()).isEmpty()); + assertTrue(registry.elements().isEmpty()); + assertTrue(unmodifiable.elements().isEmpty()); + } + + @Test + public void testKeylessUnregistration() { + final SyntaxRegistry registry = syntaxRegistry(); + final SyntaxRegistry unmodifiable = registry.unmodifiableView(); + final var info = info(); + + registry.register(key(), info); + registry.register(key("OtherKey"), info); + // should not contain duplicates + assertArrayEquals(new SyntaxInfo[]{info}, registry.elements().toArray()); + + registry.unregister(info); + assertThrows(UnsupportedOperationException.class, () -> unmodifiable.unregister(info)); + assertTrue(registry.elements().isEmpty()); + assertTrue(unmodifiable.elements().isEmpty()); + } + + @Test + public void testOrdering() { + final SyntaxRegistry registry = syntaxRegistry(); + final SyntaxRegistry unmodifiable = registry.unmodifiableView(); + final Priority priority = Priority.base(); + + // ordering should be info2, info1, info3 + final var info1 = info().toBuilder().priority(priority).build(); + final var info2 = info().toBuilder().priority(Priority.before(priority)).build(); + final var info3 = info().toBuilder().priority(Priority.after(priority)).build(); + + // test multiple registrations (differing order) + registry.register(key(), info3); + registry.register(key(), info2); + registry.register(key(), info1); + + assertArrayEquals(new SyntaxInfo[]{info2, info1, info3}, registry.syntaxes(key()).toArray()); + assertArrayEquals(new SyntaxInfo[]{info2, info1, info3}, unmodifiable.syntaxes(key()).toArray()); + } + +} diff --git a/src/test/java/org/skriptlang/skript/test/junit/registration/ExprJUnitTest.java b/src/test/java/org/skriptlang/skript/test/junit/registration/ExprJUnitTestName.java similarity index 86% rename from src/test/java/org/skriptlang/skript/test/junit/registration/ExprJUnitTest.java rename to src/test/java/org/skriptlang/skript/test/junit/registration/ExprJUnitTestName.java index e75f348b410..5635e417d28 100644 --- a/src/test/java/org/skriptlang/skript/test/junit/registration/ExprJUnitTest.java +++ b/src/test/java/org/skriptlang/skript/test/junit/registration/ExprJUnitTestName.java @@ -19,11 +19,11 @@ @Name("JUnit Test Name") @Description("Returns the currently running JUnit test name otherwise nothing.") @NoDoc -public class ExprJUnitTest extends SimpleExpression { +public class ExprJUnitTestName extends SimpleExpression { static { if (TestMode.JUNIT) - Skript.registerExpression(ExprJUnitTest.class, String.class, ExpressionType.SIMPLE, "[the] [current[[ly] running]] junit test [name]"); + Skript.registerExpression(ExprJUnitTestName.class, String.class, ExpressionType.SIMPLE, "[the] [current[[ly] running]] junit test [name]"); } @Override diff --git a/src/test/java/org/skriptlang/skript/test/tests/files/BackupPurgeTest.java b/src/test/java/org/skriptlang/skript/test/tests/files/BackupPurgeTest.java index 5e87023e8b4..a75ed1e760f 100644 --- a/src/test/java/org/skriptlang/skript/test/tests/files/BackupPurgeTest.java +++ b/src/test/java/org/skriptlang/skript/test/tests/files/BackupPurgeTest.java @@ -1,51 +1,77 @@ package org.skriptlang.skript.test.tests.files; import ch.njol.skript.util.FileUtils; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; +import org.junit.After; +import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertThrows; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import static org.junit.Assert.*; public class BackupPurgeTest { - @Test - public void testPurge() throws IOException { - File dir = new File("plugins/Skript/backups/"); - if (!dir.exists()) { - dir.mkdir(); - } + private static final Path FOLDER = Path.of("plugins", "Skript", "backups"); + private static final Path VARIABLES = Path.of("plugins", "Skript", "variables.csv"); - File vars = new File("plugins/Skript/variables.csv"); - if (!vars.exists()) { - vars.createNewFile(); + @Before + public void setup() throws IOException { + if (Files.exists(FOLDER)) { + clearFolder(); + } else { + Files.createDirectory(FOLDER); } - // Create Filler Files to be used for testing for (int i = 0; i < 100; i++) { - (new File(dir, ("PurgeTest_"+i))).createNewFile(); + Files.createFile(FOLDER.resolve("purge test " + i)); + } + + if (!Files.exists(VARIABLES)) { + Files.createFile(VARIABLES); + } + } + + @Test + public void testPurge() throws IOException { + try (Stream files = Files.list(FOLDER)) { + assertEquals(100, files.count()); } - // Test 100 filler files were created - assertEquals("Filler Files != 100", 100, (new ArrayList(Arrays.asList(dir.listFiles()))).size()); - // Test 'backupPurge' method deleting to 50 - FileUtils.backupPurge(vars, 50); - assertEquals("Backup Purge did not delete down to 50", 50, (new ArrayList(Arrays.asList(dir.listFiles()))).size()); + testBackupPurge(50); + testBackupPurge(20); + testBackupPurge(0); + + assertThrows(IllegalArgumentException.class, () -> FileUtils.backupPurge(VARIABLES.toFile(), -1)); + } + + @After + public void cleanUp() throws IOException { + clearFolder(); + } - // Test 'backupPurge' method deleting to 20 - FileUtils.backupPurge(vars, 20); - assertEquals("Backup Purge did not delete down to 20", 20, (new ArrayList(Arrays.asList(dir.listFiles()))).size()); + private static void clearFolder() throws IOException { + try (Stream list = Files.list(FOLDER)) { + list.forEach(path -> { + try { + Files.delete(path); + } catch (IOException ignored) { - // Test 'backupPurge' method deleting all files - FileUtils.backupPurge(vars, 0); - assertEquals("Backup Purge did not delete all files", 0, (new ArrayList(Arrays.asList(dir.listFiles()))).size()); + } + }); + } + } - // Test calling with invalid input - assertThrows("Backup Purge did not throw exception for invalid input", IllegalArgumentException.class, () -> FileUtils.backupPurge(vars, -1)); + private static void testBackupPurge(int toKeep) throws IOException { + FileUtils.backupPurge(VARIABLES.toFile(), toKeep); + try (Stream files = Files.list(FOLDER)) { + long count = files.count(); + assertNotNull(files); + assertEquals("backup purge did not delete all files", toKeep, count); + } } } diff --git a/src/test/java/org/skriptlang/skript/test/tests/files/FilesGenerate.java b/src/test/java/org/skriptlang/skript/test/tests/files/FilesGenerateTest.java similarity index 96% rename from src/test/java/org/skriptlang/skript/test/tests/files/FilesGenerate.java rename to src/test/java/org/skriptlang/skript/test/tests/files/FilesGenerateTest.java index 2aa52124105..da69b9a9fcf 100644 --- a/src/test/java/org/skriptlang/skript/test/tests/files/FilesGenerate.java +++ b/src/test/java/org/skriptlang/skript/test/tests/files/FilesGenerateTest.java @@ -13,7 +13,7 @@ /** * Ensures that the default files from Skript are generated. */ -public class FilesGenerate { +public class FilesGenerateTest { @Test public void checkFiles() { diff --git a/src/test/java/org/skriptlang/skript/test/tests/localization/UtilsPlurals.java b/src/test/java/org/skriptlang/skript/test/tests/localization/UtilsPluralsTest.java similarity index 96% rename from src/test/java/org/skriptlang/skript/test/tests/localization/UtilsPlurals.java rename to src/test/java/org/skriptlang/skript/test/tests/localization/UtilsPluralsTest.java index 37dc090984a..ea705c08805 100644 --- a/src/test/java/org/skriptlang/skript/test/tests/localization/UtilsPlurals.java +++ b/src/test/java/org/skriptlang/skript/test/tests/localization/UtilsPluralsTest.java @@ -6,7 +6,7 @@ import ch.njol.skript.util.Utils; -public class UtilsPlurals { +public class UtilsPluralsTest { /** * Testing method {@link Utils#getEnglishPlural(String)} diff --git a/src/test/java/org/skriptlang/skript/test/tests/regression/BlockDataNotCloned6829.java b/src/test/java/org/skriptlang/skript/test/tests/regression/BlockDataNotCloned6829Test.java similarity index 94% rename from src/test/java/org/skriptlang/skript/test/tests/regression/BlockDataNotCloned6829.java rename to src/test/java/org/skriptlang/skript/test/tests/regression/BlockDataNotCloned6829Test.java index 088cc3fdd78..86bbebab5ac 100644 --- a/src/test/java/org/skriptlang/skript/test/tests/regression/BlockDataNotCloned6829.java +++ b/src/test/java/org/skriptlang/skript/test/tests/regression/BlockDataNotCloned6829Test.java @@ -12,7 +12,7 @@ import java.util.Objects; -public class BlockDataNotCloned6829 extends SkriptJUnitTest { +public class BlockDataNotCloned6829Test extends SkriptJUnitTest { public void run(String unparsedEffect, Event event) { Effect effect = Effect.parse(unparsedEffect, "Can't understand this effect: " + unparsedEffect); diff --git a/src/test/java/org/skriptlang/skript/test/tests/regression/EffSendEffConnectConflict7517Test.java b/src/test/java/org/skriptlang/skript/test/tests/regression/EffSendEffConnectConflict7517Test.java new file mode 100644 index 00000000000..e255a3eb32c --- /dev/null +++ b/src/test/java/org/skriptlang/skript/test/tests/regression/EffSendEffConnectConflict7517Test.java @@ -0,0 +1,46 @@ +package org.skriptlang.skript.test.tests.regression; + +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.TriggerItem; +import ch.njol.skript.lang.util.ContextlessEvent; +import ch.njol.skript.test.runner.SkriptJUnitTest; +import ch.njol.skript.variables.Variables; +import org.bukkit.command.CommandSender; +import org.bukkit.event.Event; +import org.easymock.Capture; +import org.easymock.EasyMock; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class EffSendEffConnectConflict7517Test extends SkriptJUnitTest { + + private static final String MESSAGE = "Hello, world!"; + + private CommandSender sender; + private Effect sendEffect; + + @Before + public void setup() { + sender = EasyMock.niceMock(CommandSender.class); + sendEffect = Effect.parse("send {_message} to {_sender}", null); + if (sendEffect == null) + throw new IllegalStateException(); + } + + @Test + public void test() { + Event event = ContextlessEvent.get(); + Variables.setVariable("sender", sender, event, true); + Variables.setVariable("message", MESSAGE, event, true); + + Capture messageCapture = EasyMock.newCapture(); + sender.sendMessage(EasyMock.capture(messageCapture)); + EasyMock.replay(sender); + + TriggerItem.walk(sendEffect, event); + EasyMock.verify(sender); + Assert.assertEquals(MESSAGE, messageCapture.getValue()); + } + +} diff --git a/src/test/java/org/skriptlang/skript/util/ClassLoaderTest.java b/src/test/java/org/skriptlang/skript/util/ClassLoaderTest.java new file mode 100644 index 00000000000..145035b8ab4 --- /dev/null +++ b/src/test/java/org/skriptlang/skript/util/ClassLoaderTest.java @@ -0,0 +1,80 @@ +package org.skriptlang.skript.util; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.*; + +public class ClassLoaderTest { + + private static void load(ClassLoader loader) { + loader.loadClasses(ClassLoaderTest.class); + } + + @Test + public void testBasePackage() { + Set> classes = new HashSet<>(); + load(ClassLoader.builder() + .basePackage("org.skriptlang.skript.util") + .forEachClass(classes::add) + .build()); + assertTrue(classes.contains(ClassLoader.class)); + assertTrue(classes.contains(ClassLoaderTest.class)); + } + + @Test + public void testSubPackages() { + Set> classes = new HashSet<>(); + ClassLoader.Builder builder = ClassLoader.builder() + .basePackage("org.skriptlang.skript") + .addSubPackages("fake1") + .addSubPackages(List.of("fake2")) + .forEachClass(classes::add); + + // test without this subpackage + load(builder.build()); + assertFalse(classes.contains(ClassLoader.class)); + assertFalse(classes.contains(ClassLoaderTest.class)); + + // test with this subpackage + classes.clear(); + load(builder.addSubPackage("util").build()); + assertTrue(classes.contains(ClassLoader.class)); + assertTrue(classes.contains(ClassLoaderTest.class)); + } + + @Test + public void testFilter() { + Set> classes = new HashSet<>(); + load(ClassLoader.builder() + .basePackage("org.skriptlang.skript.util") + .filter(fqn -> !fqn.endsWith("Test")) // filter out class names ending with "Test" + .forEachClass(classes::add) + .build()); + assertTrue(classes.contains(ClassLoader.class)); + assertFalse(classes.contains(ClassLoaderTest.class)); + } + + @Test + public void testDeep() { + Set> classes = new HashSet<>(); + ClassLoader.Builder builder = ClassLoader.builder() + .basePackage("org.skriptlang.skript") + .forEachClass(classes::add); + + // test without deep + load(builder.deep(false).build()); + assertFalse(classes.contains(ClassLoader.class)); + assertFalse(classes.contains(ClassLoaderTest.class)); + + // test with deep + classes.clear(); + load(builder.deep(true).build()); + assertTrue(classes.contains(ClassLoader.class)); + assertTrue(classes.contains(ClassLoaderTest.class)); + } + +} diff --git a/src/test/java/org/skriptlang/skript/util/ClassUtilsTest.java b/src/test/java/org/skriptlang/skript/util/ClassUtilsTest.java new file mode 100644 index 00000000000..61cb91a7ec0 --- /dev/null +++ b/src/test/java/org/skriptlang/skript/util/ClassUtilsTest.java @@ -0,0 +1,22 @@ +package org.skriptlang.skript.util; + +import org.junit.Test; + +import java.util.AbstractCollection; +import java.util.Collection; + +import static org.junit.Assert.*; + +public class ClassUtilsTest { + + @Test + public void testIsNormalClass() { + assertTrue(ClassUtils.isNormalClass(String.class)); + assertFalse(ClassUtils.isNormalClass(Test.class)); + assertFalse(ClassUtils.isNormalClass(String[].class)); + assertFalse(ClassUtils.isNormalClass(int.class)); + assertFalse(ClassUtils.isNormalClass(Collection.class)); + assertFalse(ClassUtils.isNormalClass(AbstractCollection.class)); + } + +} diff --git a/src/test/java/org/skriptlang/skript/util/PriorityTest.java b/src/test/java/org/skriptlang/skript/util/PriorityTest.java new file mode 100644 index 00000000000..91ff270458d --- /dev/null +++ b/src/test/java/org/skriptlang/skript/util/PriorityTest.java @@ -0,0 +1,79 @@ +package org.skriptlang.skript.util; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class PriorityTest { + + @Test + public void testBase() { + Priority base = Priority.base(); + + assertTrue(base.before().isEmpty()); + assertTrue(base.after().isEmpty()); + + // Different instances, but functionally equal + assertEquals(base, Priority.base()); + } + + @Test + public void testBefore() { + Priority base = Priority.base(); + Priority before = Priority.before(base); + + assertTrue(before.before().contains(base)); + assertTrue(before.after().isEmpty()); + assertTrue(before.compareTo(base) < 0); + assertTrue(base.compareTo(before) > 0); + + // Different instances, but functionally equal + assertEquals(before, Priority.before(base)); + } + + @Test + public void testAfter() { + Priority base = Priority.base(); + Priority after = Priority.after(base); + + assertTrue(after.before().isEmpty()); + assertTrue(after.after().contains(base)); + assertTrue(after.compareTo(base) > 0); + assertTrue(base.compareTo(after) < 0); + + // Different instances, but functionally equal + assertEquals(after, Priority.after(base)); + } + + @Test + public void testBoth() { + Priority base = Priority.base(); + Priority before = Priority.before(base); + Priority after = Priority.after(base); + + // 'before' should be before 'after' + assertTrue(before.compareTo(after) < 0); + // 'after' should be after 'before' + assertTrue(after.compareTo(before) > 0); + } + + @Test + public void testComplex() { + Priority base = Priority.base(); + + Priority before = Priority.before(base); + Priority afterBefore = Priority.after(before); + // 'afterBefore' should be before 'base' + assertTrue(afterBefore.compareTo(base) < 0); + // 'base' should be after 'afterBefore' + assertTrue(base.compareTo(afterBefore) > 0); + + Priority after = Priority.after(base); + Priority beforeAfter = Priority.before(after); + // 'beforeAfter' should be after 'base' + assertTrue(beforeAfter.compareTo(base) > 0); + // 'base' should be before 'beforeAfter' + assertTrue(base.compareTo(beforeAfter) < 0); + } + +} diff --git a/src/test/skript/junit/CancelledEventTest.sk b/src/test/skript/junit/CancelledEventTest.sk index ae8b2563367..5051afa3c10 100644 --- a/src/test/skript/junit/CancelledEventTest.sk +++ b/src/test/skript/junit/CancelledEventTest.sk @@ -11,7 +11,7 @@ test "ExprDropsJUnit" when running JUnit: ensure junit test {@test} completes {_tests::*} on load: - set {-cancelled-event-test::call-count} to 0 + set {cancelled-event-test::call-count} to 0 on form: junit test is {@test} @@ -26,7 +26,7 @@ on cancelled form: on any form: junit test is {@test} - add 1 to {-cancelled-event-test::call-count} + add 1 to {cancelled-event-test::call-count} - if {-cancelled-event-test::call-count} is 2: + if {cancelled-event-test::call-count} is 2: complete objective "listen for any event" for {@test} diff --git a/src/test/skript/junit/EvtFurnaceTest.sk b/src/test/skript/junit/EvtFurnaceTest.sk index f65a04135ea..6999fe35b7b 100644 --- a/src/test/skript/junit/EvtFurnaceTest.sk +++ b/src/test/skript/junit/EvtFurnaceTest.sk @@ -17,7 +17,6 @@ on load: on smelt: junit test is {@EvtFurnaceTest} complete objective "smelt event" for junit test {@EvtFurnaceTest} - broadcast "%smelted item%" if smelted item is an iron ingot: complete objective "smelt - got smelted item" for junit test {@EvtFurnaceTest} diff --git a/src/test/skript/junit/PlayerElytraBoostEventTest.sk b/src/test/skript/junit/PlayerElytraBoostEventTest.sk index 1a4384e8f33..bcd57eb9b30 100644 --- a/src/test/skript/junit/PlayerElytraBoostEventTest.sk +++ b/src/test/skript/junit/PlayerElytraBoostEventTest.sk @@ -1,7 +1,7 @@ options: test: "org.skriptlang.skript.test.tests.syntaxes.events.PlayerElytraBoostEventTest" -test "PlayerElytraBoosEventTest" when running JUnit: +test "PlayerElytraBoostEventTest" when running JUnit: set {_tests::*} to "boost event called", "boost event - firework item", "boost event - player" and "boost event - firework entity" ensure junit test {@test} completes {_tests::*} diff --git a/src/test/skript/tests/regressions/7536-for-loop-ending.sk b/src/test/skript/tests/regressions/7536-for-loop-ending.sk new file mode 100644 index 00000000000..58e7b40a4ea --- /dev/null +++ b/src/test/skript/tests/regressions/7536-for-loop-ending.sk @@ -0,0 +1,17 @@ +using for each loops + +test "for each loops ending (start)": + clear {7536 For Each::*} + for each {_word} in ("test", "test2"): + add {_word} to {7536 For Each::*} + for each {_word 2} in ("example", "example2"): + add {_word 2} to {7536 For Each::*} + + assert the size of {7536 For Each::*} is 6 with "Wrong number of variables: %{7536 For Each::*}%" + +# Need to make sure that trigger didn't just die +test "for each loops ending (result)": + assert the size of {7536 For Each::*} is 6 with "Wrong number of variables: %{7536 For Each::*}%" + assert {7536 For Each::*} is ("test", "example", "example2", "test2", "example", "example2") with "Wrong loop order: %{7536 For Each::*}%" + + delete {7536 For Each::*} diff --git a/src/test/skript/tests/syntaxes/expressions/ExprColorOf.sk b/src/test/skript/tests/syntaxes/expressions/ExprColorOf.sk index 06e9931c958..9f108816ac1 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprColorOf.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprColorOf.sk @@ -12,7 +12,11 @@ test "color of displays": spawn a text display at spawn of world "world": set {_e} to entity - assert color of {_e} is rgb(0,0,0, 64) with "default background colour failed" + if running minecraft "1.21.4": + # Paper changed return behaviour in 1.21.4#125 + assert color of {_e} is not set with "default background colour failed" + else: + assert color of {_e} is rgb(0,0,0, 64) with "default background colour failed" set colour of {_e} to red assert color of {_e} is red with "failed to set background colour" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprName.sk b/src/test/skript/tests/syntaxes/expressions/ExprName.sk index f6969161e07..d5e6066bcb5 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprName.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprName.sk @@ -34,6 +34,16 @@ test "name of item": set the name of {_thing} to "blob" assert name of {_thing} is "blob" with "item name didn't change" +test "name of block": + set {_data} to blockdata of block at event-location + set block at event-location to a chest + assert name of block at event-location is not set with "The block shouldn't have a name yet" + set name of block at event-location to "Mr Chesty" + assert name of block at event-location = "Mr Chesty" with "The block should have a name now" + reset name of block at event-location + assert name of block at event-location is not set with "The block should no longer have a name" + set block at event-location to {_data} + using script reflection test "config name (new)": @@ -53,7 +63,7 @@ test "node name (new)": assert name of {_node} is "test ""name of world""" with "first node name was wrong" set {_node} to the current script - set {_node} to the 4th element of nodes of {_node} # Obviously, this changes if this file changes + set {_node} to the 5th element of nodes of {_node} # Obviously, this changes if this file changes assert name of {_node} is "using script reflection" with "4th node name was wrong" # root node diff --git a/src/test/skript/tests/syntaxes/expressions/ExprQueue.sk b/src/test/skript/tests/syntaxes/expressions/ExprQueue.sk index ecac33aa839..435fa38b56d 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprQueue.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprQueue.sk @@ -1,15 +1,15 @@ using queues test "queue creation": - set {_queue} to a new queue + set {_queue} to a queue assert {_queue} exists with "queue was not created" delete {_queue} - set {_queue} to a new queue of "hello" and "there" + set {_queue} to a queue of "hello" and "there" assert {_queue} exists with "queue was not created" test "queue polling behaviour": - set {_queue} to a new queue of "hello" and "there" + set {_queue} to a queue of "hello" and "there" set {_word} to the first element in {_queue} assert {_word} is "hello" with "element not polled" @@ -17,7 +17,7 @@ test "queue polling behaviour": set {_word} to the first element in {_queue} assert {_word} is "there" with "element not polled" - set {_queue} to a new queue of "hello" and "there" + set {_queue} to a queue of "hello" and "there" set {_word} to the last element in {_queue} assert {_word} is "there" with "element not polled" @@ -27,7 +27,7 @@ test "queue polling behaviour": assert {_queue} is empty with "queue was not empty" test "queue start/end": - set {_queue} to a new queue of "hello" and "there" + set {_queue} to a queue of "hello" and "there" set {_word} to the start of {_queue} assert {_word} is "hello" with "element not found" @@ -44,13 +44,13 @@ test "queue start/end": assert {_queue} is empty with "queue was not empty" test "queue emptiness": - set {_queue} to a new queue + set {_queue} to a queue assert {_queue} is empty with "queue was not empty" - set {_queue} to a new queue of "hello" and "there" + set {_queue} to a queue of "hello" and "there" assert {_queue} is not empty with "queue was empty" - set {_queue} to a new queue + set {_queue} to a queue add "hello" to {_queue} assert {_queue} is not empty with "queue was empty" set {_var} to the first element in {_queue} @@ -58,7 +58,7 @@ test "queue emptiness": test "dequeue queue": - set {_queue} to a new queue of "hello" and "there" + set {_queue} to a queue of "hello" and "there" assert {_queue} is not empty with "queue was empty" set {_words::*} to dequeued {_queue} @@ -69,3 +69,15 @@ test "dequeue queue": assert {_words::*} are "hello" and "there" with "elements not polled" assert {_queue} is empty with "queue was not empty" + +using for loops +test "loop queue": + set {_queue} to a queue of "hello" and "there" + set {_count} to 0 + + for each {_word} in {_queue}: + add 1 to {_count} + if {_count} is less than 10: + add {_word} to {_queue} + + assert {_count} is 11 diff --git a/src/test/skript/tests/syntaxes/expressions/ExprScript.sk b/src/test/skript/tests/syntaxes/expressions/ExprScript.sk index bc40123c79b..d21537de6f3 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprScript.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprScript.sk @@ -4,7 +4,7 @@ options: test: (join ("..", "..", "..", "..", "..", "..", "src", "test", "skript", "tests" and "") by file_separator()) # Princess test script is in another castle, Mario! - # paths are relativised to the ", "scripts", " directory + # paths are relativised to the "/scripts/" directory # but we are loading these scripts from the test folder :( using script reflection diff --git a/src/test/skript/tests/syntaxes/expressions/ExprTagContents.sk b/src/test/skript/tests/syntaxes/expressions/ExprTagContents.sk index 312c545e40c..d7df530e47b 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprTagContents.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprTagContents.sk @@ -5,3 +5,8 @@ test "tags contents": assert tag contents of item tag "slabs" contains oak slab with "oak slab is not a slab" assert tag contents of entity tag "minecraft:skeletons" contains a skeleton with "skeleton is not a skeleton" + + parse: + loop tag contents of minecraft item tag "logs": + add loop-item to {_list::*} + assert last parse logs is not set with "failed to parse tag looping (%last parse logs%)" diff --git a/src/test/skript/tests/syntaxes/expressions/ExprTypeOf.sk b/src/test/skript/tests/syntaxes/expressions/ExprTypeOf.sk new file mode 100644 index 00000000000..a7084659e51 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprTypeOf.sk @@ -0,0 +1,18 @@ +test "type of expression": + spawn a sheep at event-location: + set {_e} to entity + assert type of {_e} = sheep with "Type of entity should be sheep" + assert type of zombie = zombie with "Type of entitydata should be zombie" + delete entity within {_e} + + set {_i} to 1 of diamond sword named "BOB" + assert type of {_i} = diamond sword with "Type of item should be diamond sword" + + set {_p} to potion effect of night vision for 10 seconds + assert type of {_p} = night vision with "Type of potion effect should be nice vision" + + set {_data} to oak_stairs[] + assert type of {_data} = oak stairs with "Type of blockdata should be oak stairs" + + set {_ench} to sharpness 10 + assert type of {_ench} = sharpness with "Type of enchantment type should be sharpness" diff --git a/src/test/skript/tests/syntaxes/sections/SecFor.sk b/src/test/skript/tests/syntaxes/sections/SecFor.sk index adaea6b36b4..d4d29bb49c6 100644 --- a/src/test/skript/tests/syntaxes/sections/SecFor.sk +++ b/src/test/skript/tests/syntaxes/sections/SecFor.sk @@ -15,6 +15,7 @@ test "for section": delete {_value} for {_key}, {_value} in {_list::*}: + set {_key} to {_key} parsed as integer assert {_key} is greater than 0 with "Expected key > 0, found %{_key}%" assert {_key} is less than 4 with "Expected key < 4, found %{_key}%" assert {_value} is greater than 0 with "Expected value > 0, found %{_value}%" @@ -27,6 +28,7 @@ test "for section": for key {_key} and value {_value} in {_list::*}: + set {_key} to {_key} parsed as integer assert {_key} is greater than 0 with "Expected key > 0, found %{_key}%" assert {_key} is less than 4 with "Expected key < 4, found %{_key}%" assert {_value} is greater than 0 with "Expected value > 0, found %{_value}%" @@ -38,6 +40,7 @@ test "for section": delete {_value} for {_key} and {_value} in {_list::*}: + set {_key} to {_key} parsed as integer assert {_key} is greater than 0 with "Expected key > 0, found %{_key}%" assert {_key} is less than 4 with "Expected key < 4, found %{_key}%" assert {_value} is greater than 0 with "Expected value > 0, found %{_value}%" @@ -50,6 +53,7 @@ test "for section": # 'loop' syntax alternative loop {_key} and {_value} in {_list::*}: + set {_key} to {_key} parsed as integer assert {_key} is greater than 0 with "Expected key > 0, found %{_key}%" assert {_key} is less than 4 with "Expected key < 4, found %{_key}%" assert {_value} is greater than 0 with "Expected value > 0, found %{_value}%" diff --git a/src/test/skript/tests/syntaxes/structures/StructExample.sk b/src/test/skript/tests/syntaxes/structures/StructExample.sk new file mode 100644 index 00000000000..9756a751f7f --- /dev/null +++ b/src/test/skript/tests/syntaxes/structures/StructExample.sk @@ -0,0 +1,21 @@ +using examples + +example: + # None of this should be run + broadcast "foo bar" + wait 1 second + broadcast "hello world" + +example: + spawn a pig at spawn of world "world" + +parse: + results: {examples::*} + code: + example: + aldhfgfkudshgk + +test "examples": + # Examples should get parsed + assert {examples::*} contains "Can't understand this condition/effect: aldhfgfkudshgk" with "%{examples::*}%" + delete {examples::*}