engine
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
scalenumber— The rate to run at: 1 is real time, 0.5 half speed, 0 frozen, 2 double. Finite and>= 0.optionsTimeScaleHoldOptions(optional) —owneris 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 mintedhold#nwhen the call carries no agent identity, so every hold answers to a keyengine.releaseTimeScaleaccepts andengine.timeScaleOwnerreports asid.exclusivetakes 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
idnumber— Watcher id returned byengine.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
idnumber— Watcher id returned byengine.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
idnumber— Watcher id returned byengine.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
idnumber— Watcher id returned byengine.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
idnumber— Watcher id returned byengine.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
idnumber
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
idnumber— Watcher id returned byengine.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
idnumber— Watcher id returned byengine.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
generationnumber— The mark to drop, asengine.markBaselinereturned 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
generationnumber— The mark to drop, asengine.markScriptingBaselinereturned 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
ownerstring
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
generationnumber(optional) — The generation to restore to, asmarkScriptingBaselinereturned 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
generationnumber(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) —remintmaps 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.goneanswers 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 answeringalivefalse is known without it.successoranswers, 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 answeredalivefalse when the reading was taken is the module's own record of something gone before it, and is put back as it is.sameanswers 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.onlynames 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
modestring—"edit"or"play".options{ strict: boolean?, endSession: boolean? }(optional) —{ strict: boolean?, endSession: boolean? }.strict = falsewaives the play gate for this call;trueor omitted honours the world'slsp.strict_mode.endSession = truewith"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
reasonstring(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
reasonstring(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