Lua API reference
The game global is available in every Lua module loaded by Asobi. It gives your scripts controlled access to the engine runtime — broadcasting, persistence, leaderboards, spatial queries, and more.
Sandbox: Lua scripts run in a Luerl sandbox. They cannot open files, start processes, or call system APIs. Everything the runtime needs to expose goes through game.*.
Messaging
game.broadcast(event, payload)
Broadcast an event to every subscribed player in the current match or zone.
game.broadcast("round_over", { winner = "p_alice", duration = 42 })game.send(player_id, message)
Send a message to a specific player.
game.send(player_id, { kind = "damage", amount = 12, from = attacker_id })Identity
game.id()
Generate a new UUIDv7 (time-ordered, collision-resistant).
local match_id = game.id()Chat
game.chat.send(channel_id, sender_id, content)
Send a chat message on a channel. The channel must exist; create channels via the game mode config or world lobby.
game.chat.send("world:main", player_id, "gg")Notifications
game.notify(player_id, type, subject, data)
Send a notification to one player. Persisted until read.
game.notify(winner_id, "match_ended", "You won!", { prize_id = "trophy_bronze" })game.notify_many(player_ids, type, subject, data)
Same as above, fan-out to multiple players. Uses the background notification broadcaster so it does not block the match tick.
game.notify_many(tournament_players, "bracket_advance", "Round 2 starting", {
match_at = os.time() + 300
})Storage
game.storage.get(collection, key)
Read an arbitrary JSON-serialisable value. Returns nil if missing.
local highscore = game.storage.get("highscores", "global") or 0game.storage.set(collection, key, value)
Write a value. Durable, atomic per-call.
game.storage.set("highscores", "global", new_score)game.storage.player_get(player_id, collection, key) / player_set(...)
Per-player scoped storage. Collection namespace is separate from the shared game.storage.get/set.
game.storage.player_set(player_id, "inventory", "backpack", items)
local pack = game.storage.player_get(player_id, "inventory", "backpack")Economy
game.economy.balance(player_id)
Return the full wallet as { currency_id = amount, ... }.
local wallet = game.economy.balance(player_id)
if (wallet.gold or 0) >= 100 then
-- can afford
endgame.economy.grant(player_id, currency, amount, reason)
Add currency to a player's wallet. The reason is logged in the ledger for audit.
game.economy.grant(winner_id, "gold", 50, "match_win")game.economy.debit(player_id, currency, amount, reason)
Subtract currency. Returns { ok = true } or { ok = false, error = "insufficient_funds" }.
local result = game.economy.debit(player_id, "gold", 100, "shop_buy:sword")
if result.ok then
give_item(player_id, "sword")
endgame.economy.purchase(player_id, listing_id)
Atomic purchase from a store listing. Handles price check, debit, and inventory grant.
game.economy.purchase(player_id, "shop:starter_pack")Leaderboards
game.leaderboard.submit(board_id, player_id, score)
Submit a score. Monotonic boards keep the best score; cumulative boards add to the total.
game.leaderboard.submit("arena:weekly", player_id, kills)game.leaderboard.top(board_id, count)
Return the top N entries as { {player_id, score, rank}, ... }.
for _, entry in ipairs(game.leaderboard.top("arena:weekly", 10)) do
print(entry.rank, entry.player_id, entry.score)
endgame.leaderboard.rank(board_id, player_id)
Return a specific player's current rank.
local my_rank = game.leaderboard.rank("arena:weekly", player_id)game.leaderboard.around(board_id, player_id, count)
Return the N entries surrounding a specific player (useful for “you are here” displays).
local neighbors = game.leaderboard.around("arena:weekly", player_id, 5)Spatial queries
game.spatial.query_radius(x, y, radius)
Zone-based: find entities within a radius. Only valid in world-server (zone) context.
local nearby = game.spatial.query_radius(player.x, player.y, 50)
for _, ent in ipairs(nearby) do
-- ent = { id, x, y }
endgame.spatial.query_radius(entities, x, y, radius, opts?)
In-memory: query a Lua entity table directly. Accepts options: type filter, sort, max_results, custom filter.
local close = game.spatial.query_radius(entities, 0, 0, 100, {
type = "npc",
sort = "nearest",
max_results = 5
})game.spatial.query_rect(x1, y1, x2, y2)
Zone-based rectangular query.
local in_box = game.spatial.query_rect(0, 0, 200, 200)game.spatial.nearest(entities, x, y, n, opts?)
Return the N nearest entities, sorted by distance.
local top3 = game.spatial.nearest(enemies, player.x, player.y, 3, {
type = "minion"
})game.spatial.distance(entity_a, entity_b)
Euclidean distance between two entities.
local d = game.spatial.distance(player, target)game.spatial.in_range(entity_a, entity_b, range)
Boolean: whether two entities are within `range` units of each other.
if game.spatial.in_range(player, enemy, 32) then
deal_damage(enemy, 10)
endZones (world server only)
game.zone.spawn(template_id, x, y, overrides?)
Spawn an entity from a template at (x, y). Overrides merge onto the template.
local goblin = game.zone.spawn("goblin_warrior", 100, 200, { hp = 150 })game.zone.despawn(entity_id)
Remove an entity from the zone.
game.zone.despawn(goblin.id)Terrain
game.terrain.get_chunk(cx, cy)
Fetch compressed chunk bytes for the given chunk coordinates. Chunks are served automatically on zone entry — use this only if you need the data server-side.
local bytes = game.terrain.get_chunk(4, 7)game.terrain.preload(coords_list)
Async preload chunks into the terrain cache. Useful ahead of a known player destination.
game.terrain.preload({ {5, 7}, {5, 8}, {6, 7}, {6, 8} })Where next?
- Game module callbacks — the functions you implement (init, tick, join, leave, etc.)
- Cookbook — short patterns for common tasks.
- Tic-tac-toe tutorial — see the API in context.