Docs / Lua / game.* API

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 0

game.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
end

game.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")
end

game.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)
end

game.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 }
end

game.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)
end

Zones (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?