layers
The layers namespace — 85 functions.
globals/layers/active
layers.active -> any
The root scene's proxy, re-resolved on every read.
Returns any
globals/layers/active/clearDirty
layers.active:clearDirty(opts?) -> boolean
Discard this layer's unsaved edits and respawn the live scene from canonical. Must run in a coroutine.
Parameters
optstable(optional) — Options
Returns boolean — True if overlay state was removed.
globals/layers/active/hasDirty
layers.active:hasDirty(opts?) -> boolean
True iff any unsaved dirty state exists for this layer — the manifest or at least one per-entity overlay file.
Parameters
optstable(optional) — Options
Returns boolean — True when unsaved dirty state exists.
globals/layers/active/load_additive
layers.active:load_additive(ref) -> SceneProxy
Load a scene as an additive overlay on top of this one, routed through the canonical load path (v6 player/camera config, lighting, entrypoint discovery).
Parameters
refstring— Scene asset reference to overlay
Returns SceneProxy — The new overlay layer's proxy.
globals/layers/active/offReady
layers.active:offReady(handle) -> boolean
Remove an onReady subscription by its handle.
Parameters
handlenumber— Handle returned by onReady
Returns boolean — True if the subscription was removed.
globals/layers/active/off_edit_load
layers.active:off_edit_load(handle) -> boolean
Remove an on_edit_load subscription by its handle.
Parameters
handlenumber— Handle returned by on_edit_load
Returns boolean — True if the subscription was removed.
globals/layers/active/off_load
layers.active:off_load(handle) -> boolean
Remove an on_load subscription by its handle.
Parameters
handlenumber— Handle returned by on_load
Returns boolean — True if the subscription was removed.
globals/layers/active/off_load_play
layers.active:off_load_play(handle) -> boolean
Remove an on_load_play subscription by its handle.
Parameters
handlenumber— Handle returned by on_load_play
Returns boolean — True if the subscription was removed.
globals/layers/active/off_unload
layers.active:off_unload(handle) -> boolean
Remove an on_unload subscription by its handle.
Parameters
handlenumber— Handle returned by on_unload
Returns boolean — True if the subscription was removed.
globals/layers/active/onReady
layers.active:onReady(cb) -> number
Subscribe to this scene reaching the 'ready' state. LATCHED — a callback registered after the scene is already ready fires immediately.
Parameters
cbfunction— Called with the SceneProxy once the scene is ready
Returns number — Subscription handle for offReady.
globals/layers/active/on_edit_load
layers.active:on_edit_load(cb) -> number
Subscribe to THIS scene's load, firing only when it loads with engine.mode == "edit".
Parameters
cbfunction— Called when this scene loads in edit mode
Returns number — Subscription handle for off_edit_load.
globals/layers/active/on_load
layers.active:on_load(cb) -> number
Subscribe to THIS scene's load event (fires only for this layer, unlike the global layers.onLoad).
Parameters
cbfunction— Called on this scene's load
Returns number — Subscription handle for off_load.
globals/layers/active/on_load_play
layers.active:on_load_play(cb) -> number
Subscribe to THIS scene's load, firing only when it loads with engine.mode == "play".
Parameters
cbfunction— Called when this scene loads in play mode
Returns number — Subscription handle for off_load_play.
globals/layers/active/on_unload
layers.active:on_unload(cb) -> number
Subscribe to THIS scene's unload event (fires only for this layer).
Parameters
cbfunction— Called on this scene's unload
Returns number — Subscription handle for off_unload.
globals/layers/active/promoteDirty
layers.active:promoteDirty(opts?) -> string?
Promote the dirty overlay to canonical — compose canonical + overlay, write canonical, then clear the dirty state. Raises when the canonical write is refused, leaving the overlay holding this session's edits.
Parameters
optstable— { to?
Returns string — Canonical path on success; nil when nothing to promote.
globals/layers/active/reload
layers.active:reload(opts?) -> SceneBuildReport?
Round-trip reload — unload then reload the same scene ref, firing the full unload / before-load / load lifecycle, and running the scene's build.luau against what it resolves now. Must run in a coroutine.
Parameters
optstable— { rebuild?
Returns table — What the scene's build.luau did — { built = true, content, editorOnly }, carrying { refused, message } when an operation the build ran was refused, or { built = false, reason, message }.
globals/layers/active/root
layers.active:root() -> SceneProxy
Return the root SceneProxy — walks up the additive-overlay parent chain to the non-additive root layer.
Returns SceneProxy — The root layer's proxy.
globals/layers/active/save
layers.active:save(opts?) -> string
Publish the layer's current state to canonical scene.json — the ONLY path that writes canonical (edit/play cycles never save). Additive overlays cannot be saved.
Parameters
optstable— { to?
Returns string — Full VFS path of the canonical scene.json written.
globals/layers/active/set_visible
layers.active:set_visible(v)
Show or hide this layer's entities.
Parameters
vboolean— Whether the layer's entities are shown
globals/layers/active/unload
layers.active:unload()
Unload this layer: despawn every entity attributed to it and remove its slot from SceneLayers.
globals/layers/active/writeDirty
layers.active:writeDirty(opts?) -> string
Force-drain every pending edit into the dirty overlay directory (deltas only — changed-entity files + manifest); never touches canonical.
Parameters
optstable(optional) — Write options
Returns string — Absolute VFS path of the dirty manifest.
globals/layers/camera
layers.camera -> any
The active root scene's camera handle, the same value layers.active.camera answers.
Returns any
globals/layers/cost
layers.cost() -> { SceneLayerCost }
What each loaded scene's per-frame tick costs, attributed to the layer
that owns it — the update / editorUpdate its entrypoint declares,
timed where it runs. totalMs is a SUM across the window
layers.observe().window reports, so divide by calls (or read avgMs)
for the per-tick figure; a tick that runs every frame makes that the
per-frame figure. Call layers.resetCostWindow() first to time a
particular stretch. A layer whose entrypoint declares no tick is absent.
Returns { SceneLayerCost } — An array of SceneLayerCost.
layers.resetCostWindow(); task.wait(1); for _, c in layers.cost() do print(c.name, c.avgMs) end
globals/layers/find
layers.find(ref: AssetRef<scene> | string) -> any?
The loaded layer for a scene, matched on guid — the canonical identity, since display names can collide and paths drift when assets move. A layer torn down but not yet pumped out of the engine's loaded list reads as gone.
Parameters
refAssetRef<scene> | string— A sceneAssetRef, or an identity string resolved throughasset.ref.
Returns any? — The scene proxy, or nil when that scene has no loaded layer.
local layer = layers.find("scenes.arena")
globals/layers/fireBeforeLoad
layers.fireBeforeLoad(proxy: any?) -> nil
Announce that a scene layer is about to load: clears any pending
unload for that layer slot, marks the proxy loading, and fans out to every
layers.onBeforeLoad subscriber. The scene-load pipeline calls this.
Parameters
proxyany(optional) — The scene proxy about to load.
Returns nil
layers.fireBeforeLoad(sceneProxy)
globals/layers/fireLoad
layers.fireLoad(proxy: any?) -> nil
Announce that a scene layer has loaded, fanning out to every
layers.onLoad subscriber. The layer is pinned as the active one for the
duration of the fan-out, so entities a subscriber spawns are attributed to
it rather than landing orphaned. The scene-load pipeline calls this.
Parameters
proxyany(optional) — The loaded scene proxy.
Returns nil
layers.fireLoad(sceneProxy)
globals/layers/fireUnload
layers.fireUnload(proxy: any?) -> nil
Announce that a scene layer is unloading: fans out to every
layers.onUnload subscriber, then drops the layer's cached proxy and
per-layer state so the next load of that scene rebuilds from disk. The
unload path calls this.
Parameters
proxyany(optional) — The scene proxy being unloaded.
Returns nil
layers.fireUnload(sceneProxy)
globals/layers/install
layers.install() -> nil
Install the layers global. layers.active is exposed as a property
whose every read resolves the current root scene, so it tracks scene
changes without manual invalidation; other keys resolve against this
module. The prelude calls this once at boot.
Returns nil
layers.install()
globals/layers/inventory
layers.inventory() -> { SceneLayerInventory }
What each loaded layer holds: the entities the engine attributes to it,
whether it came up whole, and how many failures it carries. unattributed
in layers.observe().totals counts what exists in the world that no layer
claims.
Returns { SceneLayerInventory } — An array of SceneLayerInventory.
for _, l in layers.inventory() do print(l.name, l.entities, l.ok) end
globals/layers/is_loaded
layers.is_loaded(ref: AssetRef<scene> | string) -> boolean
Whether a scene currently has a loaded layer — the boolean form of
layers.find. A scene counts as loaded from the frame the engine holds a
layer slot for it — the same slot its entities are attributed to — until
an unload is issued against that slot. So a gate like
if layers.is_loaded(ref) then layers.unload(ref) end sees the layer on
the frame its entities exist.
Parameters
refAssetRef<scene> | string— A sceneAssetRef, or an identity string.
Returns boolean — True when the scene is loaded as a layer.
if not layers.is_loaded("scenes.hud") then layers.load("scenes.hud", { additive = true }) end
globals/layers/lastLoad
layers.lastLoad() -> SceneLoadReport?
The most recent load's report: what it loaded, what root it replaced and which overlays went with it, the entity counts on each side, how long each phase took, and every failure it produced. Nil on an engine that has loaded nothing — which is how "nothing has loaded" reads differently from a load that changed nothing.
Returns SceneLoadReport? — A SceneLoadReport, or nil.
local r = layers.lastLoad(); print(r.name, r.outcome, r.entities.added)
globals/layers/lastUnload
layers.lastUnload() -> SceneUnloadReport?
The most recent unload's report: the layer it took down under the name it was loaded with, the overlays it cascaded, and the entities that went with them. A guid no longer resolves to a name once its layer is gone, so this is where that name survives.
Returns SceneUnloadReport? — A SceneUnloadReport, or nil.
local u = layers.lastUnload(); print(u.name, u.entities.removed)
globals/layers/list
layers.list() -> { any }
Every loaded scene layer as a proxy, root and additive alike, in the order the engine reports them.
Returns { any } — Array of scene proxies — empty before any scene is loaded.
for _, layer in ipairs(layers.list()) do print(layer.name, layer.additive) end
globals/layers/load
layers.load(ref: AssetRef<scene> | string, opts: LoadOpts?) -> any
Load a scene into the root non-additive slot ("main") OR as
an additive overlay alongside it. Identity is ref-based: pass an
AssetRef<scene> envelope (preferred — caught at the callsite
by the LSP) or an identity string (resolved via asset.ref at
entry, hard-error if no stable guid comes back). For non-additive,
idempotency is by guid: re-loading the same scene logs and
returns the existing proxy without tearing anything down.
Different guid → unloads the current root + cascades every
additive overlay it spawned + transitions the multiplayer room +
loads the new scene. Logs every step at info level so a silent
no-op is impossible.
Parameters
refAssetRef<scene> | string—AssetRef<scene>envelope (preferred) or scene identity string.optsLoadOpts(optional) — Optional load options — additive overlay flag, slot name, persistence flag, world-origin offset, and whether to rebuild.
Returns any — SceneProxy for the loaded layer (the cached instance the module also returns from layers.active / layers.find). Its lastBuild field says what the scene's build.luau did on this load — whether it ran, and what stood in the way when it did not.
layers.load(asset.ref("@builtin::scenes.test_arena", "scene"))
layers.load(myAssetRef, { additive = true, name = "hud_overlay" })
globals/layers/loadHistory
layers.loadHistory() -> { SceneLoadReport }
Every load report the engine still holds, oldest first. Bounded — old reports fall off the front, so a long session's memory does not grow with how many times a scene was swapped.
Returns { SceneLoadReport } — An array of SceneLoadReport.
for _, r in layers.loadHistory() do print(r.name, r.durationMs) end
globals/layers/loadInFlight
layers.loadInFlight() -> number
Returns the number of scene loads currently in flight (queued
but not yet visible via onLoad dispatch). Returns 0 when the
engine is in a stable load state. Used by engine.mode = ... to
block flips while a load is mid-air; agents can read this to wait
for a load to finish before driving the next operation.
Returns number
globals/layers/localPlayer
layers.localPlayer -> any
The active root scene's local player handle, the same value layers.active.players.localPlayer answers.
Returns any
globals/layers/observe
layers.observe() -> SceneObservation
What every scene load did, and what each loaded scene costs. One read covering the last load's report (what it produced, what it replaced, what it failed to produce and why, and how long each phase took), the load and unload history, a per-layer inventory of what the engine attributes to each layer, and the per-frame cost of each layer's entrypoint tick. Answers in edit mode as well as play.
Returns SceneObservation — A SceneObservation.
local o = layers.observe(); print(o.lastLoad.outcome, o.lastLoad.durationMs)
for _, c in layers.observe().cost do print(c.name, c.avgMs) end
globals/layers/offBeforeLoad
layers.offBeforeLoad(h: number) -> boolean
Cancel a layers.onBeforeLoad subscription.
Parameters
hnumber— The handlelayers.onBeforeLoadreturned.
Returns boolean — True when a subscription was removed.
layers.offBeforeLoad(h)
globals/layers/offEntityChanged
layers.offEntityChanged(h: number) -> boolean
Remove a subscription made with layers.onEntityChanged.
Parameters
hnumber— The handle returned bylayers.onEntityChanged.
Returns boolean — True when the subscription existed and was removed.
layers.offEntityChanged(handle)
globals/layers/offLoad
layers.offLoad(h: number) -> boolean
Cancel a layers.onLoad subscription.
Parameters
hnumber— The handlelayers.onLoadreturned.
Returns boolean — True when a subscription was removed.
layers.offLoad(h)
globals/layers/offUnload
layers.offUnload(h: number) -> boolean
Cancel a layers.onUnload subscription.
Parameters
hnumber— The handlelayers.onUnloadreturned.
Returns boolean — True when a subscription was removed.
layers.offUnload(h)
globals/layers/onBeforeLoad
layers.onBeforeLoad(cb: (any) -> ()) -> number
Run a callback just before a scene layer loads, while the previous layer's entities are still present.
Parameters
cb(any) -> ()— Receives the scene proxy about to load.
Returns number — A handle to pass to layers.offBeforeLoad.
local h = layers.onBeforeLoad(function(scene) print("loading", scene.name) end)
globals/layers/onEntityChanged
layers.onEntityChanged(cb: (any) -> ()) -> number
Subscribe to authored entity changes. The callback runs once per
frame with every entity edited since the previous frame, batched by
layer as { { scene = string, entities = { string } } } — a moved
transform, an edited component field, a spawn, or a despawn (the id
of a despawned entity arrives with entity.exists already false).
Any number of subscribers can watch the same edits.
Scope: authored edits in edit mode — what lands in the scene's dirty
overlay. Mutations a component makes from its own update are runtime
behavior and do not appear, so a subscriber that rebuilds derived data
cannot re-trigger itself.
Parameters
cb(any) -> ()— Called with the change batch.
Returns number — A handle for layers.offEntityChanged.
layers.onEntityChanged(function(batch)
for _, row in ipairs(batch) do
for _, id in ipairs(row.entities) do rebuild(id) end
end
end)
globals/layers/onLoad
layers.onLoad(cb: (any) -> ()) -> number
Run a callback once a scene layer has loaded — the point where its entities exist and player / camera spawners can attach to them.
Parameters
cb(any) -> ()— Receives the loaded scene proxy.
Returns number — A handle to pass to layers.offLoad.
local h = layers.onLoad(function(scene) spawnPlayerFor(scene) end)
globals/layers/onUnload
layers.onUnload(cb: (any) -> ()) -> number
Run a callback as a scene layer unloads, while its entities are still addressable — the place to release anything keyed to them.
Parameters
cb(any) -> ()— Receives the scene proxy being unloaded.
Returns number — A handle to pass to layers.offUnload.
local h = layers.onUnload(function(scene) releaseHandlesFor(scene) end)
globals/layers/problems
layers.problems(ref: (AssetRef<scene> | string | any)?) -> { SceneLoadFailure }
What a layer failed to produce, and why. Each entry names the phase it happened in, one reason from the closed set, and the engine's own words — plus the entity, component or lifecycle hook it is about when it is about one.
Parameters
ref(AssetRef<scene> | string | any)(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit for the active root layer.
Returns { SceneLoadFailure } — An array of SceneLoadFailure — empty for a layer that came up whole.
for _, f in layers.problems() do print(f.reason, f.entity, f.message) end
globals/layers/rebuildInFlight
layers.rebuildInFlight() -> boolean
Whether the engine is rebuilding the live scene right now — a scene load is carrying entities in, or an edit↔play flip's transition is materialising the layer set. A flip unloads the root layer and loads it again for the new mode across many frames, and each mode materialises a different set of entities, so the live entities are a stage of a scene being built while this reads true. A caller whose answer belongs to the settled scene — a test taking a root, a validator judging the live tree — polls it down to false first.
Returns boolean — true while a load or a mode-flip transition is converging.
if not layers.rebuildInFlight() then judge(layers.active) end
globals/layers/reload
layers.reload(ref: (AssetRef<scene> | string)?) -> any?
Unload and re-load a scene layer in place, so an edited scene asset
takes effect without rebuilding the surrounding layer stack. The scene's
build.luau runs against what it resolves right now, so a build script
whose inputs moved — a component that now exists, an asset that now
resolves — produces the scene it describes today.
Parameters
ref(AssetRef<scene> | string)(optional) — A sceneAssetRef, or an identity string. Omit to reload the active root scene.
Returns any? — What the scene's build.luau did — { built = true, content, editorOnly } with the entity counts each half placed, carrying refused and a message reading them out when an operation the build ran was refused, or { built = false, reason, message } naming what stood in the way. Nil when no layer matched, which is a no-op.
layers.reload("scenes.arena")
globals/layers/resetCostWindow
layers.resetCostWindow() -> nil
Open a new cost window, discarding what the previous one measured. Call this before timing a stretch of frames; the load history is untouched.
Returns nil
layers.resetCostWindow()
globals/layers/unload
layers.unload(refOrProxy: (AssetRef<scene> | string | any)?) -> nil
Unload a scene layer. Unloading the root cascades through its additive overlays first, most-recently-loaded first, so none is left as a layer the engine still lists after its entities are gone; persistent additive layers survive the cascade. A scene with no loaded layer is a no-op.
Parameters
refOrProxy(AssetRef<scene> | string | any)(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit to unload the active root scene.
Returns nil
layers.unload("scenes.hud")
layers.unload() -- the active root, plus its non-persistent overlays
globals/layers/whyPartial
layers.whyPartial(ref: (AssetRef<scene> | string | any)?) -> (string?, string?)
Why a layer is not whole. Returns nil when it IS — everything the scene
declared was produced — and otherwise the nearest cause from the closed set
loaderRaised, entrypointCompileFailed, entrypointBodyRaised,
entrypointRaised, buildRaised, entityFailed, parentMissing,
parentRefused, parentAbandoned, componentUnresolved,
componentRefused, subscriberRaised, updateRaised. A second return
carries the engine's own words for that cause.
Parameters
ref(AssetRef<scene> | string | any)(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit for the active root layer.
Returns (string?, string?) — (reason, detail).
local why, detail = layers.whyPartial(); if why then print(why, detail) end
modules/layers/README
layers (global)
Top-level scene-management namespace + Scene proxy. Owns the public layer-management API surface exposed on _G.layers. Pure Luau composition over internal __layers.* ECS-glue primitives; the LSP discovers the public shape via this --!global layers directive.
Also available as global: layers
modules/layers/cost
cost(): { SceneLayerCost }
What each loaded scene's per-frame tick costs, attributed to the layer
that owns it — the update / editorUpdate its entrypoint declares,
timed where it runs. totalMs is a SUM across the window
layers.observe().window reports, so divide by calls (or read avgMs)
for the per-tick figure; a tick that runs every frame makes that the
per-frame figure. Call layers.resetCostWindow() first to time a
particular stretch. A layer whose entrypoint declares no tick is absent.
layers.resetCostWindow(); task.wait(1); for _, c in layers.cost() do print(c.name, c.avgMs) end
modules/layers/find
find(ref: AssetRef<scene> | string): any?
The loaded layer for a scene, matched on guid — the canonical identity, since display names can collide and paths drift when assets move. A layer torn down but not yet pumped out of the engine's loaded list reads as gone.
Parameters
refAssetRef<scene> | string— A sceneAssetRef, or an identity string resolved throughasset.ref.
local layer = layers.find("scenes.arena")
modules/layers/fireBeforeLoad
fireBeforeLoad(proxy: any): nil
Announce that a scene layer is about to load: clears any pending
unload for that layer slot, marks the proxy loading, and fans out to every
layers.onBeforeLoad subscriber. The scene-load pipeline calls this.
Parameters
proxyany(optional) — The scene proxy about to load.
layers.fireBeforeLoad(sceneProxy)
modules/layers/fireLoad
fireLoad(proxy: any): nil
Announce that a scene layer has loaded, fanning out to every
layers.onLoad subscriber. The layer is pinned as the active one for the
duration of the fan-out, so entities a subscriber spawns are attributed to
it rather than landing orphaned. The scene-load pipeline calls this.
Parameters
proxyany(optional) — The loaded scene proxy.
layers.fireLoad(sceneProxy)
modules/layers/fireUnload
fireUnload(proxy: any): nil
Announce that a scene layer is unloading: fans out to every
layers.onUnload subscriber, then drops the layer's cached proxy and
per-layer state so the next load of that scene rebuilds from disk. The
unload path calls this.
Parameters
proxyany(optional) — The scene proxy being unloaded.
layers.fireUnload(sceneProxy)
modules/layers/install
install(): nil
Install the layers global. layers.active is exposed as a property
whose every read resolves the current root scene, so it tracks scene
changes without manual invalidation; other keys resolve against this
module. The prelude calls this once at boot.
layers.install()
modules/layers/inventory
inventory(): { SceneLayerInventory }
What each loaded layer holds: the entities the engine attributes to it,
whether it came up whole, and how many failures it carries. unattributed
in layers.observe().totals counts what exists in the world that no layer
claims.
for _, l in layers.inventory() do print(l.name, l.entities, l.ok) end
modules/layers/is_loaded
is_loaded(ref: AssetRef<scene> | string): boolean
Whether a scene currently has a loaded layer — the boolean form of
layers.find. A scene counts as loaded from the frame the engine holds a
layer slot for it — the same slot its entities are attributed to — until
an unload is issued against that slot. So a gate like
if layers.is_loaded(ref) then layers.unload(ref) end sees the layer on
the frame its entities exist.
Parameters
refAssetRef<scene> | string— A sceneAssetRef, or an identity string.
if not layers.is_loaded("scenes.hud") then layers.load("scenes.hud", { additive = true }) end
modules/layers/lastLoad
lastLoad(): SceneLoadReport?
The most recent load's report: what it loaded, what root it replaced and which overlays went with it, the entity counts on each side, how long each phase took, and every failure it produced. Nil on an engine that has loaded nothing — which is how "nothing has loaded" reads differently from a load that changed nothing.
local r = layers.lastLoad(); print(r.name, r.outcome, r.entities.added)
modules/layers/lastUnload
lastUnload(): SceneUnloadReport?
The most recent unload's report: the layer it took down under the name it was loaded with, the overlays it cascaded, and the entities that went with them. A guid no longer resolves to a name once its layer is gone, so this is where that name survives.
local u = layers.lastUnload(); print(u.name, u.entities.removed)
modules/layers/list
list(): { any }
Every loaded scene layer as a proxy, root and additive alike, in the order the engine reports them.
for _, layer in ipairs(layers.list()) do print(layer.name, layer.additive) end
modules/layers/load
load(ref: AssetRef<scene> | string, opts: LoadOpts?): any
Load a scene into the root non-additive slot ("main") OR as
an additive overlay alongside it. Identity is ref-based: pass an
AssetRef<scene> envelope (preferred — caught at the callsite
by the LSP) or an identity string (resolved via asset.ref at
entry, hard-error if no stable guid comes back). For non-additive,
idempotency is by guid: re-loading the same scene logs and
returns the existing proxy without tearing anything down.
Different guid → unloads the current root + cascades every
additive overlay it spawned + transitions the multiplayer room +
loads the new scene. Logs every step at info level so a silent
no-op is impossible.
Parameters
refAssetRef<scene> | string—AssetRef<scene>envelope (preferred) or scene identity string.optsLoadOpts?(optional) — Optional load options — additive overlay flag, slot name, persistence flag, world-origin offset, and whether to rebuild.
layers.load(asset.ref("@builtin::scenes.test_arena", "scene"))
layers.load(myAssetRef, { additive = true, name = "hud_overlay" })
modules/layers/loadHistory
loadHistory(): { SceneLoadReport }
Every load report the engine still holds, oldest first. Bounded — old reports fall off the front, so a long session's memory does not grow with how many times a scene was swapped.
for _, r in layers.loadHistory() do print(r.name, r.durationMs) end
modules/layers/loadInFlight
loadInFlight(): number
Returns the number of scene loads currently in flight (queued
but not yet visible via onLoad dispatch). Returns 0 when the
engine is in a stable load state. Used by engine.mode = ... to
block flips while a load is mid-air; agents can read this to wait
for a load to finish before driving the next operation.
modules/layers/observe
observe(): SceneObservation
What every scene load did, and what each loaded scene costs. One read covering the last load's report (what it produced, what it replaced, what it failed to produce and why, and how long each phase took), the load and unload history, a per-layer inventory of what the engine attributes to each layer, and the per-frame cost of each layer's entrypoint tick. Answers in edit mode as well as play.
local o = layers.observe(); print(o.lastLoad.outcome, o.lastLoad.durationMs)
for _, c in layers.observe().cost do print(c.name, c.avgMs) end
modules/layers/offBeforeLoad
offBeforeLoad(h: number): boolean return remove(beforeLoadCbs, h) end
Cancel a layers.onBeforeLoad subscription.
Parameters
hnumber— The handlelayers.onBeforeLoadreturned.
layers.offBeforeLoad(h)
modules/layers/offEntityChanged
offEntityChanged(h: number): boolean
Remove a subscription made with layers.onEntityChanged.
Parameters
hnumber— The handle returned bylayers.onEntityChanged.
layers.offEntityChanged(handle)
modules/layers/offLoad
offLoad(h: number): boolean return remove(loadCbs, h) end
Cancel a layers.onLoad subscription.
Parameters
hnumber— The handlelayers.onLoadreturned.
layers.offLoad(h)
modules/layers/offUnload
offUnload(h: number): boolean return remove(unloadCbs, h) end
Cancel a layers.onUnload subscription.
Parameters
hnumber— The handlelayers.onUnloadreturned.
layers.offUnload(h)
modules/layers/onBeforeLoad
onBeforeLoad(cb: (any) -> ()): number return push(beforeLoadCbs, cb) end
Run a callback just before a scene layer loads, while the previous layer's entities are still present.
Parameters
cb(any) -> ()— Receives the scene proxy about to load.
local h = layers.onBeforeLoad(function(scene) print("loading", scene.name) end)
modules/layers/onEntityChanged
onEntityChanged(cb: (any) -> ()): number
Subscribe to authored entity changes. The callback runs once per
frame with every entity edited since the previous frame, batched by
layer as { { scene = string, entities = { string } } } — a moved
transform, an edited component field, a spawn, or a despawn (the id
of a despawned entity arrives with entity.exists already false).
Any number of subscribers can watch the same edits.
Scope: authored edits in edit mode — what lands in the scene's dirty
overlay. Mutations a component makes from its own update are runtime
behavior and do not appear, so a subscriber that rebuilds derived data
cannot re-trigger itself.
Parameters
cb(any) -> ()— Called with the change batch.
layers.onEntityChanged(function(batch)
for _, row in ipairs(batch) do
for _, id in ipairs(row.entities) do rebuild(id) end
end
end)
modules/layers/onLoad
onLoad(cb: (any) -> ()): number return push(loadCbs, cb) end
Run a callback once a scene layer has loaded — the point where its entities exist and player / camera spawners can attach to them.
Parameters
cb(any) -> ()— Receives the loaded scene proxy.
local h = layers.onLoad(function(scene) spawnPlayerFor(scene) end)
modules/layers/onUnload
onUnload(cb: (any) -> ()): number return push(unloadCbs, cb) end
Run a callback as a scene layer unloads, while its entities are still addressable — the place to release anything keyed to them.
Parameters
cb(any) -> ()— Receives the scene proxy being unloaded.
local h = layers.onUnload(function(scene) releaseHandlesFor(scene) end)
modules/layers/problems
problems(ref: (AssetRef<scene> | string | any)?): { SceneLoadFailure }
What a layer failed to produce, and why. Each entry names the phase it happened in, one reason from the closed set, and the engine's own words — plus the entity, component or lifecycle hook it is about when it is about one.
Parameters
ref(AssetRef<scene> | string | any)?(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit for the active root layer.
for _, f in layers.problems() do print(f.reason, f.entity, f.message) end
modules/layers/rebuildInFlight
rebuildInFlight(): boolean
Whether the engine is rebuilding the live scene right now — a scene load is carrying entities in, or an edit↔play flip's transition is materialising the layer set. A flip unloads the root layer and loads it again for the new mode across many frames, and each mode materialises a different set of entities, so the live entities are a stage of a scene being built while this reads true. A caller whose answer belongs to the settled scene — a test taking a root, a validator judging the live tree — polls it down to false first.
if not layers.rebuildInFlight() then judge(layers.active) end
modules/layers/reload
reload(ref: (AssetRef<scene> | string)?): any?
Unload and re-load a scene layer in place, so an edited scene asset
takes effect without rebuilding the surrounding layer stack. The scene's
build.luau runs against what it resolves right now, so a build script
whose inputs moved — a component that now exists, an asset that now
resolves — produces the scene it describes today.
Parameters
ref(AssetRef<scene> | string)?(optional) — A sceneAssetRef, or an identity string. Omit to reload the active root scene.
layers.reload("scenes.arena")
modules/layers/resetCostWindow
resetCostWindow(): nil
Open a new cost window, discarding what the previous one measured. Call this before timing a stretch of frames; the load history is untouched.
layers.resetCostWindow()
modules/layers/unload
unload(refOrProxy: (AssetRef<scene> | string | any)?): nil
Unload a scene layer. Unloading the root cascades through its additive overlays first, most-recently-loaded first, so none is left as a layer the engine still lists after its entities are gone; persistent additive layers survive the cascade. A scene with no loaded layer is a no-op.
Parameters
refOrProxy(AssetRef<scene> | string | any)?(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit to unload the active root scene.
layers.unload("scenes.hud")
layers.unload() -- the active root, plus its non-persistent overlays
modules/layers/whyPartial
whyPartial(ref: (AssetRef<scene> | string | any)?): (string?, string?)
Why a layer is not whole. Returns nil when it IS — everything the scene
declared was produced — and otherwise the nearest cause from the closed set
loaderRaised, entrypointCompileFailed, entrypointBodyRaised,
entrypointRaised, buildRaised, entityFailed, parentMissing,
parentRefused, parentAbandoned, componentUnresolved,
componentRefused, subscriberRaised, updateRaised. A second return
carries the engine's own words for that cause.
Parameters
ref(AssetRef<scene> | string | any)?(optional) — A sceneAssetRef, an identity string, or a scene proxy. Omit for the active root layer.
local why, detail = layers.whyPartial(); if why then print(why, detail) end
typed/builtin//modules/api/engine/layers/layers/active
layers.active -> any
The root scene's proxy, re-resolved on every read.
Returns any
typed/builtin//modules/api/engine/layers/layers/camera
layers.camera -> any
The active root scene's camera handle, the same value layers.active.camera answers.
Returns any
typed/builtin//modules/api/engine/layers/layers/localPlayer
layers.localPlayer -> any
The active root scene's local player handle, the same value layers.active.players.localPlayer answers.
Returns any