-
Notifications
You must be signed in to change notification settings - Fork 0
Async
This page is about doing things without freezing the server. A lambda can run off the main thread, a trigger can pause and wait for something to happen, and a watcher can poll a value or a condition for you so you never write the loop yourself.
The golden rule, up front: a lambda you run in the background must be pure. No Bukkit API. No blocks, entities, inventories, or players. Touching the server off the main thread will throw. Stick to plain computation (and I/O that has no Bukkit API), and read any values you need on the main thread before the work starts.
When a background lambda breaks this rule, the future it belongs to fails: %future% has failed becomes true, result of stays nothing, and failure reason of tells you what went wrong. A wait for on it runs its on failure: block instead of the body.
Changed in 1.4.0. A background lambda that errors now fails its future. Before, it quietly resolved to nothing and was indistinguishable from a lambda that returned nothing. This is the one intentional behaviour break in 1.4.0.
A future is a promise of a value that isn't ready yet. You start some work now, get a future back right away, and read the answer out of it later.
future of calling lambda %lambda% [with %objects%] runs the lambda on a background thread immediately and hands you a future for its result. Your trigger keeps going; the work happens off to the side.
set {_hash} to lambda (payload: string) -> string:
return {_payload} hashed with SHA-256 # pure compute, safe off-thread
set {_f} to future of calling lambda {_hash} with "hello"The arguments are read on the main thread before the work starts, so it's safe to pass things like {_p}'s name. What the lambda does with them must still be Bukkit-free.
result of %future% is the resolved value. It's nothing for a future that isn't done yet, or one that failed. Read it once the future is guaranteed resolved, normally right after a wait for.
wait for {_f}:
send "sha256 = %result of {_f}%" to playera new future is an empty, unresolved future. Nothing is computing in the background; you decide when it's done. This is the bridge for pulling a callback, or another thread, back into a waiting trigger.
set {_token} to a new future
# ... hand {_token} to something that will finish later ...complete %future% with %object% resolves one or more futures with a value, and wakes any trigger that's waiting on them.
complete {_token} with "abc-123"complete %future% with no value resolves it with nothing. A future that is already resolved is left untouched, so a late second complete is harmless.
fail %future% [with %text%] is the other half of complete: it settles the future as failed and wakes every trigger waiting on it into its on failure: block. Use it when the answer can't be produced.
set {_f} to a new future
listen for quit where event-player is {_p}:
owner: {_p}
triggers: 1
on trigger:
fail {_f} with "player disconnected"reject %future% is the same thing under another name. The same immutability rule applies as complete: a future that is already settled (resolved or failed) is left untouched, so the first outcome wins and a late fail is harmless.
Leave off with ... and the reason defaults to future failed, so a failed future always has a reason to read.
failure reason of %future% is the text explaining a failure. It's nothing for a future that is still pending or that resolved successfully, so it doubles as a failure test.
if {_f} has failed:
send "<red>%failure reason of {_f}%" to console- Failed with
fail ... with "reason"→ that reason. - Failed because a background lambda errored → the error text. Skript logs the real exception to the console itself, so the reason here is a short generic note pointing you there.
error of, failure of, and exception of all work too.
Added in 1.4.0.
Sometimes you know the answer immediately but the caller still expects a future. These two build one that's already settled:
set {_hit} to a completed future with "from cache" # already resolved
set {_miss} to a failed future with "no such player" # already failedThat's what makes a "sometimes cached, sometimes async" helper uniform: the caller always gets a future and can always wait for it, whether or not any work actually happened.
set {_lookup} to lambda (p: player) -> object:
if {cache::%uuid of {_p}%} is set:
return a completed future with {cache::%uuid of {_p}%}
return future of calling lambda {_slow-lookup} with {_p}a resolved future with no value is an already-done future carrying nothing. Added in 1.4.0.
%future% then %lambda% returns a new future that resolves with the lambda applied to the original's result. The source future is untouched and can still be awaited on its own.
set {_hash} to lambda (text: string) -> string:
return {_text} hashed with SHA-256
set {_shorten} to lambda (h: string) -> string:
return first 8 characters of {_h}
set {_full} to future of calling lambda {_hash} with "payload"
set {_short} to {_full} then {_shorten}- If the source fails, the chained future fails with the same reason and the lambda never runs.
- If the lambda itself errors, the chained future fails with that.
- The lambda runs on the same background pool, so the same purity rule applies: no Bukkit API.
- Chain as far as you like; each
thenadds a stage that runs once the previous one resolves.
chained with is the same thing. Note it is not spelled mapped with: that form belongs to list mapping. Added in 1.4.0.
result of alone can't tell "not done yet" apart from "done, but the value is nothing". These conditions can, and each has a negated form:
| Condition | True when… |
|---|---|
future %future% is done (also resolved / completed) |
the future has finished |
future %future% is pending |
it hasn't finished yet |
future %future% has failed |
it was failed, or the background job errored |
The leading future is optional (future of %future% works too), but write it if you also run SkBee. SkBee defines %advancementprogress% is done and claims that wording first, so a plain if {_f} is done: quietly becomes an advancement check and answers nonsense. {_f} is resolved and {_f} is completed were never in conflict, so those are the shortest safe way to ask. Added in 1.5.0.
if future {_token} is pending:
send "not ready yet" to player
if {_f} has failed:
send "background job blew up: %failure reason of {_f}%" to playerA wait for block suspends the trigger, not the server. The server keeps ticking while the trigger sleeps; when the thing you waited for happens, the body runs. Locals you set inside the block (in either branch) carry on to the code after it.
Wherever a wait for takes a time cap below, within %timespan%, for at most %timespan%, and for up to %timespan% all mean the same thing. This page uses within; the Examples often use for at most.
wait for %future% [within %timespan%]: pauses until the future resolves, then runs the body, where result of %future% is ready to read.
wait for {_f} within 5 seconds:
send "sha256 = %result of {_f}%" to player
on failure:
send "hashing failed: %failure reason of {_f}%" to player
on timeout:
send "hash took too long" to player-
within %timespan%caps the wait. If the time runs out first, the optionalon timeout:block runs instead of the main body. - Without
within, it waits as long as it takes. - The optional
on failure:block runs if the future fails. It needs no timeout.
Both on failure: and on timeout: are nested inside the wait for block, level with the body, not dedented next to it. Locals set in whichever branch runs carry on past the block, the same as the main body.
Leave on failure: off and a failure falls through to the main body, where result of reads as nothing. That's how it behaved before 1.4.0, so old scripts keep working.
on failure: was added in 1.4.0.
wait for all of {_a} and {_b} within 10 seconds: # when-all: every future resolved
send "both done: %result of {_a}% and %result of {_b}%" to player
on failure:
send "one of them broke" to player
wait for any of {_jobs::*}: # the first one to succeed
send "first result in" to player
on failure:
send "every job failed" to playerwait for all of continues once every future has resolved; wait for any of continues as soon as the first one does.
When failures are involved the two differ, and it's worth knowing which you want:
| Form | Body runs when |
on failure: runs when |
|---|---|---|
all of |
every future resolved | the first failure (it short-circuits, without waiting for the rest) |
any of |
the first future to succeed | every future has failed |
So any of ignores failures as long as something eventually succeeds.
wait for next <event> [where <condition>] [within %timespan%]: pauses the trigger until that event fires, then runs the body with the event's values in scope, so message works for chat, event-block for block break, and so on.
send "type 'yes' in chat within 15 seconds." to player
wait for next chat where player is sender within 15 seconds:
if message is "yes":
set {_ok} to true
on timeout:
set {_ok} to false- A trailing
where <condition>filters which event you're waiting for; non-matching events are ignored and the wait continues. -
within %timespan%caps it, withon timeout:for the no-show. - The event has already dispatched by the time the body runs, so you can't
cancelit or change it there.
A watcher re-reads something on a timer and runs your code only when it matters. A watcher is a listener: pause, resume, unregister, an owner:, and /sklambda listeners all work on it the same way.
watch %value% every %timespan%: re-reads the expression on a timer and runs on change: only when it differs from the last reading. Inside on change:, old value and new value hold the previous and current readings.
watch (food level of player) every 1 second within 2 minutes:
on change:
send action bar "food: %old value% -> %new value%" to player
on timeout:
send "stopped watching food" to player
on end:
send action bar "" to player-
within %timespan%gives the watcher a lifetime and unlockson timeout:. -
on end:runs whenever the watcher stops, for teardown, just like on any listener. - Add
owner: %owner%so it stops itself when the owner goes away (see Scoping to an owner).
You can also watch a lambda instead of a bare expression: watch %lambda% for %args% every %timespan%: calls the lambda with those arguments on each poll and compares the result. It's the same change detection, handy when the value needs a computation you've already wrapped in a lambda.
watch when <condition> every %timespan%: is edge-triggered. It polls the condition and fires only on the transitions:
| Block | Fires when the condition goes… |
|---|---|
on rising: |
false → true |
on falling: |
true → false |
watch when (health of player < 6) every 1 second within 10 minutes:
on rising:
send "low health!" to player
on falling:
send "back to safe health" to playerBy default the state at the first poll is the baseline, so a condition that is already true when the watcher starts won't fire on rising: until it goes false and then true again. This is the clean way to say "do X the first time Y becomes true" with no poll loop of your own.
That baseline rule is the usual reason a watcher "doesn't work": the thing you're watching was already true when you started watching. The initial: entry declares the state to assume before the first poll, so the first poll is already a comparison.
watch when (health of player < 6) every 1 second:
initial: false
on rising:
send "low health!" to player # fires immediately if already hurt-
initial: false→ an already-true condition fireson rising:on the very first poll. -
initial: true→ the mirror image, foron falling:. - Leave the entry off and you get the baseline behaviour above, unchanged.
Added in 1.4.0.
| Want | Syntax |
|---|---|
| start a lambda in the background | future of calling lambda %lambda% [with %objects%] |
| an empty promise to resolve yourself | a new future |
| one that is already finished |
a completed future [with %object%] / a failed future [with %text%]
|
| read a resolved value | result of %future% |
| resolve a promise | complete %future% [with %object%] |
| break a promise |
fail %future% [with %text%] (also reject) |
| why it broke |
failure reason of %future% (also error / failure / exception) |
| chain a lambda onto it |
%future% then %lambda% (also chained with) |
| is it finished / pending / broken |
future %future% is done / is pending / has failed
|
| wait for one future | wait for %future% [within %timespan%]: |
| wait for several |
wait for all of %futures%: / wait for any of %futures%:
|
| handle a failed wait |
on failure: nested in the wait for block |
| wait for an event | wait for next <event> [where <cond>] [within %timespan%]: |
| poll a value for changes |
watch %value% every %timespan% [within %timespan%]: + on change:
|
| poll a condition for edges |
watch when <cond> every %timespan% [within %timespan%]: + on rising: / on falling:
|
| fire a watcher on the first poll |
initial: %boolean% entry on watch when
|
| find futures that never resolved | /sklambda futures |
For full working scripts, see Examples. For the listener machinery a watcher inherits (owners, pause/resume, /sklambda listeners), see Listeners. For hunting futures that never settle, see Configuration.
Guides
Reference





