Skip to content

Examples

Ahmad Saleem edited this page Aug 25, 2026 · 11 revisions

Examples

Real, runnable examples that mix the features from Lambdas, Predicates, List operations, Listeners and Async.

Each example is one command you can paste into a .sk file and try in-game.

1. Stone-mining challenge

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

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

What this shows:

  • triggers: counts events for you.
  • countdown: ends the listener if the player is too slow.
  • remaining triggers tells the player how much is left.

2. Shield with hits and time

You get protection for 30 seconds or 5 hits, whichever happens first. Eating a golden apple gives you more hits and more time.

command /shield:
    trigger:
        set {_p} to sender
        send "shielded for 30s or 5 hits. eat a golden apple to extend." to {_p}

        set {shield::%{_p}%} to listener for damage where victim is {_p}:
            countdown: 30 seconds
            triggers: 5
            on trigger:
                cancel event
                send "blocked! (%remaining triggers% hits, %remaining countdown% left)" to {_p}
            on completion:
                send "shield used up" to {_p}
            on timeout:
                send "shield expired" to {_p}
        register {shield::%{_p}%}

        set {shield_upgrade::%{_p}%} to listener for consume where event-player is {_p}:
            on trigger:
                if event-item is not golden apple:
                    skip trigger
                add 1 to {shield::%{_p}%}'s triggers
                add 10 seconds to {shield::%{_p}%}'s countdown
                send "shield upgraded!" to {_p}
        register {shield_upgrade::%{_p}%}

What this shows:

  • Saving a listener in a variable and starting it with register.
  • skip trigger to ignore the wrong food.
  • One listener changes another listener's triggers and countdown.

3. 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}

What this shows:

  • cancel listener to stop early when a goal is met.
  • No triggers: is needed. The listener ends either by cancel listener or by timeout.

4. Guess the number

Pick a number between 1 and 20. You get 5 guesses or 45 seconds.

command /guess:
    trigger:
        set {_p} to sender
        set {target::%{_p}%} to random integer between 1 and 20
        send "I'm thinking of a number 1-20. you have 5 guesses, 45s." to {_p}

        listen for chat where player is {_p}:
            countdown: 45 seconds
            triggers: 5
            on trigger:
                cancel event
                if message parsed as integer is not set:
                    send "not a number, try again" to {_p}
                    skip trigger
                set {_n} to message parsed as integer
                if {_n} is {target::%{_p}%}:
                    send "right! (%remaining countdown% left)" to {_p}
                    give 1 diamond to {_p}
                    cancel listener
                else if {_n} < {target::%{_p}%}:
                    send "higher, %remaining triggers% guesses left" to {_p}
                else:
                    send "lower, %remaining triggers% guesses left" to {_p}
            on completion:
                send "out of guesses (number was %{target::%{_p}%}%)" to {_p}
            on timeout:
                send "time's up (number was %{target::%{_p}%}%)" to {_p}

What this shows:

  • skip trigger to ignore invalid input without using up a guess.
  • cancel listener to end early on a win.
  • remaining triggers and remaining countdown used together.

5. Pause and resume

Mine 10 stone. After 5, the listener pauses for a 10-second break. Then it comes back.

command /milestone:
    trigger:
        set {_p} to sender
        send "break 10 stone. it pauses for 10s at the halfway point." to {_p}

        set {milestone::%{_p}%} to listener for block break where event-player is {_p}:
            countdown: 60 seconds
            triggers: 10
            on trigger:
                if remaining triggers is 5:
                    send "halfway! pausing 10s..." to {_p}
                    pause {milestone::%{_p}%}
                    wait 10 seconds
                    if {milestone::%{_p}%} is registered:
                        resume {milestone::%{_p}%}
                        send "go!" to {_p}
            on completion:
                send "all 10 done!" to {_p}
            on timeout:
                send "ran out of time" to {_p}
        register {milestone::%{_p}%}

What this shows:

  • pause and resume on a saved listener.
  • The countdown is frozen during pause, so the break doesn't eat into the time limit.
  • Checking is registered before resuming, in case the player ended things another way.

6. Admin check with predicates

A player counts as admin only if they pass every check in the list.

command /adminsetup:
    trigger:
        clear {is-admin::*}
        add lambda (p: player): {_p} is op to {is-admin::*}
        add lambda (p: player): name of {_p} is "eult" to {is-admin::*}
        send "admin checks ready" to sender

command /admincheck:
    trigger:
        if {is-admin::*} passes for player:        # true only if every check passes
            send "welcome, admin" to player
        if {is-admin::*} doesn't pass for player:  # true if any check fails
            send "not quite admin" to player

What this shows:

  • Inline predicates stored in a list.
  • passes for to test all of them at once.
  • doesn't pass for the opposite check.

7. Predicate as a listener filter

Make a predicate once, then reuse it as the where filter.

command /stonewatch:
    trigger:
        set {is-stone} to lambda (b: block): {_b} is stone
        send "break some stone..." to player
        listen for block break where {is-stone} passes for event-block:
            triggers: 5
            on trigger:
                send "stone! (%remaining triggers% to go)" to event-player
            on completion:
                send "nice, 5 stone broken" to event-player

What this shows:

  • A predicate used inside where.
  • The same idea works for any event with any value you can test.

8. Wrap a function in a lambda

Turn an existing function into a lambda you can store and pass around.

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

command /functionlambda:
    trigger:
        set {_reward} to function lambda "double"
        set {_x} to call lambda {_reward} with 5
        send "double(5) = %{_x}%" to player

What this shows:

  • function lambda "name" to reuse a function as a lambda.
  • After that, it behaves like any other lambda.

9. Cleaning up listeners

Spawn a couple of watchers, then stop them in bulk. Use /sklambda listeners to see what's alive.

command /spawnwatchers:
    trigger:
        listen for block break:
            on trigger:
                send "block broken" to player
        listen for chat:
            on trigger:
                send "chat seen" to player
        send "two watchers running, try /sklambda listeners" to player

command /undolast:
    trigger:
        unregister the last created listener
        send "stopped the last watcher" to sender

command /listenerpanic:
    trigger:
        unregister all listeners     # stops EVERY listener on the server
        send "all listeners cleared" to sender

What this shows:

  • unregister the last created listener to undo just the most recent one.
  • unregister all listeners to clear everything (server-wide, so use with care).
  • /sklambda listeners to inspect what's still running. See Configuration.

10. Owner-scoped watchers

Give each player their own watchers with owner:. They clean up on their own when the player leaves, or you can drop just one player's with owned by.

command /watchme:
    trigger:
        set {_p} to sender
        send "watching your blocks & chat, these auto-clean when you leave." to {_p}
        listen for block break:
            owner: {_p}                  # auto-unregisters when {_p} disconnects
            where:
                event-player is {_p}
            on trigger:
                send "you broke %event-block%" to {_p}
        listen for chat:
            owner: {_p}
            where:
                player is {_p}
            on trigger:
                send "you said: %message%" to {_p}

command /stopwatching:
    trigger:
        set {_p} to sender
        unregister all listeners owned by {_p}   # only this player's, others keep running
        send "stopped watching you" to {_p}

What this shows:

  • owner: to tie a listener to a player, so it ends itself when they leave.
  • unregister all listeners owned by {_p} to clean up one owner's listeners only.

11. 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}

What this shows:

  • on end: as a single teardown path, however the listener stops.
  • end reason to branch on why it ended (completion, timeout, or unregistered).

12. always() and never()

Constant predicates that ignore their input. Swap one for the other to flip a gate, or use them as list filters.

command /gatecheck:
    trigger:
        set {_p} to sender
        set {_gate} to always()          # swap to never() to close the gate for everyone
        if {_gate} passes for {_p}:
            send "gate open" to {_p}
        else:
            send "gate closed" to {_p}

        set {_kept::*} to all players where [always() passes for input]    # keeps everyone
        set {_dropped::*} to all players where [never() passes for input]  # keeps no one
        send "always() kept %size of {_kept::*}%, never() kept %size of {_dropped::*}%" to {_p}

What this shows:

  • always() / never() as drop-in predicates anywhere a predicate is expected.
  • A feature flag you flip by swapping a single line.

13. List operations

Map, reduce, and sort with a lambda; filter, count, and find with a predicate. Let the addon (and Skript core) do the looping.

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

        # map: transform 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 (Skript's own `where`, fed a predicate)
        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 / first: same predicate via Skript's `number of` and `first element of`
        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 out of 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}

What this shows:

  • One stored lambda or predicate, applied across a whole list.
  • mapped with / reduced with / sorted by take value lambdas; filtering, counting, and finding pair a predicate with Skript's own where [... passes for input]. See List operations.

14. Live display with every and cooldown

every refreshes a display on a timer while the listener runs. cooldown ignores rapid repeats so they don't count.

command /shieldbar:
    trigger:
        set {_p} to sender
        send "shield up: survive 30s or block 5 hits. rapid hits are debounced to 1/sec." to {_p}
        listen for damage where victim is {_p}:
            countdown: 30 seconds
            triggers: 5
            cooldown: 1 second                 # at most one counted hit per second
            on trigger:
                cancel event
                send "blocked! (%remaining triggers% hits left)" to {_p}
            every 1 second:                    # live action-bar readout
                send action bar "shield %remaining countdown% | %remaining triggers% hits left" to {_p}
            on completion:
                send "shield shattered, took 5 hits" to {_p}
            on timeout:
                send "shield held for the full 30s" to {_p}
            on end:
                send action bar "" to {_p}     # clear the bar however it ended

What this shows:

  • every 1 second: for a live display that pauses and stops with the listener.
  • cooldown: to debounce rapid events, so they don't fire on trigger and don't count toward triggers.
  • on end: to clear the display however the listener stopped.

15. Freezable sprint with on pause / on resume

A timed mining sprint a staff member can freeze for a break. The countdown and the live display both hold while paused, so the break doesn't eat into the clock.

command /sprint:
    trigger:
        set {_p} to sender
        send "mine 15 blocks in 60s. staff can /freeze you for a break." to {_p}
        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}%}

command /freeze <player>:
    trigger:
        if {sprint::%arg-1%} is running:
            pause {sprint::%arg-1%}               # fires on pause, countdown stops ticking
            send "froze %arg-1%'s sprint" to sender
        else if {sprint::%arg-1%} is paused:
            resume {sprint::%arg-1%}              # fires on resume
            send "unfroze %arg-1%'s sprint" to sender

What this shows:

  • on pause: / on resume: to react when the listener is frozen and unfrozen.
  • The countdown and the every timer are held during a pause, so a break doesn't burn time.
  • is running vs is paused to make /freeze toggle.

16. Owner types: entity, chunk, and world

owner: works for more than players now. A listener auto-unregisters the moment its owner leaves: an entity dies or leaves the loaded world, or a chunk/world unloads. Each watcher's on end: fires with end reason = unregistered. (Players on disconnect are covered by example 10.)

# entity owner: a watcher bound to a spawned zombie, it dies with the zombie
command /entityguard:
    trigger:
        set {_p} to sender
        spawn a zombie at location of {_p}
        set {_zombie} to last spawned entity
        send "<yellow>spawned a zombie, hit it. the watcher auto-cleans when the zombie dies." to {_p}
        listen for damage:
            owner: {_zombie}                 # auto-unregisters when the zombie leaves the world
            where:
                victim is {_zombie}
            on trigger:
                wait 1 tick                     # ensure the damage applies before we check health
                send "<aqua>your zombie took a hit (%health of {_zombie}% hp left)" to {_p}
            on end:
                send "<gray>zombie gone (%end reason%), watcher cleaned itself up" to {_p}


# chunk owner: a watcher bound to the chunk you're standing in, it unloads with the chunk
command /chunkwatch:
    trigger:
        set {_p} to sender
        set {_chunk} to chunk at location of {_p}
        send "<yellow>watching this chunk for block breaks. walk away + let it unload to auto-clean." to {_p}
        listen for block break:
            owner: {_chunk}                  # auto-unregisters when this chunk unloads
            where:
                chunk at location of event-block is {_chunk}
            on trigger:
                send "<aqua>block broken in the watched chunk" to {_p}
            on end:
                send "<gray>chunk watcher ended (%end reason%)" to {_p}


# world owner: a watcher bound to a world, it stops if that world is unloaded
command /worldwatch:
    trigger:
        set {_p} to sender
        set {_w} to world of {_p}
        send "<yellow>watching '%{_w}%' for lightning. unloading the world auto-cleans it." to {_p}
        listen for lightning strike:
            owner: {_w}                      # auto-unregisters when this world unloads
            where:
                event-world is {_w}
            on trigger:
                send "<aqua>lightning struck in %{_w}%" to {_p}
            on end:
                send "<gray>world watcher ended (%end reason%)" to {_p}

What this shows:

  • owner: accepting an entity, chunk, or world, not just a player.
  • Auto-cleanup tied to the owner's lifecycle, with on end: reporting end reason = unregistered.

17. Recursive vein miner

A bigger, real-world build: sneak-mine an ore and the whole connected vein breaks. The flood-fill is a lambda that captures the ore set from on load and recurses by passing itself as its first argument.

options:
    veinminer_enabled: true
    veinminer_max_size: 128    # safety cap on vein size, prevents runaway recursion


on load:
    if {veinminer::listener} is registered:
        unregister {veinminer::listener}

    parse if {@veinminer_enabled} is true:

        set {veinminer::ores::*} to tag values of tag "ores"
        add Andesite, Diorite and Granite to {veinminer::ores::*}

        set {_flood} to lambda (self: object, loc: location, ore: itemtype, miner: player) -> object:
            if {veinminer::mined::%{_miner}'s uuid%} >= {@veinminer_max_size}:
                return 0
            set {_min} to {_loc} offset by vector(-1, -1, -1)
            set {_max} to {_loc} offset by vector(1, 1, 1)
            loop blocks within {_min} and {_max}:
                if loop-block is {_ore}:
                    break loop-block using {_miner}'s tool     # drops respect fortune / silk touch
                    add 1 to {veinminer::mined::%{_miner}'s uuid%}
                    run lambda {_self} with {_self}, location of loop-block, {_ore}, {_miner}
            return 0

        set {veinminer::listener} to listener for block break:
            on trigger:
                if event-player is not sneaking: # hold sneak to vein-mine
                    skip trigger
                # find which configured ore was mined (canonical block-vs-itemtype match)
                delete {_ore}
                loop {veinminer::ores::*}:
                    if event-block is loop-value:
                        set {_ore} to loop-value
                if {_ore} is not set: # not a vein-mineable ore
                    skip trigger
                cancel event # we break the whole vein ourselves
                set {veinminer::mined::%event-player's uuid%} to 0
                # {_flood} is captured from `on load`; pass it to itself to kick off the recursion
                run lambda {_flood} with {_flood}, location of event-block, {_ore}, event-player
                delete {veinminer::mined::%event-player's uuid%}
        register {veinminer::listener}

What this shows:

  • A lambda that recurses by taking itself as a parameter ({_self}) and re-running it.
  • A lambda captured at parse/load time and used much later inside a listener's on trigger:.
  • A triggers-free server-wide listener re-armed cleanly on every reload.

18. Inline value lambdas

One-liners can return a value now, with return, or just a bare expression.

command /inlinevalue:
    trigger:
        set {_p} to sender
        set {_add} to lambda (a: object, b: object): return {_a} + {_b}
        set {_double} to lambda (n: number): {_n} * 2

        send "<green>add(3, 4) = %call lambda {_add} with 3, 4%" to {_p}        # 7
        send "<green>double(21) = %call lambda {_double} with 21%" to {_p}      # 42

        # because they're values, they drop straight into the list ops from example 13
        set {_nums::*} to 3, 5, and 8
        send "<aqua>doubled: %{_nums::*} mapped with {_double}%" to {_p}        # 6, 10, 16

What this shows:

  • A one-line lambda returning a value, both with return and as a bare expression.
  • The result slotting straight into mapped with. See Lambdas.

19. Closures: capturing locals

A lambda closes over the local variables around it: a by-value snapshot is taken when it's created and stays readable inside the body when it's called later, even from another trigger. A parameter shadows a captured local of the same name.

command /closures:
    trigger:
        set {_p} to sender

        # a lambda factory: each returned adder closes over its own {_step}
        set {_make_adder} to lambda (step: number) -> object:
            set {_adder} to lambda (n: number) -> number:
                return {_n} + {_step}            # {_step} captured from this factory call
            return {_adder}
        set {_add5} to call lambda {_make_adder} with 5
        set {_add100} to call lambda {_make_adder} with 100
        send "<green>add5(10) = %call lambda {_add5} with 10%" to {_p}          # 15
        send "<green>add100(10) = %call lambda {_add100} with 10%" to {_p}      # 110

        # a parameter named like a captured local wins: this returns the PARAMETER, not any outer {_step}
        set {_shadow} to lambda (step: number) -> number:
            return {_step}
        send "<gray>param shadows capture: %call lambda {_shadow} with 42%" to {_p}   # 42

        # the registry pattern: {_sum} is a LOCAL, but the snapshot keeps it alive inside {-combine}
        set {_sum} to lambda (a: number, b: number) -> number:
            return {_a} + {_b}
        set {-combine} to lambda (n: number) -> number:
            set {_nums::*} to 10, 20, and {_n}
            return {_nums::*} reduced with {_sum}      # {_sum} captured at definition time
        send "<aqua>combine(5) = %call lambda {-combine} with 5%" to {_p}       # 35

What this shows:

  • A factory whose returned lambdas each close over their own captured value.
  • A parameter shadowing a captured local of the same name.
  • A captured local kept alive inside a global-stored lambda for later calls. See Lambdas.

20. Combinators: bound and negated

Pre-fill leading arguments with %lambda% with %args% bound, and flip a predicate with negated %lambda%. Both compose with everything else.

command /combinators:
    trigger:
        set {_p} to sender

        # bind: pre-fill the first argument, get a one-arg lambda back
        set {_add} to lambda (a: number, b: number) -> number:
            return {_a} + {_b}
        set {_add5} to {_add} with 5 bound
        send "<green>add5(10) = %call lambda {_add5} with 10%" to {_p}          # 15
        send "<green>add5(100) = %call lambda {_add5} with 100%" to {_p}        # 105

        # bind every argument -> a zero-arg lambda (a thunk)
        set {_answer} to {_add} with (40, 2) bound
        send "<green>answer() = %call lambda {_answer}%" to {_p}                # 42

        # negated: flip a predicate
        set {_is-op} to lambda (pl: player): {_pl} is op
        set {_not-op} to negated {_is-op}
        if {_not-op} passes for {_p}:
            send "<gray>negated: you are NOT op" to {_p}
        else:
            send "<gold>negated: you ARE op" to {_p}

        # negated drops straight into Skript's filter
        set {_visitors::*} to all players where [{_not-op} passes for input]
        send "<aqua>%size of {_visitors::*}% non-op player(s) online" to {_p}

What this shows:

  • bound for partial application, including binding every argument into a thunk. See Lambdas.
  • negated to flip a predicate and reuse it in Skript's where filter. See Predicates.

21. Paired setup and teardown with on register

on register: runs when the listener becomes active; on end: runs when it stops. For a declared listener the setup fires on register, not at definition.

command /onregister:
    trigger:
        set {_p} to sender
        send "<yellow>arming a watcher with matched setup + teardown..." to {_p}
        set {reg_demo::%{_p}%} to listener for block break where player is {_p}:
            owner: {_p}                  # makes it "yours": shows up under `listeners owned by` and auto-cleans on quit
            triggers: 3
            on register:
                send "<green>[setup] watcher armed, break 3 blocks" to {_p}
            on trigger:
                send "<aqua>%remaining triggers% blocks left" to {_p}
            on completion:
                send "<gold>all 3 broken!" to {_p}
            on end:
                send "<gray>[teardown] watcher gone (%end reason%)" to {_p}
        register {reg_demo::%{_p}%}      # <- 'on register' fires here, not at the `set` above

What this shows:

  • on register: as the setup partner to on end:.
  • For a saved listener, setup firing at register, not at the set. See Listeners.

22. Listing and clearing listeners

all active listeners and listeners owned by %object% are real lists you can loop, count, or feed to unregister. Note: "owned by" means the listener declared an owner: entry. A where player is {_p} filter does not make it yours. Run /watchme (example 10) or /onregister (example 21) first, since both set owner: {_p}, to see your count climb.

command /listenerquery:
    trigger:
        set {_p} to sender
        send "<gray>%size of all active listeners% listener(s) active server-wide" to {_p}
        send "<gray>%size of listeners owned by {_p}% of them are yours" to {_p}
        # cleanup via the query + a loop (equivalent to `unregister all listeners owned by {_p}`)
        loop listeners owned by {_p}:
            unregister loop-value
        send "<gray>cleared yours, %size of listeners owned by {_p}% left" to {_p}

What this shows:

  • all active listeners and listeners owned by as countable, loopable lists.
  • Bulk cleanup by looping the query and unregistering each. See Listeners.

23. Aggregates: min/max by and seeded reduce

highest/lowest of … by returns the element with the smallest/largest key, and reduced with … from %seed% runs for every element starting from a seed (safe on empty lists, and the seed may be a different type).

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

        # seeded reduce: lambda runs for every element, starting from the seed
        set {_add} to lambda (a: number, b: number) -> number:
            return {_a} + {_b}
        send "<green>sum from 100 = %{_nums::*} reduced with {_add} from 100%" to {_p}   # 118
        clear {_empty::*}
        send "<green>empty from 0 = %{_empty::*} reduced with {_add} from 0%" to {_p}     # 0 (lambda never runs)

        # min/max by a key lambda: returns the element with the smallest/largest key
        set {_abs} to lambda (n: number) -> number:
            return abs({_n})
        set {_signed::*} to -7, 3, and -1
        send "<aqua>closest to zero: %lowest of {_signed::*} by {_abs}%" to {_p}     # -1
        send "<aqua>farthest from zero: %highest of {_signed::*} by {_abs}%" to {_p}   # -7

        # players: top by level, hands back the player, not the number
        set {_score} to lambda (pl: player) -> number:
            return {_pl}'s level
        send "<gold>top player by level: %highest of all players by {_score}%" to {_p}

What this shows:

  • A seeded reduced with … from that's safe on an empty list and can change type.
  • highest/lowest of … by returning the winning element rather than its key. See List operations.

24. Default parameter values

A lambda parameter can carry a default. When the caller leaves that trailing argument off, the default fills in.

command /defaults:
    trigger:
        set {_p} to sender
        set {_advance} to lambda (value: number, step: number = 1) -> number:
            return {_value} + {_step}
        send "<aqua>advance(10)    = %call lambda {_advance} with 10%" to {_p}        # step defaults to 1 -> 11
        send "<aqua>advance(10, 5) = %call lambda {_advance} with 10 and 5%" to {_p}  # 15

What this shows:

  • A parameter written name: type = value, filled when the caller skips it.
  • Only trailing arguments can be skipped. See Lambdas.

25. Running totals with scanned

scanned with keeps every intermediate result of a reduce, not just the final one. Great for a running balance.

command /runningtotal:
    trigger:
        set {_p} to sender
        set {_add} to lambda (a: number, b: number) -> number:
            return {_a} + {_b}
        set {_deposits::*} to 100, 50, 25, and 200
        set {_balance::*} to {_deposits::*} scanned with {_add}
        send "<green>balance after each deposit: %{_balance::*}%" to {_p}   # 100, 150, 175, 375
        # a seed becomes the opening balance and is emitted first
        set {_balance::*} to {_deposits::*} scanned with {_add} from 1000
        send "<green>from 1000: %{_balance::*}%" to {_p}                    # 1000, 1100, 1150, 1175, 1375

What this shows:

  • scanned with for cumulative totals.
  • A seed that opens the list and comes out first. See List operations.

26. Pairing two lists with zipped

zipped with … using walks two lists side by side and combines each pair. It stops at the shorter list.

command /pricetotal:
    trigger:
        set {_p} to sender
        set {_mul} to lambda (qty: number, price: number) -> number:
            return {_qty} * {_price}
        set {_quantities::*} to 2, 1, and 5
        set {_prices::*} to 10, 50, and 3
        set {_lines::*} to {_quantities::*} zipped with {_prices::*} using {_mul}
        send "<aqua>line totals: %{_lines::*}%" to {_p}   # 20, 50, 15

What this shows:

  • zipped with … using to combine two lists element by element. See List operations.

27. Threading a value with pipe

pipe %value% through %lambdas% feeds a value through a chain of one-argument lambdas, left to right.

command /pipeline:
    trigger:
        set {_p} to sender
        set {_inc} to lambda (n: number) -> number:
            return {_n} + 1
        set {_double} to lambda (n: number) -> number:
            return {_n} * 2
        set {_square} to lambda (n: number) -> number:
            return {_n} * {_n}
        set {_out} to pipe 5 through {_inc}, {_double}, and {_square}
        send "<green>5 -> inc -> double -> square = %{_out}%" to {_p}   # ((5+1)*2)^2 = 144

What this shows:

  • A chain of lambdas applied in order, each result feeding the next. See Lambdas.

28. Paging a list with page and window

page N of … by S gives non-overlapping chunks; window N of … by S slides one element at a time. page count tells you how many pages there are.

command /pagelist:
    trigger:
        set {_p} to sender
        set {_items::*} to "a", "b", "c", "d", "e", "f", and "g"
        set {_per} to 3
        send "<yellow>%size of {_items::*}% items, %page count of {_items::*} by {_per}% pages of %{_per}%:" to {_p}
        loop (page count of {_items::*} by {_per}) times:
            send "<aqua>page %loop-value%: %page loop-value of {_items::*} by {_per}%" to {_p}
        send "<gray>2nd sliding window of 3: %window 2 of {_items::*} by 3%" to {_p}   # b, c, d

What this shows:

  • page N of … by S for a paged GUI, with page count to drive the loop.
  • window N of … by S for overlapping slices. See List operations.

29. Counting quantifiers on a predicate list

at least N of, at most N of, and exactly N of … passes check how many predicates in a list pass.

command /access:
    trigger:
        set {_p} to sender
        clear {_reqs::*}
        add lambda (pl: player): {_pl} is op to {_reqs::*}
        add lambda (pl: player): {_pl} has permission "group.vip" to {_reqs::*}
        add lambda (pl: player): {_pl}'s level >= 10 to {_reqs::*}
        if at least 2 of {_reqs::*} passes for {_p}:
            send "<green>access granted — met 2+ of the 3 checks" to {_p}
        else:
            send "<red>need at least 2 of: op, vip, level 10+" to {_p}
        if exactly 3 of {_reqs::*} passes for {_p}:
            send "<gold>full clearance — all three" to {_p}
        if at most 0 of {_reqs::*} passes for {_p}:
            send "<gray>you meet none of them" to {_p}

What this shows:

  • at least / at most / exactly N of … passes next to the existing all/any/none forms. See Predicates.

30. Reading and changing a listener's cooldown

cooldown of %listener% reads the cooldown: you set, and it's settable from outside with set / add / remove.

command /minecooldown:
    trigger:
        set {_p} to sender
        set {dig::%{_p}%} to listener for block break where player is {_p}:
            cooldown: 2 seconds
            on trigger:
                send "<aqua>counted! (cooldown %cooldown of {dig::%{_p}%}%)" to {_p}
        register {dig::%{_p}%}
        send "<yellow>break blocks — at most one counts per %cooldown of {dig::%{_p}%}%." to {_p}

command /slowdig:
    trigger:
        set {_p} to sender
        if {dig::%{_p}%} is registered:
            add 1 second to {dig::%{_p}%}'s cooldown
            send "<gray>cooldown is now %cooldown of {dig::%{_p}%}%" to {_p}
        else:
            send "<red>run /minecooldown first" to {_p}

What this shows:

  • cooldown of %listener% read inside and outside the listener.
  • Widening the cooldown live with add … to …'s cooldown. See Listeners.

31. Owner expression and bulk pause/resume

owner of %listener% reads back the owner, and pause/resume all listeners owned by %object% freezes or revives every listener scoped to that owner.

command /guardon:
    trigger:
        set {_p} to sender
        listen for block break:
            owner: {_p}
            where:
                player is {_p}
            on trigger:
                send "<aqua>you broke %event-block%" to {_p}
        listen for chat:
            owner: {_p}
            where:
                player is {_p}
            on trigger:
                send "<aqua>you said: %message%" to {_p}
        send "<green>two guards online, owned by %owner of the last created listener%." to {_p}

command /guardpause:
    trigger:
        pause all listeners owned by sender   # freeze every listener scoped to you, one line
        send "<yellow>your guards are paused" to sender

command /guardresume:
    trigger:
        resume all listeners owned by sender
        send "<green>your guards are back" to sender

What this shows:

  • owner of %listener% to read a listener's owner.
  • pause / resume all listeners owned by as the pause-counterpart to unregistering by owner. See Listeners.

32. Script options inside listener entries

Script options: now expand inside a listen section, so one value can drive countdown:, cooldown:, and the rest.

options:
    grace: 30 seconds
    debounce: 1 second

command /optdemo:
    trigger:
        set {_p} to sender
        listen for damage where victim is {_p}:
            countdown: {@grace}
            cooldown: {@debounce}
            triggers: 5
            on trigger:
                cancel event
                send "<blue>blocked (%remaining countdown% left)" to {_p}
            on timeout:
                send "<green>survived the {@grace} grace period" to {_p}
        send "<yellow>shield up for {@grace}, debounced to {@debounce}." to {_p}

What this shows:

  • {@option} values resolved inside countdown:, cooldown:, and other entries. See Listeners.

33. Background work with futures

Hash some text off the main thread, then read the result once it's ready, without freezing the server. The lambda body must be thread-safe: pure computation, no Bukkit API.

command /hashit <text>:
    trigger:
        set {_p} to sender
        set {_hash} to lambda (payload: string) -> string:
            return {_payload} hashed with SHA-256        # pure compute, safe off-thread
        send "<gray>hashing in the background..." to {_p}
        set {_f} to future of calling lambda {_hash} with arg-1
        wait for {_f} for at most 5 seconds:
            send "<aqua>sha256 = %result of {_f}%" to {_p}
            on failure:
                send "<red>hashing failed: %failure reason of {_f}%" to {_p}
            on timeout:
                send "<red>hash took too long" to {_p}

What this shows:

  • future of calling lambda … with … runs a lambda on a background thread. See Async.
  • wait for %future%: suspends (not blocks) the trigger until it resolves, with on timeout: for the cap.
  • result of %future% reads the answer after the wait.
  • on failure: catches a background lambda that errored, and failure reason of says why. Both nest inside the wait for block. Added in 1.4.0.

34. Manual futures as promises, plus when-all

a new future is a promise you resolve yourself with complete. The second half fires two background jobs and joins them into one wait for all of.

command /promise:
    trigger:
        set {_p} to sender

        # a promise you resolve yourself
        set {_token} to a new future
        if {_token} is pending:
            send "<gray>token not ready yet" to {_p}
        complete {_token} with "abc-123"
        if future {_token} is done:
            send "<aqua>token = %result of {_token}%" to {_p}

        # two background jobs, joined into one wait
        set {_work} to lambda (n: number) -> number:
            return {_n} * {_n}                       # pure compute, safe off-thread
        set {_a} to future of calling lambda {_work} with 6
        set {_b} to future of calling lambda {_work} with 9
        wait for all of {_a} and {_b} within 10 seconds:
            send "<aqua>both done: %result of {_a}% and %result of {_b}%" to {_p}
            on failure:
                send "<red>a job failed" to {_p}
            on timeout:
                send "<red>a job stalled" to {_p}

What this shows:

  • a new future with complete %future% with %object% and the is pending / is done conditions. See Async.
  • wait for all of %futures% joins several futures (when-all). Its on failure: fires on the first failure, without waiting for the rest.

35. Await the next event inline

Pause a trigger until the player confirms in chat. The event's values (message) are in scope in the body; locals set in either branch carry forward.

command /confirm:
    trigger:
        set {_p} to sender
        send "<yellow>type 'yes' in chat within 15 seconds to confirm." to {_p}
        wait for next chat where player is {_p} for at most 15 seconds:
            if message is "yes":
                set {_ok} to true
            on timeout:
                set {_ok} to false
        if {_ok} is true:
            send "<green>confirmed!" to {_p}
        else:
            send "<red>cancelled (no reply)." to {_p}

What this shows:

  • wait for next <event> where <condition> [within …]: pauses for an event with its values in scope. See Async.
  • on timeout: and locals carrying past the block.

36. Poll a value and react to changes

watch … every re-reads an expression on a timer and runs on change: only when it differs, with old value and new value in scope. A watcher is a normal listener, so /sklambda listeners lists it.

command /watchfood:
    trigger:
        set {_p} to sender
        send "<gray>watching your food level for 2 minutes..." to {_p}
        watch (food level of {_p}) every 1 second within 2 minutes:
            on change:
                send action bar "<gold>food: %old value% -> %new value%" to {_p}
            on timeout:
                send "<gray>stopped watching food" to {_p}
            on end:
                send action bar "" to {_p}

What this shows:

  • watch %value% every %timespan% within %timespan%: with built-in change detection. See Async.
  • old value / new value, plus on timeout: and on end: teardown.

37. Edge-triggered condition watcher

watch when … every polls a condition and fires only on the transitions: on rising: (false → true) and on falling: (true → false). It's "do X the moment Y first becomes true" without a poll loop.

command /lowhealth:
    trigger:
        set {_p} to sender
        send "<gray>alerting you whenever you drop below 6 hearts of health." to {_p}
        watch when (health of {_p} < 6) every 1 second within 10 minutes:
            on rising:
                send "<red>low health!" to {_p}
            on falling:
                send "<green>back to safe health" to {_p}

What this shows:

  • watch when <condition> every %timespan%: is edge-triggered. See Async.

  • on rising: and on falling: fire on the transition, not every poll.

  • on rising: fires only on a transition, so a condition already true when the watcher starts stays quiet until it goes false and back. Add initial: false (below) if you want it to fire straight away.

38. Failing a future when the answer can't come

A future doesn't only succeed. fail settles it as broken and wakes every waiting trigger into on failure:. This waits for a player to confirm, and fails the promise if they leave first.

command /awaitconfirm:
    trigger:
        set {_p} to sender
        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"

        send "<yellow>type anything in chat within 30 seconds to confirm." to {_p}
        listen for chat where player is {_p}:
            owner: {_p}
            triggers: 1
            on trigger:
                complete {_f} with message

        wait for {_f} for at most 30 seconds:
            send "<green>confirmed: %result of {_f}%" to {_p}
            on failure:
                send "<red>could not confirm: %failure reason of {_f}%" to console
            on timeout:
                send "<yellow>you took too long." to {_p}

What this shows:

  • fail %future% with %text% breaks a promise; reject is the same. See Async.
  • failure reason of %future% explains why, and is nothing for a pending or successful future.
  • Whichever settles first wins: a later fail or complete on a settled future is ignored.
  • The failure goes to console, not to {_p}, because the player leaving is the failure.

Added in 1.4.0.

39. Chaining futures with then

then builds a new future from an existing one, running a lambda on the result once it arrives. The source is untouched, and a failure skips straight past the chain.

command /shorthash <text>:
    trigger:
        set {_p} to sender
        set {_hash} to lambda (payload: string) -> string:
            return {_payload} 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 arg-1
        set {_short} to {_full} then {_shorten}

        wait for {_short} for at most 10 seconds:
            send "<aqua>short = %result of {_short}%" to {_p}
            send "<gray>full is still there = %result of {_full}%" to {_p}
            on failure:
                send "<red>%failure reason of {_short}%" to {_p}

What this shows:

  • %future% then %lambda% returns a new future; the source stays readable on its own.
  • If the source fails, the chained future fails with the same reason and the lambda never runs.
  • The chained lambda runs on the same background pool, so it must be Bukkit-free too.
  • It is then, not mapped with: that spelling belongs to list mapping.

Added in 1.4.0.

40. Already-finished futures for a cache

When an answer is sometimes cached and sometimes needs work, hand back a future either way. The caller writes one code path.

command /lookup <text>:
    trigger:
        set {_p} to sender
        set {_slow} to lambda (key: string) -> string:
            return "%{_key}% computed the slow way"

        if {cache::%arg-1%} is set:
            set {_f} to a completed future with {cache::%arg-1%}
        else:
            set {_f} to future of calling lambda {_slow} with arg-1

        wait for {_f} for at most 5 seconds:
            send "<aqua>%result of {_f}%" to {_p}
            set {cache::%arg-1%} to result of {_f}
            on failure:
                set {_f2} to a failed future with "lookup failed"
                send "<red>%failure reason of {_f2}%" to {_p}

What this shows:

  • a completed future with %object% is already resolved, so wait for returns on the spot.
  • a failed future with %text% is already broken, useful for an error path that still returns a future.
  • The caller wait fors the same way whether or not any background work happened.

Added in 1.4.0.

41. Take and drop while

taken while and dropped while split a list at the first element that fails a predicate. Position matters, which is what separates them from a filter.

command /streak:
    trigger:
        set {_p} to sender
        set {_scores::*} to 5, 3, 1, -2, and 4
        set {_positive} to lambda (n: number): {_n} > 0

        send "<aqua>opening streak: %{_scores::*} taken while {_positive} passes%" to {_p}
        send "<gray>from the first drop on: %{_scores::*} dropped while {_positive} passes%" to {_p}

        # a filter keeps every match instead, wherever it sits
        send "<gray>all positives: %{_scores::*} where [{_positive} passes for input]%" to {_p}

What this shows:

  • taken while stops at the first failure; the trailing 4 stays in dropped while even though it passes.
  • The two always partition the list.
  • Skript's where is the position-independent alternative. See List operations.

Added in 1.4.0.

42. Firing a watcher on the first poll

A watch when watcher normally treats the first poll as its baseline, so something already true stays quiet. initial: declares the state to assume instead.

command /hurtalert:
    trigger:
        set {_p} to sender
        watch when (health of {_p} < 6) every 1 second within 10 minutes:
            initial: false
            on rising:
                send "<red>low health!" to {_p}
            on falling:
                send "<green>back to safe health" to {_p}

What this shows:

  • initial: false assumes the condition started false, so a player who is already hurt is alerted on the first poll.
  • initial: true is the mirror image for on falling:.
  • Leave the entry off for the old baseline behaviour. See Async.

Added in 1.4.0.

43. Finding leaks with /sklambda futures

Listeners and futures both leak quietly. These expressions let you audit listeners from a script; /sklambda futures does the same for futures that never resolved.

command /audit:
    trigger:
        set {_p} to sender
        send "<gray>%amount of all active listeners% listeners active" to {_p}
        loop all active listeners:
            if age of loop-value > 10 minutes:
                send "<yellow>stale: %script of loop-value%, alive %age of loop-value%" to {_p}
                unregister loop-value

What this shows:

  • script of, age of, and creation date of a listener. See Listeners.
  • age of counts from creation, not registration.
  • Run /sklambda futures for the future side: origin, how long it has been pending, and how many wait for blocks are suspended on it.

Added in 1.4.0.

Clone this wiki locally