Luau Scripting
Setonix Luau plugins now get a high-level Setonix runtime object that wraps the low-level bridge. The goal is to move everyday scripting away from raw JSON-shaped events and toward wrapped players, tables, cells, commands, and generic utility helpers.
The raw bridge is published as SetonixRaw and is also available through Setonix.raw when you need direct access, but the old standalone State, Events, and Server globals are no longer part of the public scripting surface.
Run a script
Section titled “Run a script”Put source in scripts/<filename>.lua or .luau inside a pack, then reference that exact filename from a mode’s script field. Install the pack and create a game with that mode to run it. See game modes and scripts for the complete setup.
Luau runs on the local host in singleplayer and app-hosted games, and on the dedicated server in multiplayer. Native and web builds share the runtime API; self-hosted web builds must include the browser runtime. Additional dedicated-server scripts can be placed in its scripts/ directory with the .lua extension.
The type declarations complement this guide. Blackjack is a complete built-in example.
Quick Start
Section titled “Quick Start”local deck = Setonix.cells.get(0, 0)local stack = Setonix.cells.get(0, 1)
Setonix.events.on("player_join", function(event) event.player:sendMessage("Welcome to " .. Setonix.namespace()) event:setToolbar({ editable = false, actions = { Setonix.server.toolbarAction("blackjack_hit", "Hit"), Setonix.server.toolbarAction("blackjack_stand", "Stand"), }, })end)
Setonix.commands.register("stand", function(event) event:reply("Standing with current hand.") event:cancel()end)
Setonix.events.action("blackjack_hit", function(event) deck:moveTopTo(stack, 1)end)API Shape
Section titled “API Shape”You can use either the flat helpers or the namespaced variants:
Setonix.on(...)orSetonix.events.on(...)Setonix.command(...)orSetonix.commands.register(...)Setonix.player(id)orSetonix.players.get(id)Setonix.players()orSetonix.players.all()Setonix.currentTable()orSetonix.tables.current()Setonix.getTable(name)orSetonix.tables.get(name)Setonix.cell(...)orSetonix.cells.get(...)Setonix.process(...)orSetonix.requests.*(...)Setonix.send(...)orSetonix.server.*(...)Setonix.storage.get(...),Setonix.storage.set(...), andSetonix.storage.update(...)
The top-level helpers are convenient for short scripts. The namespaced entry points are better once a script grows beyond a few callbacks.
Events
Section titled “Events”Register listeners
Section titled “Register listeners”Setonix.events.on("message", function(event) Setonix.log("chat from " .. tostring(event.source) .. ": " .. event.message)end)
Setonix.events.action("blackjack_stand", function(event) if event:isFrom(event.player) then event:reply("You pressed stand.") endend)world_saving runs before Setonix writes the world file. Setonix waits for the handler, so this is the best place to collect derived Lua state and flush it into Setonix.storage.
Setonix.events.on("world_saving", function() Setonix.storage.set("round", { currentPlayer = CurrentPlayer, playerScores = PlayerScores, })end)world_closing runs when the world is being closed or scripts are being reloaded. Setonix waits for the handler before disposing the plugin, so this is the right place to clean up resources or write final state that only matters on shutdown.
Setonix.events.on("world_closing", function() Setonix.storage.set("closedCleanly", true)end)Supported aliases include:
player_join,player_joined,user_join,user_joinedworld_saving,savingworld_closing,closingmessage,chattoolbar_action,actionobjects_moved,objects_spawned,objects_removedcell_hide_changed,cell_hidden- many direct snake_case aliases for client, hybrid, and server event names from the Setonix event model
Event wrapper
Section titled “Event wrapper”Every high-level event includes:
event.raw: the original raw event tableevent.details: the low-level event details userdataevent.player: wrapped player when availableevent.cell,event.from,event.to: wrapped cells when present on the raw eventevent.tableRef: wrapped table for table-bound eventsevent.action: convenience alias foractionIdevent.cancelled: read/write cancellation state
Methods:
event:cancel()event:isCancelled()event:schedule(playableEvent, target)event:reply(message, author)event:broadcast(message, author)event:setToolbar(toolbar)event:getPlayer()event:getCell()event:isFrom(playerOrId)event:table()
Commands
Section titled “Commands”Setonix.command(name, handler) and Setonix.commands.register(name, handler) create simple chat commands for both !name and /name by default.
Setonix.commands.register("deal", function(event) event:reply("Dealing a card...") event:cancel()end)Inside the handler:
event.commandcontains the matched command nameevent.argumentscontains whitespace-split arguments
user_left (also player_left, player_leave, and user_leave) runs when a player disconnects. event.player.id and event.source identify the departing player. This lifecycle notification does not contain a board update to cancel or schedule; send any cleanup updates directly through the server helpers.
Setonix.channels.authority is the local host channel and Setonix.channels.all is the broadcast target. Setonix.players.all() includes the local player in offline and peer-hosted games.
Players
Section titled “Players”local player = Setonix.players.get(7)
if player:isOnline() then player:sendMessage("Your turn")endPlayer fields and methods:
player.idplayer:sendMessage(message, author)player:setToolbar(toolbar)player:isOnline()player:equals(playerOrId)
Tables And Cells
Section titled “Tables And Cells”Tables and cells are live views into the current state. They read from the underlying world state whenever queried.
local tableRef = Setonix.tables.current()local discard = tableRef:cell(4, 0)
if discard:hasObjects() then discard:clear()end
tableRef:spawnObject({ x = 0, y = 0 }, Setonix.namespace() .. ":cards", { variation = "heart-ace", hidden = true,})Table methods:
tableRef.nametableRef:raw()tableRef:cell(positionOrX, y)tableRef:getCell(positionOrX, y)tableRef:cells()tableRef:position(x, y)tableRef:bounds()— returns minimum and maximum positions, or twonilvalues for an unrestricted tabletableRef:contains(positionOrX, y)— checks the inclusive bounds and table nametableRef:setBounds(minimum, maximum, target)— restricts the canvas to two integer cell coordinatestableRef:clearBounds(target)— removes the restrictiontableRef:spawn(positionOrX, y, objects, target)tableRef:spawnObject(positionOrX, y, asset, options, target)
local board = Setonix.tables.current()board:setBounds({ x = 0, y = 0 }, { x = 5, y = 3 })board:cell(0, 1):layout({ type = "DistributeCellMergeStrategy", direction = "horizontal", maxCards = 52,}, 6)board:cell(0, 1):setObjects({}) -- replace the entire hand, including clearing itBounds include both corners and accept reversed corners. Both corners must be supplied together; use clearBounds() to return to the sandbox. The client limits grid rendering and camera movement to the configured table. Restrictions are saved with the world. Restore previous bounds and layouts in world_closing if they belong only to your mode.
Outbound board updates are queued in order. Keep authoritative game progress in Lua state rather than reading a cell immediately after sending its update. This also makes it possible to restore the same round from persistent storage.
State helpers:
Setonix.state.raw()Setonix.state.tableName()Setonix.state.tables()Setonix.state.info()Setonix.state.players()Setonix.state.teamMembers()Setonix.state.currentTable()
Persistent Storage
Section titled “Persistent Storage”Each Luau plugin gets a private JSON-backed storage object saved with the world. Missing values and optional JSON null fields are represented as nil in Luau; Setonix.storage.get(key, default) returns the default for a missing value. Values must be JSON-compatible: strings, numbers, booleans, arrays, and plain tables.
local visits = Setonix.storage.update("visits", function(value) return (value or 0) + 1end, 0)
Setonix.log("This script has seen " .. tostring(visits) .. " loads.")Storage helpers:
Setonix.storage.get(key, default)Setonix.storage.set(key, value)Setonix.storage.update(key, updater, default)Setonix.storage.remove(key)Setonix.storage.clear()Setonix.storage.all()
Use storage for Lua-local game progress that must survive closing and reopening a world, such as the current player, round phase, score tables, or shuffled order references. Board objects are saved with the world, but plain Lua variables are recreated when the script VM starts again.
Cell methods:
cell.table,cell.x,cell.ycell:key()cell:toGlobal()cell:copy()cell:withTable(name)cell:equals(positionOrCell)cell:raw()cell:objects()cell:tiles()cell:isEmpty()cell:topObject()cell:object(index)cell:topObjects(count)cell:countObjects()cell:hasObjects()cell:spawn(objects, target)cell:setObjects(objects, target)— replaces the cell’s complete object listcell:layout(strategy, span, target)— sets a merge layout; usenilto remove itcell:spawnObject(asset, options, target)cell:remove(objects, target)cell:clear(target)cell:show(object, target)cell:hide(object, target)cell:moveTo(targetCell, objects, target)cell:moveTopTo(targetCell, count, target)cell:shuffle(force)cell:roll(object, force)
Collections And Scoring
Section titled “Collections And Scoring”The Setonix.collections and Setonix.scoring namespaces are intentionally generic so game-mode scripts can build their own domain logic without the API assuming a particular pack structure.
local order = Setonix.collections.shuffle({ "a", "b", "c" })local total = Setonix.scoring.bestTotal(items, function(item) return item.valuesend, 21)Available helpers:
Setonix.collections.copy(values)— copies a sequence and preserves its JSON array type, even when emptySetonix.collections.sorted(values, comparator)— returns a sorted copy without changing the inputSetonix.collections.shuffle(values)Setonix.scoring.bestTotal(items, resolverOrMap, maxScore)
Server Helpers
Section titled “Server Helpers”The server namespace collects common outbound actions:
Setonix.server.broadcast("Round started")
Setonix.server.setToolbar({ editable = false, actions = { Setonix.server.toolbarAction("draw", "Draw"), },}, event.source)Available helpers:
Setonix.server.send(event, target)Setonix.server.process(event, force)Setonix.server.message(message, target, author)Setonix.server.broadcast(message, author)Setonix.server.setToolbar(toolbar, target)Setonix.server.toolbarAction(id, label, enabled, showInToolbar)Setonix.server.events.tableBoundsChanged(tableName, minimum, maximum)Setonix.server.events.objectsChanged(cell, objects)Setonix.server.events.mergeStrategyChanged(cell, strategy, span)Setonix.server.events.*builders for hybrid and server events such asobjectsMoved,objectsRemoved,cellHideChanged,message,toolbarUpdated,dialogsClosed, and more
The flat aliases Setonix.send, Setonix.process, Setonix.message, Setonix.broadcast, Setonix.setToolbar, and Setonix.toolbarAction remain available too.
Interactive notes
Section titled “Interactive notes”Notes support Markdown links to other notes and buttons for game actions:
[Read the rules](note:Rules)[Open turn guide](note:Turn%20guide)[Draw a card](action:draw)A note: link opens the named note in the current reader. Encode spaces as %20. Set a note’s Home page property in the editor to choose the default note for the game.
An action: link renders a Material 3 button. Its identifier must match an action in the current player’s toolbar, and it stays disabled if that action is missing or disabled. Clicking sends the same ToolbarActionRequest as a toolbar button, so scripts must enforce turn and permission checks in their action handler.
Use showInToolbar = false to expose an action only inside notes:
Setonix.player(id):setToolbar({ editable = false, actions = { Setonix.server.toolbarAction("draw", "Draw", isPlayersTurn), Setonix.server.toolbarAction("switch_rules", "Switch rules", isHost, false), },})Setonix.notes.set("Rules", "[Switch rules](action:switch_rules)")Both enabled and showInToolbar default to true. Note action buttons remain usable when editable is false; that flag disables sandbox editing and note mutations.
Requests
Section titled “Requests”Client-style requests now have a high-level namespace too, so scripts no longer need to build request tables by hand.
Examples:
Setonix.requests.teamJoin(team, force)Setonix.requests.teamLeave(team, force)Setonix.requests.cellRoll(cell, object, force)Setonix.requests.shuffleCell(cell, force)Setonix.requests.packsChange(packs, force)Setonix.requests.boardsSpawn(tableName, assets, force)Setonix.requests.boardRemove(cell, index, force)Setonix.requests.boardMove(tableName, fromCell, toCell, index, force)Setonix.requests.dialogClose(id, value, force)Setonix.requests.images(ids, force)Setonix.requests.modeChange(location, force)Setonix.requests.modePlain(force)Setonix.requests.authenticate(signature, publicKey, force)Setonix.requests.toolbarAction(actionId, force)
Object Helpers
Section titled “Object Helpers”local hiddenAce = Setonix.object(Setonix.namespace() .. ":cards", { variation = "spade-ace", hidden = true,})Use Setonix.object(asset, options) to create spawnable object payloads without manually building raw tables.
Raw Escape Hatch
Section titled “Raw Escape Hatch”The low-level bridge is still available when needed:
SetonixRaw.Server:Send({ type = "MessageSent", user = 0, message = "raw access still works",}, nil)Raw access points:
SetonixRaw.EventsSetonixRaw.StateSetonixRaw.ServerSetonixRaw.eventsSetonixRaw.stateSetonixRaw.serverSetonix.raw.EventsSetonix.raw.StateSetonix.raw.ServerSetonix.raw.eventsSetonix.raw.stateSetonix.raw.server
Live notes
Section titled “Live notes”Notes are shared Markdown documents saved with the world. Players can follow a note beside the board while scripts update it. Use them for scoreboards, turn reminders, or rules:
Setonix.notes.set("Scoreboard", "# Round 1\n\n**Player 1** is playing.")Setonix.notes.remove("Scoreboard")Both methods accept an optional target channel. The standard event builders are also available as Setonix.server.events.noteChanged(name, content) and noteRemoved(name). When a script manages a note, it can cancel note_changed and note_removed requests for that name. Keep secret information out of shared notes.
For card layouts, fillVariableSpace = false keeps cards centered with spacing of at most one cell. Longer hands compress to fit their merged span; reverse reverses either layout.