Bundle Event System Developer Guide
Bundle Event System Developer Guide
Selene exposes engine events to bundle Lua as callable event fields on Lua modules. Use them when your bundle needs to react to lifecycle changes, player activity, entity movement, map updates, camera changes, or periodic ticks without polling.
Events are exposed through the same module system as the rest of the bundle API:
local Server = require("selene.server")
Server.serverStarted(function()
print("Server is ready")
end)
The event field is the registration function. Call it once with a callback from an entrypoint or from a module loaded by an entrypoint.
Event registration
Every exposed event follows this shape:
Module.eventName(function(...)
-- event-specific arguments
end)
For example, server entity movement events pass the entity and the affected coordinate:
local Entities = require("selene.entities")
Entities.steppedOnTile(function(entity, coordinate)
print(entity:getName(), coordinate.x, coordinate.y, coordinate.z)
end)
Event callbacks run in the bundle context that registered them. This matters for bundle-relative module loading and runtime ownership: if bundle myworld registers a callback, Selene restores that bundle context when the callback is invoked.
Callback behavior
Most events call every registered listener in registration order. Keep callbacks short and deterministic:
- Do lightweight checks first and return early when the event is not relevant.
- Avoid expensive work in high-frequency events such as
selene.game.preTickandselene.schedules.everySecond. - Do not register the same event repeatedly inside another event callback unless you intentionally want multiple listeners.
- Treat client-side events as presentation hooks and keep authoritative game decisions on the server.
Lua errors are reported with the registration trace captured when the callback was connected. Entity tile events also isolate listener failures so one failing listener does not prevent later listeners from running.
Common events
Common events are available to both client and server bundle entrypoints.
selene.schedules
Use scheduled events for coarse periodic work. For precise timers or cancelable repeated work, use setTimeout, clearTimeout, setInterval, and clearInterval from the same module.
local Schedules = require("selene.schedules")
Schedules.everyMinute(function()
print("A minute passed")
end)
| Event | Arguments | When it fires |
|---|---|---|
everySecond | none | Once per second. |
everyMinute | none | Once per full minute. |
everyHour | none | Once per full hour. |
Server events
Server events are available from server and common entrypoints running on the server.
selene.server
local Server = require("selene.server")
Server.serverStarted(function()
print("Server started")
end)
Server.serverReloaded(function()
print("Server data was loaded or reloaded")
end)
| Event | Arguments | When it fires |
|---|---|---|
serverStarted | none | During server startup, before the HTTP server, network server, heartbeat, bundle watcher, and main event loop are started. |
serverReloaded | none | During server startup after serverStarted. The event exists as the reload hook for server data and bundle state. |
selene.players
Player events cover the login queue and connected players.
local Players = require("selene.players")
Players.playerQueued(function(entry)
if shouldReject(entry:getUserId()) then
entry:reject("Access denied")
else
entry:accept()
end
end)
Players.playerJoined(function(player)
print("Joined:", player:getName())
end)
| Event | Arguments | When it fires |
|---|---|---|
playerQueued | LoginQueueEntry entry | A user enters or updates their login queue entry. The callback can call entry:accept(), entry:reject(message), or entry:notify(message). |
playerDequeued | LoginQueueEntry entry | A user is removed from the login queue. |
playerJoined | Player player | A player finishes authentication and joins the game. |
playerLeft | Player player | A connected player disconnects and is removed from the player manager. |
playerQueued is special because the queue entry's final status controls the login result. If multiple listeners are registered, later listeners can overwrite the status set by earlier listeners.
selene.entities
Entity events fire for server-side entity movement performed through movement logic such as entity:move(direction). Direct coordinate changes that teleport an entity do not emit stepped-on or stepped-off tile events.
local Entities = require("selene.entities")
Entities.steppedOffTile(function(entity, coordinate)
print(entity:getName(), "left", coordinate.x, coordinate.y, coordinate.z)
end)
Entities.steppedOnTile(function(entity, coordinate)
print(entity:getName(), "entered", coordinate.x, coordinate.y, coordinate.z)
end)
| Event | Arguments | When it fires |
|---|---|---|
steppedOffTile | Entity entity, Coordinate coordinate | After a successful entity move, for the previous coordinate. |
steppedOnTile | Entity entity, Coordinate coordinate | After a successful entity move, for the new coordinate. |
For a successful move, steppedOffTile fires before steppedOnTile.
Client events
Client events are available from client and common entrypoints running on the client.
selene.ui.lml
Use the UI setup event to create HUD widgets, load LML files, or attach UI actors after client bundle entrypoints have loaded.
local UI = require("selene.ui.lml")
UI.setup(function()
local hud = UI.loadUI("my-bundle/client/ui/hud.xml")
UI.addToRoot(hud)
end)
| Event | Arguments | When it fires |
|---|---|---|
setup | none | Once during client startup after client and common bundle entrypoints are loaded and client registries are initialized. |
selene.game
local Game = require("selene.game")
Game.preTick(function()
-- Runs every rendered frame before input, movement, camera, and drawable updates.
end)
| Event | Arguments | When it fires |
|---|---|---|
preTick | none | Every client render frame, before network work, pending bundle updates, input, movement, camera, and drawable updates. |
selene.map
Map events are client-side notifications about visible map data changes.
local Map = require("selene.map")
Map.onChunkChanged(function(coordinate, width, height)
print("Map changed at", coordinate.x, coordinate.y, coordinate.z, width, height)
end)
| Event | Arguments | When it fires |
|---|---|---|
onChunkChanged | Coordinate coordinate, number width, number height | A client map chunk or individual tile area changes. coordinate is the changed area's origin. |
selene.camera
local Camera = require("selene.camera")
Camera.onCoordinateChanged(function(coordinate)
print("Camera moved to", coordinate.x, coordinate.y, coordinate.z)
end)
| Event | Arguments | When it fires |
|---|---|---|
onCoordinateChanged | Coordinate coordinate | The client's camera coordinate changes. |
Choosing the right hook
Use lifecycle events such as serverStarted, serverReloaded, and ui.setup for initialization that needs engine systems to exist. Use playerJoined, playerLeft, and entity tile events for authoritative server gameplay. Use map, camera, and game tick events for client presentation and UI updates.
If an action needs data from the other side, combine an event with the network API. For example, a client ui.setup callback can register HUD widgets, while server playerJoined can send the initial HUD state with selene.network.