Troubleshooting
Something you built doesn't behave — a component never runs, a weapon never equips, a value comes back nil, a mesh isn't where it should be. The engine almost always already knows why: errors land in…
The order matters. Work through these steps and stop at the first one that names the cause.
1. Read the errors first
Most "mysteries" are one hidden error string. A throw swallowed by a pcall,
an awake() that errored and disabled its component, a data value violating
its contract, an unresolved require — each logs an error or warning the
moment it happens, whether or not anything surfaced in your execute result.
debug.problems is the first call whenever something misbehaves. It
returns the errors and warnings logged since your last call, oldest first —
so the root-cause error reads ahead of the cascade it triggered — and
advances a read-cursor so repeated calls report only what is new:
tools.use("debug", "problems") -- everything unread, oldest first
tools.use("debug", "problems", { level = "error" }) -- errors only
tools.use("debug", "problems", { all = true }) -- recent overview, cursor untouched
diagnostics.errors collects the per-entity component errors in the
scene — everything components have raised through reportError plus
engine-origin failures attached to entities (a broken model URL, a failing
asset field):
tools.use("diagnostics", "errors") -- every entity with errors
tools.use("diagnostics", "errors", "lamp") -- one entity's errors
Component errors apply through the mutation queue, so an error reported this frame is visible on the next call.
Behind both sits the full log ring — every level, chronological,
searchable — as the logs.* namespace: logs.query (filtered), logs.errors
/ logs.warnings (recent, newest first), logs.tail, logs.find(text), and
logs.count() for lifetime totals that survive eviction.
2. Inspect the state
When the logs are clean, the next question is what does the engine think the state is — not what your code intended it to be.
debug.inspect reports entity component state for every entity matching
a target — an id, a name (names are not unique; a name resolves to every
match, and globs like enemy_* work), an entity proxy, or an array of them:
tools.use("debug", "inspect", "player") -- script + ECS
tools.use("debug", "inspect", "lamp", { include = "ecs" }) -- native components only
tools.use("debug", "inspect", "enemy_*", { types = { "Health" } }) -- one component across matches
Each match reports script (every script component's serialized public
data) and ecs (every native component's field values — Transform, Light,
Model, ...). Comparing the two sides catches a whole class of bugs where a
script component's data and the native state it drives have drifted apart.
To react to state rather than read it, gameplay code uses the live surface:
entity(id).component.get(type) returns the component's live public proxy,
and getAll() returns { type, instance?, data } records where data is
that same live proxy — writes through it hit the running component. The
serialized form (what debug.inspect shows, and what persistence saves)
comes from the component_snapshot module.
To see the structure itself, scene.tree renders the hierarchy; to find
entities, scene.findAll filters by name, pattern, root status, or depth, and
scene.findByComponent finds every entity carrying a component.
3. See the invisible
Plenty of state has no pixels: bounds, camera frustums, skeleton bones, physics colliders, meshless entities. The ambient debug overlay draws them:
tools.use("debug", "set", "colliders") -- show a category
tools.use("debug", "set", "all", false) -- hide everything
tools.use("debug", "scope", "player") -- focus every category on one entity
tools.use("debug", "playSession", true) -- keep the overlay visible in play mode
tools.use("debug", "state") -- what draws right now
Categories: bounds, frustum, bones, colliders, icons, wireframe.
The overlay is ambient — no per-entity setup — and scope keeps a busy scene
readable by drawing only for the entities under investigation. The play-mode
session is in-memory only and never saved, so bones and colliders stay
visible on a running, animating scene without touching the world's source.
A capture proves what the overlay shows: take one after enabling a category and read the actual pixels.
4. Ask the renderer the right question
capture with the default final pass answers "does it look right" —
aesthetics only. For concrete debugging, pick the diagnostic pass that
matches the question, because each renders a known encoding whose answer is
unambiguous in one frame:
- Is the geometry there / facing the right way? →
pass = "normal" - Is it positioned / layered where I think? →
pass = "depth" - Is it actually moving? →
pass = "motion_vectors"(static = black) - Is the material what I set? →
albedo,roughness,metallic,emissive - Is the light reaching it? →
pass = "shadow"
Anything that moves or animates needs mode = "collage" — a single frame of
motion proves nothing. The capture tool's schema documents every pass and
parameter.
Two rules keep captures honest. Screenshots are never same-frame — seconds
pass between an execute and a capture, so if the effect should be there
and isn't, it's broken, not "deferred". And a capture is data: read the
pixels against what the operation should have produced, don't glance and
declare.
5. Let the static layer catch it before it runs
Every execute runs a static check first — unknown globals, unknown members
(with did-you-mean), wrong argument counts, unresolved requires — and in
strict mode an error-severity diagnostic blocks execution entirely, so broken
code never corrupts state. Read the diagnostics field on the response
instead of silencing it.
The same layer checks saved content: lsp.check("<identity>") re-checks one
module, lsp.checkAll({ scope = "user" }) sweeps everything world-authored
and lists every path:line — message. A component that "does nothing" often
simply failed its check and never loaded.
The method
- Reproduce first. A bug you can't trigger on demand isn't understood yet; fixes against an un-reproduced bug are guesses.
- Verify with data AND pixels. A correct-looking return value with a wrong capture (or the reverse) means the investigation isn't done.
- Change one thing, re-test twice. First-time success is meaningless; the second call catches state leaked by the first.
- Never restart the engine to make a symptom go away. If it only works fresh, it is broken — the restart just hid the evidence.
- When the cause is an engine defect, file it — with the reproduction, the expectation, and the observed difference — rather than working around it in content. Workarounds outlive the bug and become bugs themselves.
Related: core/scripting-and-tasks (where state lives, task errors),
core/components (lifecycle, reportError), core/discovering (finding the
right API surface), topics/rendering (the pass pipeline behind captures).