Log inGet started

layers

Updated 5 September 2026

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

  • opts table (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

  • opts table (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

  • ref string — 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

  • handle number — 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

  • handle number — 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

  • handle number — 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

  • handle number — 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

  • handle number — 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

  • cb function — 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

  • cb function — 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

  • cb function — 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

  • cb function — 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

  • cb function — 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

  • opts table — { 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

  • opts table — { 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

  • opts table — { 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

  • v boolean — 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

  • opts table (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

  • ref AssetRef<scene> | string — A scene AssetRef, or an identity string resolved through asset.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

  • proxy any (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

  • proxy any (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

  • proxy any (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

  • ref AssetRef<scene> | string — A scene AssetRef, 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

  • ref AssetRef<scene> | stringAssetRef<scene> envelope (preferred) or scene identity string.
  • opts LoadOpts (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

  • h number — The handle layers.onBeforeLoad returned.

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

  • h number — The handle returned by layers.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

  • h number — The handle layers.onLoad returned.

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

  • h number — The handle layers.onUnload returned.

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 scene AssetRef, 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 scene AssetRef, 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 scene AssetRef, 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 scene AssetRef, 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

  • ref AssetRef<scene> | string — A scene AssetRef, or an identity string resolved through asset.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

  • proxy any (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

  • proxy any (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

  • proxy any (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

  • ref AssetRef<scene> | string — A scene AssetRef, 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

  • ref AssetRef<scene> | stringAssetRef<scene> envelope (preferred) or scene identity string.
  • opts LoadOpts? (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

  • h number — The handle layers.onBeforeLoad returned.
layers.offBeforeLoad(h)

modules/layers/offEntityChanged

offEntityChanged(h: number): boolean

Remove a subscription made with layers.onEntityChanged.

Parameters

  • h number — The handle returned by layers.onEntityChanged.
layers.offEntityChanged(handle)

modules/layers/offLoad

offLoad(h: number): boolean return remove(loadCbs, h) end

Cancel a layers.onLoad subscription.

Parameters

  • h number — The handle layers.onLoad returned.
layers.offLoad(h)

modules/layers/offUnload

offUnload(h: number): boolean return remove(unloadCbs, h) end

Cancel a layers.onUnload subscription.

Parameters

  • h number — The handle layers.onUnload returned.
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 scene AssetRef, 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 scene AssetRef, 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 scene AssetRef, 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 scene AssetRef, 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

  • api
  • reference