Lua API reference
The scripting surface a game reaches from a script component (projects/*/scripts/*.component.lua). Game behavior lives in these scripts; projects/jumper-lua/scripts/player.lua and projects/roller/scripts/ball.component.lua are the reference reads.
The file name says what a .lua file IS — the suffix marks the kind:
| File | What it is |
|---|---|
scripts/player.component.lua | a component kind named player: attachable by name in the editor, over MCP and in a scene (see Script components) |
scripts/mathutil.lua | a library: shared code another script loads with script.require. It is not a component kind, so nothing offers it in the Add Component list — but the low-level path-bound ScriptComponent can still be pointed at any .lua file by path |
scripts/retag.editor.lua | an editor tool: a one-shot command in the editor's Tools menu (see Editor scripts) |
tests/movement.test.lua | a test file: run against the project with orkige_player --run-tests — testing.md |
This page is layered. The signature index below is the whole API, one line per symbol, for a fast top-to-bottom scan (an agent ingests it in a single read). Below it are the conventions, canonical snippets and a fuller type reference.
The index and the type reference are GENERATED from the binding sources by Util/update_docs.py (a ctest keeps them from drifting). Signatures are curated in Util/lua_api_annotations.json; fix a signature there, never in the generated text. Everything gui-widget lives in Docs/gui.md.
Scripts written against this API are edited and debugged inside the editor: the embedded code editor (completion generated from this same registry, live syntax checking) and the breakpoint debugger (pause mid-statement, step, stack
-
locals, break on next statement or on errors) are covered in
script-debugging.md — including the MCP verbs that give an AI agent the identical debug loop.
Sandbox / security
Threat model. In an AI-agent dev setting a scene or script file may be authored by an agent or fetched from an untrusted source. Loading a scene is loading CONTENT, not executing arbitrary code. A script attached to a scene runs in a sandbox that MUST NOT grant it arbitrary file read/write, process execution, or code loading — otherwise a hostile .oscene's attached script owns the machine.
Every script sandbox is a fresh per-instance environment whose reads fall through to one shared, hardened Lua state (ScriptManager — the sol2 backend). The allowlist below is applied there ONCE at boot, so it covers both the game ScriptComponent sandbox (self + world/shared) and the editor-script sandbox (editor.*) identically. In ORKIGE_SCRIPTING=OFF builds nothing runs at all, so the denials hold trivially.
Permitted — the pure-computation stdlib (no capability, safe on untrusted input): math, string, table; tostring, tonumber, type, ipairs, pairs, next, select; pcall, xpcall, error, assert; setmetatable, getmetatable, rawget, rawset, rawequal, rawlen; print (writes only to the process log stream — no file/process access). os is kept only as a pruned read-only subset: os.time, os.clock, os.date (RNG seeding / timing / timestamp formatting carry no capability). Plus the sanctioned engine API tables — world, shared, self, music, save, screen, haptics, input, data, script, loc, the wait functions, the component handles, and (editor only) editor.* — which are the intended API.
data is the read-only content access granted in place of the denied file globals: it reads authored data files by project-relative name and grants strictly less than io — no writes, no handles, no path outside the project, and only content that is actually mounted. See Reading data files.
coroutine is opened and then replaced. The library is pure control flow and carries no capability at all, so nothing about it is dangerous to compute with — but a coroutine a script creates itself puts the resume point in the script's hands, and a resume that lands inside a physics contact callback or an event dispatch re-enters the world mid-update. The engine therefore keeps that point: coroutine.yield is captured into the three wait functions (wait, waitFrames, waitUntil), the raw table is set to nil, and the only way to create a suspendable piece of game code is script.async, whose tasks the engine owns and resumes at exactly one place in the frame. See Tasks.
script.require is the library loader granted in place of the denied require. It is not a relaxation of the sandbox, and the reasoning is the point: a scene can already attach a path-bound ScriptComponent naming any project-relative .lua file, so a hostile .oscene can already cause any project script to run. A loader jailed to project-relative .lua names reaches exactly the same files — the same capability with better ergonomics, not a new one. What stock require additionally granted (the package path, C modules, an arbitrary search) and what load grants (code synthesised from a string) stay denied. See Sharing code between scripts.
Denied — every escape from "content" to the machine (each is nil; a call errors honestly):
| Global | Why denied |
|---|---|
io | raw file handles (read/write/delete arbitrary files) |
os.execute, os.remove, os.rename, os.exit, os.getenv, os.tmpname | process execution, filesystem + environment access (dropped from the os subset) |
require, package | load Lua/C modules off the package path (the package library is never opened) — script.require is the jailed, project-only replacement |
load, loadstring, loadfile, dofile | compile+run arbitrary source / read+run an arbitrary file |
debug | the reflection + hook library (bypasses every sandbox boundary) |
coroutine | not a denial of capability but of the RESUME POINT — replaced by script.async + wait/waitFrames/waitUntil, which the engine resumes at one place in the tick order |
collectgarbage is permitted: it controls only the garbage collector (force a collection, read the live count) and carries no file/process/code-loading capability, so a game (a loading-screen collect) and the engine's own weak-handle orphan tests can drive it. io, debug and package are never opened; they are additionally set to nil so the denial is explicit and survives a future library-open edit. The one shared state means a script cannot reconstruct a denied capability — the building blocks (load, require, full os/io) are simply gone.
The hardening lives entirely in the sol2 backend (ScriptManager::applySandboxAllowlist), so no #ifdef ORKIGE_LUA leaks outside Meta*.h and the ScriptRuntime implementation. Denials are verified per sandbox: the game sandbox by ScriptRuntimeTests ([security]), the editor sandbox by the editor_scripts selfcheck's fixture_security tool. This is one facet of the engine's security posture — see security.md for the whole picture.
Signature index
Legend: table.fn(args) -> ret is a global-table call, Type:method(...) an instance method, Type.field a member, Type(ctor) a constructor. ? marks a value that can be nil. [x] is an optional argument.
# GLOBAL TABLES (reach any object by id; ? = may be nil)
## world
world.exists(id) -> bool -- is a GameObject with this id alive
world.get(id) -> GameObject? -- the live GameObject by id (nil if gone)
world.getTransform(id) -> TransformComponent? -- an object's TransformComponent (nil if none)
world.getRigidBody(id) -> RigidBodyComponent? -- an object's RigidBodyComponent (nil if none)
world.getModel(id) -> ModelComponent? -- an object's ModelComponent (nil if none)
world.getAnimation(id) -> AnimationComponent? -- an object's skeletal AnimationComponent (nil if none) - drive another rig's clip playback/crossfade/phase
world.getSprite(id) -> SpriteComponent? -- an object's SpriteComponent (nil if none)
world.getParticles(id) -> ParticleComponent? -- an object's ParticleComponent (nil if none)
world.getSound(id) -> SoundComponent? -- an object's SoundComponent (nil if none)
world.getCamera(id) -> CameraComponent -- the object's CameraComponent (nil when absent); drives smooth follow
world.getLevel(id) -> LevelComponent? -- an object's LevelComponent (nil if none)
world.getAtmosphere(id) -> AtmosphereComponent? -- an object's AtmosphereComponent (nil if none) - the scene-authored sky/fog base (the first active instance owns the world atmosphere)
world.getBoneAttach(id) -> BoneAttachComponent? -- an object's BoneAttachComponent (nil if none) - retarget a bone follower
world.getWorldText(id) -> WorldTextComponent? -- an object's WorldTextComponent (nil if none)
world.getComponent(id, name) -> Component? -- any component by script or reflected-kind name - the generic world.get* (nil if absent/unknown)
world.getScript(id) -> ScriptComponent? -- an object's ScriptComponent (nil if none)
world.loadScene(path) -- deferred scene switch at the next frame boundary
world.findByTag(tag) -> {GameObject} -- array of live objects carrying the tag
world.spawn(prefabRef, id) -> bool -- instantiate a .oprefab (project-relative ref) as a NEW object under id; false + error log on a taken id / missing file (reach it via world.get(id))
world.despawn(id) -> bool -- queue an object for deletion at the next world update (safe mid-script, incl. the caller's own object)
world.setTimeScale(scale) -- gameplay time scale (1 normal, 0 hitstop)
world.getTimeScale() -> number -- the current gameplay time scale
## screen
screen.fadeOut(seconds) -- ramp the full-screen overlay to opaque
screen.fadeIn(seconds) -- ramp the full-screen overlay to clear
screen.setFadeColor(r, g, b) -- fade overlay colour (default black), 0..1
screen.isFading() -> bool -- is a fade in progress (gate input)
screen.loadScene(path, outSeconds, inSeconds) -- fade out, switch scene at opacity, fade in
screen.shake(amplitude, duration [, frequency]) -- decaying camera shake (world units, seconds, Hz)
screen.stopShake() -- end the camera shake immediately
screen.isShaking() -> bool -- is the camera currently shaking
## sound
sound.setGroupVolume(group, volume) -- set a mixer group volume 0..1
sound.getGroupVolume(group) -> number -- read a mixer group volume
sound.setMasterVolume(volume) -- set the master volume 0..1
sound.getMasterVolume() -> number -- read the master volume
## music
music.play(id, file [, loop]) -> bool -- start/replace a streamed track (loops by default)
music.stop(id) -> bool -- stop and free one streamed track
music.stopAll() -- stop every streamed track
music.isPlaying(id) -> bool -- is that track currently playing
music.setVolume(id, volume) -- per-track own volume 0..1
music.getPosition(id) -> number -- playback position in seconds
music.crossFade(id, file, seconds) -- swap `id` in and fade every other track out over `seconds` (equal-power)
## tween
tween.to(from, to, duration, ease [, onUpdate [, onComplete [, delay]]]) -> TweenHandle -- generic value tween; onUpdate(v) applies it
tween.move(id, x, y, z, duration [, ease [, delay]]) -> TweenHandle -- tween an object's local position
tween.scale(id, x, y, z, duration [, ease [, delay]]) -> TweenHandle -- tween an object's local scale
tween.rotate(id, degrees, duration [, ease [, delay]]) -> TweenHandle -- tween Z rotation (the 2D spin)
tween.fade(id, alpha, duration [, ease [, delay]]) -> TweenHandle -- tween a sprite's tint alpha
tween.volume(group, volume, duration [, ease [, delay]]) -> TweenHandle -- tween a mixer group volume (the ducking recipe)
tween.property(id, componentType, propertyName, toValue, duration [, ease [, onComplete [, delay]]]) -> TweenHandle -- tween any reflected Float/Int/Vec3/Color by name
## guitween
guitween.alpha(id, alpha, duration [, ease [, delay [, onComplete]]]) -> TweenHandle -- widget group alpha 0..1 (cascades)
guitween.scale(id, scale, duration [, ...]) -> TweenHandle -- uniform widget scale about the centre
guitween.rotate(id, degrees, duration [, ...]) -> TweenHandle -- widget Z rotation about the centre
guitween.move(id, x, y, duration [, ...]) -> TweenHandle -- widget anchoredPosition / position
guitween.size(id, w, h, duration [, ...]) -> TweenHandle -- widget sizeDelta / size
guitween.color(id, r, g, b, a, duration [, ...]) -> TweenHandle -- decor-widget tint
guitween.show(id) -> bool -- play the widget's enter transition
guitween.hide(id) -> bool -- play the exit transition, then park hidden
guitween.stop(id) -- cancel every running tween on the widget
## screens
screens.define(name, ouiPath) -- register a .oui-backed screen
screens.defineBuilder(name, builder) -- register a Lua-built screen
screens.push(name) -- push a screen onto the stack
screens.replace(name) -- swap the top screen
screens.pop() -> string -- pop the top screen; returns its name
screens.current() -> string -- the top screen's name
screens.depth() -> int -- screens on the stack
screens.clear() -- pop every screen
screens.setBackHandler(handler) -- own the back gesture (handler decides)
## input
input.touchCount() -> number -- fingers reported this frame (a lifting one counts once more)
input.touch(i) -> id, x, y, phase -- 1-based finger: window pixels + 'began'/'moved'/'ended'/'none'
input.touchDelta(i) -> dx, dy -- that finger's movement since the previous frame, window pixels
input.pointer() -> Vector2 -- the one pointer (mouse or finger) in window pixels
input.pointerDown([button]) -> bool -- pointer button held ('left' default, 'middle', 'right')
input.pointerPressed([button]) -> bool -- pointer button went down this frame
input.pointerReleased([button]) -> bool -- pointer button came up this frame
input.keyDown(name) -> bool -- raw key by name ('SPACE', 'LEFT'); prefer a named action
input.gamepadCount() -> number -- connected controllers
input.gamepadConnected() -> bool -- is a controller present (show pad button prompts)
input.gamepadButton(name) -> bool -- pad button held; positional names ('south', 'dpleft'), a/b/x/y alias
input.gamepadAxis(name) -> number -- RAW stick/trigger reading, undeadzoned (sticks -1..1, +y down)
## haptics
haptics.play(strength, ms) -- generic vibration; no-op off-device
haptics.pattern(name) -- named haptic (light..selection); preferred on iOS
haptics.isAvailable() -> bool -- does this device have a vibrator
haptics.setEnabled(enabled) -- honour an in-game vibration on/off setting
## cvar
cvar.registerNumber(name, default) -- register a Float cvar (idempotent)
cvar.registerString(name, default) -- register a String cvar (idempotent)
cvar.getNumber(name [, fallback]) -> number -- read a cvar as a number
cvar.getBool(name [, fallback]) -> bool -- read a cvar as a bool
cvar.get(name) -> string -- read a cvar's canonical string ('' if unset)
cvar.exists(name) -> bool -- is the cvar registered
cvar.set(name, value) -> bool -- set from a string (validated); false + logs on reject
## save
save.set(key, value) -- store number/bool/string by its type
save.getNumber(key [, fallback]) -> number -- read a saved number
save.getBool(key [, fallback]) -> bool -- read a saved bool
save.getString(key [, fallback]) -> string -- read a saved string
save.get(key [, fallback]) -> string -- read any value as its canonical string
save.has(key) -> bool -- is the key present
save.remove(key) -- drop the key
save.flush() -> bool -- autosave point - write to disk now
## data
data.read(name) -> string | nil, error -- read a data file's text by project-relative name
data.json(text) -> table | nil, error -- decode JSON text into a table
data.readJson(name) -> table | nil, error -- read a data file and decode it as JSON
## events
events.subscribe(name, fn) -> EventSubscription -- subscribe fn(payload) to an event (script phase, subscription order)
events.emit(name [, payload]) -- queue an event; payload a table of string/number/bool (one nesting level)
## draw
draw.line(x1, y1, z1, x2, y2, z2, r, g, b [, seconds]) -- world-space debug line; seconds omitted/0 = this frame only, else a TTL
draw.box(cx, cy, cz, hx, hy, hz, r, g, b [, seconds]) -- world-space wireframe box (centre + half extents); seconds = frame-only or TTL
draw.sphere(cx, cy, cz, radius, r, g, b [, seconds]) -- world-space wireframe sphere; seconds = frame-only or TTL
## locale
locale.set(tag) -> bool -- switch the active language (true if the tag is loaded); re-push screens after
locale.get() -> string -- the active language code
locale.list() -> table -- the loaded language codes, sorted (source included)
locale.getSource() -> string -- the source (authored) language code
## benchmark
benchmark.begin(name) -- start/restart/rename the current benchmark scene aggregation (no-op unless armed)
benchmark.endScene() -- close the current benchmark scene and write its record (no-op unless armed)
benchmark.isArmed() -> bool -- is a benchmark run being recorded
## timer
timer.after(seconds, fn) -> TimerHandle -- run fn() ONCE after a delay; sandbox-scoped (auto-cancels on retire)
timer.every(seconds, fn) -> TimerHandle -- run fn() every `seconds`; sandbox-scoped (auto-cancels on retire)
timer.cancel(handle) -> bool -- stop a scheduled timer (also handle:cancel())
## script
script.async(fn) -> ScriptTaskHandle -- run fn as a TASK that spans frames (wait/waitFrames/waitUntil suspend it); sandbox-scoped (auto-cancels on retire), resumed only in the script phase
script.require(name) -> value -- load another project .lua as a LIBRARY and return its value (cached per sandbox); raises on a refused name, a miss or a cycle
## game
game.setState(name) -- set the game state; fires game.stateChanged {old,new} on the event bus
game.getState() -> string -- the current game state name ("" when unset)
## http
http.get(url, onComplete [, onProgress]) -> id -- GET a url; onComplete(res) at the next frame boundary (0 = refused)
http.post(url, body, contentType, onComplete [, onProgress]) -> id -- POST a body; onComplete(res) at the next frame boundary
http.download(url, savePath, onComplete [, onProgress]) -> id -- stream a url straight to a file (nothing kept in memory)
http.request(options) -> id -- the full form: url/method/body/headers/timeout/maxBytes/savePath/onComplete/onProgress
http.cancel(id) -> bool -- abort a request; its onComplete still fires once with error='cancelled'
http.isAvailable() -> bool -- does this runtime have an HTTP client (false in the editor)
http.pending() -> number -- how many requests are still awaiting an answer
## globals
loc(key [, ...]) -> string -- localised string; %%N%% filled by trailing args
# CORE TYPES (Type(ctor); Type:method; Type.field)
## Vector3
Vector3(x, y, z) -- 3D vector / point
Vector3:length() -> number -- vector magnitude
Vector3:distance(v) -> number -- distance to another point
Vector3:squaredDistance(v) -> number -- squared distance (cheaper compare)
Vector3:dotProduct(v) -> number -- dot product
Vector3:crossProduct(v) -> Vector3 -- cross product
Vector3:normalisedCopy() -> Vector3 -- unit-length copy
Vector3.x -- x component
Vector3.y -- y component
Vector3.z -- z component
## Vector2
Vector2(x, y) -- 2D vector (UI pixels / analog2D)
Vector2.x -- x component
Vector2.y -- y component
## Quaternion
Quaternion(w, x, y, z) -- orientation quaternion
Quaternion.w -- w component
Quaternion.x -- x component
Quaternion.y -- y component
Quaternion.z -- z component
## InputActions
InputActions.getSingleton() -> InputActions -- the named-input action map singleton
InputActions:down(name) -> bool -- action held this frame (digital)
InputActions:pressed(name) -> bool -- action went down this frame
InputActions:released(name) -> bool -- action went up this frame
InputActions:value(name) -> number -- analog 1D value
InputActions:value2(name) -> Vector2 -- analog 2D value (e.g. 'move' stick)
InputActions:hasAction(name) -> bool -- is the action mapped
## TweenHandle
TweenHandle:cancel() -- stop the tween now
TweenHandle:isActive() -> bool -- is the tween still running (completion poll)
TweenHandle:setLoops(count, pingpong) -- loop count (<0 forever); pingpong runs it back and forth
## EventSubscription
EventSubscription:cancel() -> bool -- drop the subscription (true if it was live)
EventSubscription:isActive() -> bool -- is the subscription still live
## SafeAreaInsets
SafeAreaInsets.mLeft -- left inset in pixels
SafeAreaInsets.mTop -- top inset in pixels
SafeAreaInsets.mRight -- right inset in pixels
SafeAreaInsets.mBottom -- bottom inset in pixels
## RayHit
RayHit.hit
RayHit.position
RayHit.bodyId
Conventions
Script components
A behavior script is an attachable component kind when its file name ends in .component.lua. Its kind name is the base name: scripts/player.component.lua → the component player. Plain .lua files are libraries/helpers and never attach.
-
Several scripts, one object. Different script kinds attach to the same
GameObject, each in its own sandbox with its own lifecycle. The same kind twice on one object is unsupported. The editor's Add Component menu lists the project's script kinds; the runtime hierarchy/inspector and MCP (
add_component("player")/get_component/set_component/list_addable_components) address them by kind name. -
Declared properties. A top-level
propertiestable auto-exposesdesigner-tunable fields: each shows in the Inspector, serializes per-instance overrides into the scene, is injected onto
self.<name>beforeinit, and is read/written live over the debug protocol / MCP by name. The value is read WITHOUT running behavior (a brokenpropertiestable is an honest error, never executesinit). Types:number/bool/string/vec3/color/asset/object;min/maxadd a slider range,kindan asset/object hint.-- scripts/enemy.component.lua -> the "enemy" component kind properties = { speed = { type = "number", default = 3.0, min = 0, max = 20 }, patrol = { type = "bool", default = true }, tint = { type = "color", default = {1, 1, 1, 1} }, icon = { type = "asset", kind = "texture" }, } function init(self) -- self.speed / self.tint ... are the (overridable) values self.dir = self.speed end function update(self, dt) end
The low-level ScriptComponent (an explicit script file path property) still exists for any script bound by path rather than by kind name.
-
Event delivery reaches EVERY component. The lifecycle hooks fire on each
script component of an object independently:
update(the tick),onContactBegin/onContactEnd(the physics contact drain),onAppPause/onAppResume(the app-lifecycle broadcast). Two scripts on one object each get their own call in their own sandbox. -
The
eventsmessage bus (multi-consumer). For signals SEVERAL scriptsshould hear — or that cross objects/systems — use the bus:
events.subscribe(name, fn) -> handleandevents.emit(name [, payload]). A handler runs asfn(payload),payloada plain table. This is the Lua face of the engine's ONE event bus (core_event/GlobalEventManager), so a C++ system and a script share it — a C++ listener bound to an event name receives a script-emitted event of that name, and vice versa. It is the multi-consumer complement toshared/ tags / the object graph, not a replacement — reach a specific object by id, broadcast a signal on the bus.Delivery is deterministic. An
emitQUEUES onto the engine bus; it never calls a handler inline. The player loop drains the queue ONCE per frame in the script phase (right after the component updates, before tweens/physics), delivering each event to its subscribers IN SUBSCRIPTION ORDER. The bus's double-buffered queue is the cascade-safety: anemitfrom INSIDE a handler lands in the next buffer and is delivered at the NEXT frame's drain — a handler can never recurse into the same drain. Anemitthat happens later in the tick — the mirroredphysics.contactBegin/contactEnd, emitted at the contact drain which runs after the script phase — is likewise delivered next frame. gui input is pumped before the script phase, so a widget event is seen the SAME frame it was clicked.Subscriptions are sandbox-scoped. A subscription belongs to the script component that made it; removing that component, tearing down its scene, or hot-reloading it CANCELS its subscriptions. So the idiom is subscribe in
init— a hot reload re-runsinitand re-subscribes naturally.handle:cancel()drops one early.Payload bounds. A payload is a plain table of
string/number/boolvalues, one nesting level deep (a field may itself be such a table). Anything else — a function, userdata, a deeper table — is an honest ERROR raised at theemitcall site.events.emit("name")with no payload is an empty event.Engine mirrors. Engine producers emit the same named events so scripts can react without polling:
gui.clicked {id},gui.toggled {id, state},gui.submitted {id, text},gui.valueChanged {id, value},gui.dialogResult {id, result},gui.screenPushed/gui.screenPopped {name},gui.toastShown {text},physics.contactBegin/physics.contactEnd {a, b}(object ids),animation.ended {clip, object}(aonceclip on a vector-animation rig finished;objectis the rig's owner id, so several rigs sharing clip names stay distinguishable),app.pause/app.resume(no payload), andui.reloaded {file}(a declarative.ouiscreen was hot-reloaded during Play — its widgets were destroyed and rebuilt, so any handle a script holds to that screen is stale; subscribe to re-acquire them viagui:findWidget(id)).function init(self) self.sub = events.subscribe("gui.clicked", function(e) if e.id == "startBtn" then screens.push("game") end end) events.subscribe("player.died", function(e) self.lives = self.lives - 1 end) end -- somewhere else: events.emit("player.died", { by = "spikes" }) -
GUI events are ALSO single-consumer-pollable. The gui poll idiom is
latch-and-clear:
widget:wasClicked()/wasSubmitted()/group:pollChanged()/gui:getDialogResult()return the pending event ONCE and clear it. So if two script components poll the SAME widget in one frame, the FIRST to poll consumes it and the second sees nothing. Convention for polling: exactly one script component owns a given widget's events. When SEVERAL scripts must react to the same widget, use thegui.*bus events above (multi-consumer) instead of racing pollers — the two channels coexist and the bus never consumes the poll latch.
Script component performance
The multi-sandbox model is cheap: measured ~0.25 µs per component per frame (500 components — a quarter carrying declared properties and doing per-frame work — tick in ~0.12 ms/frame; the [perf] unit test logs the number). What scales and what to watch:
-
The floor (an empty
update) is a sol2protected_functioncall plus thecomponent gate — sub-microsecond. Attaching many script components is fine.
-
Allocation: the per-frame
update(self, dt)path packs its arguments forthe sol2 call; it does not allocate engine-side per component per frame (no per-tick container growth). Declared properties add NO per-frame cost — they are injected onto
selfonce at init, then read as plain Lua fields. -
GC: the Lua state runs the standard incremental collector; a script that
allocates each frame (building tables/strings in
update) feeds it — prefer reusingselffields over per-frame table churn in hot scripts. A stress tick showed no GC spikes in the frame log at this scale. -
Avoid: heavy per-frame work in
updateon hundreds of instances (do itevent-driven or on a subset), and per-frame string/table allocation in hot scripts.
The script shape. A script file defines free functions the runtime calls on a per-instance sandbox. None are required; define the ones you need:
function init(self) -- once, after load (self is populated)
function update(self, dt) -- every frame while playing
function shutdown(self) -- on stop / hot-reload retire / removal
function onContactBegin(self, other) -- physics contact enter (other: GameObject)
function onContactEnd(self, other) -- physics contact exit
function onAppPause(self) -- app backgrounded (mobile)
function onAppResume(self) -- app foregrounded (mobile)
self carries the owner and its sibling components, resolved at load: self.id (string), self.gameObject, self.script, and — when attached — self.transform, self.rigidbody, self.model, self.animation, self.sprite, self.particles, self.shape (a VectorShapeComponent), self.anim (a VectorAnimationComponent), self.boneAttach (a BoneAttachComponent - a bone-follower: setTarget/setBone/setOffsetXYZ/setFollowRotation/ setFollowScale/applyAttachment) and self.decal. Each self.<name> is declared once at its component's registration site (the OSCRIPT_HANDLE macro), so the set grows by declaration, never by editing the script loader. Script-declared export properties are injected as self.<name> before init (so self.moveSpeed is a designer-tunable that also shows in the Inspector). Any declared component KIND — including one added after load — is also reachable generically with self:getComponent("name") (nil for an absent or unknown kind), by the same weak-handle currency as the named fields; the name is the script vocabulary name ("transform") or the reflected kind name ("TransformComponent", the one MCP get_component uses).
world reaches OTHER objects by id. The typed world.getTransform(id) … family and the generic world.getComponent(id, "name") both hand back a WEAK component handle (valid while the object lives — re-fetch when in doubt rather than caching across frames); both are driven by the same per-component OSCRIPT_HANDLE registry as self.
shared is the one cross-instance table (per-instance sandboxes make sharing opt-in). Use it for game-wide state; world/shared are the two globals every script sees.
Honest no-ops. Most tables tolerate the editor's edit mode (no runtime ticking scripts): a call with no backing singleton logs once and returns a default rather than erroring. ORKIGE_SCRIPTING=OFF builds keep ScriptRuntime::available() false and every call honest.
Wiring a .oui screen. Author the layout as text, load it, then find widgets by id and wire behavior — the bridge between declarative UI and script:
factory:loadLayout("screens/pause.oui") -- or gui:loadLayout(...)
-- TYPED finders return the leaf so its own methods work; nil when absent OR
-- a different type. findWidget returns the base GuiWidget (layout setters only).
local resume = gui:findButton("resumeBtn")
-- each frame: if resume and resume:wasClicked() then ... end
Widget & object handles (weak, per-call-locked)
Accessors that hand a script a reference to an engine-owned object return a weak handle, not an owning reference. This is the whole object surface:
-
every
GuiManagerfinder (findWidget,findLabel, …),gui:getFactory(),getToggleGroup, every factorycreate*; -
every
world.*lookup —world.get(id), the per-componentworld.getTransform/
getRigidBody/getModel/getSprite/getParticles/getScript/getSound/getCamera/getLevel, the genericworld.getComponent(id, name), andworld.findByTag(an array of handles); -
the
selffields aScriptComponentinjects —self.gameObject,self.scriptand each sibling component (
self.transform,self.rigidbody,self.sprite,self.shape,self.anim, …), plus the genericself:getComponent(name); -
the OTHER object delivered to
onContactBegin/onContactEnd; -
GameObject:getParent(); -
widget:getLayer()— a screen-scopedUiLayerhandle. A layer dies with itsview, so an
.ouihot-reload or a preview teardown makes a cached layer handle raiselayer handle is dead(keyed on the view's liveness) rather than dangle.
Lua never owns the object; the engine does (the GuiManager owns its widgets, the world owns its objects, a GameObject owns its components). Each method call locks the handle for the duration of the call. If the object is already gone, the call raises a normal, pcall-catchable Lua error at the line that made it — e.g. widget handle is dead (GuiLabel 'coinLabel'), handle is dead (GameObject 'Player'), or component handle is dead (TransformComponent 'Player') (a component names its OWNING object) — instead of a crash, a zombie that keeps the object alive, or a silent no-op. A mistake is visible where it happened, and the app keeps running. self.id stays a plain string, not a handle.
There is one widget handle type (WidgetHandle). It carries the whole widget method surface and resolves each method against the object's live type, so there are two complementary idioms:
-
Filter at acquisition — the typed finders (
findLabel,findButton, …)return
nilfor a wrong-kind or absent id, sogui:findLabel("x")hands back a handle only whenxreally is a label. -
Gate at the call — any handle exposes any widget method; the call succeeds
when the live object actually is that kind and otherwise raises distinctly, e.g.
widget 'x' is a GuiButton, not a GuiLabel(for a name several kinds share, the error names the accepted kinds:… ; setText needs GuiLabel or GuiTextEntry). Sogui:findWidget("x"):setText(...)works whenxis a label and errors honestly when it is not.
You never keep a handle alive to keep the widget alive — destroy it through the manager (or let the manager tear down), and any handle to it simply raises on the next touch.
The GameObject and component surface follows the same rule. world.get(id), self.gameObject, GameObject:getParent() and the contact-event OTHER object are all one GameObjectHandle type carrying the read / hierarchy / tag / active surface. Each component accessor (world.getTransform, self.transform, …) is its own per-type handle carrying that component's methods — a TransformComponent handle exposes setPosition / teleport / … plus the mobility flag (getStaticFlag/setStaticFlag — declare an object's world transform immutable so the renderer takes its immobile fast path; moving a static object still works but warns once and pays a repair cost, and the flag refuses a dynamic parent — Docs/performance.md), a RigidBodyComponent handle exposes applyImpulse / setLinearVelocity / …, and so on. There is no wrong-type gate for components (each type has its own accessor), so a component handle only ever raises the dead-handle error, naming the object it belonged to. A cached handle — self.gameObject stored across frames, a contact OTHER stashed for later — is always safe: it locks per call and raises honestly once its object is destroyed, never dereferencing freed engine state.
Input: named actions and raw devices
Input reaches a game at two levels, and which one to use is decided by one question: does the input carry an intent, or a position?
Named actions — InputActions. A game asks for intent ("jump", "move", "steer"), never for hardware, so the same script runs under a keyboard, a tilted phone or a controller. Each action combines any number of bindings — keys, a key axis, the tilt, a gamepad button, a gamepad stick — and several bindings on one component combine by max magnitude: whichever source pushes harder owns the value that frame. A half-pushed stick loses to a held arrow key; a full stick deflection matches it. That rule is why a key binding and a controller binding on the same action need no special case at all.
Edges follow a once-per-frame snapshot: pressed/released are computed once in the game loop's input slot, before the scripts that read them, so two reads in the same frame always agree and a press stays true for exactly one frame.
local actions = InputActions.getSingleton()
if actions:pressed("jump") then ... end -- SPACE or the pad's south button
local move = actions:value2("move") -- WASD/arrows or the left stick
Controllers are answered by the built-in defaults, so a project is playable on a pad with zero authoring: the left stick drives move and steer, the bottom face button (A on one vendor's pad, B on another's — the engine names it south) is jump, start is menu_toggle and the dpad drives menu_left/right/up/down. A stick's +y is down, matching the key axis (W/UP is negative). Sticks read -1..1 through a deadzone (0.25 by default) that is a rescaled curve, not a cut-off: motion starts at 0 just past the zone and full deflection still reaches ±1, so there is no step at the edge. Triggers read 0..1.
A project overrides the whole set with an input.oactions file (the config-asset convention: a project-relative path under the manifest Settings key input.actions, outside assets/, not id-tracked). A binding stores a gamepad button list, an axis, a deadzone and an invert flag alongside the key groups. Pad state is merged across connected controllers — one player, several pads; per-pad state is not a v1 surface.
The input table (raw devices)
Touch is deliberately not a binding shape: a finger carries a position, and a position has no intent. On-screen controls are gui widgets (gui.md); everything else positional is read from the global input table.
One coordinate space: window pixels of the game's drawable — the same numbers the gui hit-tests widgets in, that get_ui_layout reports over MCP and that the send_input grammar spells. Touch arrives from the platform normalised and is converted once on the way in; the pointer is already in that space. Content scale and safe-area insets are separate, composable concepts and are not applied here — read them from engine:getContentScale() and engine:getSafeAreaInsets() when a layout needs them.
for i = 1, input.touchCount() do
local id, x, y, phase = input.touch(i) -- window pixels
if phase == "began" then -- "began" / "moved" / "ended"
aimAt(x, y)
elseif phase == "moved" then
local dx, dy = input.touchDelta(i) -- movement since last frame
pan(dx, dy)
end
end
local p = input.pointer() -- Vector2, window pixels
if input.pointerPressed() then fire(p.x, p.y) end -- optional "left"/"middle"/"right"
if input.gamepadConnected() then showPadPrompts() end
A finger is reported in began for exactly one frame, in moved for every frame it stays down (whether it moved or not) and in ended for exactly one frame — that last frame is where a game reads the release, so a finger is still counted by touchCount() while lifting. A tap that goes down and up inside a single frame is still seen as began and then ended, never swallowed. input.touch(i) is 1-based like every Lua sequence; an index outside 1..touchCount() answers id -1 and phase "none" rather than erroring.
The pointer is one pointer: the mouse on desktop and the finger on a touch screen (the platform raises pointer events for touches too), so a tap and a click hit-test with the same numbers. pointerDown reads the held state, pointerPressed/pointerReleased the one-frame edges of the same snapshot the action map uses.
The controller half of the table is what a game needs to show the right button prompts and to read a pad directly: input.gamepadCount(), input.gamepadConnected(), input.gamepadButton(name) and input.gamepadAxis(name). Names are positional ("south", "dpleft", "lefttrigger") with the lettered "a"/"b"/"x"/"y" as aliases; an unknown name reads false/0 instead of erroring, so a prompt never crashes a game. The axis reading is raw (undeadzoned) — the deadzone belongs to whatever reads it, which is why one stick can feed several actions at different tolerances. input.keyDown(name) polls a raw key by the same name vocabulary the injected input grammar spells; prefer a named action for anything a player would rebind.
The table carries no capability — it is read-only device state — which is why it sits in the permitted sandbox set beside world, save and haptics. There is deliberately no way to PRESS anything from here: isKeyDown answering true for something nobody pressed would muddy the input model for every reader. The one place that capability is opened is a project's own test files, through t.press / t.release / t.tap on the assertion table (testing.md) — installed for a test run and bound into a test file's own sandbox, so a game script never sees it.
Everything here is driveable over MCP with send_input (touch …, gamepad …), so an agent can test a touch or controller game it just wrote on a machine with neither device attached — see mcp.md.
Component enable / disable
Every component carries a generic enabled flag (default true) — declared ONCE on the component base and inherited by every kind, so it surfaces uniformly in the Inspector (the header checkbox), over MCP (get_component / set_component on the reflected enabled property) and in serialization (a scene records the flag ONLY when a component is disabled, so untouched scenes stay byte-identical). Disabling a component suspends it and re-enabling restores it exactly; the flag composes with the owning object's active state — a component is live only when it is enabled AND its object is active in the hierarchy (effectivelyEnabled), so GameObject:setActive(false) and disabling a single component route through the ONE suspend path.
Per-kind meaning of disabled (each restores to the identical state on re-enable):
| Kind | Disabled means |
|---|---|
| Model / Sprite / VectorShape / VectorAnimation / Line / WorldText / Water / Decal | render node hidden (physics untouched) |
| Light | contributes no light |
| RigidBody | body leaves the simulation (no collision, no motion); re-enable re-enters at the current transform, at rest (velocities zeroed) |
| Sound | playing sounds stop and no new playback starts; re-enable does NOT resume |
| Particle | emission stops, live particles finish their lifetimes; re-enable resumes emitting |
| Animation / SpriteAnimation / VectorAnimation | playback pauses; re-enable resumes at the paused position |
| Script | no update while disabled (its existing semantics) |
| Atmosphere | enabled=false is the authored "no sky" (its existing meaning) |
Kinds where switching off is meaningless expose no checkbox and no enabled property: TransformComponent (an object's place in space — deactivate the whole object instead), TileComponent, LevelComponent and CameraComponent (its disable collides with the window-camera take-over contract; out of v1).
The one flag is also the visibility control on the visual kinds — there is no separate visible property. The convenience aliases (SpriteComponent's setSpriteVisible / isSpriteVisible, and the equivalents on VectorShapeComponent, LineComponent, VectorAnimationComponent and WorldTextComponent) drive that ONE enabled flag.
Persistent objects (survive a scene switch)
A world.loadScene(path) / LevelManager:loadLevel(i) is a deferred scene switch: at the next frame boundary the running world is torn down and the new scene loads. An object marked persistent is the exception — it is carried into the arriving scene WHOLE (its components, render node, physics body, sound and its script sandbox, with every counter and subscription intact) while everything non-persistent tears down as usual and the new scene's objects load in beside it.
obj:setPersistent(true) -- carry this object across the next scene switch
obj:isPersistent() -- read the flag back
-- a runtime-spawned object can be made persistent too:
world.spawn("assets/hero.oprefab", "Hero")
world.get("Hero"):setPersistent(true)
Rules to know: a persistent parent keeps its whole subtree (its children need no flag of their own); a persistent child of a non-persistent parent re-roots to the scene root and survives. If the arriving scene contains an object with the same id as a surviving persistent object, the survivor wins and the incoming duplicate is skipped (with one log line). The flag is authored data on the object (Inspector checkbox / MCP set_persistent / the scene file), so it is inert in the editor, which never runs the level system. Running tweens and timers still end at the switch (they close over the outgoing scene) — a survivor keeps its own state but should restart any tween it needs in the new scene.
Reading data files (the data table)
Game data is content, not code. A level table, an item list, a dialogue tree or a tuning table is authored as a data file and read at runtime through the data table — it never has to be baked into executable .lua source, and an agent generating sixty levels writes sixty data files, not sixty scripts.
local text, err = data.read("data/notes.txt") -- text, or nil + a reason
local tuning, err = data.readJson("data/tuning.json") -- decoded, or nil + reason
local parsed = data.json('{"a":[1,2,3]}') -- decode text you already have
Every function returns its value on success and nil plus a message on failure, so a read is checked the same way everywhere:
local levels, err = data.readJson("data/levels.json")
if levels == nil then
error("levels unreadable: " .. err)
end
Where data files live. data/ at the project root is their home: it ships into every export beside scenes/, scripts/ and assets/. A name is read as a project-relative path ("data/levels.json"), and the read resolves through the content mounts — so a loose file in the editor, an entry in a mounted pak and a file inside a phone's package all answer the identical call. Nothing is extracted and nothing is opened by absolute path, which is why the same script works on a desktop, in a pak and on a device.
Names outside the project are refused: absolute paths, drive/UNC roots and any .. traversal segment. A read is capped in size. Reads are synchronous — do them in init, not per frame.
Format-neutral by design. JSON, CSV, INI or a game's own text grammar all come back from data.read as the bytes on record; data.json / data.readJson are a convenience for the common case, never the only road. data.readJson maps JSON onto natural Lua values: objects become string-keyed tables, arrays 1-based array tables, and a JSON null reads as nil.
Reading is not executing. Restricting formats would buy nothing — only executing content was ever the risk. Reading a .lua file as TEXT is fine and allowed; running it is not, and load, loadstring, loadfile, dofile and require stay denied. That distinction is about execution, not about file kinds.
projects/jumper-lua is the worked example: its feel numbers live in data/tuning.json and scripts/player.lua reads them in init.
Sharing code between scripts (script.require)
A plain .lua file is a library: shared helpers, common math, a wrapper around a data table. Another script picks it up by project-relative name.
-- scripts/mathutil.lua
local M = {}
function M.clamp(v, lo, hi) return math.max(lo, math.min(hi, v)) end
return M
-- scripts/player.component.lua
local m = script.require("scripts/mathutil.lua")
function update(self, dt)
self.transform:setPosition(Vector3(m.clamp(x, -8, 8), y, z))
end
script.require(name) returns the value the library's chunk returns (true when it returns nothing, matching stock require). It raises on failure rather than returning nil plus a reason the way data does, and the difference is deliberate: absent data is a situation a game handles, a missing library is a broken dependency. The message names the library and carries the requiring line's own file:line prefix.
Where a library may live. Anywhere in the project, under any directory — the name is a project-relative path and the only shape rules are that it must not escape the project (no absolute path, no drive/UNC root, no .. segment) and must end in .lua. The read resolves through the content mounts, so a loose file, an entry in a mounted pak and a file inside a phone's package all answer the identical call.
Each sandbox gets its own copy. A library is loaded at most once per requiring sandbox, not once per process. Requiring it twice inside one script returns the identical value; two script instances requiring the same library get two independent copies. That follows the sandbox rule this whole surface is built on — one instance's state never leaks into another's, and deliberate sharing goes through the shared table. It is also what makes hot-reload correct: a rebuilt sandbox re-reads its libraries with no cache to invalidate.
A library does not see its caller. The chunk runs in its own environment with the globals underneath it, so it has no self, no view of the requiring script's locals, and no way to write into them. Pass what it needs as arguments.
A dependency cycle is refused by name, not by a stack overflow: if a.lua requires b.lua which requires a.lua, the second require raises circular library dependency (scripts/a.lua -> scripts/b.lua -> scripts/a.lua).
Libraries are available in every sandbox — game script components, editor tools and test files alike — because they are all the same hardened state. projects/jumper-lua/scripts/jumperlib.lua is the worked example: the jumper's feel math lives there, scripts/player.lua runs on it, and projects/jumper-lua/tests/ tests it (testing.md).
Tasks: code that spans frames (script.async)
Some game logic is a sequence, not a state machine: open the door, wait half a second, play the chime, wait for the player to walk through, close it. Written against update that becomes a hand-rolled step counter. Written as a task it stays one readable function.
function init(self)
self.intro = script.async(function()
wait(0.5) -- seconds of GAMEPLAY time
world.getAnimation("Door"):play("open")
waitFrames(2) -- ticks, for a one-frame settle
waitUntil(function() return shared.doorOpen end) -- a condition, asked once a frame
events.emit("intro.done")
end)
end
script.async(fn) starts fn and returns a handle: task:cancel() stops it, task:isActive() says whether it is still going. The three waits suspend the task that calls them:
| Wait | Comes back |
|---|---|
wait(seconds) | after that many seconds of scaled gameplay time — a hitstop (world.setTimeScale(0)) holds a wait too |
waitFrames(n) | after n task ticks (one per advanced frame) |
waitUntil(fn [, limitFrames]) | the first tick fn() returns true. With limitFrames, a condition that never comes true gives up and fails the task by name instead of waiting forever |
The rules a task lives by — the same four every other live thing in the engine carries, which is why they need no separate bookkeeping:
-
It is owned by the sandbox that started it. Removing the component,
destroying the object, tearing the scene down or hot-reloading the script cancels it. You never cancel tasks in
shutdown. - It dies with the scene, like a tween or a timer.
-
It is never serialized. A task is live control flow, not state: a save file
records what the game decided, never where a coroutine was parked. A sequence that must survive a save has to write down its step itself.
-
It is resumed at exactly ONE point in the frame — the script phase of the
tick order, right after the component updates. A task can therefore never continue inside a physics contact callback, an event handler or a render pass, and two tasks never interleave mid-statement. It is also why
wait(0)still costs one frame: the earliest a suspended task can come back is the next tick.
A wait called outside a task raises (there is nothing to suspend), and script.async is an honest no-op in the editor, which never ticks game scripts.
Tests use the same waits through their assertion table (t.wait, t.waitFrames, t.waitUntil) — see testing.md.
Canonical snippets
Screen from a .oui + wire it.
function init(self)
factory:loadLayout("screens/hud.oui")
self.coins = gui:findLabel("coinLabel") -- typed: label:setText works
end
function update(self, dt)
if self.coins then self.coins:setText(tostring(save.getNumber("coins", 0))) end
end
Save round-trip (autosave on a checkpoint).
save.set("hero.coins", save.getNumber("hero.coins", 0) + 1)
save.set("hero.level", "forest") -- number / bool / string by type
save.flush() -- write to disk NOW (a set alone is dirty-only)
Fetch something from a web service (async, never blocks a frame).
-- the short form: a callback and nothing else to think about
http.get("https://api.example.com/scores/top", function(res)
if res.ok then -- ok = completed AND a 2xx status
self.board:setText(res.body)
else
-- one error path for every outcome: a 404, a timeout, a refused URL
print("scores unavailable: " .. res.error .. " (" .. res.reason .. ")")
end
end)
-- the full form: headers, a body, a timeout, a progress bar
local id = http.request{
url = "https://api.example.com/scores",
method = "POST",
body = '{"name":"ada","score":42}',
contentType = "application/json",
headers = { "Authorization: Bearer " .. token },
timeout = 10, -- seconds
onComplete = function(res) print(res.status) end,
}
-- an asset download straight to a file, with progress, cancellable
self.download = http.download("https://cdn.example.com/pack.zip",
"downloads/pack.zip",
function(res) if res.ok then self:install(res.path) end end,
function(received, total) self.bar:setValue(received / math.max(total, 1)) end)
-- ...later: http.cancel(self.download) -- onComplete still fires, error="cancelled"
res is { ok, status, body, path, bytes, url, error, reason, headers }. An HTTP status is an ANSWER, not a failure: a 404 arrives with ok=false, status=404 and error="". A refusal (no https, a bad URL, a timeout, the size cap) arrives with error naming it. Requests are https-only unless the options table carries allowInsecureHttp = true, and a request belongs to the script that made it: when its component or scene goes away, its pending requests retire silently. Full reference: HTTP client.
Music (survives scene switches) + ducking under a stinger.
music.play("bgm", "music/level1.ogg") -- loops by default
tween.volume("music", 0.3, 0.2) -- duck
tween.volume("music", 1.0, 0.8, "quadOut", 1.5) -- restore after 1.5s
Drive a vector-animation rig (self.anim).
function init(self)
self.anim:play("idle") -- a looping clip
events.subscribe("animation.ended", function(e)
-- a `once` clip finished; e.object filters to THIS rig's owner
if e.object == self.id and e.clip == "hop" then
self.anim:crossFade("idle", 0.3) -- blend back over 0.3s
end
end)
end
function onContactBegin(self, other)
self.anim:crossFade("hop", 0.2) -- one-shot, then the event
end
-- clip discovery at runtime: self.anim:getClipNames() -> "idle,hop";
-- an unknown name refuses (returns false) and warns once in the log,
-- naming the available clips. Scrub: self.anim:scrub(0.5) (seconds).
Haptics (mobile; no-op on desktop).
if haptics.isAvailable() then haptics.pattern("selection") end
haptics.play(0.8, 40) -- generic strength + milliseconds
Soft-body impulse (a VectorShapeComponent blob reacting to a hit).
function onContactBegin(self, other)
if self.shape then self.shape:impulse(Vector3(0, 12, 0), 0.6) end
end
guitween chains (an endless spinner, a pulse, a menu transition).
guitween.rotate("spinner", 360, 1.0, "linear"):setLoops(-1, false)
guitween.scale("badge", 1.2, 0.3, "quadInOut"):setLoops(-1, true)
panel:setTransition("slide-up 0.3"); guitween.show("menu")
Material accents — a hit flash on a mesh (self.model).
-- RUNTIME-ONLY, per instance: the .omat asset stays the authored truth (a
-- scene save never records accents - the properties are transient), and the
-- OTHER instances of the same material keep their look. Tint multiplies the
-- albedo (white = off), the emissive boost ADDS glow (black = off) - a tint
-- can only darken, the boost is what makes a flash BRIGHT.
function onContactBegin(self, other)
self.model:setTint(1.0, 0.3, 0.25) -- red-shift the surface
self.model:setEmissiveBoost(0.3, 0.05, 0.05) -- and make it glow
tween.to(0, 1, 0.15, "quadOut", nil, function()
self.model:setTint(1, 1, 1) -- restore EXACTLY
self.model:setEmissiveBoost(0, 0, 0)
end)
end
Screen shake + hitstop (time scale).
screen.shake(0.4, 0.25) -- amplitude (world units), duration
world.setTimeScale(0) -- freeze the world (still renders)
tween.to(0, 1, 0.12, "linear", nil, function() world.setTimeScale(1) end)
Scene wipe (fade over a deferred switch).
screen.loadScene("scenes/level2.oscene", 0.3, 0.3) -- out, switch at opacity, in
Editor scripts (Tools menu)
A script whose file name ends in .editor.lua is not game behavior — it is an editor TOOL: a one-shot command listed in the editor's Tools menu and run on demand (also over MCP with run_editor_script). It is a different surface from the .component.lua scripts above: it runs in the EDITOR, edits the open scene, and never ticks. The two never mix — a .component.lua attaches to a GameObject and sees self/world/events; a .editor.lua sees only the editor table.
-
Discovery + the header. Any
scripts/*.editor.luain the open project isdiscovered (rescanned on project open and on any
scripts/write). The menu label is a title-cased file name unless the FIRST line is a-- tool: <label>comment, which overrides it. -
One-shot, in a fresh sandbox. A menu click (or
run_editor_script) runs thefile's top-level code ONCE in a fresh Lua sandbox. There is no
update, no timer, no event delivery — the editor never ticks. The game-runtime tables (world/events/tween/…) are absent in an editor tool; referencing one is an honest nil error. -
The
editortable routes through the SAME verb handler an MCP agent drives,so each function mirrors an MCP tool 1:1. Every function takes ONE table of named arguments and returns a result table (fields as strings, arrays as list tables —
tonumberwhat you need). A refused verb RAISES a Lua error at the call site (guard with a read if you expect it). Available verbs:-
reads:
list_hierarchy,get_object,get_component,list_addable_components,list_assets,list_paintable_assets,list_project_files,read_project_file,get_state; -
object CRUD:
create_object,delete_object,duplicate_object,rename_object,reparent_object,reorder_object,set_active,select; -
component CRUD (script components included):
add_component,remove_component,set_component; -
2D grid painting:
paint_asset,erase_cell; -
scene + files + prefabs + levels:
save_scene,open_scene,new_scene,write_project_file,import_asset,create_prefab,instantiate_prefab,add_scene_to_levels; -
plus
editor.log(text)to write a line to the editor Console.
-
reads:
-
One undo step. The WHOLE run folds into a SINGLE undo step, so one Cmd/Ctrl+Z
reverts everything the tool did. A tool that ERRORS is rolled back entirely — it leaves no partial edits — and the error (with its
file:line) lands in the Console. -
Noscript. In
ORKIGE_SCRIPTING=OFFbuilds the Tools menu shows a disablednote and running is an honest no-op; the project still loads.
-
Same sandbox hardening. An editor tool is CONTENT too (an agent authors
.editor.lua), so it runs under the identical global allowlist as a game script —io/os-process/require/load/dofile/debugare all denied. See Sandbox / security.
A tool (frame the level with wall tiles):
-- tool: Paint Wall Border
-- reads the scene's LevelComponent, then paints wall tiles around its edge -
-- all ONE undo step. (projects/roller/scripts/border_walls.editor.lua)
local function findLevel()
for _, id in ipairs(editor.list_hierarchy().ids or {}) do
for _, c in ipairs(editor.get_object{ id = id }.components or {}) do
if c == "LevelComponent" then return id end
end
end
end
local id = findLevel()
if not id then editor.log("no level here"); return end
local lvl = editor.get_component{ id = id, component = "LevelComponent" }
local cols, rows = math.floor(tonumber(lvl.cols)), math.floor(tonumber(lvl.rows))
for c = 0, cols - 1 do
for r = 0, rows - 1 do
if c == 0 or c == cols-1 or r == 0 or r == rows-1 then
editor.paint_asset{ asset = "assets/wall.png", cell = { col = c, row = r } }
end
end
end
An AGENT authors such a tool once with write_project_file; a HUMAN then runs it from the Tools menu forever after (or the agent triggers it with run_editor_script). See Docs/mcp-workflows.md for that worked loop.
Type reference
Every registered usertype not already in the index above: constructors, methods (:) and fields (.), with the source's own inline notes where present. Reached from the index accessors (world.get*, self.*, engine:*, the gui factory). Generated from the OSIMPLEEXPORT/OEXPORT blocks.
## LevelManager
LevelManager.getSingleton() -> LevelManager -- the runtime level director singleton
LevelManager:count() -> int -- number of levels in the sequence
LevelManager:currentIndex() -> int -- the live level index
LevelManager:levelName(i) -> string -- a level's display name
LevelManager:levelPar(i) -> int -- a level's par slide count
LevelManager:levelScene(i) -> string -- a level's project-relative scene path
LevelManager:hasNext() -> bool -- is there a level after the current one
LevelManager:loadLevel(i) -- request the deferred load of level i
LevelManager:loadScenePath(path) -- deferred load of any scene path
LevelManager:resumeLevel() -> int -- the persisted resume index
LevelManager:setResumeLevel(i) -- persist the resume index
LevelManager:bestMoves(i) -> int -- fewest recorded slides (-1 = none)
LevelManager:recordBestMoves(i, moves) -- record a best-moves result for level i
LevelManager:saveProgress() -- write the progression save file
## TimerHandle
TimerHandle:cancel() -- stop the timer now
TimerHandle:isActive() -> bool -- is the timer still scheduled
## ScriptTaskHandle
ScriptTaskHandle:cancel(...)
ScriptTaskHandle:isActive(...)
## GameObject
GameObject(...)
GameObject:loadTemplate(...)
GameObject:saveTemplate(...)
GameObject:getParentId(...)
GameObject:getParent(...)
GameObject:getChildIds(...)
GameObject:getPrefabRef(...)
GameObject:setActive(...)
GameObject:isActiveSelf(...)
GameObject:isActiveInHierarchy(...)
GameObject:setPersistent(...)
GameObject:isPersistent(...)
GameObject:hasTag(...)
GameObject:addTag(...)
GameObject:removeTag(...)
## GameObjectManager
GameObjectManager.getSingleton(...)
GameObjectManager:addGameObject(...)
GameObjectManager:delGameObject(...)
GameObjectManager:getGameObject(...)
GameObjectManager:objectExists(...)
GameObjectManager:createGameObject(...)
GameObjectManager:enableEvent(...)
GameObjectManager:disableEvent(...)
## LevelComponent
LevelComponent:getCols(...)
LevelComponent:getRows(...)
LevelComponent:getTileSize(...)
LevelComponent:getOriginX(...)
LevelComponent:getOriginY(...)
LevelComponent:getGoalSlot(...)
LevelComponent:getPar(...)
LevelComponent:getSlotCount(...)
LevelComponent:slotForPosition(...)
LevelComponent:slotForCell(...)
LevelComponent:slotCol(...)
LevelComponent:slotRow(...)
LevelComponent:slotCenterX(...)
LevelComponent:slotCenterY(...)
LevelComponent:starsForMoves(...)
LevelComponent.cols
LevelComponent.rows
LevelComponent.tileSize
LevelComponent.originX
LevelComponent.originY
LevelComponent.goalSlot
LevelComponent.par
## TileComponent
TileComponent:getOpenEdges(...)
TileComponent:setOpenEdges(...)
TileComponent:isEdgeOpen(...)
TileComponent:getSourceAssetId(...)
TileComponent:setSourceAssetId(...)
TileComponent.openEdges
TileComponent.sourceAssetId
## GuiFactory
GuiFactory(...)
GuiFactory:createLabel(...)
GuiFactory:createButton(...)
GuiFactory:createProgressBar(...)
GuiFactory:createCheckBox(...)
GuiFactory:createSlider(...)
GuiFactory:createSelectMenu(...)
GuiFactory:createDecorWidget(...)
GuiFactory:createTextEntry(...)
GuiFactory:createScrollView(...)
GuiFactory:createDropDown(...)
GuiFactory:loadLayout(...)
## GuiToggleGroup
GuiToggleGroup:addMember(...)
GuiToggleGroup:getSelected(...)
GuiToggleGroup:setSelected(...)
GuiToggleGroup:setAllowNone(...)
GuiToggleGroup:getMemberCount(...)
GuiToggleGroup:pollChanged(...)
## GuiTabBar
GuiTabBar:addTab(...)
GuiTabBar:getSelected(...)
GuiTabBar:setSelected(...)
GuiTabBar:getTabCount(...)
GuiTabBar:pollChanged(...)
## RenderNode
RenderNode:getPosition(...)
RenderNode:setPosition(...)
RenderNode:getOrientation(...)
RenderNode:setOrientation(...)
RenderNode:getScale(...)
RenderNode:setScale(...)
RenderNode:getWorldPosition(...)
RenderNode:getWorldOrientation(...)
RenderNode:translate(...)
RenderNode:lookAt(...)
RenderNode:setDirection(...)
RenderNode:setFixedYawAxis(...)
RenderNode:setVisible(...)
RenderNode:createChild(...)
RenderNode:getParent(...)
RenderNode:numChildren(...)
RenderNode.TransformSpace = { TS_LOCAL, TS_PARENT, TS_WORLD }
## RenderCamera
RenderCamera:getNode(...)
RenderCamera:setOrthographic(...)
RenderCamera:getProjectionType(...)
RenderCamera:getNearClip(...)
RenderCamera:getFarClip(...)
RenderCamera:setAspectRatio(...)
RenderCamera:setWireframe(...)
RenderCamera.ProjectionType = { PT_PERSPECTIVE, PT_ORTHOGRAPHIC }
## RenderSystem
RenderSystem:getWindowCamera(...)
RenderSystem:getWorld(...)
RenderSystem:saveWindowContents(...)
## RenderWorld
RenderWorld:getRootNode(...)
RenderWorld:createNode(...)
## TransformComponent
TransformComponent:getPosition(...)
TransformComponent:getScale(...)
TransformComponent:getOrientation(...)
TransformComponent:setPosition(...)
TransformComponent:setScale(...)
TransformComponent:setOrientation(...)
TransformComponent:getWorldPosition(...)
TransformComponent:getWorldOrientation(...)
TransformComponent:getWorldScale(...)
TransformComponent:setWorldPosition(...)
TransformComponent:setWorldOrientation(...)
TransformComponent:teleport(...)
TransformComponent.position
TransformComponent.orientation
TransformComponent.scale
TransformComponent.static
## ModelComponent
ModelComponent:loadModel(...)
ModelComponent:getCurrentModelFileName(...)
ModelComponent:setMaterialReference(...)
ModelComponent:getMaterialFileName(...)
ModelComponent:setTint(...)
ModelComponent:setEmissiveBoost(...)
ModelComponent.mesh
ModelComponent.material
ModelComponent.castShadows
ModelComponent.receiveShadows
ModelComponent.tint
ModelComponent.emissiveBoost
## WaterComponent
WaterComponent:getSizeX(...)
WaterComponent:setSizeX(...)
WaterComponent:getSizeZ(...)
WaterComponent:setSizeZ(...)
WaterComponent:getOpacity(...)
WaterComponent:setOpacity(...)
WaterComponent:getWaveScale(...)
WaterComponent:setWaveScale(...)
WaterComponent:getWaveSpeed(...)
WaterComponent:setWaveSpeed(...)
WaterComponent:getFresnelPower(...)
WaterComponent:setFresnelPower(...)
WaterComponent:getNormalTexture(...)
WaterComponent.sizeX
WaterComponent.sizeZ
WaterComponent.deepColour
WaterComponent.shallowColour
WaterComponent.opacity
WaterComponent.waveScale
WaterComponent.waveSpeed
WaterComponent.waveHeight
WaterComponent.fresnelPower
WaterComponent.normalTexture
WaterComponent.receiveShadows
WaterComponent.screenSpaceRefraction
WaterComponent.refractionStrength
WaterComponent.planarReflection
WaterComponent.reflectionStrength
## DecalComponent
DecalComponent:hasDecal(...)
DecalComponent:getTexture(...)
DecalComponent:setSizeX(...)
DecalComponent:getSizeX(...)
DecalComponent:setSizeZ(...)
DecalComponent:getSizeZ(...)
DecalComponent:setOpacity(...)
DecalComponent:getOpacity(...)
DecalComponent:setLifetime(...)
DecalComponent:getLifetime(...)
DecalComponent:setFadeDuration(...)
DecalComponent:getFadeDuration(...)
DecalComponent:place(...)
DecalComponent:fade(...)
DecalComponent.texture
DecalComponent.sizeX
DecalComponent.sizeZ
DecalComponent.projectionDepth
DecalComponent.opacity
DecalComponent.lifetime
DecalComponent.fadeDuration
## SpriteComponent
SpriteComponent:loadSprite(...)
SpriteComponent:loadSpriteFromAtlas(...)
SpriteComponent:removeSprite(...)
SpriteComponent:getTextureName(...)
SpriteComponent:hasSprite(...)
SpriteComponent:setSize(...)
SpriteComponent:getWidth(...)
SpriteComponent:getHeight(...)
SpriteComponent:getRenderedWidth(...)
SpriteComponent:getRenderedHeight(...)
SpriteComponent:setUVRect(...)
SpriteComponent:setTint(...)
SpriteComponent:setFlip(...)
SpriteComponent:getFlipX(...)
SpriteComponent:getFlipY(...)
SpriteComponent:setZOrder(...)
SpriteComponent:getZOrder(...)
SpriteComponent:setSpriteVisible(...)
SpriteComponent:isSpriteVisible(...)
SpriteComponent.width
SpriteComponent.height
SpriteComponent.u0
SpriteComponent.v0
SpriteComponent.u1
SpriteComponent.v1
SpriteComponent.tint
SpriteComponent.flipX
SpriteComponent.flipY
SpriteComponent.zOrder
SpriteComponent.texture
## LineComponent
LineComponent:hasMesh(...)
LineComponent:setPoints(...)
LineComponent:setPointsFlat(...)
LineComponent:beginPoints(...)
LineComponent:addPoint(...)
LineComponent:commitPoints(...)
LineComponent:clearPoints(...)
LineComponent:getPointCount(...)
LineComponent:getVertexCount(...)
LineComponent:getRebuildCount(...)
LineComponent:setMode(...)
LineComponent:getMode(...)
LineComponent:setColour(...)
LineComponent:setDepthTest(...)
LineComponent:getDepthTest(...)
LineComponent:setLineVisible(...)
LineComponent:isLineVisible(...)
LineComponent.mode
LineComponent.colour
LineComponent.depthTest
## WorldTextComponent
WorldTextComponent:setText(...)
WorldTextComponent:getText(...)
WorldTextComponent:setSize(...)
WorldTextComponent:getSize(...)
WorldTextComponent:setColour(...)
WorldTextComponent:setBillboard(...)
WorldTextComponent:getBillboard(...)
WorldTextComponent:setTextVisible(...)
WorldTextComponent:isTextVisible(...)
WorldTextComponent.text
WorldTextComponent.size
WorldTextComponent.colour
WorldTextComponent.billboard
## VectorShapeComponent
VectorShapeComponent:loadShape(...)
VectorShapeComponent:removeShape(...)
VectorShapeComponent:getShapeName(...)
VectorShapeComponent:hasShape(...)
VectorShapeComponent:getTriangleCount(...)
VectorShapeComponent:setTint(...)
VectorShapeComponent:setScale(...)
VectorShapeComponent:getScale(...)
VectorShapeComponent:setEdgeSoftness(...)
VectorShapeComponent:getEdgeSoftness(...)
VectorShapeComponent:setZOrder(...)
VectorShapeComponent:getZOrder(...)
VectorShapeComponent:setShapeVisible(...)
VectorShapeComponent:isShapeVisible(...)
VectorShapeComponent:setSoftBodyEnabled(...)
VectorShapeComponent:isSoftBodyEnabled(...)
VectorShapeComponent:impulse(...)
VectorShapeComponent:playMorph(...)
VectorShapeComponent:stopMorph(...)
VectorShapeComponent:getDeformDisplacement(...)
VectorShapeComponent:getSquash(...)
VectorShapeComponent:isDeforming(...)
VectorShapeComponent:getControlPointCount(...)
VectorShapeComponent:getMorphTargetCount(...)
VectorShapeComponent.tint
VectorShapeComponent.scale
VectorShapeComponent.edgeSoftness
VectorShapeComponent.zOrder
VectorShapeComponent.softBody
VectorShapeComponent.wobbleStiffness
VectorShapeComponent.wobbleDamping
VectorShapeComponent.wobbleAmount
VectorShapeComponent.squashAmount
VectorShapeComponent.morphClip
VectorShapeComponent.morphSpeed
VectorShapeComponent.morphLoop
VectorShapeComponent.shape
## VectorAnimationComponent
VectorAnimationComponent:loadAnimation(...)
VectorAnimationComponent:removeAnimation(...)
VectorAnimationComponent:getAnimationName(...)
VectorAnimationComponent:hasAnimation(...)
VectorAnimationComponent:getTriangleCount(...)
VectorAnimationComponent:getVertexCount(...)
VectorAnimationComponent:getRunCount(...)
VectorAnimationComponent:getTexturedRunCount(...)
VectorAnimationComponent:getPoseSignature(...)
VectorAnimationComponent:play(...)
VectorAnimationComponent:stop(...)
VectorAnimationComponent:setClip(...)
VectorAnimationComponent:crossFade(...)
VectorAnimationComponent:scrub(...)
VectorAnimationComponent:isPlaying(...)
VectorAnimationComponent:currentClip(...)
VectorAnimationComponent:getClipCount(...)
VectorAnimationComponent:getClipNames(...)
VectorAnimationComponent:currentFrame(...)
VectorAnimationComponent:isAtEnd(...)
VectorAnimationComponent:setSpeed(...)
VectorAnimationComponent:getSpeed(...)
VectorAnimationComponent.clip
VectorAnimationComponent.speed
VectorAnimationComponent.playing
VectorAnimationComponent.transitionTime
VectorAnimationComponent.tint
VectorAnimationComponent.scale
VectorAnimationComponent.edgeSoftness
VectorAnimationComponent.zOrder
VectorAnimationComponent.animation
## SpriteAnimationComponent
SpriteAnimationComponent:setGrid(...)
SpriteAnimationComponent:getGridColumns(...)
SpriteAnimationComponent:getGridRows(...)
SpriteAnimationComponent:addClip(...)
SpriteAnimationComponent:removeClip(...)
SpriteAnimationComponent:hasClip(...)
SpriteAnimationComponent:getClipCount(...)
SpriteAnimationComponent:setDefaultClip(...)
SpriteAnimationComponent:getDefaultClip(...)
SpriteAnimationComponent:play(...)
SpriteAnimationComponent:stop(...)
SpriteAnimationComponent:setClip(...)
SpriteAnimationComponent:isPlaying(...)
SpriteAnimationComponent:getCurrentClip(...)
SpriteAnimationComponent:setFrame(...)
SpriteAnimationComponent:getCurrentFrame(...)
SpriteAnimationComponent:setSpeed(...)
SpriteAnimationComponent:getSpeed(...)
SpriteAnimationComponent.gridColumns
SpriteAnimationComponent.gridRows
SpriteAnimationComponent.defaultClip
## ParticleComponent
ParticleComponent:setTexture(...)
ParticleComponent:getTextureName(...)
ParticleComponent:burst(...)
ParticleComponent:start(...)
ParticleComponent:stop(...)
ParticleComponent:setEmitting(...)
ParticleComponent:isEmitting(...)
ParticleComponent:getLiveCount(...)
ParticleComponent.space3D
ParticleComponent.worldSpace
ParticleComponent.emissionVolume
ParticleComponent.volumeExtents
ParticleComponent.gravity3D
ParticleComponent.wind
ParticleComponent.direction3D
ParticleComponent.stretch
ParticleComponent.flutterAmplitude
ParticleComponent.flutterFrequency
ParticleComponent.additive
ParticleComponent.maxParticles
## AnimationComponent
AnimationComponent:getAvailableAnimations(...)
AnimationComponent:getBoneNames(...)
AnimationComponent:getDefaultAnimation(...)
AnimationComponent:setDefaultAnimation(...)
AnimationComponent:playAnimation(...)
AnimationComponent:stopAnimation(...)
AnimationComponent:setAnimationTime(...)
AnimationComponent:setSpeed(...)
AnimationComponent:getSpeed(...)
AnimationComponent:getHandleMotion(...)
AnimationComponent:getHandleRotation(...)
AnimationComponent:getExtractMotion(...)
AnimationComponent:getExtractRotation(...)
AnimationComponent:setHandleMotion(...)
AnimationComponent:setHandleRotation(...)
AnimationComponent:setExtractMotion(...)
AnimationComponent:setExtractRotation(...)
AnimationComponent:getMotionBone(...)
AnimationComponent:setMotionBone(...)
AnimationComponent:crossFadeTo(...)
AnimationComponent:isCrossFading(...)
AnimationComponent:getCrossFadeProgress(...)
AnimationComponent.clip
AnimationComponent.clipTime
AnimationComponent.clipLoop
AnimationComponent.speed
AnimationComponent.paused
## BoneAttachComponent
BoneAttachComponent:getTarget(...)
BoneAttachComponent:setTarget(...)
BoneAttachComponent:getBone(...)
BoneAttachComponent:setBone(...)
BoneAttachComponent:setOffsetXYZ(...)
BoneAttachComponent:getFollowRotation(...)
BoneAttachComponent:setFollowRotation(...)
BoneAttachComponent:getFollowScale(...)
BoneAttachComponent:setFollowScale(...)
BoneAttachComponent:applyAttachment(...)
BoneAttachComponent.target
BoneAttachComponent.bone
BoneAttachComponent.offset
BoneAttachComponent.followRotation
BoneAttachComponent.followScale
## CameraComponent
CameraComponent:setProjectionMode(...)
CameraComponent:getProjectionMode(...)
CameraComponent:setOrthoSize(...)
CameraComponent:getOrthoSize(...)
CameraComponent:setFitMode(...)
CameraComponent:getFitMode(...)
CameraComponent:setDesignWidth(...)
CameraComponent:getDesignWidth(...)
CameraComponent:setDesignHeight(...)
CameraComponent:getDesignHeight(...)
CameraComponent:follow(...)
CameraComponent:stopFollow(...)
CameraComponent:setFollowTarget(...)
CameraComponent:getFollowTarget(...)
CameraComponent:setFollowDamping(...)
CameraComponent:getFollowDamping(...)
CameraComponent:setFollowOffset(...)
CameraComponent:getFollowOffset(...)
CameraComponent.projectionMode
CameraComponent.orthoSize
CameraComponent.fitMode
CameraComponent.designWidth
CameraComponent.designHeight
CameraComponent.followTarget
CameraComponent.followDamping
CameraComponent.followOffset
CameraComponent.ProjectionMode = { PM_PERSPECTIVE, PM_ORTHOGRAPHIC }
CameraComponent.FitMode = { FM_HEIGHT, FM_WIDTH, FM_EXPAND }
## LightComponent
LightComponent:hasLight(...)
LightComponent:setType(...)
LightComponent:getType(...)
LightComponent:setColour(...)
LightComponent:setIntensity(...)
LightComponent:getIntensity(...)
LightComponent:setRange(...)
LightComponent:getRange(...)
LightComponent:setInnerAngle(...)
LightComponent:getInnerAngle(...)
LightComponent:setOuterAngle(...)
LightComponent:getOuterAngle(...)
LightComponent:setCastsShadows(...)
LightComponent:getCastsShadows(...)
LightComponent.type
LightComponent.colour
LightComponent.intensity
LightComponent.range
LightComponent.innerAngle
LightComponent.outerAngle
LightComponent.castsShadows
## AtmosphereComponent
AtmosphereComponent:isAtmosphereOwner() -> bool -- does THIS instance currently own the world atmosphere (dormant siblings store state only)
AtmosphereComponent:setEnabled(enabled) -> nil -- master switch of the armed desc (false = the owner arms a plain clear background)
AtmosphereComponent:getEnabled() -> bool -- the armed desc's master switch
AtmosphereComponent:setPreset(name) -> nil -- seed every look field from a named preset ('day'/'sunset'/'night'; 'custom' seeds nothing; unknown words warn and keep the state)
AtmosphereComponent:getPreset() -> string -- the last seed word
AtmosphereComponent:setSky(sky) -> nil -- the visible-sky part switch: false hides the dome/cubemap while the lighting drive + fog stay live (independent of the preset)
AtmosphereComponent:getSky() -> bool -- whether the visible sky part is on
AtmosphereComponent:setFog(fog) -> nil -- the distance-fog part switch: false drops the object fog while the dome + lighting drive stay live (independent of the preset)
AtmosphereComponent:getFog() -> bool -- whether the distance-fog part is on
AtmosphereComponent:setSkyColour(r, g, b) -> nil -- zenith sky tint, linear 0..1
AtmosphereComponent:setSkyPower(power) -> nil -- HDR sky-dome brightness multiplier (1 = neutral)
AtmosphereComponent:getSkyPower() -> number -- the sky-dome brightness multiplier
AtmosphereComponent:setDensity(density) -> nil -- sky Rayleigh density coefficient (thicker = hazier horizon)
AtmosphereComponent:getDensity() -> number -- the sky Rayleigh density coefficient
AtmosphereComponent:setSunPower(power) -> nil -- linked directional light power - the exposure knob
AtmosphereComponent:getSunPower() -> number -- the linked sun power
AtmosphereComponent:setAmbientPower(power) -> nil -- scales the atmosphere-driven hemisphere ambient fill (1 = native)
AtmosphereComponent:getAmbientPower() -> number -- the ambient fill scale
AtmosphereComponent:setFogDensity(density) -> nil -- per-object exponential distance fog (0 = none)
AtmosphereComponent:getFogDensity() -> number -- the object-fog density
AtmosphereComponent:setFogColour(r, g, b) -> nil -- fog colour, linear 0..1 (classic fixed-function fallback fog)
AtmosphereComponent.enabled
AtmosphereComponent.preset
AtmosphereComponent.sky
AtmosphereComponent.fog
AtmosphereComponent.skyColour
AtmosphereComponent.skyPower
AtmosphereComponent.density
AtmosphereComponent.sunPower
AtmosphereComponent.ambientPower
AtmosphereComponent.fogDensity
AtmosphereComponent.fogColour
## SoundComponent
SoundComponent:addSound(...)
SoundComponent:play(...)
SoundComponent:stop(...)
SoundComponent:stopAllSounds(...)
SoundComponent:setVolume(...)
SoundComponent:getVolume(...)
SoundComponent:setGroup(...)
SoundComponent:getGroup(...)
SoundComponent:setPitchVariation(...)
SoundComponent:setVolumeVariation(...)
## RigidBodyComponent
RigidBodyComponent:setBodyType(...)
RigidBodyComponent:setBoxShape(...)
RigidBodyComponent:setSphereShape(...)
RigidBodyComponent:setCapsuleShape(...)
RigidBodyComponent:setShapeAsset(...)
RigidBodyComponent:setMass(...)
RigidBodyComponent:setFriction(...)
RigidBodyComponent:setRestitution(...)
RigidBodyComponent:setPlanarMode(...)
RigidBodyComponent:getPlanarMode(...)
RigidBodyComponent:setLayer(...)
RigidBodyComponent:getLayer(...)
RigidBodyComponent:setIsSensor(...)
RigidBodyComponent:isSensor(...)
RigidBodyComponent:setLinearVelocity(...)
RigidBodyComponent:getLinearVelocity(...)
RigidBodyComponent:setAngularVelocity(...)
RigidBodyComponent:getAngularVelocity(...)
RigidBodyComponent:applyImpulse(...)
RigidBodyComponent:applyForce(...)
RigidBodyComponent:teleport(...)
RigidBodyComponent:hasBody(...)
RigidBodyComponent:getBodyId(...)
RigidBodyComponent.bodyType
RigidBodyComponent.shapeType
RigidBodyComponent.halfExtents
RigidBodyComponent.radius
RigidBodyComponent.halfHeight
RigidBodyComponent.shapeAsset
RigidBodyComponent.mass
RigidBodyComponent.friction
RigidBodyComponent.restitution
RigidBodyComponent.planar
RigidBodyComponent.layer
RigidBodyComponent.isSensor
RigidBodyComponent.linear_velocity
RigidBodyComponent.angular_velocity
RigidBodyComponent.has_body
## ScriptComponent
ScriptComponent:setScriptFile(...)
ScriptComponent:getScriptFile(...)
ScriptComponent:setScriptEnabled(...)
ScriptComponent:isScriptEnabled(...)
ScriptComponent:hasScriptError(...)
ScriptComponent:getScriptError(...)
ScriptComponent:isScriptStarted(...)
ScriptComponent:reloadScript(...)
ScriptComponent:hotReload(...)
ScriptComponent:hasReloadError(...)
ScriptComponent:getLastReloadError(...)
ScriptComponent.script
ScriptComponent.enabled
ScriptComponent.started
ScriptComponent.error
## Engine
Engine(...)
Engine.getSingleton(...)
Engine:setup(...)
Engine:getTopLevelWindowHandle(...)
Engine:renderOneFrame(...)
Engine:enableWireframeMode(...)
Engine:disableWireframeMode(...)
Engine:getCamera(...)
Engine:getRenderSystem(...)
Engine:getWindowWidth(...)
Engine:getWindowHeight(...)
Engine:getSafeAreaInsets(...)
Engine:getContentScale(...)
Engine:setCameraOrthographic(...)
Engine:setCameraOrthographicFit(...)
Engine:setCameraPerspective(...)
Engine:setWindowBackgroundColour(...)
Engine:setAtmosphere(enabled, skyRed, skyGreen, skyBlue, density, fogDensity) -> nil -- set the scene sky/fog atmosphere; sun = the first directional light (next renders the native atmospheric dome, classic a gradient-dome subset)
Engine:setAtmosphereBlend(...)
Engine:setAtmosphereSky(skyType, skyboxTexture) -> nil -- choose the sky visual: 'procedural' (default), 'skybox' (a cubemap .dds resource, e.g. baked by make_sky_assets.py) or 'colour' (flat sky tint); sticky across setAtmosphere/setAtmosphereBlend calls - fog and the sun drive are sky-type-independent
Engine:setAtmosphereParts(sky, fog) -> nil -- toggle the enabled atmosphere's two part switches: sky = the visible dome/cubemap (false keeps lighting + fog, drops the sky), fog = the distance object fog (false keeps the dome + lighting); sticky across setAtmosphere/setAtmosphereBlend calls, gated by the master enabled
Engine:setImageLighting(enabled, intensity) -> nil -- opt into image-based lighting sourced from the skybox cubemap the enabled atmosphere shows: cubemap specular reflections + a diffuse fill ADDED to the analytic lights on PBS materials, intensity scaling the added part; sticky like the atmosphere, OFF by default (off renders byte-identically). Quality tiers ride the r.iblQuality cvar; enabling under a procedural/colour sky logs one honest line and renders unchanged. Probe: engine:supports('iblReflections')
Engine:setBloom(enabled, threshold, intensity) -> nil -- set the scene's LDR bloom (a highlight glow on the 3D tier); default OFF and byte-identical when disabled. threshold is the bright-pass luminance cutoff in [0,1) (near-white highlights bloom), intensity scales the additive glow. The 2D tier (sprites/vector shapes/gui) stays crisp; the r.bloomQuality cvar sets the blur budget. Renders on both flavors (next = compositor quad chain, classic = viewport compositor); a GLES2/WebGL classic context is runtime-gated and degrades honestly
Engine:setGrade(...)
Engine:hasUISystem(...)
Engine:supports(capabilityName) -> bool -- does the active render backend support a capability? (e.g. "skyDome"/"dynamicShadows"/"hemisphereAmbient") - degrade a script's look per flavor; an unknown name is false
Engine:getLightBudget() -> integer -- the active render backend's sane concurrent dynamic-light ceiling - size a many-lights ramp to the flavor (next's clustered-forward headroom is far above classic's forward floor); 0 before the render system exists
## FrameEventData
FrameEventData.timeSinceLastEvent
FrameEventData.timeSinceLastFrame
## KeyEventData
KeyEventData.text
KeyEventData.key
KeyEventData.KeyCode = { KC_A, KC_B, KC_C, KC_D, KC_E, KC_F, KC_G, KC_H, KC_I, KC_J, KC_K, KC_L, KC_M, KC_N, KC_O, KC_P, KC_Q, KC_R, KC_S, KC_T, KC_U, KC_V, KC_W, KC_X, KC_Y, KC_Z, KC_0, KC_1, KC_2, KC_3, KC_4, KC_5, KC_6, KC_7, KC_8, KC_9, KC_LEFT, KC_RIGHT, KC_UP, KC_DOWN, KC_SPACE, KC_ESCAPE, KC_RETURN, KC_TAB, KC_LSHIFT, KC_RSHIFT, KC_LCONTROL, KC_RCONTROL }
## MouseEventData
MouseEventData.button
## InputManager
InputManager(...)
InputManager.getSingleton(...)
InputManager:enable(...)
InputManager:disable(...)
InputManager:isKeyDown(...)
InputManager:getTilt(...)
InputManager:isTiltSensorAvailable(...)
InputManager:setTiltAngle(...)
InputManager:getTiltAngle(...)
InputManager:calibrateTilt(...)
InputManager:clearTiltCalibration(...)
InputManager:getTiltCalibration(...)
## PhysicsWorld
PhysicsWorld.getSingleton(...)
PhysicsWorld:update(...)
PhysicsWorld:setPaused(...)
PhysicsWorld:isPaused(...)
PhysicsWorld:setGravity(...)
PhysicsWorld:getGravity(...)
PhysicsWorld:setBodyTransform(...)
PhysicsWorld:setBodyPlanarMode(...)
PhysicsWorld:setLinearVelocity(...)
PhysicsWorld:getLinearVelocity(...)
PhysicsWorld:setAngularVelocity(...)
PhysicsWorld:getAngularVelocity(...)
PhysicsWorld:applyImpulse(...)
PhysicsWorld:applyForce(...)
PhysicsWorld:isBodyActive(...)
PhysicsWorld:castRayHit(...)
## GuiWidget
GuiWidget:setPosition(...)
GuiWidget:setSize(...)
GuiWidget:getSize(...)
GuiWidget:getPosition(...)
GuiWidget:centerHorizontal(...)
GuiWidget:setEnabled(...)
GuiWidget:isEnabled(...)
GuiWidget:setAnchors(...)
GuiWidget:setAnchorPreset(...)
GuiWidget:setPivot(...)
GuiWidget:setOffsets(...)
GuiWidget:setAnchoredPosition(...)
GuiWidget:setSizeDelta(...)
GuiWidget:setUseSafeArea(...)
GuiWidget:setLayoutGroup(...)
GuiWidget:setGroupPadding(...)
GuiWidget:setGroupSpacing(...)
GuiWidget:setChildAlignment(...)
GuiWidget:setChildForceExpand(...)
GuiWidget:setGridCellSize(...)
GuiWidget:setGridConstraint(...)
GuiWidget:setContentSizeFit(...)
GuiWidget:setRenderScale(...)
GuiWidget:getRenderScaleX(...)
GuiWidget:getRenderScaleY(...)
GuiWidget:setRenderRotation(...)
GuiWidget:getRenderRotation(...)
GuiWidget:setGroupAlpha(...)
GuiWidget:getGroupAlpha(...)
GuiWidget:getEffectiveAlpha(...)
GuiWidget:setAlphaBlocksInput(...)
GuiWidget:setTransition(...)
GuiWidget:setEnterTransition(...)
GuiWidget:setExitTransition(...)
GuiWidget:setFontIndex(...)
GuiWidget:getFontIndex(...)
GuiWidget:setTextColour(...)
GuiWidget:setTextScale(...)
GuiWidget:getTextScale(...)
GuiWidget:setTextMarkup(...)
GuiWidget:getTextMarkup(...)
GuiWidget:hasTextStyle(...)
## GuiTextbox
GuiTextbox:setWrap(...)
## GuiTextEntry
GuiTextEntry:setPosition(...)
GuiTextEntry:setSize(...)
GuiTextEntry:getSize(...)
GuiTextEntry:getPosition(...)
GuiTextEntry:getText(...)
GuiTextEntry:setText(...)
GuiTextEntry:setPlaceholder(...)
GuiTextEntry:setMaxLength(...)
GuiTextEntry:getMaxLength(...)
GuiTextEntry:isFocused(...)
GuiTextEntry:wasSubmitted(...)
GuiTextEntry:setMultiline(...)
GuiTextEntry:isMultiline(...)
GuiTextEntry:getLineCount(...)
GuiTextEntry:getFirstVisibleLine(...)
## GuiSelectMenu
GuiSelectMenu:setItems(...)
GuiSelectMenu:setItemsString(...)
GuiSelectMenu:getSelectedItemIndex(...)
GuiSelectMenu:getSelectedItem(...)
GuiSelectMenu:selectItemIndex(...)
GuiSelectMenu:selectItem(...)
GuiSelectMenu:getCaption(...)
GuiSelectMenu:setCaption(...)
## GuiProgressBar
GuiProgressBar:setProgress(...)
GuiProgressBar:addProgress(...)
GuiProgressBar:getProgress(...)
GuiProgressBar:getCaption(...)
GuiProgressBar:setCaption(...)
## GuiManager
GuiManager(...)
GuiManager.getSingleton(...)
GuiManager:getFactory(...)
GuiManager:enableInputEvents(...)
GuiManager:disableInputEvents(...)
GuiManager:widgetExists(...)
GuiManager:findWidget(...)
GuiManager:findLabel(...)
GuiManager:findButton(...)
GuiManager:findCheckBox(...)
GuiManager:findSlider(...)
GuiManager:findSelectMenu(...)
GuiManager:findProgressBar(...)
GuiManager:findDecor(...)
GuiManager:findTextEntry(...)
GuiManager:findScrollView(...)
GuiManager:destroyWidget(...)
GuiManager:destroyAllWidgets(...)
GuiManager:hideAllViews(...)
GuiManager:showAllViews(...)
GuiManager:isPointOverWidget(...)
GuiManager:setDesignResolution(...)
GuiManager:setLayoutMatchMode(...)
GuiManager:setRootSpace(...)
GuiManager:getLayoutScale(...)
GuiManager:getUiScale(...)
GuiManager:showModal(...)
GuiManager:getModalContentZ(...)
GuiManager:registerModalWidget(...)
GuiManager:dismissModal(...)
GuiManager:dismissTopModal(...)
GuiManager:dismissAllModals(...)
GuiManager:isModalActive(...)
GuiManager:getTopModalId(...)
GuiManager:getModalCount(...)
GuiManager:showConfirm(...)
GuiManager:showAlert(...)
GuiManager:getDialogResult(...)
GuiManager:cancelCurrentInputUpdate(...)
GuiManager:createToggleGroup(...)
GuiManager:getToggleGroup(...)
GuiManager:destroyToggleGroup(...)
GuiManager:createTabBar(...)
GuiManager:getTabBar(...)
GuiManager:destroyTabBar(...)
GuiManager:showToast(...)
GuiManager:isToastVisible(...)
GuiManager:getLastBatchCount(...)
GuiManager:getRebuildCount(...)
GuiManager:getGeometryRebuildCount(...)
GuiManager:getScratchCapacity(...)
## GuiLabel
GuiLabel:setText(...)
GuiLabel:setAlignment(...)
GuiLabel:setAlpha(...)
GuiLabel:setWrap(...)
GuiLabel.LabelAlignment = { LA_TOPLEFT, LA_TOP, LA_TOPRIGHT, LA_LEFT, LA_CENTER, LA_RIGHT, LA_BOTTOMLEFT, LA_BOTTOM, LA_BOTTOMRIGHT }
## GuiDecorWidget
GuiDecorWidget:setSprite(...)
GuiDecorWidget:setColour(...)
GuiDecorWidget:setAlpha(...)
GuiDecorWidget:setNineSlice(...)
GuiDecorWidget:setTiled(...)
## GuiCheckBox
GuiCheckBox:isChecked(...)
GuiCheckBox:setChecked(...)
GuiCheckBox:toggle(...)
## GuiButton
GuiButton:getCaption(...)
GuiButton:setCaption(...)
GuiButton:getState(...)
GuiButton:wasClicked(...)
GuiButton:setNineSlice(...)
GuiButton:setTiled(...)
GuiButton:setPressFeedback(...)
GuiButton.ButtonState = { BS_DISABLED, BS_UP, BS_OVER, BS_DOWN }
## GuiDropDown
GuiDropDown:setItems(...)
GuiDropDown:setItemsString(...)
GuiDropDown:getSelectedIndex(...)
GuiDropDown:getSelectedItem(...)
GuiDropDown:selectIndex(...)
GuiDropDown:isMenuOpen(...)
## GuiScrollView
GuiScrollView:setScroll(...)
GuiScrollView:getScroll(...)
GuiScrollView:getMaxScroll(...)
## GuiListView
GuiListView:addItem(...)
GuiListView:removeItem(...)
GuiListView:clear(...)
GuiListView:getItemCount(...)
GuiListView:getItemId(...)
GuiListView:getItemText(...)
GuiListView:setVirtualized(...)
GuiListView:isVirtualized(...)
GuiListView:setItemHeight(...)
GuiListView:getItemHeight(...)
GuiListView:getMaterializedCount(...)
GuiListView:getFirstMaterializedIndex(...)
## DragEventData
DragEventData.button
DragEventData.state
DragEventData.position