Log inGet started

engine

Updated October 20, 2026

The engine namespace: 65 functions.

globals/engine/discardPlayChanges

engine.discardPlayChanges() -> { paths: { string }, keep: string }

Arm the leave-play safeguard's deliberate discard for your next engine.mode = "edit" in the play session this is called from, so it proceeds and discards your unaccepted changes. While other callers stay in the session that is your own work: the /source edits you made in play, which revert, and the entities you tuned. The last caller to leave takes the engine to edit, and its discard throws away everything play made: the entities and tuning, and every /source file edit play holds on its shadow. Called by a caller outside the session, it arms the discard of engine.setMode("edit", { endSession = true }), which ends the session for every caller in it. A call that names no caller (a transport that attests no caller for it) cannot be told apart from any caller, so the exit that ends the session takes an arm made for the whole session from the other side: an exit that names no caller takes the arm a named caller made, and a named caller's exit takes the arm a call that named no caller made. Returns paths, the source files the flip would throw away if it ran now, and keep, the routes that keep any of them first; a notice names the same paths. The flip throws away what the shadow holds when it runs, so an edit made after the arm goes with the rest, and the flip names every path it took in a notice and the engine log (vfs.discardPlayShadow() states the same set). A discarded source edit whose pre-play copy is not on this engine yet keeps the play bytes until that copy lands and is put back; the flip names each such path, and vfs.playDiscardsOwed() lists them until then.

Returns { paths: { string }, keep: string }

globals/engine/editWaiters

engine.editWaiters() -> { EditWaiter }

Everyone waiting for this engine to leave play so their edit-mode work can run, oldest first. Each entry is { key, id, name, reason, heldSeconds }: id is the waiting caller, the same value vfs.currentAuthor().id reads, and heldSeconds is how long it has been waiting. A session holding play reads this to see whose work it is holding. Empty in edit mode and whenever nobody has said they are waiting.

Returns { EditWaiter } — The waiter records.

for _, who in ipairs(engine.editWaiters()) do
log.warn(("%s has waited %.0fs for edit mode"):format(who.name or who.key, who.heldSeconds))
end

globals/engine/engineRecord

engine.engineRecord(record: { [any]: any })

Declare record an engine record: a table a subsystem keeps as its own account of what the engine holds — the renderer's registry of live features, its capture views — and puts back through its own reset. engine.snapshotModuleState neither copies nor walks it, and engine.restoreModuleState leaves it as that reset left it, so a module restore never seats a record back around what the reset did: a feature a suite destroyed stays destroyed, and one it made again under its identity stays the one the renderer ticks.

Parameters

  • record { [any]: any } — The table.
engine.engineRecord(activeFeatures)

globals/engine/gameplayReady

engine.gameplayReady -> boolean

Whether gameplay simulation is running: not paused, and the play scene materialized. Read-only.

Returns boolean

globals/engine/gpuCompute

engine.gpuCompute -> boolean

Whether this process holds a live GPU device, so compute dispatch is available. Read-only.

Returns boolean

globals/engine/headless

engine.headless -> boolean

Whether this boot renders offscreen with no window a person can see. Content that only serves someone at a display stands down when it reads true. Read-only.

Returns boolean

globals/engine/holdTimeScale

engine.holdTimeScale(scale: number, options: TimeScaleHoldOptions?) -> () -> ()

Run the simulation at scale for as long as the hold stands, and return the release. engine.timeScale is one number for the whole process, so a rate one agent sets is the rate every other agent's entities and measurements run at; a hold records who set it and refuses a plain engine.timeScale write from anywhere else while it stands, so the rate cannot move under a measurement without the measurement hearing about it. Holds nest: the innermost names the rate, and the rate that stood before the outermost is back once the last release is called. Each release takes its own hold off the stack whatever order the releases come in. exclusive takes the rate for the owner key the call states: while that hold stands, a hold is admitted only when it states the same key, and every other one is refused with an error naming the key and the rate. That is what lets one caller pin the rate its run is measured at while another agent drives the same engine.

Parameters

  • scale number — The rate to run at: 1 is real time, 0.5 half speed, 0 frozen, 2 double. Finite and >= 0.
  • options TimeScaleHoldOptions (optional) — owner is the key this hold is taken under, a non-empty string, and an exclusive hold states one. A hold that states no key takes the key of the agent the call is attributed to, or a minted hold#n when the call carries no agent identity, so every hold answers to a key engine.releaseTimeScale accepts and engine.timeScaleOwner reports as id. exclusive takes the rate for the stated key until the hold is released.

Returns () -> () — A function that releases this hold. Calling it twice releases once.

local release = engine.holdTimeScale(0.08) ; task.wait(0.12) ; release()
local release = engine.holdTimeScale(1, { exclusive = true, owner = "duel-measurement" })

globals/engine/markBaseline

engine.markBaseline() -> number

Read this module's own memory — the time-scale holds standing, who wrote the rate last, the rate that stood before any hold, the onTimeScaleChange subscribers and the highest watcher id each registry has handed out — and answer with a generation engine.resetToBaseline puts exactly that back from. Take it once, before the work whose changes are to be undone.

Returns number — The generation of this reading.

local mark = engine.markBaseline() -- then run, then engine.resetToBaseline(mark)

globals/engine/markScriptingBaseline

engine.markScriptingBaseline() -> number

Record the scripting registries — world-event subscriptions, the four lifecycle-watcher lists, and the require cache — as they stand right now, as a mark engine.resetScriptingState() restores to. Marks stack: a reset names the generation it restores to, and the marks taken after that one go with it. Returns the new mark's generation, counting from 1. Mark once the engine is serving rather than while it boots: the registries keep growing as the prelude subscribes, the world entrypoint runs and the startup scene loads, so a mark taken partway through sits below the rest of that work and the first reset would remove it.

Returns number

engine.markScriptingBaseline()
world.on("player_join", function() end)
engine.resetScriptingState() -- the subscription above is gone

globals/engine/mode

engine.mode -> "edit" | "play"

The engine mode this process is in, edit or play. Assigning it takes the flip, side effects and all.

Returns "edit" | "play"

globals/engine/modeOwner

engine.modeOwner -> { id: string?, agent: string?, name: string?, heldSeconds: number }?

Who flipped the engine out of the mode it booted into, and how long ago, while that caller still holds it. In play, engine.playSession names every caller the session belongs to now. id is the caller key, the same value vfs.currentAuthor().id reads, and nil when that flip carried no agent identity; agent is the name the world's roster holds for that caller now, the one to ask by, and name the username of its account, which every agent of the account shares. Nil in the mode the engine booted into (a flip back to it ends the hold), once the caller that flipped it has left the play session it entered, and once that caller is no longer on the world's roster. Read-only.

Returns { id: string?, agent: string?, name: string?, heldSeconds: number }?

globals/engine/offDeviceRebuilt

engine.offDeviceRebuilt(id: number) -> boolean

Remove an engine.onDeviceRebuilt subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none — already removed, or never registered.

Parameters

  • id number — Watcher id returned by engine.onDeviceRebuilt.

Returns boolean

local id = engine.onDeviceRebuilt(function() end)
engine.offDeviceRebuilt(id)

globals/engine/offMemoryPressure

engine.offMemoryPressure(id: number) -> boolean

Remove an engine.onMemoryPressure subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none: already removed, or never registered.

Parameters

  • id number — Watcher id returned by engine.onMemoryPressure.

Returns boolean

local id = engine.onMemoryPressure(function() end)
engine.offMemoryPressure(id)

globals/engine/offModeChange

engine.offModeChange(id: number) -> boolean

Remove an engine.onModeChange subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none — already removed, or never registered.

Parameters

  • id number — Watcher id returned by engine.onModeChange.

Returns boolean

local id = engine.onModeChange(function() end)
engine.offModeChange(id)

globals/engine/offPauseChange

engine.offPauseChange(id: number) -> boolean

Remove an engine.onPauseChange subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none.

Parameters

  • id number — Watcher id returned by engine.onPauseChange.

Returns boolean

globals/engine/offTimeScaleChange

engine.offTimeScaleChange(id: number) -> boolean

Remove an engine.onTimeScaleChange subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none.

Parameters

  • id number — Watcher id returned by engine.onTimeScaleChange.

Returns boolean

local id = engine.onTimeScaleChange(function() end)
engine.offTimeScaleChange(id)

globals/engine/offWorldLoaded

engine.offWorldLoaded(id: number) -> boolean

Remove an onWorldLoaded subscriber by its watcher id.

Parameters

  • id number

Returns boolean

globals/engine/offWorldReady

engine.offWorldReady(id: number) -> boolean

Remove an engine.onWorldReady subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none.

Parameters

  • id number — Watcher id returned by engine.onWorldReady.

Returns boolean

globals/engine/offWorldUnloading

engine.offWorldUnloading(id: number) -> boolean

Remove an engine.onWorldUnloading subscriber by its watcher id. Returns true when a live watcher carried that id, false when it named none.

Parameters

  • id number — Watcher id returned by engine.onWorldUnloading.

Returns boolean

globals/engine/onDeviceRebuilt

engine.onDeviceRebuilt(callback: (number) -> ()) -> number

Register a callback that fires after the engine has answered a lost render device by building another one. The callback receives the new device generation — a number that counts the devices this session has run on, and moves exactly once per rebuild. Returns a watcher id.

A device is lost when the driver resets, when the GPU is taken away, or when a browser reclaims a WebGPU context. Everything the engine can re-derive by itself it does: meshes, materials, shaders, render passes and the UI are all back on the new device before this fires. What it cannot re-derive is what YOUR content made and only the GPU held — a texture uploaded from pixels a script computed, a compute buffer it filled, a render target it created. Make those again here.

Content that owns no GPU resource of its own needs no subscriber: asset handles re-materialise on their next use.

Parameters

  • callback (number) -> () — Function invoked as (generation: number).

Returns number

engine.onDeviceRebuilt(function(generation)
-- the noise field lived only on the GPU, so it is computed again
print("upload the noise field again for device generation", generation)
end)

globals/engine/onMemoryPressure

engine.onMemoryPressure(callback: (string, string) -> ()) -> number

Register a callback that fires when the system says it is short of memory, after the engine has released what that level of pressure releases. The callback receives the level ("normal", "moderate", "low" or "critical") and who reported it: onTrimMemory(<level>) for Android's report, script for renderer.signalMemoryPressure. Returns a watcher id.

The engine frees what it can rebuild by itself: mip levels the frame does not sample, render-target storage nothing is drawing into, and from "critical" half the texture budget. What it cannot free is what YOUR content keeps: a cache of generated meshes, a table of decoded audio, pixels kept for a later upload. Drop those here and build them again when they are next needed. A level stands for a minute after the last report, and renderer.memoryPressure() reads the level in force and what the engine released.

Parameters

  • callback (string, string) -> () — Function invoked as (level: string, source: string).

Returns number

engine.onMemoryPressure(function(level, source)
if level ~= "normal" then
decodedClips = {} -- decoded again on next play
end
end)

globals/engine/onModeChange

engine.onModeChange(callback: (string, string) -> ()) -> number

Parameters

  • callback (string, string) -> ()

Returns number

globals/engine/onPauseChange

engine.onPauseChange(callback: (boolean, boolean) -> ()) -> number

Register a callback that fires synchronously whenever the gameplay pause flag flips via an explicit engine.paused write. Callback receives (newPaused, oldPaused) as booleans. Returns a watcher id. Pause is independent of engine.mode: pausing play mode returns the editor authoring surface (free camera + EditorOnly entities) over the frozen play world, and resuming hides it again. Mode-driven pause resets (the edit=paused / play=running defaults applied on a mode flip) are delivered through onModeChange, not this hook.

Parameters

  • callback (boolean, boolean) -> () — Function invoked as (newPaused: boolean, oldPaused: boolean).

Returns number

local id = engine.onPauseChange(function(paused)
print(paused and "frozen" or "running")
end)

globals/engine/onTimeScaleChange

engine.onTimeScaleChange(callback: (number, number, TimeScaleOwner?) -> ()) -> number

Register a callback that fires on every move of the simulation rate, whoever moved it: an engine.timeScale write, a hold taken at a rate other than the one in force, a release that hands a different rate back. The callback receives (newScale, oldScale, owner), where owner is the record engine.timeScaleOwner answers with. It counts transitions, so a write or a hold landing on the rate already in force moves nothing and announces nothing; engine.timeScaleOwner is what answers who holds a rate that has stayed where it is. Several agents drive one engine at once, so this is how a run whose numbers depend on the rate learns that somebody else moved it instead of reading the number and finding out afterwards. Returns a watcher id.

Parameters

  • callback (number, number, TimeScaleOwner?) -> () — Function invoked as (newScale: number, oldScale: number, owner: TimeScaleOwner?).

Returns number

local id = engine.onTimeScaleChange(function(new, old, who)
log.warn(("rate %g -> %g by %s"):format(old, new, who and who.id or "nobody"))
end)

globals/engine/onWorldLoaded

engine.onWorldLoaded(callback: () -> ()) -> number

Register a callback fired (no args) when the world is fully LOADED — its .world_entrypoint.luau ran AND its onWorldLoad returned (the startup scene loaded, defaults seeded, editor UI mounted). This is strictly AFTER onWorldReady (content synced): ready = "bytes are in the VFS"; loaded = "the entrypoint has run". LATCHED — a callback registered after the world is already loaded fires immediately, so a late consumer never misses it and never has to poll. Read the same state synchronously via engine.worldLoaded.

Parameters

  • callback () -> () — Function invoked with no arguments.

Returns number

globals/engine/onWorldReady

engine.onWorldReady(callback: () -> ()) -> number

Register a callback fired (no args) when the bound world's content has been synced into the VFS and the world is ready to load. This is the race-free, user-space hook that drives the whole world-VM lifecycle: the builtin world-entrypoint loader subscribes to it and, when it fires, loadstring(vfs.read(...))s /source/.world_entrypoint.luau and runs its onWorldLoad — exactly the way a scene entrypoint loads. The trusted VM fires this (via world.markReady()) ONLY once the bytes are in the VFS, so a subscriber never sees a half-synced world. Returns a watcher id.

Parameters

  • callback () -> () — Function invoked with no arguments.

Returns number

globals/engine/onWorldUnloading

engine.onWorldUnloading(callback: () -> ()) -> number

Symmetric teardown of engine.onWorldReady: register a callback fired (no args) when the bound world is unbinding/swapping out. The builtin loader runs the world entrypoint's onWorldUnload here, so the world entrypoint has the same load/unload parity a scene entrypoint has. Returns a watcher id.

Parameters

  • callback () -> () — Function invoked with no arguments.

Returns number

globals/engine/pauseOwner

engine.pauseOwner -> { id: string?, agent: string?, name: string?, heldSeconds: number }?

Who asked for the standing gameplay pause, and for how long they have held it. One process runs one simulation, so a pause another agent took freezes update(dt), fixedUpdate(dt), physics and every gameplay clock for everybody: a component reading its own state while this is non-nil is reading the same instant over and over, and a sampling loop across task.wait returns the identical sample every time. id is the caller key, the same value vfs.currentAuthor().id reads, and nil when that call carried no agent identity; agent is the name the world's roster holds for that caller now, and name the username of its account, which every agent of the account shares. Nil while the engine is running, and nil when the standing pause is the mode's own rather than something somebody asked for. Read-only.

Returns { id: string?, agent: string?, name: string?, heldSeconds: number }?

globals/engine/paused

engine.paused -> boolean

Whether gameplay is paused: update(dt) component callbacks are gated off while editorUpdate(dt) keeps firing in edit mode. A component's gameplay task (one spawned from awake, start, onEnable, update, fixedUpdate or another gameplay task) is held for the whole pause: a task.wait it started keeps the time it had left, and a task.waitRealtime is held too, but its deadline runs through the pause, so it comes due on the first frame after it. A task spawned from editorUpdate and a task.spawnSystem task run while paused. The scripting-and-tasks guide's "When a task runs" states the whole rule.

Returns boolean

globals/engine/platform

engine.platform -> "native" | "browser" | "android" | "unknown"

Which build of the engine this process is: native for the desktop binary, browser for the wasm one, android for the Android app. unknown says the binary named no build, which a binary older than the field does. Read-only.

Returns "native" | "browser" | "android" | "unknown"

globals/engine/playSession

engine.playSession -> { agents: { { key: string, agent: string? } }, takesPart: boolean }?

The play session the engine is running, as you stand towards it. Play belongs to the callers that take part in it: the caller that entered play, and every caller that writes engine.mode = "play" while it runs. agents are those callers still connected, in the order they joined, each with its stable key (vfs.currentAuthor().stable) and the name the world's roster holds for it. takesPart is whether you are one of them: your /source writes are play work, and your engine.mode = "edit" takes you out of the session, which keeps running for the others and ends with the last of them. While it is false you author beside the session: your /source writes land on canonical source, and the session is theirs to end. Nil in edit mode, in the runtime profile, and while the engine plays for nobody in particular (a Play button press, a boot into play). Read-only.

Returns { agents: { { key: string, agent: string? } }, takesPart: boolean }?

globals/engine/profile

engine.profile -> "editor" | "runtime"

The boot profile this process started under, editor or runtime. Read-only.

Returns "editor" | "runtime"

globals/engine/releaseBaseline

engine.releaseBaseline(generation: number) -> boolean

Drop the mark generation names, and every mark taken after it, once the caller that took it is done resetting to it. It touches the marks alone.

Parameters

  • generation number — The mark to drop, as engine.markBaseline returned it.

Returns boolean — True when the mark stood.

engine.releaseBaseline(mark) -- the run is over

globals/engine/releaseScriptingBaseline

engine.releaseScriptingBaseline(generation: number) -> boolean

Drop the scripting mark generation names, and every mark taken after it, once the caller that took it is done resetting to it. The registries stay as they stand. A mark holds the require cache's keys as they stood, so a caller that marks once per run drops each mark when its run is over.

Parameters

  • generation number — The mark to drop, as engine.markScriptingBaseline returned it.

Returns boolean — True when the mark was in force.

engine.releaseScriptingBaseline(mark) -- the run is over

globals/engine/releaseTimeScale

engine.releaseTimeScale(owner: string) -> number

Parameters

  • owner string

Returns number

globals/engine/resetScriptingState

engine.resetScriptingState(generation: number?) -> { [string]: number }

Drop every world-event subscription, lifecycle watcher and cached module registered since the mark generation names — the newest engine.markScriptingBaseline() when omitted — leaving everything registered before it in place, including the builtin world-entrypoint loader, which subscribes at VM boot and so always sits below any mark. The marks taken after that one go with the reset. A module with a frame on the stack keeps its cache entry and its registrations. Raises when no such mark is in force. Returns per-registry counts of what was removed: worldEvents, modeWatchers, worldReadyWatchers, worldUnloadingWatchers, pauseWatchers, deviceRebuiltWatchers, memoryPressureWatchers, modules, and total.

Parameters

  • generation number (optional) — The generation to restore to, as markScriptingBaseline returned it; the newest mark when omitted.

Returns { [string]: number }

globals/engine/resetToBaseline

engine.resetToBaseline(generation: number?) -> number

Put this module's own memory back as engine.markBaseline read it: every time-scale hold taken since comes off, every onTimeScaleChange subscriber added since is removed, the last writer and the base rate read as they did, the rate in force follows, and the record of who registered each watcher forgets the ids handed out since. The marks taken after generation go with the reset.

Parameters

  • generation number (optional) — The mark to restore to; the newest when omitted.

Returns number — How many things were put back.

engine.resetToBaseline(mark)

globals/engine/restoreModuleState

engine.restoreModuleState(snapshot: { [string]: any }, options: { remint: { [string]: string }?, gone: ((any) -> boolean)?, successor: ((any) -> any?)?, only: { [string]: boolean }? }?) -> { [string]: any }

Put every module's state back to what engine.snapshotModuleState read. A table is refilled in place, so a closure holding it sees the restored contents, and the upvalue is pointed back at it, so a module that assigned its local a new table is reading the restored one again.

The same snapshot restores to the same state however many times it is used, so one reading taken before a run puts every later point in that run back to it.

A value whose reading names an object the engine no longer holds — a buffer whose storage was destroyed since, answering alive false — is left as it stands: what it remembered cannot be given back to it, and what it holds now is what it went on to make. The module holding it is left whole, because what it remembers beside that object — the count it sized the object for, the flag that says it made one — is the state of the object it now holds instead; it is counted under stale. A data table naming a gone object in one of its fields is a registry, and is refilled around that field. Called from a coroutine that can yield, the restore gives the frame back every few milliseconds. the engine re-minted since replaced by the id it now uses.

Parameters

  • snapshot { [string]: any }
  • options { remint: { [string]: string }?, gone: ((any) -> boolean)?, successor: ((any) -> any?)?, only: { [string]: boolean }? } (optional) — remint maps an id the reading holds to the id the engine now uses for the same thing — a render feature's guid after the feature was made again under its identity. gone answers whether a value the reading holds names something the engine no longer holds — a record carrying a render feature's guid when no feature carries it now; a handle answering alive false is known without it. successor answers, for an object that has gone, the live object standing in for it — the handle under a buffer's name after the buffer was made again under it — and the reading is put back holding that one, its own fields left as they are. A handle that already answered alive false when the reading was taken is the module's own record of something gone before it, and is put back as it is. same answers whether a value a module holds now already stands in for the gone object a successor was found for — the module's own live handle to the buffer made again under the name — and such a value is kept, the successor released. only names the modules to put back, by module name, and every other module's reading is left untouched — a second pass over the modules an earlier one left as they stood, once what stands in for the objects they named exists.

Returns { [string]: any } — { restored, refused, recached, stale, staleModules, staleObjects, failed, failedModules } — how many values were written, how many the engine would not write, how many require-cache entries were put back to the module they held, how many modules hold a value left as it stood because its reading names something the engine no longer holds, and those modules by name, each with the gone object the reading named; then how many modules hold a value whose restore raised, and those by name with the error. A value whose restore raises is counted and passed over, and the rest of the reading is put back.

engine.restoreModuleState(snap)
engine.restoreModuleState(snap, { remint = { [oldGuid] = newGuid } })

globals/engine/scriptingRegistryCounts

engine.scriptingRegistryCounts() -> { [string]: number }

How many subscriptions each scripting registry holds right now, plus the size of the require cache and the generation of the mark in force. Keys: worldEvents, modeWatchers, worldReadyWatchers, worldUnloadingWatchers, pauseWatchers, deviceRebuiltWatchers, memoryPressureWatchers, modules, and baselineGeneration (nil when no mark has been taken).

Returns { [string]: number }

globals/engine/setMode

engine.setMode(mode: string, options: { strict: boolean?, endSession: boolean? }?) -> { mode: string, bypassed: { any }, leftPlaySession: boolean }

Change the engine mode with per-call control over the play gate, and read back what the change went past. engine.mode = value is the same flip with the defaults.

options.strict = false lets THIS call enter play while your own content carries error-severity diagnostics. It settles with the call: the world's lsp.strict_mode is untouched, so no other session and no later session of the world sees a different gate. The returned bypassed array holds the diagnostics the call went past — each { path, line, col, code, message, severity } — and the engine log carries the same list. An error in content another session wrote never gates the flip, so it never appears here; a push still refuses to publish while any of them stands.

Parameters

  • mode string — "edit" or "play".
  • options { strict: boolean?, endSession: boolean? } (optional) — { strict: boolean?, endSession: boolean? }. strict = false waives the play gate for this call; true or omitted honours the world's lsp.strict_mode. endSession = true with "edit" ends the running play session for every caller in it, whether or not you take part, and answers for everything the session holds (engine.discardPlayChanges() arms its discard); without it, an "edit" from one of several callers in play takes only that caller out, and one from a caller outside the session is refused.

Returns { mode: string, bypassed: { any }, leftPlaySession: boolean } — { mode, bypassed, leftPlaySession }: the mode now in force, the diagnostics this call entered play past (empty when it went past none), and whether an "edit" took this caller out of a play session that keeps running for other callers, in which case mode still reads "play".

local report = engine.setMode("play", { strict = false })
for _, d in ipairs(report.bypassed) do
print(("entered play past %s:%d — %s"):format(d.path, d.line, d.message))
end

globals/engine/snapshotModuleState

engine.snapshotModuleState() -> { [string]: any }

Read the state every cached module holds — the upvalues of the closures it hands back — and answer with it, so engine.restoreModuleState can put exactly that back later.

Take it once, before the work whose changes are to be undone. Data is copied, so a table a module goes on mutating does not change what was read; an object — a table carrying a metatable or methods of its own — is kept by reference and its fields copied, so restoring returns the object every other holder is still attached to rather than a lookalike that answers none of its methods.

A module with a frame on the stack while the reading is taken — the caller, and whatever called it — is mid-operation, and what it holds is left out: neither copied here nor written back by the restore, however another module reaches it. Called from a coroutine that can yield, the walk gives the frame back every few milliseconds, so the engine keeps stepping through a reading that takes seconds.

Returns { [string]: any } — { entries, cache, modules, inUse, unreachable } — what was captured, the require cache as { key, value } pairs, how many modules it covers, how many were left out for executing, and the count of values that could not be read.

local snap = engine.snapshotModuleState()

globals/engine/timeScale

engine.timeScale -> number

The global time scale applied to the fixed-timestep accumulator, to update(dt), and to the seconds task.wait and task.delay count: 1.0 is real time, 0.0 frozen, 2.0 double speed. So a task.wait(1) returns after four wall-clock seconds at 0.25 and never comes due at 0. task.waitRealtime(1) is one wall-clock second at every scale. One number for the whole process, shared by every agent driving this engine: engine.timeScaleOwner names who it answers to, engine.onTimeScaleChange reports every move of it, and engine.holdTimeScale takes it for a named owner so a write from anywhere else is refused while the hold stands. A script computing a delta of its own from a wall clock multiplies it by this, and one reading time.deltaTime() or a callback's dt already has it applied.

Returns number

globals/engine/timeScaleHolds

engine.timeScaleHolds() -> { string }

Returns { string }

globals/engine/timeScaleOwner

engine.timeScaleOwner -> { id: string?, name: string?, scale: number, exclusive: boolean, held: boolean }?

Who engine.timeScale answers to: the hold naming the rate while one stands, and otherwise whoever wrote the rate outright last. scale is the rate in force, the same number engine.timeScale reads. While held is true, id is a key engine.releaseTimeScale hands the rate back by; for a write it is the agent the call was attributed to, and nil when the call carried no agent identity. name is that agent's username when its token carried one. exclusive is true while the hold refuses every other key. Nil when nothing has moved the rate this session. Read-only.

Returns { id: string?, name: string?, scale: number, exclusive: boolean, held: boolean }?

globals/engine/vertexStride

engine.vertexStride -> number

Byte stride of the engine's standard GPU Vertex layout, which a mesh built from a compute buffer sizes and strides its writes to. Read-only.

Returns number

globals/engine/waitForEdit

engine.waitForEdit(reason: string?) -> () -> ()

Say that this caller's edit-mode work is waiting on the play session, and get back the call that takes the wait off.

One process runs one simulation, so a play session holds the engine in play for every caller outside it, and an agent whose edit-mode work cannot proceed has no way to say so. While this wait stands it rides the run-state block of every response any session gets from this engine, so the callers in the play session learn somebody is waiting for it. Read engine.playSession for who they are, and engine.modeOwner for who entered play and how long ago.

A wait is keyed by the caller the call was attributed to, so a named caller that registers twice moves its own entry forward rather than queueing behind itself: one caller waits once. A call carrying no agent identity is named by nothing to key on, so each such ask stands as its own waiter and the release it handed back takes that one off. Reaching edit mode takes every wait off, so a caller whose release is lost with the task that held it leaves nothing standing.

Parameters

  • reason string (optional) — What this caller is waiting to do, shown beside its name.

Returns () -> () — A function that takes this caller's wait off.

local done = engine.waitForEdit("promote my scene edits")
-- ... once the engine reaches edit and the work is taken ...
done()

globals/engine/watcherSources

engine.watcherSources() -> { [string]: { [number]: string } }

Who registered each watcher still standing, by registry: modeWatchers, pauseWatchers, deviceRebuiltWatchers, memoryPressureWatchers, worldReadyWatchers and worldUnloadingWatchers, each a map from watcher id to the source and line that registered it. A watcher registered before this module loaded is absent. What a reading of the registry counts names when it finds a watcher more than it began with.

Returns { [string]: { [number]: string } } — The sources, by registry and watcher id.

for id, at in pairs(engine.watcherSources().modeWatchers) do print(id, at) end

globals/engine/worldLoaded

engine.worldLoaded -> boolean

Whether the world entrypoint's onWorldLoad has run to completion. Read-only.

Returns boolean

typed/builtin//modules/api/engine/engine/engine/discardPlayChanges

engine.discardPlayChanges() -> { paths: { string }, keep: string }

Arm the leave-play safeguard's deliberate discard for your next engine.mode = "edit" in the play session this is called from, so it proceeds and discards your unaccepted changes. While other callers stay in the session that is your own work: the /source edits you made in play, which revert, and the entities you tuned. The last caller to leave takes the engine to edit, and its discard throws away everything play made: the entities and tuning, and every /source file edit play holds on its shadow. Called by a caller outside the session, it arms the discard of engine.setMode("edit", { endSession = true }), which ends the session for every caller in it. A call that names no caller (a transport that attests no caller for it) cannot be told apart from any caller, so the exit that ends the session takes an arm made for the whole session from the other side: an exit that names no caller takes the arm a named caller made, and a named caller's exit takes the arm a call that named no caller made. Returns paths, the source files the flip would throw away if it ran now, and keep, the routes that keep any of them first; a notice names the same paths. The flip throws away what the shadow holds when it runs, so an edit made after the arm goes with the rest, and the flip names every path it took in a notice and the engine log (vfs.discardPlayShadow() states the same set). A discarded source edit whose pre-play copy is not on this engine yet keeps the play bytes until that copy lands and is put back; the flip names each such path, and vfs.playDiscardsOwed() lists them until then.

Returns { paths: { string }, keep: string }

typed/builtin//modules/api/engine/engine/engine/editWaiters

engine.editWaiters() -> { { key: string, id: string?, agent: string?, name: string?, reason: string?, heldSeconds: number } }

Everyone waiting for this engine to leave play, oldest first. A session holding play reads this to see whose work it is holding; heldSeconds is how long each has been waiting. Empty in edit mode and whenever nobody has said they are waiting.

Returns { { key: string, id: string?, agent: string?, name: string?, reason: string?, heldSeconds: number } }

typed/builtin//modules/api/engine/engine/engine/gameplayReady

engine.gameplayReady -> boolean

Whether gameplay simulation is running: not paused, and the play scene materialized. Read-only.

Returns boolean

typed/builtin//modules/api/engine/engine/engine/gpuCompute

engine.gpuCompute -> boolean

Whether this process holds a live GPU device, so compute dispatch is available. Read-only.

Returns boolean

typed/builtin//modules/api/engine/engine/engine/headless

engine.headless -> boolean

Whether this boot renders offscreen with no window a person can see. Content that only serves someone at a display stands down when it reads true. Read-only.

Returns boolean

typed/builtin//modules/api/engine/engine/engine/mode

engine.mode -> "edit" | "play"

The engine mode this process is in, edit or play. Assigning it takes the flip, side effects and all.

Returns "edit" | "play"

typed/builtin//modules/api/engine/engine/engine/modeOwner

engine.modeOwner -> { id: string?, agent: string?, name: string?, heldSeconds: number }?

Who flipped the engine out of the mode it booted into, and how long ago, while that caller still holds it. In play, engine.playSession names every caller the session belongs to now. id is the caller key, the same value vfs.currentAuthor().id reads, and nil when that flip carried no agent identity; agent is the name the world's roster holds for that caller now, the one to ask by, and name the username of its account, which every agent of the account shares. Nil in the mode the engine booted into (a flip back to it ends the hold), once the caller that flipped it has left the play session it entered, and once that caller is no longer on the world's roster. Read-only.

Returns { id: string?, agent: string?, name: string?, heldSeconds: number }?

typed/builtin//modules/api/engine/engine/engine/pauseOwner

engine.pauseOwner -> { id: string?, agent: string?, name: string?, heldSeconds: number }?

Who asked for the standing gameplay pause, and for how long they have held it. One process runs one simulation, so a pause another agent took freezes update(dt), fixedUpdate(dt), physics and every gameplay clock for everybody: a component reading its own state while this is non-nil is reading the same instant over and over, and a sampling loop across task.wait returns the identical sample every time. id is the caller key, the same value vfs.currentAuthor().id reads, and nil when that call carried no agent identity; agent is the name the world's roster holds for that caller now, and name the username of its account, which every agent of the account shares. Nil while the engine is running, and nil when the standing pause is the mode's own rather than something somebody asked for. Read-only.

Returns { id: string?, agent: string?, name: string?, heldSeconds: number }?

typed/builtin//modules/api/engine/engine/engine/paused

engine.paused -> boolean

Whether gameplay is paused: update(dt) component callbacks are gated off while editorUpdate(dt) keeps firing in edit mode. A component's gameplay task (one spawned from awake, start, onEnable, update, fixedUpdate or another gameplay task) is held for the whole pause: a task.wait it started keeps the time it had left, and a task.waitRealtime is held too, but its deadline runs through the pause, so it comes due on the first frame after it. A task spawned from editorUpdate and a task.spawnSystem task run while paused. The scripting-and-tasks guide's "When a task runs" states the whole rule.

Returns boolean

typed/builtin//modules/api/engine/engine/engine/platform

engine.platform -> "native" | "browser" | "android" | "unknown"

Which build of the engine this process is: native for the desktop binary, browser for the wasm one, android for the Android app. unknown says the binary named no build, which a binary older than the field does. Read-only.

Returns "native" | "browser" | "android" | "unknown"

typed/builtin//modules/api/engine/engine/engine/playSession

engine.playSession -> { agents: { { key: string, agent: string? } }, takesPart: boolean }?

The play session the engine is running, as you stand towards it. Play belongs to the callers that take part in it: the caller that entered play, and every caller that writes engine.mode = "play" while it runs. agents are those callers still connected, in the order they joined, each with its stable key (vfs.currentAuthor().stable) and the name the world's roster holds for it. takesPart is whether you are one of them: your /source writes are play work, and your engine.mode = "edit" takes you out of the session, which keeps running for the others and ends with the last of them. While it is false you author beside the session: your /source writes land on canonical source, and the session is theirs to end. Nil in edit mode, in the runtime profile, and while the engine plays for nobody in particular (a Play button press, a boot into play). Read-only.

Returns { agents: { { key: string, agent: string? } }, takesPart: boolean }?

typed/builtin//modules/api/engine/engine/engine/profile

engine.profile -> "editor" | "runtime"

The boot profile this process started under, editor or runtime. Read-only.

Returns "editor" | "runtime"

typed/builtin//modules/api/engine/engine/engine/timeScale

engine.timeScale -> number

The global time scale applied to the fixed-timestep accumulator, to update(dt), and to the seconds task.wait and task.delay count: 1.0 is real time, 0.0 frozen, 2.0 double speed. So a task.wait(1) returns after four wall-clock seconds at 0.25 and never comes due at 0. task.waitRealtime(1) is one wall-clock second at every scale. One number for the whole process, shared by every agent driving this engine: engine.timeScaleOwner names who it answers to, engine.onTimeScaleChange reports every move of it, and engine.holdTimeScale takes it for a named owner so a write from anywhere else is refused while the hold stands. A script computing a delta of its own from a wall clock multiplies it by this, and one reading time.deltaTime() or a callback's dt already has it applied.

Returns number

typed/builtin//modules/api/engine/engine/engine/timeScaleOwner

engine.timeScaleOwner -> { id: string?, name: string?, scale: number, exclusive: boolean, held: boolean }?

Who engine.timeScale answers to: the hold naming the rate while one stands, and otherwise whoever wrote the rate outright last. scale is the rate in force, the same number engine.timeScale reads. While held is true, id is a key engine.releaseTimeScale hands the rate back by; for a write it is the agent the call was attributed to, and nil when the call carried no agent identity. name is that agent's username when its token carried one. exclusive is true while the hold refuses every other key. Nil when nothing has moved the rate this session. Read-only.

Returns { id: string?, name: string?, scale: number, exclusive: boolean, held: boolean }?

typed/builtin//modules/api/engine/engine/engine/vertexStride

engine.vertexStride -> number

Byte stride of the engine's standard GPU Vertex layout, which a mesh built from a compute buffer sizes and strides its writes to. Read-only.

Returns number

typed/builtin//modules/api/engine/engine/engine/waitForEdit

engine.waitForEdit(reason: string?) -> () -> ()

Say that this caller's edit-mode work is waiting on whoever holds play, and get back the call that takes the wait off. While the wait stands it rides the run-state block of every response any session gets from this engine, so the session holding play reads that somebody is waiting for it rather than finding out hours later. reason is what you are waiting to do, and it is shown beside your name. The wait is keyed by the caller the call was attributed to, so a named caller asking twice holds one entry carrying its newer reason, and a call carrying no agent identity stands as a waiter of its own. Reaching edit mode takes every wait off, so a caller that never calls the release leaves nothing behind.

Parameters

  • reason string (optional)

Returns () -> ()

typed/builtin//modules/api/engine/engine/engine/worldLoaded

engine.worldLoaded -> boolean

Whether the world entrypoint's onWorldLoad has run to completion. Read-only.

Returns boolean

  • api
  • reference