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..188faa11f3c 100644 --- a/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java +++ b/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java @@ -38,6 +38,7 @@ import org.skriptlang.skript.lang.converter.Converter; import org.skriptlang.skript.lang.converter.Converters; import org.skriptlang.skript.lang.script.Script; +import org.skriptlang.skript.util.Cancellable; public class DefaultConverters { @@ -257,6 +258,20 @@ public void setAmount(Number amount) { return null; }); + // Cancellable (task to bukkit) - for checking whether things are cancelled + Converters.registerConverter(Cancellable.class, org.bukkit.event.Cancellable.class, cancellable -> new org.bukkit.event.Cancellable() { + @Override + public boolean isCancelled() { + return cancellable.isCancelled(); + } + + @Override + public void setCancelled(boolean cancel) { + if (cancel) + cancellable.cancel(); + } + }); + // Enchantment - EnchantmentType Converters.registerConverter(Enchantment.class, EnchantmentType.class, e -> new EnchantmentType(e, -1)); 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 79cc665ae60..a0554bfc02b 100644 --- a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java @@ -25,13 +25,16 @@ import ch.njol.skript.util.visual.VisualEffect; import ch.njol.skript.util.visual.VisualEffects; import ch.njol.yggdrasil.Fields; +import org.bukkit.event.Cancellable; 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.util.Completable; import org.skriptlang.skript.util.Executable; +import org.skriptlang.skript.util.Task; import java.io.File; import java.io.StreamCorruptedException; @@ -846,6 +849,29 @@ public String toVariableNameString(final Script script) { .examples("run {_function} with arguments 1 and true") .since("2.10")); + Classes.registerClass(new ClassInfo<>(Completable.class, "completable") + .user("completables?") + .name("Completable") + .description("Something that can be completed (e.g. a task).") + .examples("complete the current task", "{task} is completed") + .since("INSERT VERSION")); + + Classes.registerClass(new ClassInfo<>(Cancellable.class, "cancellable") + .user("cancellables?") + .name("Cancellable") + .description("Something that can be cancelled: an event, a task, a timer.") + .examples("cancel {_task}") + .since("INSERT VERSION")); + + Classes.registerClass(new ClassInfo<>(Task.class, "task") + .user("tasks?") + .name("Task") + .description("A task is an executable section of code. Other triggers can wait for its completion.") + .examples("run {_task}") + .since("INSERT VERSION") + .serializer(new YggdrasilSerializer<>()) + ); + Classes.registerClass(new ClassInfo<>(DynamicFunctionReference.class, "function") .user("functions?") .name("Function") diff --git a/src/main/java/ch/njol/skript/conditions/CondActive.java b/src/main/java/ch/njol/skript/conditions/CondActive.java new file mode 100644 index 00000000000..9892a1567d7 --- /dev/null +++ b/src/main/java/ch/njol/skript/conditions/CondActive.java @@ -0,0 +1,57 @@ +package ch.njol.skript.conditions; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Condition; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.registrations.Feature; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.util.Completable; +import org.skriptlang.skript.util.Task; + +// todo doc +public class CondActive extends Condition { + + static { + Skript.registerCondition(CondActive.class, + "%completable% is (running|active|:incomplete)", + "%completable% (is not|isn't) (running|active|:incomplete)", + "%completable% is inactive" + ); + } + + private Expression completable; + private boolean incomplete; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + this.setNegated(matchedPattern > 0); + if (matchedPattern > 1 && !this.getParser().hasExperiment(Feature.TASKS)) + return false; + //noinspection unchecked + this.completable = (Expression) exprs[0]; + this.incomplete = parseResult.hasTag("incomplete"); + return true; + } + + @Override + public boolean check(Event event) { + Completable thing = completable.getSingle(event); + if (thing == null) + return false; + // If it's a task, running/active also means not cancelled + if (thing instanceof Task task && !incomplete) + return (task.isCancelled() || thing.isComplete()) ^ isNegated(); + return thing.isComplete() ^ isNegated(); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + if (incomplete) + return completable.toString(event, debug) + (isNegated() ? " is not incomplete" : " is incomplete"); + return completable.toString(event, debug) + (isNegated() ? " is not active" : " is active"); + } + +} diff --git a/src/main/java/ch/njol/skript/conditions/CondCancelled.java b/src/main/java/ch/njol/skript/conditions/CondCancelled.java index 29e98ae3c54..bb1c16227d5 100644 --- a/src/main/java/ch/njol/skript/conditions/CondCancelled.java +++ b/src/main/java/ch/njol/skript/conditions/CondCancelled.java @@ -8,40 +8,57 @@ import ch.njol.skript.lang.Condition; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.registrations.Feature; import ch.njol.util.Kleenean; import org.bukkit.event.Cancellable; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; -@Name("Event Cancelled") -@Description("Checks whether or not the event is cancelled.") +@Name("Is Cancelled") +@Description("Checks whether or not the event or a task is cancelled.") @Examples({"on click:", "\tif event is cancelled:", "\t\tbroadcast \"no clicks allowed!\"" }) -@Since("2.2-dev36") +@Since("2.2-dev36, INSERT VERSION (tasks: experimental)") public class CondCancelled extends Condition { static { Skript.registerCondition(CondCancelled.class, - "[the] event is cancel[l]ed", - "[the] event (is not|isn't) cancel[l]ed" - ); + "[the] event is cancel[l]ed", + "[the] event (is not|isn't) cancel[l]ed", + "%cancellable% is cancel[l]ed", + "%cancellable% (is not|isn't) cancel[l]ed" + ); } - + + private @Nullable Expression cancellable; + @Override public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - setNegated(matchedPattern == 1); + this.setNegated(matchedPattern % 2 != 0); + if (matchedPattern > 1 && !this.getParser().hasExperiment(Feature.TASKS)) + return false; + if (matchedPattern > 1) + this.cancellable = (Expression) exprs[0]; return true; } @Override - public boolean check(Event e) { - return (e instanceof Cancellable && ((Cancellable) e).isCancelled()) ^ isNegated(); + public boolean check(Event event) { + if (cancellable != null) { + Cancellable single = cancellable.getSingle(event); + if (single != null) + return single.isCancelled() ^ isNegated(); + return false; + } + return (event instanceof Cancellable cancellable && cancellable.isCancelled() ^ isNegated()); } @Override - public String toString(@Nullable Event e, boolean debug) { + public String toString(@Nullable Event event, boolean debug) { + if (cancellable != null) + return cancellable.toString(event, debug) + (isNegated() ? " is not cancelled" : " is cancelled"); return isNegated() ? "event is not cancelled" : "event is cancelled"; } diff --git a/src/main/java/ch/njol/skript/conditions/CondComplete.java b/src/main/java/ch/njol/skript/conditions/CondComplete.java new file mode 100644 index 00000000000..e01ad6903d0 --- /dev/null +++ b/src/main/java/ch/njol/skript/conditions/CondComplete.java @@ -0,0 +1,48 @@ +package ch.njol.skript.conditions; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Condition; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.registrations.Feature; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.util.Completable; + +// todo doc +public class CondComplete extends Condition { + + static { + Skript.registerCondition(CondComplete.class, + "%completable% is (finished|complete[d])", + "%completable% (is not|isn't) (finished|complete[d])" + ); + } + + private Expression completable; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + this.setNegated(matchedPattern == 1); + if (matchedPattern > 1 && !this.getParser().hasExperiment(Feature.TASKS)) + return false; + //noinspection unchecked + this.completable = (Expression) exprs[0]; + return true; + } + + @Override + public boolean check(Event event) { + Completable task = completable.getSingle(event); + if (task == null) + return false; + return task.isComplete() ^ isNegated(); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return completable.toString(event, debug) + (isNegated() ? " is not complete" : " is complete"); + } + +} diff --git a/src/main/java/ch/njol/skript/effects/Delay.java b/src/main/java/ch/njol/skript/effects/Delay.java index 72e1a0dd786..ec7bacce6d0 100644 --- a/src/main/java/ch/njol/skript/effects/Delay.java +++ b/src/main/java/ch/njol/skript/effects/Delay.java @@ -5,12 +5,9 @@ import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Effect; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.*; import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.Trigger; -import ch.njol.skript.lang.TriggerItem; +import ch.njol.skript.registrations.Feature; import ch.njol.skript.timings.SkriptTimings; import ch.njol.skript.util.Timespan; import ch.njol.skript.variables.Variables; @@ -18,83 +15,143 @@ import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.util.Task; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.Set; import java.util.WeakHashMap; +import java.util.concurrent.TimeUnit; @Name("Delay") -@Description("Delays the script's execution by a given timespan. Please note that delays are not persistent, e.g. trying to create a tempban script with ban player → wait 7 days → unban player will not work if you restart your server anytime within these 7 days. You also have to be careful even when using small delays!") +@Description("Delays the script's execution by a given timespan. Please note that delays are not persistent, e.g. " + + "trying to create a tempban script with ban player → wait 7 days → unban player will not work if you " + + "restart your server anytime within these 7 days. You also have to be careful even when using small delays!") @Examples({ "wait 2 minutes", "halt for 5 minecraft hours", "wait a tick" }) -@Since("1.4") +@Since("1.4, INSERT VERSION (tasks: experimental)") +// todo doc public class Delay extends Effect { static { - Skript.registerEffect(Delay.class, "(wait|halt) [for] %timespan%"); + Skript.registerEffect(Delay.class, + "wait %timespan% for %timespan/task%", + "wait for %timespan/task%", + "(wait|halt) [for] %timespan%" + ); } - @SuppressWarnings("NotNullFieldNotInitialized") - protected Expression duration; + protected Expression target; + protected @Nullable Expression delay; - @SuppressWarnings({"unchecked", "null"}) @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - getParser().setHasDelayBefore(Kleenean.TRUE); - - duration = (Expression) exprs[0]; - if (duration instanceof Literal) { // If we can, do sanity check for delays - long millis = ((Literal) duration).getSingle().getAs(Timespan.TimePeriod.MILLISECOND); - if (millis < 50) { + public boolean init(Expression[] expressions, int pattern, Kleenean delayed, ParseResult result) { + if (pattern == 1 && !this.getParser().hasExperiment(Feature.TASKS)) + return false; + this.getParser().setHasDelayBefore(Kleenean.TRUE); + + if (pattern == 1) { + //noinspection unchecked + this.delay = (Expression) expressions[0]; + this.target = expressions[1]; + } else { + this.target = expressions[0]; + } + if (target instanceof Literal literal && literal.getSingle() instanceof Timespan duration) { + // If we can, do sanity check for delays + long millis = duration.getAs(Timespan.TimePeriod.MILLISECOND); + if (millis < 50) Skript.warning("Delays less than one tick are not possible, defaulting to one tick."); - } } return true; } @Override - @Nullable - protected TriggerItem walk(Event event) { + protected @Nullable TriggerItem walk(Event event) { + Object single = this.target.getSingle(event); debug(event, true); + if (this.getNext() == null || !Skript.getInstance().isEnabled()) + return null; + Task currentTask = getCurrentTask(event); + if (currentTask != null && currentTask.isCancelled()) + return null; // A delay is a good opportunity to exit from this trigger if the current task is cancelled + addDelayedEvent(event); + if (single instanceof Timespan timespan) + return this.walk(event, timespan); + if (single instanceof Task task) + return this.walk(event, task); + return super.walk(event); + } // todo note breaking change: wait for will now not wait, before it would just kill the trigger + + protected @Nullable TriggerItem walk(Event event, Timespan duration) { + TriggerItem next = getNext(); long start = Skript.debug() ? System.nanoTime() : 0; + + // Back up local variables + Object variables = Variables.removeLocals(event); + + Task currentTask = getCurrentTask(event); + + int taskId = Bukkit.getScheduler().scheduleSyncDelayedTask(Skript.getInstance(), + () -> reschedule(start, variables, next, event), + Math.max(duration.getAs(Timespan.TimePeriod.TICK), 1));// Minimum delay is one tick + + if (currentTask != null && !currentTask.isCancelled()) + currentTask.scheduleCancellationStep(() -> Bukkit.getScheduler().cancelTask(taskId)); + return null; + } + + protected @Nullable TriggerItem walk(Event event, Task task) { + Timespan timeout = delay != null ? delay.getSingle(event) : null; + TriggerItem next = getNext(); - if (next != null && Skript.getInstance().isEnabled()) { // See https://github.com/SkriptLang/Skript/issues/3702 - addDelayedEvent(event); - - Timespan duration = this.duration.getSingle(event); - if (duration == null) - return null; - - // Back up local variables - Object localVars = Variables.removeLocals(event); - - Bukkit.getScheduler().scheduleSyncDelayedTask(Skript.getInstance(), () -> { - Skript.debug(getIndentation() + "... continuing after " + (System.nanoTime() - start) / 1_000_000_000. + "s"); - - // Re-set local variables - if (localVars != null) - Variables.setLocalVariables(event, localVars); - - Object timing = null; // Timings reference must be kept so that it can be stopped after TriggerItem execution - if (SkriptTimings.enabled()) { // getTrigger call is not free, do it only if we must - Trigger trigger = getTrigger(); - if (trigger != null) - timing = SkriptTimings.start(trigger.getDebugLabel()); - } - - TriggerItem.walk(next, event); - Variables.removeLocals(event); // Clean up local vars, we may be exiting now - - SkriptTimings.stop(timing); // Stop timing if it was even started - }, Math.max(duration.getAs(Timespan.TimePeriod.TICK), 1)); // Minimum delay is one tick, less than it is useless! - } + long start = Skript.debug() ? System.nanoTime() : 0; + + // Back up local variables + Object variables = Variables.removeLocals(event); + Task currentTask = getCurrentTask(event); + + Bukkit.getScheduler().runTaskAsynchronously(Skript.getInstance(), () -> { + if (timeout != null) { + Duration duration = timeout.getDuration(); + task.await(duration.get(ChronoUnit.MILLIS), TimeUnit.MILLISECONDS); + } else { + task.await(); + } + if (currentTask != null && !currentTask.isCancelled()) { + // Don't restart the trigger if its execution was cancelled while we were asleep + Bukkit.getScheduler().runTask(Skript.getInstance(), + () -> reschedule(start, variables, next, event)); + } + }); return null; } + protected void reschedule(long start, Object variables, TriggerItem next, Event event) { + Skript.debug(getIndentation() + "... continuing after " + (System.nanoTime() - start) / 1_000_000_000. + "s"); + + // Re-set local variables + if (variables != null) + Variables.setLocalVariables(event, variables); + + Object timing = null; // Timings reference must be kept so that it can be stopped after TriggerItem execution + if (SkriptTimings.enabled()) { // getTrigger call is not free, do it only if we must + Trigger trigger = getTrigger(); + if (trigger != null) + timing = SkriptTimings.start(trigger.getDebugLabel()); + } + + TriggerItem.walk(next, event); + Variables.removeLocals(event); // Clean up local vars, we may be exiting now + + SkriptTimings.stop(timing); // Stop timing if it was even started + } + @Override protected void execute(Event event) { throw new UnsupportedOperationException(); @@ -102,7 +159,9 @@ protected void execute(Event event) { @Override public String toString(@Nullable Event event, boolean debug) { - return "wait for " + duration.toString(event, debug) + (event == null ? "" : "..."); + if (delay != null) + return "wait " + delay.toString(event, debug) + " for " + target.toString(event, debug); + return "wait for " + target.toString(event, debug); } private static final Set DELAYED = @@ -110,6 +169,7 @@ public String toString(@Nullable Event event, boolean debug) { /** * The main method for checking if the execution of {@link TriggerItem}s has been delayed. + * * @param event The event to check for a delay. * @return Whether {@link TriggerItem} execution has been delayed. */ @@ -119,10 +179,15 @@ public static boolean isDelayed(Event event) { /** * The main method for marking the execution of {@link TriggerItem}s as delayed. + * * @param event The event to mark as delayed. */ public static void addDelayedEvent(Event event) { DELAYED.add(event); } + public static @Nullable Task getCurrentTask(Event event) { + return event instanceof Task.TaskEvent taskEvent ? taskEvent.task() : null; + } + } diff --git a/src/main/java/ch/njol/skript/effects/EffCancelTask.java b/src/main/java/ch/njol/skript/effects/EffCancelTask.java new file mode 100644 index 00000000000..65ab7529d87 --- /dev/null +++ b/src/main/java/ch/njol/skript/effects/EffCancelTask.java @@ -0,0 +1,45 @@ +package ch.njol.skript.effects; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.registrations.Feature; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.util.Task; + +// todo doc +public class EffCancelTask extends Effect { + + static { + Skript.registerEffect(EffCancelTask.class, "cancel %task%"); + } + + private Expression task; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, + Kleenean isDelayed, ParseResult parseResult) { + if (!this.getParser().hasExperiment(Feature.TASKS)) + return false; + //noinspection unchecked + this.task = (Expression) expressions[0]; + return true; + } + + @Override + public void execute(Event event) { + Task task = this.task.getSingle(event); + if (task == null) + return; + task.cancel(); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "cancel " + task.toString(event, debug); + } + +} diff --git a/src/main/java/ch/njol/skript/effects/EffComplete.java b/src/main/java/ch/njol/skript/effects/EffComplete.java new file mode 100644 index 00000000000..26960b91951 --- /dev/null +++ b/src/main/java/ch/njol/skript/effects/EffComplete.java @@ -0,0 +1,43 @@ +package ch.njol.skript.effects; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.registrations.Feature; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.skriptlang.skript.util.Completable; + +// todo doc +// todo expr +public class EffComplete extends Effect { + + static { + Skript.registerEffect(EffComplete.class, "(complete|finish) %completable%"); + } + + private Expression completable; + + @Override + public boolean init(Expression[] expressions, int pattern, Kleenean delayed, ParseResult result) { + if (!this.getParser().hasExperiment(Feature.TASKS)) + return false; + //noinspection unchecked + this.completable = (Expression) expressions[0]; + return true; + } + + @Override + protected void execute(Event event) { + Completable single = completable.getSingle(event); + if (single != null) + single.complete(); + } + + @Override + public String toString(Event event, boolean debug) { + return "complete " + completable.toString(event, debug); + } + +} diff --git a/src/main/java/ch/njol/skript/effects/IndeterminateDelay.java b/src/main/java/ch/njol/skript/effects/IndeterminateDelay.java deleted file mode 100644 index 5179d20fd36..00000000000 --- a/src/main/java/ch/njol/skript/effects/IndeterminateDelay.java +++ /dev/null @@ -1,53 +0,0 @@ -package ch.njol.skript.effects; - -import org.bukkit.Bukkit; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; - -import ch.njol.skript.Skript; -import ch.njol.skript.lang.TriggerItem; -import ch.njol.skript.util.Timespan; -import ch.njol.skript.variables.Variables; - -/** - * @author Peter Güttinger - */ -public class IndeterminateDelay extends Delay { - - @Override - @Nullable - protected TriggerItem walk(Event event) { - debug(event, true); - - long start = Skript.debug() ? System.nanoTime() : 0; - TriggerItem next = getNext(); - - if (next != null && Skript.getInstance().isEnabled()) { // See https://github.com/SkriptLang/Skript/issues/3702 - Delay.addDelayedEvent(event); - Timespan duration = this.duration.getSingle(event); - if (duration == null) - return null; - - // Back up local variables - Object localVars = Variables.removeLocals(event); - - Bukkit.getScheduler().scheduleSyncDelayedTask(Skript.getInstance(), () -> { - Skript.debug(getIndentation() + "... continuing after " + (System.nanoTime() - start) / 1_000_000_000. + "s"); - - // Re-set local variables - if (localVars != null) - Variables.setLocalVariables(event, localVars); - - TriggerItem.walk(next, event); - }, duration.getAs(Timespan.TimePeriod.TICK)); - } - - return null; - } - - @Override - public String toString(@Nullable Event event, boolean debug) { - return "wait for operation to finish"; - } - -} diff --git a/src/main/java/ch/njol/skript/effects/Pass.java b/src/main/java/ch/njol/skript/effects/Pass.java new file mode 100644 index 00000000000..8987ebba7e5 --- /dev/null +++ b/src/main/java/ch/njol/skript/effects/Pass.java @@ -0,0 +1,34 @@ +package ch.njol.skript.effects; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.registrations.Feature; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; + +// todo doc +public class Pass extends Effect { + + static { + Skript.registerEffect(Pass.class, "pass", "do nothing"); + } + + @Override + public boolean init(Expression[] expressions, int matchedPattern, + Kleenean isDelayed, ParseResult parseResult) { + return this.getParser().hasExperiment(Feature.TASKS); + } + + @Override + public void execute(Event event) { + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "do nothing"; + } + +} diff --git a/src/main/java/ch/njol/skript/expressions/ExprSecTask.java b/src/main/java/ch/njol/skript/expressions/ExprSecTask.java new file mode 100644 index 00000000000..15393c1c2d3 --- /dev/null +++ b/src/main/java/ch/njol/skript/expressions/ExprSecTask.java @@ -0,0 +1,88 @@ +package ch.njol.skript.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.expressions.base.SectionExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.TriggerItem; +import ch.njol.skript.registrations.Feature; +import ch.njol.skript.test.runner.TestMode; +import ch.njol.skript.variables.Variables; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.util.Task; + +import java.util.List; + +// todo doc +public class ExprSecTask extends SectionExpression { + + static { + if (TestMode.ENABLED) + Skript.registerExpression(ExprSecTask.class, Task.class, ExpressionType.SIMPLE, + "[an] auto[matic|[ |-]completing] task", + "[a] [new] task", + "the [current] task" + ); + } + + protected boolean automatic, current; + + @Override + public boolean init(Expression[] expressions, + int pattern, + Kleenean delayed, + ParseResult result, + @Nullable SectionNode node, + @Nullable List triggerItems) { + if (!this.getParser().hasExperiment(Feature.TASKS)) + return false; + this.automatic = pattern == 0; + this.current = pattern == 2; + if (current) + return true; + if (node == null) { +// Skript.error("Task expression needs a section!"); + return false; // We don't error here because the `a task` classinfo will take over instead + } + this.loadCode(node); + return true; + } + + @Override + protected Task[] get(Event event) { + if (current) { + if (event instanceof Task.TaskEvent ours) + return new Task[] {ours.task()}; + // todo runtime error + return new Task[0]; + } + Object variables = Variables.copyLocalVariables(event); + return new Task[] { + new Task(automatic, variables, this::runSection) + }; + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return Task.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + if (automatic) + return "an automatic task"; + if (current) + return "the current task"; + return "a new task"; + } + +} diff --git a/src/main/java/ch/njol/skript/lang/Variable.java b/src/main/java/ch/njol/skript/lang/Variable.java index 3fe427c9bda..14f9ba87e71 100644 --- a/src/main/java/ch/njol/skript/lang/Variable.java +++ b/src/main/java/ch/njol/skript/lang/Variable.java @@ -1,14 +1,5 @@ package ch.njol.skript.lang; -import java.lang.reflect.Array; -import java.util.*; -import java.util.Map.Entry; -import java.util.regex.Pattern; -import java.util.NoSuchElementException; -import java.util.TreeMap; -import java.util.function.Predicate; -import java.util.function.Function; - import ch.njol.skript.Skript; import ch.njol.skript.SkriptAPIException; import ch.njol.skript.SkriptConfig; @@ -16,10 +7,6 @@ import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.classes.Changer.ChangerUtils; import ch.njol.skript.classes.ClassInfo; -import ch.njol.skript.variables.VariablesStorage; -import org.skriptlang.skript.lang.arithmetic.Arithmetics; -import org.skriptlang.skript.lang.arithmetic.OperationInfo; -import org.skriptlang.skript.lang.arithmetic.Operator; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.parser.ParserInstance; import ch.njol.skript.lang.util.SimpleExpression; @@ -29,6 +16,7 @@ import ch.njol.skript.util.Utils; import ch.njol.skript.variables.TypeHints; import ch.njol.skript.variables.Variables; +import ch.njol.skript.variables.VariablesStorage; import ch.njol.util.Kleenean; import ch.njol.util.Pair; import ch.njol.util.StringUtils; @@ -55,6 +43,7 @@ import java.util.Map.Entry; import java.util.function.Function; import java.util.function.Predicate; +import java.util.regex.Pattern; public class Variable implements Expression, KeyReceiverExpression, KeyProviderExpression { diff --git a/src/main/java/ch/njol/skript/registrations/Feature.java b/src/main/java/ch/njol/skript/registrations/Feature.java index 1001e767f07..8b229938eb6 100644 --- a/src/main/java/ch/njol/skript/registrations/Feature.java +++ b/src/main/java/ch/njol/skript/registrations/Feature.java @@ -15,6 +15,7 @@ public enum Feature implements Experiment { QUEUES("queues", LifeCycle.EXPERIMENTAL), FOR_EACH_LOOPS("for loop", LifeCycle.EXPERIMENTAL, "for [each] loop[s]"), SCRIPT_REFLECTION("reflection", LifeCycle.EXPERIMENTAL, "[script] reflection"), + TASKS("tasks", LifeCycle.EXPERIMENTAL, "tasks"), ; private final String codeName; diff --git a/src/main/java/ch/njol/skript/sections/SecLoop.java b/src/main/java/ch/njol/skript/sections/SecLoop.java index 38bd0366bea..27f95dfc532 100644 --- a/src/main/java/ch/njol/skript/sections/SecLoop.java +++ b/src/main/java/ch/njol/skript/sections/SecLoop.java @@ -7,6 +7,7 @@ import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.effects.Delay; import ch.njol.skript.lang.*; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.util.ContainerExpression; @@ -19,6 +20,7 @@ import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.UnknownNullability; +import org.skriptlang.skript.util.Task; import java.util.*; @@ -126,6 +128,9 @@ public boolean init(Expression[] exprs, @Override protected @Nullable TriggerItem walk(Event event) { + Task currentTask = Delay.getCurrentTask(event); + if (currentTask != null && currentTask.isCancelled()) + return null; // The task this is running inside has been cancelled Iterator iter = iteratorMap.get(event); if (iter == null) { if (iterableSingle) { diff --git a/src/main/java/ch/njol/skript/sections/SecWhile.java b/src/main/java/ch/njol/skript/sections/SecWhile.java index 18626490ee3..34f9f9be0b9 100644 --- a/src/main/java/ch/njol/skript/sections/SecWhile.java +++ b/src/main/java/ch/njol/skript/sections/SecWhile.java @@ -6,11 +6,13 @@ import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; +import ch.njol.skript.effects.Delay; import ch.njol.skript.lang.*; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.util.Kleenean; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.util.Task; import java.util.List; @@ -67,9 +69,11 @@ public boolean init(Expression[] exprs, return true; } - @Nullable @Override - protected TriggerItem walk(Event event) { + protected @Nullable TriggerItem walk(Event event) { + Task currentTask = Delay.getCurrentTask(event); + if (currentTask != null && currentTask.isCancelled()) + return null; // The task this is running inside has been cancelled if ((doWhile && !ranDoWhile) || condition.check(event)) { ranDoWhile = true; currentLoopCounter.put(event, (currentLoopCounter.getOrDefault(event, 0L)) + 1); diff --git a/src/main/java/org/skriptlang/skript/util/Cancellable.java b/src/main/java/org/skriptlang/skript/util/Cancellable.java new file mode 100644 index 00000000000..22877e60a39 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/Cancellable.java @@ -0,0 +1,10 @@ +package org.skriptlang.skript.util; + +// todo doc +public interface Cancellable { + + void cancel(); + + boolean isCancelled(); + +} diff --git a/src/main/java/org/skriptlang/skript/util/Completable.java b/src/main/java/org/skriptlang/skript/util/Completable.java new file mode 100644 index 00000000000..395c8fd55cd --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/Completable.java @@ -0,0 +1,10 @@ +package org.skriptlang.skript.util; + +// todo doc +public interface Completable { + + void complete(); + + boolean isComplete(); + +} diff --git a/src/main/java/org/skriptlang/skript/util/Task.java b/src/main/java/org/skriptlang/skript/util/Task.java new file mode 100644 index 00000000000..d744601e724 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/Task.java @@ -0,0 +1,171 @@ +package org.skriptlang.skript.util; + +import ch.njol.skript.variables.Variables; +import ch.njol.yggdrasil.YggdrasilSerializable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +// todo doc +public final class Task + implements Executable, Completable, Cancellable, YggdrasilSerializable { // todo change to context with context api + + private transient final Object variables; + private transient final Consumer runner; + private boolean autoComplete; + + private transient final CountDownLatch latch = new CountDownLatch(1); + private volatile boolean started, ready, cancelled; + /** + * A collection of tasks to be run when the task is shut down prematurely. + * This is used for cancelling parts of the task that are still in progress. + */ + private transient final List cancellationSteps; + + public Task() { // For serialisation + this.variables = null; + this.runner = null; + this.cancellationSteps = new ArrayList<>(); + } + + public Task(boolean autoComplete, Object variables, Consumer runner) { + this.variables = variables; + this.runner = runner; + this.autoComplete = autoComplete; + this.cancellationSteps = new ArrayList<>(8); + } + + @Override + public Void execute(Event event, Object... arguments) { + if (this.markStarted()) + return null; // Don't restart old tasks + TaskEvent here = new TaskEvent(this); + Variables.setLocalVariables(here, variables); + try { + if (runner != null) + this.runner.accept(here); + } finally { + if (autoComplete) + this.complete(); + Variables.removeLocals(here); + } + return null; + } + + /** + * Marks this task as having been started. + * If the task has previously been started, it should be aborted here. + * + * @return Whether to abort the task + */ + private synchronized boolean markStarted() { + if (started) + return true; + this.started = true; + return false; + } + + public Object variables() { + return variables; + } + + public Consumer runner() { + return runner; + } + + public void scheduleCancellationStep(Runnable runnable) { + synchronized (cancellationSteps) { + this.cancellationSteps.add(runnable); + } + } + + @Contract(pure = false) + public boolean await(long timeout, TimeUnit unit) { + try { + if (ready || cancelled) return true; + return latch.await(timeout, unit); + } catch (InterruptedException e) { + return false; + } + } + + @Contract(pure = false) + public boolean await() { + try { + if (ready || cancelled) return true; + this.latch.await(); + return true; + } catch (InterruptedException e) { + return false; + } + } + + @Override + public void complete() { + this.ready = true; + this.latch.countDown(); + } + + @Override + public boolean isComplete() { + return ready; + } + + @Override + public void cancel() { + if (cancelled) + return; + this.cancelled = true; + this.latch.countDown(); + this.cancellationSteps.forEach(Runnable::run); + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + public static class TaskEvent extends Event { + + private final Task task; + + public TaskEvent(Task task) { + this.task = task; + } + + public Task task() { + return task; + } + + @Override + @NotNull + public HandlerList getHandlers() { + throw new IllegalStateException(); + } + + } + + @Override + public boolean equals(Object object) { + if (this == object) return true; + if (!(object instanceof Task task)) return false; + return autoComplete == task.autoComplete + && started == task.started + && ready == task.ready + && cancelled == task.cancelled; + } + + @Override + public int hashCode() { + return Objects.hash(autoComplete, started, ready, cancelled); + } + +} diff --git a/src/test/skript/tests/syntaxes/expressions/ExprTask.sk b/src/test/skript/tests/syntaxes/expressions/ExprTask.sk new file mode 100644 index 00000000000..9f29088a735 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprTask.sk @@ -0,0 +1,33 @@ +using script reflection +using tasks + +test "basic tasks": + set {ExprTask} to false + + set {_task} to a task: + set {ExprTask} to true + + assert {ExprTask} is false with "task ran prematurely" + assert {_task} is not complete with "task completed prematurely" + + run {_task} + assert {ExprTask} is true with "task didn't run" + assert {_task} is not complete with "task wrongly completed" + + delete {ExprTask} + + +test "auto-completing tasks": + set {ExprTask} to false + + set {_task} to an automatic task: + set {ExprTask} to true + + assert {ExprTask} is false with "task ran prematurely" + assert {_task} is not complete with "task completed prematurely" + + run {_task} + assert {ExprTask} is true with "task didn't run" + assert {_task} is complete with "task didn't autocomplete" + + delete {ExprTask}