Docs menu

Runtime API.

Globals, the ScriptGo namespace, capabilities, the value codec and the sandbox.

SDK scriptgo_version 0.1Source: scriptgo-sdk/docs/spec/runtime.mdSpecification (normative)
On this page

Every SCRIPT//GO host exposes the same runtime to resources. The shape is the familiar multiplayer resource shape — CreateThread, Wait, events, exports, RegisterCommand — so a resource author moves between environments without relearning anything. Environment differences live in adapters and are expressed as capabilities. Every host runs Lua, and the examples below are Lua; the .NET kit also runs JavaScript and TypeScript, with the same names in each language's idiom (multi-language.md).

Hosts and sides

Host Sides Implementation
standalone server + one local client, in-process sdk/lua/scriptgo + sdk/adapters/standalone (CLI)
love2d server + local client, in-process sdk/adapters/love2d
godot server + local client, in-process sdk/adapters/godot (lua-gdextension)
luanti server only (mods are server-side) sdk/adapters/luanti (a Luanti mod)
dotnet server + one local client, in one process sdk/dotnet (.NET kit: Lua 5.4 on KeraLua, JavaScript and TypeScript on Jint), console host scriptgo-dotnet
open77 real dedicated server + real clients scriptgo pack --target open77 adds a three-file shim that maps ScriptGo.* onto Open77.* (see open77.md)

In single-process hosts the client side is player 1 and network events are looped back through the value codec, so isolation rules are identical to a real network.

Globals

Global Notes
CreateThread(fn) Runs fn in a scheduler-owned coroutine
Wait(ms) Yields; Wait(0) resumes on the next host tick
SetTimeout(ms, fn) → id, ClearTimeout(id) One-shot timers
AddEventHandler(name, fn) → handler id, RemoveEventHandler(id), TriggerEvent(name, ...) Server: one host-wide bus, delivered on the next tick in resource-name order, no source. Client: local to the resource side
RegisterNetEvent(name, fn?) Only registered names receive network events
TriggerServerEvent(name, ...) Client → server; returns true or false, reason
TriggerClientEvent(name, target, ...) Server → client; target = -1 for every player
source Inside a network handler on the server: the sending player id
exports(name, fn) Publishes an export of this resource side
exports.other:name(...) Calls another resource's export inline and returns its values; failures raise export other:name <reason>. From file scope, while the calling resource is still loading, it raises resource_preparing (as on OPEN//77): call exports from a thread, a handler or onResourceStart
GetInvokingResource() Inside an export: the calling resource
RegisterCommand(name, fn(source, args, raw), restricted?) Case-insensitive, runs on the next tick, a duplicate name raises; restricted checks command.<name>; source = 0 for the console
GetCurrentResourceName(), GetResourceState(name) started, stopped, missing ...
GetConvar(name, default) Host configuration value
require(module) Confined to the resource: a.b → a/b.lua or a/b/init.lua; @lib/mod reads a declared dependency. Returns nil, reason on failure. Client-only on OPEN//77: portable server code keeps to one file per concern or shared scripts
print(...) Routed to the host log with the resource name

Lifecycle events: onResourceStart(name), onResourceStop(name, reason) (server side), onClientResourceStart(name), onClientResourceStop(name, reason) (client side). Player events (server side): playerJoining(playerId) and playerDropped(reason), both with the player in source; onPlayerConnected(playerId) and onPlayerDisconnected(playerId, reason).

Every global, namespace member and host event, with its failure tokens, is listed in runtime-idl.json (format). JavaScript and TypeScript resources, and resources that mix languages, are covered in multi-language.md.

ScriptGo namespace

Open77 is provided as an alias of the portable subset, so resources written for the open77 environment that only use that subset run unchanged on every host.

Member Capability Notes
log.debug/info/warn/error(msg) logging Always available
json.encode(v), json.decode(s) — Pure Lua, deterministic key order
time.monotonic(), time.now() — Seconds (float); Unix seconds, or nil where the side has no wall clock (OPEN//77 client)
runtime.version(), runtime.luaVersion(), runtime.environment(), runtime.side() —
resource.name(), resource.version(), resource.state(n), resource.hasPermission(p), resource.readFile(path) — readFile is confined to the resource
events.on/off/emit events Aliases of the globals
net.emitServer(name, ...), net.emitClient(name, target, ...) events Aliases
exports.call(resource, name, ...) → promise or nil, reason; promise:await() → result, err — Values are copied through the codec; nil, "resource_preparing" from file scope
capabilities.has(c), capabilities.list(), capabilities.require(c) — require raises capability_unavailable:<c>@<env>
storage.get(key), storage.set(key, value), storage.delete(key), storage.keys(prefix?) storage Per-resource, JSON values, persisted by the host
players.list(), players.name(id), players.exists(id), players.identifier(id) players Server side. Ids are integers ≥ 1; invalid ids answer nil/false
permissions.has(playerId, privilege) permissions Host privileges (Luanti privs, OPEN//77 operators, standalone config)
ui.notify(target, { type, title, message, durationMs }) ui target is a player id or -1; client side may pass nil
config.get(key, default) config Per-resource config from the host
http.request({ method, url, headers, body }) → { status, headers, body } or nil, reason http Server side, inside a thread (it waits for the answer); only where the host allows outbound HTTP
licensing.activate(opts), licensing.verify(opts) → result or nil, code, body licensing Server side; `POST {scriptgo_api}/v1/licenses/activate
perks.has(player, key), perks.list(player), perks.purchaseLink(player, key), perks.on(kind, fn) perks Server side. What the server's operator sold to a player from their own store, asked from the SCRIPT//GO API with the server link token (scriptgo_server_token). One implementation, sdk/lua/scriptgo/perks.lua; see docs/spec/perks.md

Capabilities

events, logging, storage, players, permissions, commands, config, ui, http, licensing. The authoritative matrix lives in sdk/lua/scriptgo/capabilities.lua and is exported to the registry (/v1/environments). Calling a member of a capability the host does not provide raises capability_unavailable:<capability>@<environment>; nothing fails silently.

Value codec

Network events and export calls copy values: nil, booleans, finite numbers, strings, and acyclic tables with string or integer keys. Functions, coroutines, userdata, cycles, depth > 32 and encoded size > 1 MiB are refused with a reason. Lua objects are never shared between sides or resources.

Sandbox

Each resource side gets its own environment: string, table, math, utf8 (5.3+), os.time/os.clock/os.date (standalone, LÖVE, Godot, Luanti, Neovim and CC: Tweaked hosts; on CC: Tweaked os.time() gives Unix seconds from the adapter's wallTime, not the in-game hour), pcall, xpcall, error, assert, select, type, tostring, tonumber, pairs, ipairs, next, rawget/rawset/rawequal/rawlen, setmetatable, getmetatable, unpack/table.unpack. Not exposed: io, os.execute and the rest of os, debug, package, load, loadstring, dofile, loadfile, coroutine (the scheduler owns coroutines), collectgarbage except "count" (refused where the host has no collectgarbage, as on CC: Tweaked).

OPEN//77 is stricter. Its sandbox has no os at all, and its client sandbox has no setmetatable or getmetatable. A package that lists open77 in environments must use ScriptGo.time instead of os, and plain tables in shared and client scripts. scriptgo validate and scriptgo pack --target open77 refuse code that breaks these rules (open77_no_os, open77_client_no_metatables, ...), because the standalone host would not.

The Lua sandbox is not a security boundary for untrusted code. Untrusted uploads are only executed by the validation worker inside isolated containers (see ARCHITECTURE.md).