Events API
Event system for inter-script communication and modular script architecture.
The Events API allows scripts to subscribe to game events and custom events, emit events with data, and build modular, event-driven script architectures. This enables multiple scripts to communicate without tight coupling.
Event Types
The events.EventType enum provides constants for built-in game events:
| Event Type | Value | Description |
|---|---|---|
TICK | "tick" | Fired every game tick (~600ms) |
MESSAGE_RECEIVED | "message_received" | Fired when a chat message is received |
GAME_ACTION | "game_action" | Fired when a game action is performed |
PROJECTILE_DESTINATION_CHANGED | "projectile_destination_changed" | Fired when a projectile's destination changes |
Functions
on
events:on(event_name: string, handler: function)
Subscribe to an event with a handler function.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
event_name | string | Yes | Name of the event to subscribe to (use events.EventType for built-in events) |
handler | function | Yes | Function to call when the event is emitted |
Example:
-- Listen for game tick events
events:on(events.EventType.TICK, function()
logger:info('Game tick!')
end)
-- Listen for chat messages
events:on(events.EventType.MESSAGE_RECEIVED, function(data)
logger:info('Message type: ' .. data.msg_type)
logger:info('Sender: ' .. data.sender)
logger:info('Content: ' .. data.content)
end)
-- Listen for game actions
events:on(events.EventType.GAME_ACTION, function(data)
logger:info('Action: ' .. data.interaction)
logger:info('Scene position: ' .. data.scene_x .. ', ' .. data.scene_y)
logger:info('Opcode: ' .. data.opcode)
logger:info('Entity index: ' .. data.entity_idx)
end)
-- Listen for projectile events
events:on(events.EventType.PROJECTILE_DESTINATION_CHANGED, function(data)
logger:info('Projectile ' .. data.projectile_id .. ' changed destination')
logger:info('Target index: ' .. data.target_index)
logger:info('Start: ' .. data.start_x .. ', ' .. data.start_y)
logger:info('Dest: ' .. data.dest_x .. ', ' .. data.dest_y)
logger:info('Current: ' .. data.current_x .. ', ' .. data.current_y)
end)
-- Custom events
events:on('player_level_up', function(skill, level)
logger:info(skill .. ' leveled up to ' .. level)
end)
off
events:off(event_name: string)
Unsubscribe all handlers for a specific event.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
event_name | string | Yes | Name of the event to unsubscribe from |
Example:
-- Remove all handlers for tick events
events:off(events.EventType.TICK)
-- Remove custom event handlers
events:off('player_level_up')
clear
events:clear()
Clear all event handlers for all events.
Example:
-- Remove all event handlers
events:clear()
list
events:list() -> table
List all registered events and their handler counts.
Returns:
table- Map of event names to handler counts
Example:
local handlers = events:list()
for event_name, count in pairs(handlers) do
logger:info(event_name .. ' has ' .. count .. ' handlers')
end
emit
events:emit(event_name: string, ...)
Emit a custom event with optional arguments.
Note: Built-in game events (TICK, MESSAGE_RECEIVED, etc.) are emitted automatically by the engine. Use this to emit your own custom events.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
event_name | string | Yes | Name of the event to emit |
... | any | Yes | Optional arguments to pass to handlers |
Example:
-- Emit custom event without arguments
events:emit('combat_started')
-- Emit custom event with arguments
events:emit('player_level_up', 'Strength', 99)
-- Emit event with table data
events:emit('loot_received', {
item = 'Dragon bones',
quantity = 15,
value = 3000
})
Event Data Structures
MessageReceived Event
{
msg_type = number, -- Message type ID
sender = string, -- Sender name
content = string -- Message content
}
GameAction Event
{
scene_x = number, -- Scene X coordinate
scene_y = number, -- Scene Y coordinate
opcode = number, -- Action opcode
entity_idx = number, -- Entity index
subject = number, -- Subject ID
interaction = string, -- Interaction text
mouse_x = number, -- Mouse X position
mouse_y = number -- Mouse Y position
}
ProjectileDestinationChanged Event
{
projectile_id = number, -- Projectile ID
target_index = number, -- Target entity index
start_x = number, -- Start X coordinate
start_y = number, -- Start Y coordinate
dest_x = number, -- Destination X coordinate
dest_y = number, -- Destination Y coordinate
tile_x = number, -- Tile X position
tile_y = number, -- Tile Y position
current_x = number, -- Current X position (decimal)
current_y = number, -- Current Y position (decimal)
cycle_start = number, -- Start cycle
cycle_end = number -- End cycle
}
Common Patterns
Tick-Based Logic
local tick_count = 0
events:on(events.EventType.TICK, function()
tick_count = tick_count + 1
-- Run every 5 ticks (~3 seconds)
if tick_count % 5 == 0 then
logger:info('5 ticks elapsed')
end
end)
Chat Message Filtering
events:on(events.EventType.MESSAGE_RECEIVED, function(data)
-- Filter for specific message types or senders
if data.sender == 'Friend' then
logger:info('Message from friend: ' .. data.content)
end
-- Check for specific keywords
if string.find(data.content:lower(), 'trade') then
logger:info('Trade request detected')
end
end)
Combat Detection via Projectiles
local function is_targeting_player(target_index)
local player = players:local_player()
if not player then return false end
-- Check if target index matches player index
return target_index == player:index()
end
events:on(events.EventType.PROJECTILE_DESTINATION_CHANGED, function(data)
if is_targeting_player(data.target_index) then
logger:warn('Incoming projectile! ID: ' .. data.projectile_id)
-- Take defensive action
end
end)
Action Logging and Analytics
local action_counts = {}
events:on(events.EventType.GAME_ACTION, function(data)
local action = data.interaction
action_counts[action] = (action_counts[action] or 0) + 1
logger:info('Action performed: ' .. action)
end)
-- Print statistics after script stops
events:on('script_stopping', function()
logger:info('=== Action Statistics ===')
for action, count in pairs(action_counts) do
logger:info(action .. ': ' .. count)
end
end)
Modular Script Architecture
-- combat_module.lua
local function init_combat_events()
events:on(events.EventType.TICK, function()
local player = players:local_player()
if player and not player:is_in_combat() then
local enemy = npcs:find_nearest({name = 'Goblin'})
if enemy then
events:emit('enemy_found', enemy)
end
end
end)
events:on('enemy_found', function(npc)
logger:info('Engaging ' .. npc:name())
npc:smart_interact({action = 'Attack'})
end)
end
-- health_module.lua
local function init_health_events()
local last_health = 100
events:on(events.EventType.TICK, function()
local current = stats:current_hitpoints()
if current < last_health then
events:emit('health_decreased', current, last_health - current)
end
if current < 20 then
events:emit('health_low', current)
end
last_health = current
end)
events:on('health_low', function(hp)
logger:warn('Low health: ' .. hp)
-- Eat food or teleport
end)
end
-- main.lua
init_combat_events()
init_health_events()
State Machine with Events
local state = 'IDLE'
events:on(events.EventType.TICK, function()
if state == 'IDLE' then
local resource = game_objects:find_nearest({name = 'Tree'})
if resource then
events:emit('state_change', 'GATHERING')
end
elseif state == 'GATHERING' then
local player = players:local_player()
if player and not player:is_animating() then
events:emit('state_change', 'BANKING')
end
elseif state == 'BANKING' then
if inventory:is_empty() then
events:emit('state_change', 'IDLE')
end
end
end)
events:on('state_change', function(new_state)
logger:info('State: ' .. state .. ' -> ' .. new_state)
state = new_state
end)
Conditional Event Subscriptions
local monitoring_enabled = true
-- Start monitoring
local function start_monitoring()
events:on(events.EventType.GAME_ACTION, function(data)
if not monitoring_enabled then return end
logger:info('Action logged: ' .. data.interaction)
end)
end
-- Stop monitoring without removing handler
local function stop_monitoring()
monitoring_enabled = false
end
-- Resume monitoring
local function resume_monitoring()
monitoring_enabled = true
end
Best Practices
-
Use EventType constants: Always use
events.EventType.TICKinstead of string literals like"tick"for built-in events -
Document custom events: Clearly document what arguments your custom events pass to handlers
-
Clean up handlers: Use
events:off()to remove handlers when they're no longer needed to prevent memory leaks -
Error handling: Event handlers are called safely - if one handler errors, other handlers will still execute
-
Performance: Game events (TICK, MESSAGE_RECEIVED, etc.) fire frequently. Keep handlers lightweight
-
Modularity: Use events to decouple different parts of your script for better organization
-
Subscription order: Handlers are called in the order they were registered
-
Custom events: Use custom events (
events:emit()) for your own application logic; built-in events are emitted by the engine
Related APIs
- Logger API - Logging functionality
- Players API - Player information
- Stats API - Player stats
- Chat API - Chat interaction
- Game API - Game state