xiao.signal
xiao.signal registers callbacks under named events, fires them, and receives the events of the client.
A callback is registered under a signal name and an identity. Registering the same identity again replaces its callback and keeps its place. Callbacks run in the order their identity was first added.
Signals are shared by every execution in the VM, so one script can fire a signal another script listens to. An error raised in a callback during xiao.signal.call goes back to the caller like any Lua error. To run something later rather than on an event, use xiao.timer.
At a glance
The four functions are plain Lua: they also work inside a packet hook or a cmd hook, and a call from a hook runs its callbacks inside the hook, with the hook restrictions (see Execution contexts).
Functions
xiao.signal.add(name, identity, callback)
xiao.signal.add(name: string, identity: any, callback: function)
Registers a callback under name and identity. The same identity again replaces its callback.
Returns: nothing.
Errors: bad argument #1 to 'add' (string expected), bad argument #2 to 'add' (identity expected), bad argument #3 to 'add' (function expected).
xiao.signal.add("think", "frame_time", function(dt) print(dt) end)
xiao.signal.remove(name, identity): boolean
xiao.signal.remove(name: string, identity: any): boolean
Returns: true if the callback existed and was removed, false otherwise.
Errors: none.
xiao.signal.remove("think", "frame_time")
xiao.signal.get(name, identity): function | nil
xiao.signal.get(name: string, identity: any): function | nil
Returns: the callback, or nil.
Errors: none.
print(xiao.signal.get("think", "frame_time") ~= nil)
xiao.signal.call(name, ...): any
xiao.signal.call(name: string, ...: any): any
Calls every callback of name with ..., in order, and stops at the first callback whose first return value is not nil. Callbacks added during the call wait for the next call; callbacks removed during the call are skipped.
Returns: every value returned by that callback, or nothing when no callback returned a value. Errors: an error raised by a callback goes back to the caller, and the callback stays registered.
xiao.signal.add("ask", "yes", function(q) return q == "ok" end)
print(xiao.signal.call("ask", "ok"))
Aliases
Client events
Signals fired by the client, on the render thread, only while a VM is running.
These steps follow the order of a frame:
- packet receivers (see Receiving)
world_changeentity_addandentity_remove, in arrival orderchatdamage, the hits then the local hurt- the timers and spawned coroutines that are due New
on_framethink- the paint listeners
Steps 2 to 9 share one 100 ms budget; the packet receivers of step 1 have their own 100 ms (see Time limits).
Nothing from the past
The first frame of a VM fires no world_change and no local hurt: it only records the current state. Events older than the VM are never delivered. Chat and entity events past the limit of a frame wait for the next frame; the client keeps the last 128 chat messages and the last 512 entity changes, older ones are lost. Hits are not kept: the client queues at most 32 of them between two frames.
A callback that raises an error while the client fires it is removed and the error is printed in the console: signal 'chat' callback 'log' removed: ....
world_change
Fired with the previous world, the new world and a reason.
No entity_remove is fired for every entity when a session ends: listen to world_change and forget your entities there.
xiao.signal.add("world_change", "world", function(old, new, reason)
print("world", reason, new.dimension_name, new.session)
end)
entity_add and entity_remove
xiao.signal.add("entity_add", "players", function(entity)
if entity.is_player then print("joined view", entity.name, entity.distance) end
end)
chat
xiao.signal.add("chat", "log", function(message, source, kind, meta)
if kind == "chat" or kind == "whisper" then
print(("<%s> %s"):format(source or "?", message))
elseif meta.needs_translation then
print(kind, message, table.concat(meta.parameters, ", "))
end
end)
type is one of raw, chat, translation, popup, jukebox_popup, tip, system, whisper, announcement, object_whisper, object, object_announcement. source is set for chat, whisper and announcement. Texts keep their UTF-8 bytes and color codes, cut at 511 bytes.
damage
xiao.signal.add("damage", "hurt", function(event)
if event.local_hurt then
print(("-%.1f hp, %.1f left"):format(event.amount, event.health or 0))
elseif event.amount then
print(("hit %s for %.1f"):format(event.target_name or tostring(event.target_id), event.amount))
end
end)
A hit is reported once the health of the target dropped, once it died, or 800 ms after the attack packet. The client tracks attacks only while a damage callback exists.
think and unload
local elapsed = 0
xiao.signal.add("think", "elapsed", function(dt)
elapsed = elapsed + dt
end)
xiao.signal.add("unload", "save", function()
xiao.fs.write("elapsed.txt", tostring(elapsed))
end)
The unload callbacks run before the automatic save of xiao.workspace, so they can still change the stored tables.
Types
entity_event
chat_meta
damage_event
Complete example
Forget tracked entities when the world changes, and log chat:
local seen = {}
xiao.signal.add("entity_add", "track", function(entity)
seen[entity.id] = entity.type
end)
xiao.signal.add("entity_remove", "track", function(entity)
seen[entity.id] = nil
end)
xiao.signal.add("world_change", "track", function(old, new, reason)
seen = {}
print("world:", reason, new.dimension_name)
end)
xiao.signal.add("chat", "track", function(message, source, kind)
print(kind, source or "-", message)
end)