Bundle Event System Developer Guide

Register Lua callbacks for Selene bundle events and understand the common, server, and client events currently exposed by the engine.

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.preTick and selene.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)
EventArgumentsWhen it fires
everySecondnoneOnce per second.
everyMinutenoneOnce per full minute.
everyHournoneOnce 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)
EventArgumentsWhen it fires
serverStartednoneDuring server startup, before the HTTP server, network server, heartbeat, bundle watcher, and main event loop are started.
serverReloadednoneDuring 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)
EventArgumentsWhen it fires
playerQueuedLoginQueueEntry entryA user enters or updates their login queue entry. The callback can call entry:accept(), entry:reject(message), or entry:notify(message).
playerDequeuedLoginQueueEntry entryA user is removed from the login queue.
playerJoinedPlayer playerA player finishes authentication and joins the game.
playerLeftPlayer playerA 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)
EventArgumentsWhen it fires
steppedOffTileEntity entity, Coordinate coordinateAfter a successful entity move, for the previous coordinate.
steppedOnTileEntity entity, Coordinate coordinateAfter 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)
EventArgumentsWhen it fires
setupnoneOnce 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)
EventArgumentsWhen it fires
preTicknoneEvery 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)
EventArgumentsWhen it fires
onChunkChangedCoordinate coordinate, number width, number heightA 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)
EventArgumentsWhen it fires
onCoordinateChangedCoordinate coordinateThe 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.