Events and Shared State
Events and Shared State Guide
This guide explains the event system architecture and how to use shared state for cross-engine communication in your scripts.
Architecture Overview
The scripting engine uses a dual Lua engine architecture:
- Main Lua Engine: Runs your script's main logic (
on_start,poll,on_stop) - Event Lua Engine: Dedicated to event handlers, runs on the event thread
This architecture enables immediate event dispatch with near-zero latency. When a game event occurs, it dispatches directly to the Event Lua Engine without waiting for the Main Engine.
Game Thread Event Thread Script Thread
| | |
emit() ──────────────────> Event Lua Engine |
| (immediate dispatch) |
| handler runs: |
| shared.x:increment() |
| | |
| v |
| SharedStore (Rust) <────────> Main Lua Engine
| (lock-free atomics) shared.x:get()
| poll(), on_start()
The Shared State API
Since event handlers run in a separate Lua engine, they cannot access local variables from your main script. The shared API provides a Rust-backed store for cross-engine state synchronization using atomic operations.
Creating Shared Values
-- Create typed shared values
local counter = shared:int("counter") -- Integer value
local ratio = shared:float("ratio") -- Float value
local status = shared:string("status") -- String value
local enabled = shared:bool("enabled") -- Boolean value
local tbl = shared:table("my_table") -- Table value (key-value store)
Integer Operations
local count = shared:int("ore_count")
count:set(0) -- Set value
count:get() -- Get current value (returns 0)
count:increment() -- Add 1, returns new value
count:decrement() -- Subtract 1, returns new value
count:add(5) -- Add arbitrary amount, returns new value
Float Operations
local xp_rate = shared:float("xp_per_hour")
xp_rate:set(50000.5) -- Set value
xp_rate:get() -- Get current value
xp_rate:add(100.25) -- Add amount, returns new value
String Operations
local last_action = shared:string("last_action")
last_action:set("Mining") -- Set value
last_action:get() -- Get current value
last_action:append(" Iron") -- Append to string
Boolean Operations
local is_banking = shared:bool("is_banking")
is_banking:set(true) -- Set value
is_banking:get() -- Get current value
is_banking:toggle() -- Toggle and return new value
Table Operations
Shared tables provide a key-value store that can hold integers, floats, strings, booleans, and even nested tables. Tables are useful for storing structured data that needs to be shared between engines.
local player_data = shared:table("player_data")
-- Set values with string keys
player_data:set("name", "Alice")
player_data:set("level", 99)
player_data:set("xp", 13034431.5)
player_data:set("is_member", true)
-- Get values (returns nil if key doesn't exist)
local name = player_data:get("name") -- Returns "Alice"
local level = player_data:get("level") -- Returns 99
local missing = player_data:get("missing") -- Returns nil
-- Check if a key exists
if player_data:contains("name") then
logger:info("Player name is set")
end
-- Remove a key
player_data:remove("xp")
-- Clear all entries
player_data:clear()
-- to_table() returns a snapshot as a plain Lua table (keys and values copied; independent of
-- the shared store). Use for iteration or when you need a mutable copy.
local lua_table = player_data:to_table()
for key, value in pairs(lua_table) do
logger:info(key .. " = " .. tostring(value))
end
-- Get all keys as an array
local keys = player_data:keys()
for i, key in ipairs(keys) do
logger:info("Key " .. i .. ": " .. key)
end
Nested Tables
Shared tables support nested tables for complex data structures:
local config = shared:table("config")
-- Create a nested table
local nested = {
setting1 = 100,
setting2 = "value",
setting3 = true
}
config:set("settings", nested)
-- Access nested values
local settings = config:get("settings")
if type(settings) == "table" then
logger:info("Setting 1: " .. tostring(settings.setting1))
end
Note: Table keys must be strings. When setting values, numeric keys are automatically converted to strings.
Utility Methods
shared:contains("counter") -- Check if key exists (returns bool)
shared:remove("counter") -- Remove a specific key
shared:clear() -- Clear all shared values
Using Events with Shared State
Basic Pattern
-- Create shared state at the top of your script
local ores_mined = shared:int("ores_mined")
local last_ore = shared:string("last_ore")
-- Register event handler (runs in Event Lua Engine)
events:on(events.EventType.GAME_ACTION, function(data)
if data.interaction:find("Mine") then
ores_mined:increment()
last_ore:set(data.interaction)
end
end)
-- Main script logic (runs in Main Lua Engine)
function poll()
logger:info("Mined " .. ores_mined:get() .. " ores")
logger:info("Last ore: " .. last_ore:get())
end
Combat Tracking Example
local damage_dealt = shared:int("damage_dealt")
local kills = shared:int("kills")
local in_combat = shared:bool("in_combat")
events:on(events.EventType.GAME_ACTION, function(data)
if data.interaction == "Attack" then
in_combat:set(true)
end
end)
function poll()
local player = players:local_player()
if player and not player:is_in_combat() then
in_combat:set(false)
end
logger:info(string.format(
"Combat stats - Damage: %d, Kills: %d, In combat: %s",
damage_dealt:get(),
kills:get(),
tostring(in_combat:get())
))
end
Chat Monitor Example
local trade_requests = shared:int("trade_requests")
local last_whisper = shared:string("last_whisper")
events:on(events.EventType.MESSAGE_RECEIVED, function(data)
-- Count trade requests
if data.content:lower():find("trade") then
trade_requests:increment()
end
-- Track whispers
if data.msg_type == 7 then -- Private message type
last_whisper:set(data.sender .. ": " .. data.content)
end
end)
function poll()
local requests = trade_requests:get()
if requests > 0 then
logger:info("Trade requests received: " .. requests)
end
local whisper = last_whisper:get()
if whisper ~= "" then
logger:info("Last whisper: " .. whisper)
end
end
Player Stats Tracking Example (Using Tables)
local player_stats = shared:table("player_stats")
events:on(events.EventType.GAME_ACTION, function(data)
-- Track different action types
if data.interaction:find("Attack") then
local attacks = player_stats:get("attacks") or 0
player_stats:set("attacks", attacks + 1)
elseif data.interaction:find("Pick") then
local picks = player_stats:get("picks") or 0
player_stats:set("picks", picks + 1)
end
end)
events:on(events.EventType.TICK, function()
-- Update last action time periodically
local current_time = os.time()
player_stats:set("last_action_time", current_time)
end)
function poll()
local stats = player_stats:to_table()
-- Display formatted stats
logger:info("Player Stats:")
logger:info(" Attacks: " .. tostring(stats.attacks or 0))
logger:info(" Picks: " .. tostring(stats.picks or 0))
if stats.last_action_time then
logger:info(" Last action: " .. tostring(stats.last_action_time))
end
end
Important Constraints
Event Handlers Must Be Self-Contained
Event handlers run in the Event Lua Engine, which is separate from the Main Lua Engine. This means:
- No access to local script variables - Use
sharedinstead - No calling functions that modify local state - Keep handlers simple
- Game APIs work fine - They're Rust-backed and thread-safe
Wrong: Using Local Variables
-- BAD: This will NOT work as expected
local mining_active = false
events:on(events.EventType.GAME_ACTION, function(data)
if mining_active then -- Always false in Event Lua Engine!
logger:info("Mining action detected")
end
end)
function poll()
mining_active = true -- Only affects Main Lua Engine
end
Correct: Using Shared State
-- GOOD: Use shared state for cross-engine communication
local mining_active = shared:bool("mining_active")
events:on(events.EventType.GAME_ACTION, function(data)
if mining_active:get() then -- Reads from shared store
logger:info("Mining action detected")
end
end)
function poll()
mining_active:set(true) -- Both engines see this
end
Both Engines Load the Same Script
When your script starts:
- The Main Lua Engine loads and executes your script (calls
on_start, runspollloop) - The Event Lua Engine loads and executes the same script (only registers event handlers)
This means:
optionsvalues are identical in both engines- Pure functions defined in your script work in both engines
- Top-level code that registers events via
events:on()runs in both, but only the Event Lua Engine's handlers are invoked
Error Handling
Errors in event handlers are logged but don't crash your script. This ensures that a bug in one handler doesn't break the entire event system.
events:on(events.EventType.TICK, function()
-- If this errors, it will be logged and the script continues
local result = some_function_that_might_fail()
end)
Performance Considerations
-
Keep handlers lightweight - Event handlers should complete quickly to avoid blocking the event thread
-
Use appropriate types - Integers and booleans use lock-free atomics (fastest). Floats, strings, and tables use locks (still fast, but slightly slower under high contention). Use simple types when possible, and reserve tables for structured data needs.
-
Batch reads in poll() - If you need multiple shared values, read them all at the start of your poll function
function poll()
-- Read all shared state at once
local ore_count = ores_mined:get()
local fish_count = fish_caught:get()
local is_active = script_active:get()
-- Then use the values
if is_active then
logger:info("Ores: " .. ore_count .. ", Fish: " .. fish_count)
end
end
Complete Example: Mining Script with Event Tracking
-- Shared state for cross-engine communication
local ores_mined = shared:int("ores_mined")
local xp_gained = shared:float("xp_gained")
local current_action = shared:string("current_action")
local is_mining = shared:bool("is_mining")
local ore_stats = shared:table("ore_stats") -- Track stats per ore type
-- Track mining actions
events:on(events.EventType.GAME_ACTION, function(data)
if data.interaction:find("Mine") then
ores_mined:increment()
current_action:set(data.interaction)
is_mining:set(true)
-- Track stats per ore type (extract ore name from interaction)
local ore_name = data.interaction:match("Mine (.+)")
if ore_name then
local current_count = ore_stats:get(ore_name) or 0
ore_stats:set(ore_name, current_count + 1)
end
end
end)
-- Track XP drops (example - actual implementation depends on game data)
events:on(events.EventType.TICK, function()
-- This runs every game tick in the Event Lua Engine
-- Could track XP changes here if needed
end)
function on_start()
logger:info("Mining script started")
ores_mined:set(0)
xp_gained:set(0)
current_action:set("Starting")
is_mining:set(false)
end
function poll()
local player = players:local_player()
if not player then return end
-- Check if we stopped mining
if not player:is_animating() then
is_mining:set(false)
end
-- Display stats
local stats = string.format(
"Ores: %d | XP: %.0f | Action: %s | Mining: %s",
ores_mined:get(),
xp_gained:get(),
current_action:get(),
tostring(is_mining:get())
)
logger:info(stats)
-- Display ore stats from table
local stats_table = ore_stats:to_table()
if next(stats_table) then
local ore_list = {}
for ore_name, count in pairs(stats_table) do
table.insert(ore_list, ore_name .. ": " .. tostring(count))
end
logger:info("Ore breakdown: " .. table.concat(ore_list, ", "))
end
-- Find and mine rocks if not busy
if not is_mining:get() then
local rock = game_objects:find_nearest({name = "Rocks"})
if rock then
rock:interact({action = "Mine"})
end
end
end
function on_stop()
logger:info("Final stats - Ores mined: " .. ores_mined:get())
end
Related Documentation
- Events API Reference - Complete API for
events:on(),events:off(), etc. - Script Structure and Setup - Understanding script lifecycle