diff --git a/src/main/java/ch/njol/skript/effects/Delay.java b/src/main/java/ch/njol/skript/effects/Delay.java index 16f4c50780f..e94a75898fb 100644 --- a/src/main/java/ch/njol/skript/effects/Delay.java +++ b/src/main/java/ch/njol/skript/effects/Delay.java @@ -1,17 +1,18 @@ package ch.njol.skript.effects; import ch.njol.skript.Skript; +import ch.njol.skript.config.SectionNode; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.EffectSection; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.Literal; 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.lang.util.SectionUtils; import ch.njol.skript.util.Timespan; import ch.njol.skript.variables.Variables; import ch.njol.util.Kleenean; @@ -20,29 +21,49 @@ import org.jetbrains.annotations.Nullable; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.WeakHashMap; @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.", + "When used as an effect, all code after the wait runs once the delay elapses. The whole trigger pauses until the wait finishes.", + "When used as a section, only the code within the section is deferred. Code after the section continues immediately, and the body runs on the same event once the delay elapses. This is useful for scheduling follow-up work without blocking the rest of the trigger.", + "Note that delays are not persistent. For example, ban player → wait 7 days → unban player will not resume if the server restarts during the delay.", + "Inside a section body, outer loop-values are not available because the body is parsed as a separate trigger. Event values (like player) still work. If you need a value from before the delay, copy it to a local variable. Local variables are snapshotted when the section is scheduled, so subsequent changes aren't reflected." +}) @Example("wait 2 minutes") @Example("halt for 5 minecraft hours") @Example("wait a tick") -@Since("1.4") -public class Delay extends Effect { +@Example(""" + # Section form: outer continues immediately, body runs 3 seconds later. + send "hello" to player + wait 3 seconds: + send "...and goodbye" to player + """) +@Example(""" + send "This runs first!" to player + wait 1 seconds: + send "This runs third! (one second later)" to player + send "This runs second!" to player + """) +@Since("1.4, INSERT VERSION (Delayed Sections)") +public class Delay extends EffectSection { static { - Skript.registerEffect(Delay.class, "(wait|halt) [for] %timespan%"); + Skript.registerSection(Delay.class, "(wait|halt) [for] %timespan%"); } @SuppressWarnings("NotNullFieldNotInitialized") protected Expression duration; + private @Nullable Trigger trigger; + @SuppressWarnings({"unchecked", "null"}) @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - getParser().setHasDelayBefore(Kleenean.TRUE); - + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult, + @Nullable SectionNode sectionNode, @Nullable List triggerItems) { duration = (Expression) exprs[0]; if (duration instanceof Literal) { // If we can, do sanity check for delays Timespan timespan = ((Literal) duration).getSingle(); @@ -56,58 +77,69 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye } } + if (hasSection()) { + assert sectionNode != null; + // Parse the body under the outer event context so event values still resolve inside it. + // Type hints are propagated via SectionUtils; locals are isolated at runtime via the swap dance in walk(). + Class[] outerEvents = getParser().getCurrentEvents(); + trigger = SectionUtils.loadDelayableLinkedCode("wait", (beforeLoading, afterLoading) -> { + Runnable bodyBefore = () -> { + beforeLoading.run(); + getParser().setHasDelayBefore(Kleenean.TRUE); + }; + return loadCode(sectionNode, "wait", bodyBefore, afterLoading, outerEvents); + }); + // Outer trigger is NOT delayed - code after the section runs immediately. + return trigger != null; + } + + getParser().setHasDelayBefore(Kleenean.TRUE); return true; } @Override - @Nullable - protected TriggerItem walk(Event event) { + protected @Nullable 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 - - Timespan duration = this.duration.getSingle(event); - if (duration == null) - return null; - - // Back up local variables - Object localVars = Variables.removeLocals(event); - + + if (!Skript.getInstance().isEnabled()) // See https://github.com/SkriptLang/Skript/issues/3702 + return trigger != null ? super.walk(event, false) : null; + + Timespan duration = this.duration.getSingle(event); + if (duration == null) + return trigger != null ? super.walk(event, false) : null; + + long ticks = Math.max(duration.getAs(Timespan.TimePeriod.TICK), 1); // Minimum delay is one tick, less than it is useless! + + TriggerItem afterDelay = trigger != null ? trigger : getNext(); + if (afterDelay != null) { + boolean isSection = trigger != null; + Object localVars = isSection ? Variables.copyLocalVariables(event) : Variables.removeLocals(event); + Bukkit.getScheduler().scheduleSyncDelayedTask(Skript.getInstance(), () -> { - addDelayedEvent(event); + if (!isSection) + addDelayedEvent(event); 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(afterDelay, event); - 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! + }, ticks); } - return null; - } - @Override - protected void execute(Event event) { - throw new UnsupportedOperationException(); + if (trigger != null) + return super.walk(event, false); + return null; } @Override public String toString(@Nullable Event event, boolean debug) { return "wait for " + duration.toString(event, debug) + (event == null ? "" : "..."); } - + private static final Set DELAYED = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>())); diff --git a/src/main/java/ch/njol/skript/lang/EffectSectionEffect.java b/src/main/java/ch/njol/skript/lang/EffectSectionEffect.java index 985c42ab260..49bac861189 100644 --- a/src/main/java/ch/njol/skript/lang/EffectSectionEffect.java +++ b/src/main/java/ch/njol/skript/lang/EffectSectionEffect.java @@ -49,6 +49,16 @@ public TriggerItem setNext(@Nullable TriggerItem next) { return effectSection.getNext(); } + @Override + public @Nullable TriggerItem getActualNext() { + return effectSection.getActualNext(); + } + + @Override + public @Nullable ExecutionIntent executionIntent() { + return effectSection.executionIntent(); + } + @Override public String toString(@Nullable Event event, boolean debug) { return effectSection.toString(event, debug);