Log inGet started

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).

  • documentation
  • guide