Tutorial by: eult


skLambda Tutorial

This is one big guide to skLambda, all in one place. It covers everything the addon can do.

skLambda is a Skript addon. It adds three big things. First, lambdas, which are small bits of code you can save in a variable and run later. Second, predicates, which are lambdas used as a yes or no test. Third, listeners, which are temporary event listeners that stop themselves. On top of that, it adds list operations that run a lambda over a whole list for you.

What is inside

  1. Getting started.
  2. Lambdas.
  3. Predicates.
  4. List operations.
  5. Listeners.
  6. Configuration.
  7. Full examples.
  8. Links.

1. Getting started

You need Skript on your server first. Then download skLambda from Modrinth, drop the jar into your plugins folder, and restart the server. The first time it runs, skLambda makes a config.yml file in its own folder. You can leave that file alone for now. Everything in this guide works straight away.

2. Lambdas

A lambda is a small function that you can save in a variable, pass around, and run later.

Think of it like saving a recipe in a box. The recipe does not cook anything by itself. You hand the box to someone, and they cook with it when they want to.

Making a lambda

set {_double} to lambda (n: number) -> number:
    return {_n} * 2

Here is what each part means. lambda starts a lambda. (n: number) says it takes one input called n, which must be a number. The bit right after the closing bracket says what type it gives back, here a number. return {_n} * 2 says the value it gives back is n times 2. Inside the body the inputs turn into local variables, so n becomes {_n}.

Lambdas with no return value

If the lambda just does something and gives nothing back, leave off the return type.

set {_greet} to lambda (p: player):
    send "Hello %{_p}%!" to {_p}

Lambdas with no inputs

If the lambda takes no input, leave off the round brackets.

set {_now} to lambda -> number:
    return unix timestamp of now

Inline lambdas

If the body is just one line, you can write the whole lambda on one line. Put the body right after the colon.

set {_is-op} to lambda (p: player): {_p} is op

This is handy for short lambdas. The body can be one of three things.

It can be a condition, like {_p} is op. Then the lambda gives back true or false. This kind of lambda is called a predicate, which part 3 covers.

It can be an effect, like send "hi" to {_p}. Then the lambda just does it and gives nothing back.

Or it can be a value. Then the lambda hands that value back. Write it with return, or just leave a bare expression and it is returned for you.

# a predicate, gives back yes or no
add lambda (n: number): {_n} > 0 to {positive-checks::*}

# an effect, just does something when you run it
set {_say-hi} to lambda (p: player): send "hi" to {_p}
run lambda {_say-hi} with player

# a value, both of these give back a number
set {_add} to lambda (a: number, b: number): return {_a} + {_b}
set {_double} to lambda (n: number): {_n} * 2

Returning a value from a one line lambda was added in 1.1.0. Before that, a one liner could only test something or do something. Now lambda (n): {_n} * 2 hands the value back, so inline lambdas drop straight into the list operations like mapped with.

For longer bodies, or when you need an explicit return, use the section form, which is the lambda ...: block shown earlier.

Calling a lambda

There are two ways to use a lambda once you have one.

The first is call, for when you want the value back.

set {_x} to call lambda {_double} with 5
# {_x} is now 10

There are a few ways to write this. They all mean the same thing.

set {_x} to call lambda {_double} with 5
set {_x} to invoke lambda {_double} with 5
set {_x} to calling lambda {_double} with 5
set {_x} to the result of calling lambda {_double} with 5

Pick whichever reads best in your line.

set {_t} to invoke lambda {_now}

If the lambda takes more than one input, separate them with commas.

set {_sum} to lambda (a: number, b: number) -> number:
    return {_a} + {_b}

set {_total} to call lambda {_sum} with 3, 4
# {_total} is 7

The second way is run, for when you do not care about the return value.

run lambda {_greet} with player

run is for lambdas that just do something. It throws away any return value.

Default parameter values

A parameter can carry a default with = value. When the caller leaves that argument off, the default fills in.

set {_advance} to lambda (value: number, step: number = 1) -> number:
    return {_value} + {_step}

set {_a} to call lambda {_advance} with 10        # step defaults to 1, so 11
set {_b} to call lambda {_advance} with 10, 5     # 15

Only trailing arguments can be skipped. On a two parameter lambda, call ... with 10 fills the second parameter from its default. You cannot supply the second while skipping the first. Added in 1.2.0.

Lambdas that return lambdas

A lambda can return another lambda. This lets you build small factories.

set {_make_adder} to lambda (n: number) -> object:
    set {_inner} to lambda (x: number) -> number:
        return {_x} + {_n}
    return {_inner}

set {_plus5} to call lambda {_make_adder} with 5
set {_r} to call lambda {_plus5} with 10
# {_r} is 15

The inner lambda remembers n from the outer one. So {_plus5} is now a lambda that always adds 5.

Capturing nearby variables

More generally, a lambda keeps a snapshot of the local variables ({_x}) around it from the moment you wrote it. When the lambda runs later, even from a completely different trigger, it can still read them. This is what makes the factory above work, and it was added in 1.1.0.

set {_tax} to 0.2
set {_with-tax} to lambda (price: number) -> number:
    return {_price} * (1 + {_tax})       # {_tax} is captured here

set {_total} to call lambda {_with-tax} with 100     # 120

Three things to know.

It is a copy taken at definition time. Changing {_tax} afterwards will not change what the lambda sees, and anything the lambda does to {_tax} inside its body will not leak back out.

If a parameter shares a name with a captured local, the parameter wins inside the body.

This only concerns locals ({_x}). Global variables ({x}, {-x}) were always shared across everything, and still are.

Pre-filling arguments with bound

%lambda% with %values% bound makes a new lambda with the first argument, or arguments, already filled in. It is a head start. An "add two numbers" lambda becomes "add 5 to whatever you give me".

set {_add} to lambda (a: number, b: number) -> number:
    return {_a} + {_b}

set {_add5} to {_add} with 5 bound          # a one argument lambda
set {_x} to call lambda {_add5} with 10     # 15
set {_y} to call lambda {_add5} with 100    # 105

Bind every argument and you get a zero argument lambda that just returns the answer when called.

set {_answer} to {_add} with (40, 2) bound
set {_r} to call lambda {_answer}           # 42

Added in 1.1.0.

Chaining lambdas with pipe

pipe %value% through %lambdas% runs a value through a chain of one argument lambdas, left to right. Each lambda's result is fed into the next.

set {_inc} to lambda (n: number) -> number:
    return {_n} + 1
set {_double} to lambda (n: number) -> number:
    return {_n} * 2

set {_out} to pipe 5 through {_inc}, {_double}
# inc(5) is 6, then double(6) is 12, so {_out} is 12

The lambdas come from a list, so you can build a chain up in a variable and reuse it. Anything in the list that is not a lambda is skipped, so a stray non-lambda will not break the chain. Added in 1.2.0.

Turning a function into a lambda

Already have a Skript function? You can wrap it in a lambda with function lambda "name". Now you can store it, pass it around, and call it like any other lambda.

function double(amount: number) :: number:
    return {_amount} * 2

set {_reward} to function lambda "double"

set {_x} to call lambda {_reward} with 5     # 10
run lambda {_reward} with 21                 # just runs it
add {_reward} to {_doublers::*}              # store it for later

The function is looked up by its name when the lambda runs. Inputs you pass are handed to the function in order.

Using lambdas from Java

If you write a Java plugin that talks to skLambda, you can turn a Skript lambda straight into a normal Java functional interface and call it like any other Java function: asPredicate(), asFunction(), asBiFunction(), asConsumer(), or asSupplier(). Pick the one that matches your lambda's shape, meaning its argument count and whether it returns a value. Added in 1.1.0.

Type hints

Skript has an experimental feature called type hints. When a script turns it on, Skript remembers what kind of value a local variable holds, and warns you while it parses when you use that value in a way that makes no sense.

skLambda works with this. When you write set {_x} to lambda ...:, Skript now knows {_x} holds a lambda. Listeners work the same way.

Turn it on by putting using type hints at the very top of your script.

using type hints

command /testlambda:
    trigger:
        set {_double} to lambda (n: number) -> number:
            return {_n} * 2

        set {_result} to call lambda {_double} with 5
        send "Result: %{_result}%" to sender

        # This line is a mistake. You cannot lowercase a lambda.
        # With type hints on, Skript catches it as a parse error.
        set {_bad} to {_double} in lowercase

Without type hints, the bad line still loads, because Skript treats the variable as something that could be anything. It just will not be caught early. Turning hints on is a nice way to catch slips while you write. This is a Skript feature, not a skLambda setting, and it is experimental, so it may change.

When are lambdas useful

They are useful when you want to save behavior in a variable and run it later. They help when you want a different bit of code to decide what runs. And they let you make small reusable helpers without making a full Skript function.

3. Predicates

A predicate is a lambda you use as a yes or no test. You give it a value, and it answers true or false. The easiest way to make one is an inline lambda with a condition body.

set {is-op} to lambda (p: player): {_p} is op

This lambda takes a player and gives back whether that player is op.

passes, running the test

Use passes for to run a predicate and check its answer.

set {is-op} to lambda (p: player): {_p} is op

if {is-op} passes for player:
    send "you're staff" to player

A predicate runs in its own little space, so it cannot see the variables around it. That is why you give it the value to test as an input, after the word for.

You can also write matches or holds instead of passes. They mean the same thing.

if {is-op} matches for player:
    ...

Saying no, with doesn't pass and not

To check that a test fails, you have two choices.

if {is-op} doesn't pass for player:
    send "not staff" to player

if not {is-op} passes for player:
    send "not staff" to player

Both do the same thing.

Flipping a predicate, negated

doesn't pass and not flip the answer of a single check. negated %lambda%, or negation of %lambda%, is different. It hands you back a new predicate that passes exactly when the original would fail. Because it is a predicate itself, you can store it, pass it around, and drop it into list operations.

set {_is-op} to lambda (p: player): {_p} is op
set {_not-op} to negated {_is-op}

if {_not-op} passes for player:
    send "you're not staff" to player

This shines when you filter a list, where there is no inline doesn't to reach for. Drop it into Skript's where filter.

set {_visitors::*} to all players where [{_not-op} passes for input]
send "%size of {_visitors::*}% non-op players online" to player

Added in 1.1.0. Part 4 covers how where [... passes for input] filters a list.

A list of predicates

You can keep many predicates in a list and test them together.

add lambda (p: player): {_p} is op to {is-admin::*}
add lambda (p: player): name of {_p} is "eult" to {is-admin::*}

By default, the value must pass every predicate in the list.

if {is-admin::*} passes for player:        # true only if ALL of them pass
    send "welcome, admin" to player

You can change how many must pass by putting one word in front. all of is the default, and it passes when every predicate passes. any of passes when at least one passes. none of passes when no predicate passes.

if all of {mod::*} passes for {_p}:    # every check passes
    ...

if any of {mod::*} passes for {_p}:    # at least one passes
    ...

if none of {mod::*} passes for {_p}:   # not a single one passes
    ...

Writing {mod::*} passes and all of {mod::*} passes mean exactly the same thing.

Counting quantifiers

When all, any, and none are not precise enough, name a number. These check how many predicates in the list pass. at least N of passes when N or more pass. at most N of passes when N or fewer pass. exactly N of passes when exactly N pass.

if at least 2 of {requirements::*} passes for {_p}:    # two or more hold
    send "access granted" to {_p}

if exactly 3 of {requirements::*} passes for {_p}:     # all three, no more, no less
    send "full clearance" to {_p}

if at most 0 of {requirements::*} passes for {_p}:     # not a single one
    send "you meet none of them" to {_p}

They line up with the words above. at least 1 of is any of, and at most 0 of is none of. Added in 1.2.0.

Empty lists

With all of and any of, an empty list never passes, because there is nothing that passed. With none of, an empty list always passes, because nothing passed, which is what it wants. The counting quantifiers compare against a count of zero, so at most N of and exactly 0 of pass, while at least N of, with N of 1 or more, does not.

always and never

skLambda comes with two ready to use predicates, always() and never(). They ignore whatever you give them. always() always passes, and never() never does.

if always() passes for player:     # always true
    send "everyone sees this" to player

if never() passes for player:      # always false
    send "nobody sees this" to player

They fit anywhere a predicate is expected. One handy use is a feature flag you flip by swapping a single line.

set {_gate} to always()      # change to never() to close the gate for everyone
if {_gate} passes for player:
    send "gate open" to player

Notes on predicates

A lambda that gives back anything other than true counts as not passing. You can pass more than one value after for if your predicate takes more than one input. A predicate also works as a where filter on a listener, which part 5 covers.

4. List operations

skLambda can run a lambda across a whole list for you, so you do not have to write the loop yourself. These all take a list on the left and a lambda on the right.

For filtering, counting, and finding with a predicate, you do not need a special skLambda expression. Skript's own where, number of, and first element of already do it, and the last part of this section shows how.

Changed in 1.1.1. skLambda used to ship its own filtered where ... passes, count of ... where ... passes, and first of ... where ... passes. Those duplicated what Skript core already provides, so they were removed. Combine a predicate with Skript's built-in filter instead, as shown at the end of this part. The lambda forms below are unchanged.

Map, change every element

mapped with runs a one input lambda on each element and gives back a new list the same size. Each element is replaced by what the lambda gives back.

set {_double} to lambda (n: number) -> number:
    return {_n} * 2
set {_doubled::*} to (3, 5, 2) mapped with {_double}
# {_doubled::*} is 6, 10, 4

You can also write mapped using or mapped through. If the lambda gives back nothing for an element, that element is dropped.

Reduce, fold a list to one value

reduced with combines a list down to a single value using a two input lambda. It goes from left to right. The first input is the running result, often called the accumulator, and the second input is the next element.

set {_add} to lambda (a: number, b: number) -> number:
    return {_a} + {_b}
set {_total} to (3, 5, 2) reduced with {_add}
# add(3, 5) is 8, then add(8, 2) is 10, so {_total} is 10

A one element list reduces to that element, and the lambda never runs. An empty list reduces to nothing. You can also write reduced using.

A starting value, reduced with ... from

%list% reduced with %lambda% from %start% sets what the running result begins at. Now the lambda runs once for every element, so the first call combines the start value with element 1.

set {_total} to (3, 5, 2) reduced with {_add} from 100
# add(100, 3) is 103, add(103, 5) is 108, add(108, 2) is 110, so {_total} is 110

Two things this gives you. An empty list safely returns the start value instead of nothing. And the start can be a different type than the elements, for example folding a list of items down into one piece of text. Added in 1.1.0.

Scan, every running result of a reduce

scanned with is like reduced with, but instead of only the final value it keeps every running result along the way. You get back a list, not a single value.

set {_running::*} to (3, 5, 2) scanned with {_add}
# 3, then add(3, 5) is 8, then add(8, 2) is 10, so {_running::*} is 3, 8, 10

%list% scanned with %lambda% from %start% opens from a seed. The seed comes out first, then one result per element.

set {_running::*} to (3, 5, 2) scanned with {_add} from 100
# 100, add(100, 3) is 103, add(103, 5) is 108, add(108, 2) is 110, so 100, 103, 108, 110

Handy for running balances and cumulative totals. Added in 1.2.0.

Zip, combine two lists side by side

zipped with ... using walks two lists in lockstep and combines each pair with a two input lambda. The first input is the element from the left list, the second from the right.

set {_summed::*} to (1, 2, 3) zipped with (10, 20, 30) using {_add}
# add(1, 10) is 11, add(2, 20) is 22, add(3, 30) is 33, so 11, 22, 33

It stops at the shorter list, so pairing a three element list with a five element one gives three results. Added in 1.2.0.

Sort, order by a key

sorted by orders a list using a lambda that pulls a sort key out of each element. The lambda runs once per element and gives back something you can compare, like a number or text. The list comes back ordered by those keys, lowest first.

set {_score} to lambda (p: player) -> number:
    return {_p}'s level
set {_ranked::*} to all players sorted by {_score}
# players ordered by level, lowest first

Elements whose keys cannot be compared keep their original order, so the sort is stable.

Highest and lowest, the top element by a key

highest of ... by and lowest of ... by give you back the single element with the biggest or smallest key. You write a lambda that scores each element, and you get the winning element itself, not the score.

set {_score} to lambda (p: player) -> number:
    return {_p}'s level
set {_top} to highest of all players by {_score}     # the highest level player
set {_bottom} to lowest of all players by {_score}   # the lowest level player

You can write max for highest and min for lowest. Ties keep the first one found, and an empty list gives nothing. This is cheaper than sorting the whole list when you only want the one extreme element. Added in 1.1.0.

Pages and windows, slicing a list

These cut a list into pieces of a fixed size, with no lambda needed. They are built for paging a list into a GUI.

page N of %list% by S splits the list into back to back chunks of S and hands you the Nth one, counting from 1. The last page can be shorter, and a page past the end is empty.

set {_items::*} to "a", "b", "c", "d", "e", "f", and "g"
set {_p1::*} to page 1 of {_items::*} by 3      # a, b, c
set {_p2::*} to page 2 of {_items::*} by 3      # d, e, f
set {_p3::*} to page 3 of {_items::*} by 3      # g

window N of %list% by S is like a page, but it slides one element at a time instead of jumping a whole chunk. Window 1 is elements 1 to S, window 2 is elements 2 to S+1, and so on.

set {_w1::*} to window 1 of {_items::*} by 3    # a, b, c
set {_w2::*} to window 2 of {_items::*} by 3    # b, c, d

page count of %list% by S is how many pages the list splits into, rounded up so a partial last page still counts. window count of %list% by S is length minus S plus 1. Loop over the count to fill a menu.

set {_pages} to page count of {_items::*} by 3       # 3, since 7 items split into 3, 3, 1
set {_windows} to window count of {_items::*} by 3   # 5, since 7 - 3 + 1

loop (page count of {_items::*} by 3) times:
    set {_page::*} to page loop-value of {_items::*} by 3
    # ... build GUI page loop-value from {_page::*}

Added in 1.2.0.

Filter, count, and find with a predicate

skLambda has no dedicated filter, count, or first expression, since Skript core already gives you all three. Pair a predicate with Skript's built-in filter using the input keyword, which stands for the element being tested.

Filter keeps only the elements the predicate passes for, with Skript's %list% where [...].

set {_big} to lambda (n: number): {_n} > 3
set {_kept::*} to (3, 5, 2, 8) where [{_big} passes for input]
# {_kept::*} is 5, 8

Count wraps that filter in Skript's number of.

set {_n} to number of ({_players::*} where [{_is-op} passes for input])

Find wraps it in Skript's first element of. Skript stops at the first match, so it is cheap.

set {_winner} to first element of ({_players::*} where [{_alive} passes for input])

A predicate list works too. [{_checks::*} passes for input] keeps an element only if all of them pass, the same all of rule as a bare passes check. For a doesn't match filter, reach for a negated predicate from part 3.

Quick list of operations

Here is the whole set in plain words.

  1. %list% mapped with %lambda% gives back a new list the same size, and takes a one input value lambda.
  2. %list% reduced with %lambda% [from %start%] gives back one value, and takes a two input lambda.
  3. %list% scanned with %lambda% [from %start%] gives back a list of running results, and takes a two input lambda.
  4. %list% zipped with %list2% using %lambda% gives back a new list as long as the shorter one, and takes a two input lambda.
  5. highest of %list% by %lambda% (or max) gives back one element, and takes a one input key lambda.
  6. lowest of %list% by %lambda% (or min) gives back one element, and takes a one input key lambda.
  7. %list% sorted by %lambda% gives back the list reordered, and takes a one input key lambda.

For filtering, counting, and finding, use Skript's own list tools with a predicate.

  1. Keep matching elements with %list% where [%predicate% passes for input].
  2. Count matches with number of (%list% where [%predicate% passes for input]).
  3. Find the first match with first element of (%list% where [%predicate% passes for input]).

Paging takes no lambda, just a chunk size.

  1. page N of %list% by S is the Nth non-overlapping chunk.
  2. window N of %list% by S is the Nth sliding window.
  3. page count of %list% by S is how many pages.
  4. window count of %list% by S is how many windows.

5. Listeners

A listener is a temporary event listener. It only listens for a while, then turns itself off.

Normal Skript event blocks, like on damage: or on chat:, listen forever and run for every player. A skLambda listener is the opposite. It belongs to one situation, like one command run, one player, or one fight, and it ends on its own.

The simplest listener

listen for chat:
    on trigger:
        send "%message%" to console

This starts listening for chat right now, and runs the body each time someone chats. But this listens forever and for everyone, which is usually not what you want. The next parts show how to limit it.

Filtering events with where

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 victim

Or you can use a where: section with more than one rule. 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-player

If an event does not match the rules, on trigger: does not run.

Stopping after some time, countdown

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, and the listener turns itself off.

Stopping after a number of events, triggers

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, and the listener turns itself off. You can use countdown: and triggers: together. Whichever happens first wins.

Tying it to an owner, owner

owner: ties the listener to something, usually a player. When that owner leaves the server, the listener stops itself, with 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}, which a later part covers.

As of 1.1.0, an owner can also be an entity, a chunk, or a world, and the listener cleans itself up automatically when that owner goes away. An offline or online player stops it when the player disconnects. An entity stops it when it leaves the loaded world, by death, despawn, or its chunk unloading. A chunk stops it when the chunk unloads. A world stops it when the world unloads.

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}

Cleanup fires when the owner goes away, so a player who is already offline when you register will not trigger it on their own. Each of these fires the listener's on end: with end reason set to unregistered. Also fixed in 1.1.0, teleporting a player between worlds no longer wrongly stops their listeners.

Ignoring rapid repeats, cooldown

cooldown: sets the smallest gap allowed between fires. Events that arrive during the cooldown are ignored. They do not run on trigger:, and they do not 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}

This is handy for events that can fire many times in a quick burst.

A repeating timer, every

every <time>: runs a block on a repeating timer for as long as the listener is alive. This is 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 event

The timer pauses when the listener is paused, and stops when the listener ends.

The callbacks

Inside a listener you can write these sub sections.

  1. on register: runs once, the moment the listener starts. Its partner is on end:.
  2. on trigger: runs every time the event happens and passes the where check.
  3. on completion: runs when triggers: is reached.
  4. on timeout: runs when countdown: runs out. It needs countdown:.
  5. on pause: runs when the listener is paused.
  6. on resume: runs when the listener is resumed.
  7. on end: runs always, however the listener stops, and it runs after the others.

on end: is the one callback that runs no matter what, whether by completion, timeout, cancel, unregister, or an owner leaving. It is the place for cleanup you want to happen exactly once, however things turn out.

on register: is the opposite bookend, and it is 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: for setup with on end: for 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 above

All of them are optional, but a listener with no callbacks does nothing.

Why a listener ended, end reason

Inside on end:, end reason tells you how the listener stopped. It is one of four values. completion means triggers: was reached. timeout means countdown: ran out. cancelled means cancel listener or unregister listener ran inside on trigger:. unregistered means it was stopped from outside, by unregister, an owner leaving, 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.

Inside on trigger

You have a few extra tools that only work inside on trigger:.

cancel listener stops the listener right now. This does not fire on completion: or on timeout:, but on end: still runs, with end reason set to cancelled. You can also write unregister listener, which means the same thing. This is useful for a first to reach the goal wins setup.

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}

skip trigger ignores this one event. It does not run the rest of on trigger:, it does not count the event against triggers:, and it keeps listening. This is useful when where is not 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}

remaining triggers tells you how many fires are left, and remaining countdown tells you 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:.

Saving a listener for later

So far we used listen for ..., which starts right away. 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}

The line set {var} to listener for ...: defines it, and it does not start yet. The line register {var} starts listening. The line unregister {var} stops listening. If your script uses the 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.

Stopping many listeners at once

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 listener

unregister all listeners owned by ... only touches listeners with a matching owner:, so it is 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 set to unregistered. To see what is currently running, use the /sklambda listeners command, covered in part 6.

Listing the running listeners

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-value

Added in 1.1.0.

Pausing and resuming

You can pause a listener. While it is paused, events are ignored, the countdown is frozen so it does not count down, and any every timer stops ticking.

pause {shield}
resume {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 again

This is the pause and resume counterpart to unregister all listeners owned by. 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, and 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 does not eat into the time limit, and the live readout naturally stops updating until you resume.

Changing a listener from the outside

You can change a saved listener while it is 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 off

Reading a listener's owner and cooldown

You 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 none

Both also read in the possessive form, {shield}'s cooldown and {shield}'s owner. cooldown of is settable too, with set, add, and remove, as shown above. Setting it to zero switches the cooldown off. Added in 1.2.0.

Checking a listener's state

if {shield} is registered:
    ...

if {shield} is paused:
    resume {shield}

if {shield} is running:    # registered and not paused
    ...

Script options in entries

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 event

This 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.

What goes where, a quick recap

listen for <event> [where <one condition>]:
    where:
        <more conditions>
    countdown: <time>
    triggers: <number>
    owner: <offline player, entity, chunk, or world>
    cooldown: <time>
    every <time>:
        ...
    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.

6. Configuration

skLambda makes a config.yml file in its plugin folder the first time it runs. Changes take effect when the server starts or reloads.

Turning features on and off

You can switch off a whole feature. A switched off feature adds no syntax at all. Both default to true.

# Lambdas: the lambda type, inline lambdas, default parameters, call and run, passes
# (including at-least, at-most, exactly), negated, bound, pipe, the lambda list
# operations (map, reduce, scan, zip, sort, min, max, and page/window), and always() and never().
lambda: true

# Listeners: the listen section, register, unregister, pause, resume (including by owner),
# on register and on end, remaining triggers, remaining countdown, cooldown and owner access,
# active-listener queries, and state checks.
listener: true

Filtering, counting, and finding a list use Skript core's own where, number of, and first element of with a predicate, so they are not part of this toggle. See part 4.

If you turn both off, skLambda warns you in the console and does nothing.

Update notifications

update-notifications: true

When on, skLambda checks Modrinth on startup for a newer release. If one exists, it logs a note to the console and tells op players on join, with a clickable download link. Set it to false to switch the check off. It is worth leaving on so you hear about new releases as they land.

The listener leak detector

A listener that you never stop keeps running forever. The notifier helps you find these. When it is on, skLambda warns in the console about any listener that has stayed alive longer than you allow. It is off by default to keep the console quiet.

notifier:
  enabled: false
  # How long a listener may live before it gets reported.
  warn-after: 5 minutes
  # How often to repeat the warning while it is still alive.
  warn-every: 1 minute
  # The console message. You can use these placeholders:
  #   {location} = the script and line where it was made
  #   {event}    = the event it listens for
  #   {duration} = how long it has been alive
  message: "[skLambda] Listener created in {location} ({event}) has been alive for {duration}."

Set enabled to true to switch it on. The warn after and warn every values are timespans, so you can write things like 30 seconds, 5 minutes, or 2 hours.

The /sklambda command

skLambda adds one command for admins.

/sklambda              # show the version and a link
/sklambda listeners    # list every listener that is running right now

/sklambda listeners shows, for each active listener, where it was made, the event it listens for, and how long it has been alive. This is handy for spotting one that should have stopped. There is also a short alias, /skl. By default only operators can use it. It is guarded by the sklambda.admin permission, so you can hand it to your staff with a permissions plugin.

7. Full examples

Here are a few complete commands you can paste into a .sk file and try in game. They mix the features from the parts above.

Stone mining challenge

Mine 10 stone in 30 seconds. Win a diamond, or get told off.

command /challenge:
    trigger:
        send "mine 10 stone in 30s" to player
        listen for block break where event-block is stone:
            countdown: 30 seconds
            triggers: 10
            on trigger:
                send "keep going... (%remaining triggers% left)" to event-player
            on completion:
                send "you did it!" to event-player
                give 1 diamond to event-player
            on timeout:
                send "too slow!" to event-player

First diamond wins

Break a diamond ore in 60 seconds. The first one ends the game.

command /firstdiamond:
    trigger:
        set {_p} to sender
        send "break a diamond ore within 60s to win" to {_p}
        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}

All the list operations together

Map, filter, reduce, sort, count, and first, all in one place.

command /listops:
    trigger:
        set {_p} to sender
        set {_nums::*} to 3, 5, 2, and 8

        # map: change every element
        set {_double} to lambda (n: number) -> number:
            return {_n} * 2
        set {_doubled::*} to {_nums::*} mapped with {_double}
        send "doubled: %{_doubled::*}%" to {_p}  # 6, 10, 4, 16

        # filter: keep only matching elements, with Skript's own where
        set {_big} to lambda (n: number): {_n} > 3
        set {_kept::*} to {_nums::*} where [{_big} passes for input]
        send "over 3: %{_kept::*}%" to {_p} # 5, 8

        # reduce: fold the whole list to one value
        set {_add} to lambda (a: number, b: number) -> number:
            return {_a} + {_b}
        set {_total} to {_nums::*} reduced with {_add}
        send "sum = %{_total}%" to {_p} # 18

        # count and first: same predicate, just the tally or the first hit
        send "%number of ({_nums::*} where [{_big} passes for input])% numbers over 3" to {_p}
        send "first over 3: %first element of ({_nums::*} where [{_big} passes for input])%" to {_p}

        # sort: order a list by a key the lambda pulls from each element
        set {_score} to lambda (pl: player) -> number:
            return {_pl}'s level
        set {_ranked::*} to all players sorted by {_score}    # lowest level first
        send "ranked by level: %{_ranked::*}%" to {_p}

Guaranteed cleanup with on end

on end: runs however the listener stops, so cleanup lives in exactly one place. end reason tells you why it ended.

command /session:
    trigger:
        set {_p} to sender
        set {session::%{_p}%} to true
        send "session started: break 5 blocks in 20s. /endsession to stop early." to {_p}
        set {watch::%{_p}%} to listener for block break where player is {_p}:
            countdown: 20 seconds
            triggers: 5
            on trigger:
                send "%remaining triggers% blocks left" to {_p}
            on completion:
                send "all 5 broken!" to {_p}
            on timeout:
                send "ran out of time" to {_p}
            on end:    # runs no matter how it ended
                delete {session::%{_p}%}     # single cleanup path
                if end reason is completion:
                    send "session ended, you won" to {_p}
                else if end reason is timeout:
                    send "session ended, timed out" to {_p}
                else:
                    send "session ended, %end reason%" to {_p}
        register {watch::%{_p}%}

command /endsession:
    trigger:
        set {_p} to sender
        if {watch::%{_p}%} is registered:
            unregister {watch::%{_p}%}       # on end fires with end reason = unregistered
        send "stopped your session" to {_p}

For even more worked examples, see the Examples page.

GitHub: https://github.com/ahmadmsaleem/skLambda

Modrinth: https://modrinth.com/plugin/sklambda

SkriptHub docs: http://skripthub.net/docs/?addon=skLambda

skDocs: https://skdocs.org/docs?addon=skLambda


Did you find eult's tutorial helpful?


You must be logged in to comment