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 TypeValueDescription
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:

ParameterTypeRequiredDescription
event_namestringYesName of the event to subscribe to (use events.EventType for built-in events)
handlerfunctionYesFunction 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:

ParameterTypeRequiredDescription
event_namestringYesName 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:

ParameterTypeRequiredDescription
event_namestringYesName of the event to emit
...anyYesOptional 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

  1. Use EventType constants: Always use events.EventType.TICK instead of string literals like "tick" for built-in events

  2. Document custom events: Clearly document what arguments your custom events pass to handlers

  3. Clean up handlers: Use events:off() to remove handlers when they're no longer needed to prevent memory leaks

  4. Error handling: Event handlers are called safely - if one handler errors, other handlers will still execute

  5. Performance: Game events (TICK, MESSAGE_RECEIVED, etc.) fire frequently. Keep handlers lightweight

  6. Modularity: Use events to decouple different parts of your script for better organization

  7. Subscription order: Handlers are called in the order they were registered

  8. 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