xiao.signal

Named events shared by every script in the VM, and the full list of events fired by the client.

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.

At a glance

Function Description
xiao.signal.add(name, identity, callback) register a callback
xiao.signal.remove(name, identity) remove it, true if it existed
xiao.signal.get(name, identity) read it back
xiao.signal.call(name, ...) run every callback of name
connect, disconnect, connection, fire aliases

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.

Name Type Description
name string signal name, a client event or your own
identity any key of the callback, used to replace or remove it, not nil
callback function called with the signal arguments

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
Name Type Description
name string signal name
identity any identity given to add

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
Name Type Description
name string signal name
identity any identity given to add

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.

Name Type Description
name string signal name
... any arguments passed to every callback

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

Alias Same as
xiao.signal.connect(name, callback, identity) add(name, identity, callback), other argument order; errors still name the arguments of add
xiao.signal.disconnect(name, identity) remove
xiao.signal.connection(name, identity) get
xiao.signal.fire(name, ...) call

Client events

Signals fired by the client, on the render thread, only while a VM is running.

Name Arguments When Limit per frame
world_change New old: world, new: world, reason: string a field of world changed since the last frame 1
entity_add New entity: entity_event an AddActor or AddPlayer packet created a new network entity 256 (shared with entity_remove)
entity_remove New entity: entity_event a RemoveActor packet removed a known entity 256 (shared with entity_add)
chat New message: string, source: string \| nil, type: string, meta: chat_meta an incoming TextPacket 64
damage New event: damage_event the local player hit a mob or a player and its health dropped, or the local player health dropped 64 hits, plus 1 local hurt
think dt: number (seconds, same value as on_frame) every frame, after on_frame and before the paint listeners 1
unload none when the VM is cleared (Clear VM, unload of the client, a time limit), after on_unload -

These steps follow the order of a frame:

  1. packet receivers (see Receiving)
  2. world_change
  3. entity_add and entity_remove, in arrival order
  4. chat
  5. damage, the hits then the local hurt
  6. the timers and spawned coroutines that are due New
  7. on_frame
  8. think
  9. 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).

world_change

Fired with the previous world, the new world and a reason.

reason Meaning
"enter" in_world went from false to true
"leave" in_world went from true to false
"dimension" the dimension changed (ChangeDimension also starts a new session)
"session" a new network session started (StartGame, disconnect, new server) without any other change

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

Field Type Description
id number runtime id
unique_id number unique id (used by RemoveActor)
name string player name, or the type for a mob
type string type identifier ("minecraft:zombie", "minecraft:player"), absent when unknown
is_player boolean true for AddPlayer
x, y, z, position number, vector3 spawn position (entity_add) or last known position (entity_remove), absent when unknown
distance number distance to the local player, present when both positions are known

chat_meta

Field Type Description
xuid string sender XUID, empty when none
needs_translation boolean message is a translation key
parameters string[] translation parameters (at most 8)

damage_event

Field Type Description
target_id number runtime id of the entity that lost health, absent when unknown
target_name string its name, absent when unknown
target_is_player boolean the target is a player
source_id number runtime id of the attacker: the local player for a hit, absent for local_hurt
source_name string attacker name: the local player name for a hit, absent for local_hurt or when unknown
amount number health lost, measured on the target; absent when no health drop was seen (never a guessed 0)
lethal boolean the target reached 0 health
local_hurt boolean true when the local player is the target
item string held item of the local player for a hit, absent when unknown
distance number distance of a hit, absent when unknown
health, max_health number health left on the target: the local player after a local_hurt, the mob or player after a hit when its drop was seen (0 on a kill); absent when unknown

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)