Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 71 additions & 39 deletions src/main/java/ch/njol/skript/effects/Delay.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 <code>ban player → wait 7 days → unban player</code> 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 <code>wait</code> 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, <code>ban player → wait 7 days → unban player</code> will not resume if the server restarts during the delay.",
"Inside a section body, outer <code>loop-value</code>s are not available because the body is parsed as a separate trigger. Event values (like <code>player</code>) 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
""")
Comment thread
ahmadmsaleem marked this conversation as resolved.
@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<Timespan> 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<TriggerItem> triggerItems) {
duration = (Expression<Timespan>) exprs[0];
if (duration instanceof Literal) { // If we can, do sanity check for delays
Timespan timespan = ((Literal<Timespan>) duration).getSingle();
Expand All @@ -56,58 +77,69 @@ public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelaye
}
}

if (hasSection()) {
Comment thread
ahmadmsaleem marked this conversation as resolved.
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<? extends Event>[] 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);
Comment thread
ahmadmsaleem marked this conversation as resolved.
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) {
Comment thread
ahmadmsaleem marked this conversation as resolved.
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<Event> DELAYED =
Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));

Expand Down
10 changes: 10 additions & 0 deletions src/main/java/ch/njol/skript/lang/EffectSectionEffect.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down