-
Notifications
You must be signed in to change notification settings - Fork 0
Listeners
A listener is a temporary event listener. It only listens for a while, then turns itself off.
Normal Skript event blocks (on damage:, on chat:, etc.) listen forever and run for every player. A skLambda listener is the opposite: it belongs to one situation (one command run, one player, one fight), and it ends on its own.
A watcher (
watch … every, added in 1.3.0) is also a listener and shares everything on this page: owners, pause/resume,unregister, state checks, and/sklambda listeners. It polls a value or condition instead of reacting to an event. See Async.
listen for chat:
on trigger:
send "%message%" to consoleThis starts listening for chat right now, and runs the body each time someone chats.
But this listens forever and for everyone. That's usually not what you want. The next sections show how to limit it.
Use where to make the listener only react to events that match a rule.
You can write one rule inline:
listen for damage where victim is {_p}:
on trigger:
send "ouch" to victimOr you can use a where: section with multiple rules. The event must match all of them.
listen for block break:
where:
event-player is {_p}
event-block is diamond ore
event-player is sneaking
on trigger:
send "sneaky diamond!" to event-playerIf an event doesn't match the rules, on trigger: is not run.
countdown: ends the listener after a time limit.
listen for chat where player is {_p}:
countdown: 15 seconds
on trigger:
broadcast "[%{_p}%] %message%"
on timeout:
send "shout mode off" to {_p}When the time runs out:
-
on timeout:runs (if you wrote one). - The listener turns itself off.
triggers: ends the listener after it has fired a set number of times.
listen for chat where player is {_p}:
triggers: 3
on trigger:
send "you said: %message%" to {_p}
on completion:
send "that's enough" to {_p}When the count is reached:
-
on completion:runs (if you wrote one). - The listener turns itself off.
You can use countdown: and triggers: together. Whichever happens first wins.
owner: ties the listener to something, usually a player. When that owner leaves the server, the listener stops itself. No cleanup code needed.
listen for block break:
owner: {_p} # stops on its own when {_p} disconnects
where:
event-player is {_p}
on trigger:
send "you broke %event-block%" to {_p}An owner also lets you clean up one owner's listeners by hand with unregister all listeners owned by {_p}. See Stopping many listeners at once.
As of 1.1.0, an owner can also be an entity, a chunk, or a world, and as of 1.5.0 an inventory. The listener cleans itself up automatically when that owner goes away:
| Owner | Stops itself when… |
|---|---|
| offline player (online players too) | the player disconnects |
| entity | it leaves the loaded world (death, despawn, or its chunk unloading) |
| chunk | the chunk unloads |
| world | the world unloads |
| inventory | the last viewer closes that menu |
Cleanup fires when the owner goes away, so a player who is already offline when you register won't trigger it on their own. The listener just runs until it ends some other way.
spawn a zombie at location of {_p}
set {_zombie} to last spawned entity
listen for damage:
owner: {_zombie} # auto-unregisters when the zombie dies or unloads
where:
victim is {_zombie}
on trigger:
send "your zombie got hit" to {_p}Each of these fires the listener's on end: with end reason = unregistered. Also fixed in 1.1.0: teleporting a player between worlds no longer wrongly stops their listeners.
cooldown: sets the smallest gap allowed between fires. Events that arrive during the cooldown are ignored: they don't run on trigger: and don't count toward triggers:.
listen for damage where victim is {_p}:
cooldown: 1 second # at most one counted hit per second
on trigger:
send "hit!" to {_p}Handy for events that can fire many times in a quick burst.
every <time>: runs a block on a repeating timer for as long as the listener is alive. Good for a live display that refreshes while the listener runs.
listen for damage where victim is {_p}:
countdown: 30 seconds
every 1 second:
send action bar "shield: %remaining countdown% left" to {_p}
on trigger:
cancel eventThe timer pauses when the listener is paused, and stops when the listener ends.
Inside a listener you can write these sub-sections.
| Block | When it runs |
|---|---|
on register: |
Once, the moment the listener starts. Its partner is on end:. |
on trigger: |
Every time the event happens and passes where. |
on completion: |
When triggers: is reached. |
on timeout: |
When countdown: runs out. Needs countdown:. |
on pause: |
When the listener is paused. See Pausing and resuming. |
on resume: |
When the listener is resumed. |
on end: |
Always, however the listener stops. Runs after the others. |
on end: is the one callback that runs no matter what: completion, timeout, cancel, unregister, or an owner leaving. It's the place for cleanup you want to happen exactly once, however things turn out.
on register: is the opposite bookend (new in 1.1.0): it runs once, the instant the listener becomes active. For a listener you save and start later, it fires on register, not when you define it. Pairing on register: (setup) with on end: (teardown) keeps both halves of a listener's lifecycle in one place.
set {watch::%{_p}%} to listener for block break where player is {_p}:
triggers: 3
on register:
send "watcher armed, break 3 blocks" to {_p}
on trigger:
send "%remaining triggers% to go" to {_p}
on end:
send "watcher gone (%end reason%)" to {_p}
register {watch::%{_p}%} # on register: fires here, not at the set aboveAll of them are optional, but a listener with no callbacks does nothing.
Inside on end:, end reason tells you how the listener stopped. It is one of:
| Reason | Means |
|---|---|
completion |
triggers: was reached. |
timeout |
countdown: ran out. |
cancelled |
cancel listener / unregister listener ran inside on trigger:. |
unregistered |
stopped from outside (unregister, owner left, or a bulk cleanup). |
listen for block break where event-player is {_p}:
countdown: 20 seconds
triggers: 5
on end:
if end reason is completion:
send "you broke all 5!" to {_p}
else if end reason is timeout:
send "out of time" to {_p}
else:
send "stopped early (%end reason%)" to {_p}You can also read it from a saved listener anywhere with end reason of {listener} or {listener}'s end reason. It is not set until the listener has ended.
You have a few extra tools that only work inside on trigger:.
Stop the listener right now. This does not fire on completion: or on timeout:, but on end: still runs (with end reason = cancelled). You can also write unregister listener, which means the same thing.
Useful for "first to reach the goal wins":
listen for block break where event-player is {_p}:
countdown: 60 seconds
on trigger:
if event-block is diamond ore:
give 1 diamond to {_p}
send "you got one!" to {_p}
cancel listener
on timeout:
send "time's up" to {_p}Ignore this one event. Don't run the rest of on trigger:. Don't count it against triggers:. Keep listening.
Useful when where: isn't enough and you need extra checks inside the body:
listen for damage where victim is {_p}:
triggers: 3
on trigger:
if damage cause is not fall:
skip trigger # only fall damage counts
send "fall %3 - remaining triggers% / 3" to {_p}How many fires are left, and how much time is left.
on trigger:
send "left: %remaining triggers% hits, %remaining countdown%" to {_p}These also work inside on completion: and on timeout:.
So far we've used listen for ..., which starts immediately. You can also save a listener in a variable and start it whenever you want.
set {chat_log} to listener for chat:
on trigger:
send "[chat] %sender%: %message%" to console
cancel event
register {chat_log}-
set {var} to listener for ...:defines it. It does not start yet. -
register {var}starts listening. -
unregister {var}stops listening.
If your script uses Skript's experimental using type hints, the saved variable is remembered as holding a listener, so Skript can warn you when you use it the wrong way. See Type hints.
Sometimes you want to clean up without tracking each listener in its own variable.
unregister the last created listener # stop the most recent one
unregister all listeners owned by {_p} # stop only the listeners owned by {_p}
unregister all listeners # stop EVERY active listenerunregister all listeners owned by ... only touches listeners with a matching owner: (see Scoping to an owner), so it's a safe way to clean up just one player's listeners.
Be careful with unregister all listeners: it stops every listener on the whole server, from every script, not just yours. None of these fire on completion or on timeout, but each stopped listener's on end: still runs (with end reason = unregistered).
To see what's currently running, use the /sklambda listeners command.
You can also pull the live listeners as a real list, to loop over, count, or feed to unregister.
all active listeners # every listener running right now, server-wide
listeners owned by {_p} # only the ones whose owner: is {_p}all active listeners is the same set /sklambda listeners shows. listeners owned by … matches only listeners that declared a matching owner:. A where player is {_p} filter does not make a listener "owned by" that player.
send "%size of all active listeners% listeners running" to player
loop listeners owned by {_p}: # same effect as unregister all listeners owned by {_p}
unregister loop-valueAdded in 1.1.0.
Three expressions tell you a listener's origin, so you can do the same leak hunting /sklambda listeners does, from a script:
| Expression | Gives back |
|---|---|
script of %listener% |
the script it was declared in, spelled the way Skript's own script spells it |
creation date of %listener% |
when it was created |
age of %listener% |
how long ago that was |
loop all active listeners:
if age of loop-value > 10 minutes:
send "<yellow>stale listener from %script of loop-value%" to console
unregister loop-valueage of counts from creation, not registration. A listener you declared with set {_x} to a listener on ... and registered later already has an age before it starts listening. (/sklambda listeners prints time since registration instead, so the two can differ for a listener that sat unregistered.)
Added in 1.4.0.
You can pause a listener. While paused:
- Events are ignored.
- The countdown is frozen (it does not count down).
- Any
everytimer stops ticking.
pause {shield}
resume {shield}Other addons also define pause/resume. If one clashes, prefix skLambda to force skLambda's version: skLambda pause {shield}.
You can also pause or resume every listener tied to an owner in one line:
pause all listeners owned by {_p} # freeze only {_p}'s listeners
resume all listeners owned by {_p} # unfreeze them againThis is the pause/resume counterpart to unregister all listeners owned by (see Stopping many listeners at once): it only pauses them, rather than stopping them for good. Added in 1.2.0.
A listener can react to being paused or resumed with two callbacks. on pause: runs the moment it pauses; on resume: runs when it picks back up. Both are optional.
set {sprint::%{_p}%} to listener for block break where player is {_p}:
countdown: 60 seconds
triggers: 15
every 1 second: # live readout while running
send action bar "%remaining triggers% blocks | %remaining countdown% left" to {_p}
on trigger:
send "nice (%remaining triggers% to go)" to {_p}
on pause: # countdown freezes here
send action bar "PAUSED: %remaining countdown% on the clock" to {_p}
send "sprint frozen for a break" to {_p}
on resume:
send "back on, go!" to {_p}
on completion:
send "all 15 done!" to {_p}
on timeout:
send "ran out of time" to {_p}
on end: # runs no matter how it stopped
send action bar "" to {_p} # clear the live bar
register {sprint::%{_p}%}Because the countdown and any every timer are held while paused, a pause for a break doesn't burn into the time limit, and the live readout naturally stops updating until you resume.
You can change a saved listener while it's running.
add 1 to {shield}'s triggers # one more hit allowed
add 10 seconds to {shield}'s countdown # 10 more seconds
add 1 second to {shield}'s cooldown # widen the debounce gap
set countdown of {shield} to 30 seconds
set triggers of {shield} to 5
set cooldown of {shield} to 0 # turn the cooldown offYou can read back the owner: and cooldown: you gave a listener.
set {_gap} to cooldown of {shield} # the cooldown:, or 0 if it has none
set {_who} to owner of {shield} # the owner:, or nothing if it has noneBoth also read in the possessive form: {shield}'s cooldown and {shield}'s owner. cooldown of is settable too, with set / add / remove (shown above); setting it to zero switches the cooldown off. Added in 1.2.0.
if {shield} is registered:
...
if {shield} is paused:
resume {shield}
if {shield} is running: # registered and not paused
...Script options: ({@name}) now expand inside a listen section, so you can keep shared values in one place and reuse them across listeners.
options:
grace: 30 seconds
debounce: 1 second
# ...later, in a command or event:
listen for damage where victim is {_p}:
countdown: {@grace}
cooldown: {@debounce}
on trigger:
cancel eventThis works in the entry values such as countdown:, cooldown:, every, and the inline where. Before 1.2.0 these were left as raw text and failed to parse. Fixed in 1.2.0.
listen for <event> [where <one condition>]:
where:
<more conditions>
countdown: <timestamp>
triggers: <number>
owner: <offline player, entity, chunk, world, inventory>
cooldown: <timestamp>
every <timestamp>:
...
on register:
...
on trigger:
...
on completion:
...
on timeout:
...
on pause:
...
on resume:
...
on end:
...Everything inside the listener is optional, but you usually want at least one callback.
For full working examples, see Examples.
Guides
Reference





