From 2d190ab456b154a6ea06fb9f156af893327ddf9d Mon Sep 17 00:00:00 2001 From: Moderocky Date: Thu, 16 Jan 2025 21:58:16 +0000 Subject: [PATCH 01/13] Add first API classes. --- .../skriptlang/skript/util/Completable.java | 10 ++ .../java/org/skriptlang/skript/util/Task.java | 96 +++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/main/java/org/skriptlang/skript/util/Completable.java create mode 100644 src/main/java/org/skriptlang/skript/util/Task.java 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..99ad208f8ba --- /dev/null +++ b/src/main/java/org/skriptlang/skript/util/Task.java @@ -0,0 +1,96 @@ +package org.skriptlang.skript.util; + +import ch.njol.skript.variables.Variables; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +// todo doc +public final class Task implements Executable, Completable { // todo change to context with context api + + private final Object variables; + private final Consumer runner; + + private transient final CountDownLatch latch = new CountDownLatch(1); + private transient volatile boolean ready; + + public Task(Object variables, Consumer runner) { + this.variables = variables; + this.runner = runner; + } + + @Override + public Void execute(Event event, Object... arguments) { + TaskEvent here = new TaskEvent(this); + Variables.setLocalVariables(here, variables); + this.runner.accept(here); + Variables.removeLocals(here); + return null; + } + + public Object variables() { + return variables; + } + + public Consumer runner() { + return runner; + } + + @Contract(pure = false) + public boolean await(long timeout, TimeUnit unit) { + try { + if (this.ready) return true; + return latch.await(timeout, unit); + } catch (InterruptedException e) { + return false; + } + } + + @Contract(pure = false) + public boolean await() { + try { + if (this.ready) 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; + } + + private 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(); + } + + } + +} From c054fd6eab81f1408a29a1d00f13e0bfc0419037 Mon Sep 17 00:00:00 2001 From: Moderocky Date: Thu, 16 Jan 2025 21:58:25 +0000 Subject: [PATCH 02/13] Add basic task syntax. --- .../java/ch/njol/skript/effects/Delay.java | 150 ++++++++++++------ .../ch/njol/skript/effects/EffComplete.java | 39 +++++ .../njol/skript/expressions/ExprSecTask.java | 65 ++++++++ 3 files changed, 202 insertions(+), 52 deletions(-) create mode 100644 src/main/java/ch/njol/skript/effects/EffComplete.java create mode 100644 src/main/java/ch/njol/skript/expressions/ExprSecTask.java diff --git a/src/main/java/ch/njol/skript/effects/Delay.java b/src/main/java/ch/njol/skript/effects/Delay.java index 72e1a0dd786..6e6bc170afd 100644 --- a/src/main/java/ch/njol/skript/effects/Delay.java +++ b/src/main/java/ch/njol/skript/effects/Delay.java @@ -5,12 +5,8 @@ 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.timings.SkriptTimings; import ch.njol.skript.util.Timespan; import ch.njol.skript.variables.Variables; @@ -18,83 +14,129 @@ 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) { + 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; + 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); + + Bukkit.getScheduler().scheduleSyncDelayedTask(Skript.getInstance(), + () -> reschedule(start, variables, next, event), + Math.max(duration.getAs(Timespan.TimePeriod.TICK), 1)); // Minimum delay is one tick + 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); + + Bukkit.getScheduler().runTaskAsynchronously(Skript.getInstance(), () -> { + if (timeout != null) { + Duration duration = timeout.getDuration(); + task.await(duration.get(ChronoUnit.MILLIS), TimeUnit.MILLISECONDS); + } else { + task.await(); + } + 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 +144,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 +154,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,6 +164,7 @@ 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) { 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..f1f094aa9fb --- /dev/null +++ b/src/main/java/ch/njol/skript/effects/EffComplete.java @@ -0,0 +1,39 @@ +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.util.Kleenean; +import org.bukkit.event.Event; +import org.skriptlang.skript.util.Completable; + +// todo doc +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) { + //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/expressions/ExprSecTask.java b/src/main/java/ch/njol/skript/expressions/ExprSecTask.java new file mode 100644 index 00000000000..f348fe41e6a --- /dev/null +++ b/src/main/java/ch/njol/skript/expressions/ExprSecTask.java @@ -0,0 +1,65 @@ +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.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, "[a] [new] task"); + } + + @Override + public boolean init(Expression[] expressions, + int pattern, + Kleenean delayed, + ParseResult result, + @Nullable SectionNode node, + @Nullable List triggerItems) { + 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) { + Object variables = Variables.copyLocalVariables(event); + return new Task[] { + new Task(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) { + return "a new task"; + } + +} From 13a31cbce5721d022d01cd224248c53c42b3e001 Mon Sep 17 00:00:00 2001 From: Moderocky Date: Thu, 16 Jan 2025 22:00:05 +0000 Subject: [PATCH 03/13] Lock tasks behind a feature flag. --- src/main/java/ch/njol/skript/effects/Delay.java | 3 +++ src/main/java/ch/njol/skript/effects/EffComplete.java | 3 +++ src/main/java/ch/njol/skript/registrations/Feature.java | 1 + 3 files changed, 7 insertions(+) diff --git a/src/main/java/ch/njol/skript/effects/Delay.java b/src/main/java/ch/njol/skript/effects/Delay.java index 6e6bc170afd..09b1979d7ab 100644 --- a/src/main/java/ch/njol/skript/effects/Delay.java +++ b/src/main/java/ch/njol/skript/effects/Delay.java @@ -7,6 +7,7 @@ import ch.njol.skript.doc.Since; import ch.njol.skript.lang.*; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.registrations.Feature; import ch.njol.skript.timings.SkriptTimings; import ch.njol.skript.util.Timespan; import ch.njol.skript.variables.Variables; @@ -49,6 +50,8 @@ public class Delay extends Effect { @Override 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) { diff --git a/src/main/java/ch/njol/skript/effects/EffComplete.java b/src/main/java/ch/njol/skript/effects/EffComplete.java index f1f094aa9fb..5734cf6fe1b 100644 --- a/src/main/java/ch/njol/skript/effects/EffComplete.java +++ b/src/main/java/ch/njol/skript/effects/EffComplete.java @@ -4,6 +4,7 @@ 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; @@ -19,6 +20,8 @@ public class EffComplete extends Effect { @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; 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; From 252924bc406986e37f1360bb25cbc4d5a200996d Mon Sep 17 00:00:00 2001 From: Moderocky Date: Thu, 16 Jan 2025 22:03:48 +0000 Subject: [PATCH 04/13] Automatic task support. --- .../ch/njol/skript/expressions/ExprSecTask.java | 13 +++++++++++-- src/main/java/org/skriptlang/skript/util/Task.java | 13 ++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/main/java/ch/njol/skript/expressions/ExprSecTask.java b/src/main/java/ch/njol/skript/expressions/ExprSecTask.java index f348fe41e6a..240abe3f94b 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprSecTask.java +++ b/src/main/java/ch/njol/skript/expressions/ExprSecTask.java @@ -7,6 +7,7 @@ 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; @@ -21,9 +22,14 @@ public class ExprSecTask extends SectionExpression { static { if (TestMode.ENABLED) - Skript.registerExpression(ExprSecTask.class, Task.class, ExpressionType.SIMPLE, "[a] [new] task"); + Skript.registerExpression(ExprSecTask.class, Task.class, ExpressionType.SIMPLE, + "[an] auto[matic|[ |-]completing] task", + "[a] [new] task" + ); } + protected boolean automatic; + @Override public boolean init(Expression[] expressions, int pattern, @@ -31,10 +37,13 @@ public boolean init(Expression[] expressions, ParseResult result, @Nullable SectionNode node, @Nullable List triggerItems) { + if (!this.getParser().hasExperiment(Feature.TASKS)) + return false; 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.automatic = pattern == 0; this.loadCode(node); return true; } @@ -43,7 +52,7 @@ public boolean init(Expression[] expressions, protected Task[] get(Event event) { Object variables = Variables.copyLocalVariables(event); return new Task[] { - new Task(variables, this::runSection) + new Task(automatic,variables, this::runSection) }; } diff --git a/src/main/java/org/skriptlang/skript/util/Task.java b/src/main/java/org/skriptlang/skript/util/Task.java index 99ad208f8ba..8cd8cc8517a 100644 --- a/src/main/java/org/skriptlang/skript/util/Task.java +++ b/src/main/java/org/skriptlang/skript/util/Task.java @@ -15,21 +15,28 @@ public final class Task implements Executable, Completable { // tod private final Object variables; private final Consumer runner; + private final boolean autoComplete; private transient final CountDownLatch latch = new CountDownLatch(1); private transient volatile boolean ready; - public Task(Object variables, Consumer runner) { + public Task(boolean autoComplete, Object variables, Consumer runner) { this.variables = variables; this.runner = runner; + this.autoComplete = autoComplete; } @Override public Void execute(Event event, Object... arguments) { TaskEvent here = new TaskEvent(this); Variables.setLocalVariables(here, variables); - this.runner.accept(here); - Variables.removeLocals(here); + try { + this.runner.accept(here); + } finally { + if (autoComplete) + this.complete(); + Variables.removeLocals(here); + } return null; } From ad3278d0d76436d3f7b7b2e72744fee8e42f0d9b Mon Sep 17 00:00:00 2001 From: Moderocky Date: Thu, 16 Jan 2025 22:07:29 +0000 Subject: [PATCH 05/13] Support for the current task. --- .../njol/skript/expressions/ExprSecTask.java | 22 +++++++++++++++---- .../java/org/skriptlang/skript/util/Task.java | 2 +- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/main/java/ch/njol/skript/expressions/ExprSecTask.java b/src/main/java/ch/njol/skript/expressions/ExprSecTask.java index 240abe3f94b..15393c1c2d3 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprSecTask.java +++ b/src/main/java/ch/njol/skript/expressions/ExprSecTask.java @@ -24,11 +24,12 @@ public class ExprSecTask extends SectionExpression { if (TestMode.ENABLED) Skript.registerExpression(ExprSecTask.class, Task.class, ExpressionType.SIMPLE, "[an] auto[matic|[ |-]completing] task", - "[a] [new] task" + "[a] [new] task", + "the [current] task" ); } - protected boolean automatic; + protected boolean automatic, current; @Override public boolean init(Expression[] expressions, @@ -39,20 +40,29 @@ public boolean init(Expression[] expressions, @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.automatic = pattern == 0; 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) + new Task(automatic, variables, this::runSection) }; } @@ -68,6 +78,10 @@ public Class getReturnType() { @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/org/skriptlang/skript/util/Task.java b/src/main/java/org/skriptlang/skript/util/Task.java index 8cd8cc8517a..f160e28a0e3 100644 --- a/src/main/java/org/skriptlang/skript/util/Task.java +++ b/src/main/java/org/skriptlang/skript/util/Task.java @@ -80,7 +80,7 @@ public boolean isComplete() { return ready; } - private static class TaskEvent extends Event { + public static class TaskEvent extends Event { private final Task task; From 4f01d2f7e4869b102587d63429dd4e6ab8230f2e Mon Sep 17 00:00:00 2001 From: Moderocky Date: Thu, 16 Jan 2025 22:11:18 +0000 Subject: [PATCH 06/13] Basic class infos. --- .../njol/skript/classes/data/SkriptClasses.java | 15 +++++++++++++++ .../java/ch/njol/skript/effects/EffComplete.java | 1 + 2 files changed, 16 insertions(+) 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..ea8ec0df558 100644 --- a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java @@ -32,6 +32,7 @@ import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.script.Script; import org.skriptlang.skript.util.Executable; +import org.skriptlang.skript.util.Task; import java.io.File; import java.io.StreamCorruptedException; @@ -846,6 +847,20 @@ public String toVariableNameString(final Script script) { .examples("run {_function} with arguments 1 and true") .since("2.10")); + Classes.registerClass(new ClassInfo<>(Task.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<>(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")); + Classes.registerClass(new ClassInfo<>(DynamicFunctionReference.class, "function") .user("functions?") .name("Function") diff --git a/src/main/java/ch/njol/skript/effects/EffComplete.java b/src/main/java/ch/njol/skript/effects/EffComplete.java index 5734cf6fe1b..26960b91951 100644 --- a/src/main/java/ch/njol/skript/effects/EffComplete.java +++ b/src/main/java/ch/njol/skript/effects/EffComplete.java @@ -10,6 +10,7 @@ import org.skriptlang.skript.util.Completable; // todo doc +// todo expr public class EffComplete extends Effect { static { From 5928c646379c3f1326bfd8b779fae62292f09f4a Mon Sep 17 00:00:00 2001 From: Moderocky Date: Thu, 16 Jan 2025 22:11:23 +0000 Subject: [PATCH 07/13] First test. --- .../ch/njol/skript/classes/data/SkriptClasses.java | 3 ++- .../ch/njol/skript/effects/IndeterminateDelay.java | 14 +++++++------- .../skript/tests/syntaxes/expressions/ExprTask.sk | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 src/test/skript/tests/syntaxes/expressions/ExprTask.sk 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 ea8ec0df558..d702aa8a066 100644 --- a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java @@ -31,6 +31,7 @@ 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; @@ -847,7 +848,7 @@ public String toVariableNameString(final Script script) { .examples("run {_function} with arguments 1 and true") .since("2.10")); - Classes.registerClass(new ClassInfo<>(Task.class, "completable") + Classes.registerClass(new ClassInfo<>(Completable.class, "completable") .user("completables?") .name("Completable") .description("Something that can be completed (e.g. a task).") diff --git a/src/main/java/ch/njol/skript/effects/IndeterminateDelay.java b/src/main/java/ch/njol/skript/effects/IndeterminateDelay.java index 5179d20fd36..de6d1dd4ca6 100644 --- a/src/main/java/ch/njol/skript/effects/IndeterminateDelay.java +++ b/src/main/java/ch/njol/skript/effects/IndeterminateDelay.java @@ -12,8 +12,8 @@ /** * @author Peter Güttinger */ -public class IndeterminateDelay extends Delay { - +public class IndeterminateDelay extends Delay { // todo remove??? + @Override @Nullable protected TriggerItem walk(Event event) { @@ -24,13 +24,13 @@ protected TriggerItem walk(Event event) { if (next != null && Skript.getInstance().isEnabled()) { // See https://github.com/SkriptLang/Skript/issues/3702 Delay.addDelayedEvent(event); - Timespan duration = this.duration.getSingle(event); + Timespan duration = (Timespan) this.target.getSingle(event); // todo 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"); @@ -44,10 +44,10 @@ protected TriggerItem walk(Event event) { return null; } - + @Override public String toString(@Nullable Event event, boolean debug) { return "wait for operation to finish"; } - + } 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..52d56695518 --- /dev/null +++ b/src/test/skript/tests/syntaxes/expressions/ExprTask.sk @@ -0,0 +1,14 @@ +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" + run {_task} + assert {ExprTask} is true with "task didn't run" + + delete {ExprTask} From da2fbd8013d192897e002c4f5cbbb84dbde024f2 Mon Sep 17 00:00:00 2001 From: Moderocky Date: Fri, 17 Jan 2025 09:10:21 +0000 Subject: [PATCH 08/13] Remove the forbidden syntax. --- .../skript/effects/IndeterminateDelay.java | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 src/main/java/ch/njol/skript/effects/IndeterminateDelay.java 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 de6d1dd4ca6..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 { // todo remove??? - - @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 = (Timespan) this.target.getSingle(event); // todo - 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"; - } - -} From b81853d2d3694caae71160f29dcc63476398ed00 Mon Sep 17 00:00:00 2001 From: Moderocky Date: Fri, 17 Jan 2025 09:10:36 +0000 Subject: [PATCH 09/13] Add cancellation and completion systems. --- .../classes/data/DefaultConverters.java | 15 ++++++ .../skript/classes/data/SkriptClasses.java | 8 ++++ .../njol/skript/conditions/CondCancelled.java | 39 ++++++++++----- .../njol/skript/conditions/CondComplete.java | 48 +++++++++++++++++++ .../java/ch/njol/skript/effects/Delay.java | 24 ++++++++-- .../ch/njol/skript/effects/EffCancelTask.java | 45 +++++++++++++++++ .../java/ch/njol/skript/lang/Variable.java | 15 +----- .../java/ch/njol/skript/sections/SecLoop.java | 5 ++ .../skriptlang/skript/util/Cancellable.java | 10 ++++ .../java/org/skriptlang/skript/util/Task.java | 40 +++++++++++++--- .../tests/syntaxes/expressions/ExprTask.sk | 19 ++++++++ 11 files changed, 234 insertions(+), 34 deletions(-) create mode 100644 src/main/java/ch/njol/skript/conditions/CondComplete.java create mode 100644 src/main/java/ch/njol/skript/effects/EffCancelTask.java create mode 100644 src/main/java/org/skriptlang/skript/util/Cancellable.java 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 d702aa8a066..56e13144e41 100644 --- a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java @@ -25,6 +25,7 @@ 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; @@ -855,6 +856,13 @@ public String toVariableNameString(final Script script) { .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") 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 09b1979d7ab..ec7bacce6d0 100644 --- a/src/main/java/ch/njol/skript/effects/Delay.java +++ b/src/main/java/ch/njol/skript/effects/Delay.java @@ -77,6 +77,9 @@ public boolean init(Expression[] expressions, int pattern, Kleenean delayed, 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); @@ -92,9 +95,14 @@ public boolean init(Expression[] expressions, int pattern, Kleenean delayed, // Back up local variables Object variables = Variables.removeLocals(event); - Bukkit.getScheduler().scheduleSyncDelayedTask(Skript.getInstance(), + 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 + 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; } @@ -106,6 +114,7 @@ public boolean init(Expression[] expressions, int pattern, Kleenean delayed, // Back up local variables Object variables = Variables.removeLocals(event); + Task currentTask = getCurrentTask(event); Bukkit.getScheduler().runTaskAsynchronously(Skript.getInstance(), () -> { if (timeout != null) { @@ -114,8 +123,11 @@ public boolean init(Expression[] expressions, int pattern, Kleenean delayed, } else { task.await(); } - Bukkit.getScheduler().runTask(Skript.getInstance(), - () -> reschedule(start, variables, next, event)); + 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; } @@ -174,4 +186,8 @@ 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/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/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/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/Task.java b/src/main/java/org/skriptlang/skript/util/Task.java index f160e28a0e3..3bd56421249 100644 --- a/src/main/java/org/skriptlang/skript/util/Task.java +++ b/src/main/java/org/skriptlang/skript/util/Task.java @@ -6,24 +6,32 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; // todo doc -public final class Task implements Executable, Completable { // todo change to context with context api +public final class Task implements Executable, Completable, Cancellable { // todo change to context with context api - private final Object variables; - private final Consumer runner; + private transient final Object variables; + private transient final Consumer runner; private final boolean autoComplete; private transient final CountDownLatch latch = new CountDownLatch(1); - private transient volatile boolean ready; + private transient volatile boolean 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(boolean autoComplete, Object variables, Consumer runner) { this.variables = variables; this.runner = runner; this.autoComplete = autoComplete; + this.cancellationSteps = new ArrayList<>(8); } @Override @@ -48,10 +56,16 @@ 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 (this.ready) return true; + if (ready || cancelled) return true; return latch.await(timeout, unit); } catch (InterruptedException e) { return false; @@ -61,7 +75,7 @@ public boolean await(long timeout, TimeUnit unit) { @Contract(pure = false) public boolean await() { try { - if (this.ready) return true; + if (ready || cancelled) return true; this.latch.await(); return true; } catch (InterruptedException e) { @@ -80,6 +94,20 @@ 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; diff --git a/src/test/skript/tests/syntaxes/expressions/ExprTask.sk b/src/test/skript/tests/syntaxes/expressions/ExprTask.sk index 52d56695518..9f29088a735 100644 --- a/src/test/skript/tests/syntaxes/expressions/ExprTask.sk +++ b/src/test/skript/tests/syntaxes/expressions/ExprTask.sk @@ -8,7 +8,26 @@ test "basic tasks": 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} From 49c25e83e1e6607468b8bc06f5c2743a5c2f894b Mon Sep 17 00:00:00 2001 From: Moderocky Date: Fri, 17 Jan 2025 09:44:50 +0000 Subject: [PATCH 10/13] Abort task restarting. --- .../java/org/skriptlang/skript/util/Task.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/skriptlang/skript/util/Task.java b/src/main/java/org/skriptlang/skript/util/Task.java index 3bd56421249..e41ac994164 100644 --- a/src/main/java/org/skriptlang/skript/util/Task.java +++ b/src/main/java/org/skriptlang/skript/util/Task.java @@ -20,7 +20,7 @@ public final class Task implements Executable, Completable, Cancell private final boolean autoComplete; private transient final CountDownLatch latch = new CountDownLatch(1); - private transient volatile boolean ready, cancelled; + 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. @@ -36,6 +36,8 @@ public Task(boolean autoComplete, Object variables, Consumer runner) { @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 { @@ -48,6 +50,19 @@ public Void execute(Event event, Object... arguments) { 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; } From ee0f325881405aaaab9ff22efd7b73c25f8d4478 Mon Sep 17 00:00:00 2001 From: Moderocky Date: Fri, 17 Jan 2025 09:56:13 +0000 Subject: [PATCH 11/13] Make while loops respect cancellation. --- src/main/java/ch/njol/skript/sections/SecWhile.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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); From d96e7446979067c07e0211280d1d138be47a08e4 Mon Sep 17 00:00:00 2001 From: Moderocky Date: Fri, 17 Jan 2025 10:17:23 +0000 Subject: [PATCH 12/13] Add nothing effect. --- .../skript/classes/data/SkriptClasses.java | 4 ++- .../java/ch/njol/skript/effects/Pass.java | 34 +++++++++++++++++++ .../java/org/skriptlang/skript/util/Task.java | 31 +++++++++++++++-- 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 src/main/java/ch/njol/skript/effects/Pass.java 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 56e13144e41..a0554bfc02b 100644 --- a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java @@ -868,7 +868,9 @@ public String toVariableNameString(final Script script) { .name("Task") .description("A task is an executable section of code. Other triggers can wait for its completion.") .examples("run {_task}") - .since("INSERT VERSION")); + .since("INSERT VERSION") + .serializer(new YggdrasilSerializer<>()) + ); Classes.registerClass(new ClassInfo<>(DynamicFunctionReference.class, "function") .user("functions?") 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/org/skriptlang/skript/util/Task.java b/src/main/java/org/skriptlang/skript/util/Task.java index e41ac994164..d744601e724 100644 --- a/src/main/java/org/skriptlang/skript/util/Task.java +++ b/src/main/java/org/skriptlang/skript/util/Task.java @@ -1,6 +1,7 @@ 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; @@ -8,16 +9,18 @@ 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 { // todo change to context with context api +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 final boolean autoComplete; + private boolean autoComplete; private transient final CountDownLatch latch = new CountDownLatch(1); private volatile boolean started, ready, cancelled; @@ -27,6 +30,12 @@ public final class Task implements Executable, Completable, Cancell */ 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; @@ -41,7 +50,8 @@ public Void execute(Event event, Object... arguments) { TaskEvent here = new TaskEvent(this); Variables.setLocalVariables(here, variables); try { - this.runner.accept(here); + if (runner != null) + this.runner.accept(here); } finally { if (autoComplete) this.complete(); @@ -143,4 +153,19 @@ public HandlerList getHandlers() { } + @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); + } + } From 367a1d2424872691dbb5d5f39bfcaaa610072cc6 Mon Sep 17 00:00:00 2001 From: Moderocky Date: Fri, 17 Jan 2025 11:14:56 +0000 Subject: [PATCH 13/13] Add activity check. --- .../ch/njol/skript/conditions/CondActive.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/main/java/ch/njol/skript/conditions/CondActive.java 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"); + } + +}