xiao.timer New

Run a function later or repeatedly, and write sequential code with wait inside a spawned coroutine.

xiao.timer runs a function after a delay or at a fixed interval, and runs coroutines that can wait between steps.

A sequence of actions then reads top to bottom instead of being spread over counters in the think signal.

At a glance

Function Description
xiao.timer.after(seconds, fn) call fn once, seconds from now
xiao.timer.every(seconds, fn) call fn every seconds
xiao.timer.cancel(id) stop a timer or a waiting coroutine
xiao.timer.spawn(fn, ...) run fn as a coroutine that can wait
xiao.timer.wait(seconds) inside spawn: pause the coroutine
Limit Value
Pending timers and spawned coroutines 4096 at the same time
Resolution one frame
Budget the 100 ms of the frame, shared with on_frame, think and the paint listeners

Time base

  • The clock is the sum of the frame dt (the value given to on_frame(dt), a negative or non finite dt counts as 0), starting at 0 when the VM starts. It only moves while the client renders frames.
  • A timer fires on the first frame whose clock reaches its due time. after(0, f) fires on the next frame, never during the call.
  • Every timer runs on the render thread, at step 6 of the order of a frame: after the game signals (world_change, entity_add, entity_remove, chat, damage), before on_frame, think and the paint listeners. Timers that are due on the same frame run by due time, then in creation order.
  • A timer that loops forever stops the VM like a blocking on_frame.
  • A timer created while the timers of a frame are running fires on a later frame at the earliest.
  • Clear VM drops every pending timer and coroutine without calling them.

Functions

xiao.timer.after(seconds, fn): integer

xiao.timer.after(seconds: number, fn: function(id: integer)): integer

Calls fn(id) once, on the first frame at least seconds after the call. The timer is removed before fn runs.

Name Type Description
seconds number finite, >= 0. 0 means the next frame
fn function called with the timer id

Returns: the timer id, an integer that is never reused in the VM. Errors:

  • bad argument #1 to 'after' (non-negative finite number expected): seconds negative, NaN, infinite or not a number
  • bad argument #2 to 'after' (function expected)
  • too many timers (4096): pending timers and spawned coroutines together
xiao.timer.after(2, function()
    print("two seconds later")
end)

xiao.timer.every(seconds, fn): integer

xiao.timer.every(seconds: number, fn: function(id: integer)): integer

Calls fn(id) every seconds until it is cancelled. It fires at most once per frame: when frames are slower than the interval it fires once per frame and does not catch up. every(0, fn) fires every frame.

Name Type Description
seconds number interval, finite and >= 0
fn function called with the timer id, which it can pass to cancel

Returns: the timer id. Errors: same as after, with 'every' in the message.

local count = 0
xiao.timer.every(1, function(id)
    count = count + 1
    print("tick", count)
    if count == 5 then xiao.timer.cancel(id) end
end)

xiao.timer.cancel(id): boolean

xiao.timer.cancel(id: integer): boolean

Stops the timer or the spawned coroutine id. A cancelled coroutine is never resumed. Works inside a hook.

Name Type Description
id integer id returned by after, every or spawn

Returns: true if id was pending, false otherwise (already fired, already cancelled, unknown id or not a number). Errors: none.

local id = xiao.timer.after(10, function() print("never") end)
print(xiao.timer.cancel(id), xiao.timer.cancel(id))
true	false

xiao.timer.spawn(fn, ...): integer

xiao.timer.spawn(fn: function(...), ...: any): integer

Creates a coroutine running fn(...) and starts it at once, in the caller, until its first wait, its first coroutine.yield() or its end. Later steps run on the render thread with the other timers.

Name Type Description
fn function body of the coroutine
... any arguments passed to fn

Returns: the id of the coroutine, valid for cancel while it waits. The id is returned even when fn finished or failed during the first run (the failure is printed, not raised). Errors:

  • bad argument #1 to 'spawn' (function expected)
  • xiao.timer.spawn is not available inside a packet hook or a cmd hook
  • too many timers (4096)

A plain coroutine.yield() inside a spawned coroutine waits one frame.

xiao.timer.spawn(function(name)
    print("hello", name)
    coroutine.yield()
    print("one frame later")
end, "steve")

xiao.timer.wait(seconds): number

xiao.timer.wait(seconds: number): number

Pauses the running spawned coroutine until the clock has moved seconds further. wait(0) waits one frame.

Name Type Description
seconds number finite, >= 0

Returns: the time actually waited, in seconds (seconds rounded up to whole frames). Errors:

  • bad argument #1 to 'wait' (non-negative finite number expected)
  • xiao.timer.wait must be called inside xiao.timer.spawn: called from the main chunk, a callback, or a coroutine created with coroutine.create or coroutine.wrap, even inside a spawned one
xiao.timer.spawn(function()
    print("3")
    xiao.timer.wait(1)
    print("2")
    xiao.timer.wait(1)
    print("1")
end)

Complete example

A sequence written top to bottom: announce, wait for a world, then report the position every 5 seconds until the player leaves.

local timer = xiao.timer

local job = timer.spawn(function(label)
    print(label, "waiting for a world")
    while not xiao.game.world().in_world do
        timer.wait(0.5)
    end
    print(label, "in world")
    while xiao.game.world().in_world do
        local me = xiao.game.player()
        if me ~= nil then
            print(("%s %.0f %.0f %.0f"):format(label, me.x, me.y, me.z))
        end
        timer.wait(5)
    end
    print(label, "left the world")
end, "tracker")

xiao.signal.add("chat", "stop_tracker", function(message)
    if message == "!stop" then
        print("tracker cancelled", timer.cancel(job))
    end
end)