---
title: "Troubleshooting"
description: "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…"
section: "Core"
slug: "core-troubleshooting"
canonical: "https://origozero.ai/docs/core-troubleshooting"
updated: "2026-09-05T16:41:46.554555563+00:00"
tags: ["documentation", "guide"]
---

# Troubleshooting

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.

The **problems** tool in the `debug` toolbox is the first thing to reach for
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. Narrow it to errors with `level`, or ask for `all` to get a
cursor-free overview of the recent problems.

The **errors** tool in the `diagnostics` toolbox 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). Give it an entity name or id to narrow to one.

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. The `logs` toolbox reads it:

- **search** — the general query. Narrows by severity, subsystem, substring or
  regex, the script or entity that logged a line, and a time window. It can
  also return the lines either side of each match — what led up to a failure is
  made of lines a filter rejects, so nothing else will show you them — or
  collapse a repeating message into one row with a count, so a per-frame error
  stops burying everything logged around it.
- **tail** — the most recent lines of any level, oldest first.
- **errors** — recent errors and warnings, newest first. A snapshot, where
  **problems** is a drain: that one answers what is new, this one what is
  there.
- **summary** — how many lines, of what severity, from which subsystems and
  entities, and which messages repeat. The shape of the noise before you go
  looking inside it, and the cheapest check for whether anything went wrong at
  all.
- **clear** — drops what is buffered; lifetime counts survive.

The same ring is also readable as plain files, which is the shorter reach
when you already know the string you are after: `/zero/runtime/logs/engine`
is every level, `/zero/runtime/logs/errors` the ERROR-and-above subset, and
`/zero/runtime/logs/script` what scripts logged. They read like any other
file — `cat` and `grep` work on them, and they are mounts on the engine's own
filesystem rather than paths on a host disk — so a search for a message you can
already name needs no query at all. Two
things separate them from the tools: `engine` and `errors` show the most
recent 500 lines rather than reaching the whole ring, and they include the
record of your own tool calls, which the tools leave out by default — so
grepping for a marker string can return the call that went looking for it.

A line carries the script and entity that logged it, so asking for one
entity's output is a filter rather than a guess at what its messages look
like. **summary** also reports a cursor: read it before an action, hand it back
to **search** afterwards, and you see only what that action logged.

A result that came back carrying `__unreadable = true` where you expected a
position or a rotation is a transform value read after its entity was
despawned. The object names the entity, which value it was, and the refusal in
`reason` — the entities guide's *A proxy whose entity is gone* has the full
shape.

## 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:

It reports both sides by default; `include` narrows to script components or to
native ECS ones, and `types` narrows to a single component type across every
entity the target matched.

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:

- **set** — show or hide one category, or `all` of them at once.
- **scope** — focus every category on named entities.
- **playSession** — keep the overlay visible in play mode.
- **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.

For collision specifically, the overlay draws wireframe outlines and
`capture(pass = "physics")` draws the collider set as solid shaded surfaces —
easier to read at a glance, and it carries what each collider takes part in:
**blue static, orange dynamic, green kinematic, magenta and translucent for a
sensor**, with lightness separating individual objects inside a family.

```lua
capture { pass = "physics" }          -- the colliders alone, on black
capture { pass = "physics_context" }  -- the same over the scene's meshes in dim grey
```

That is the first thing to take when something behaves wrongly rather than
looks wrongly: falling through a surface, an invisible wall, a body launched on
the first frame of play, a trigger firing in the wrong place. A gap in an
otherwise continuous run of blue is the tile something falls through; two
colliders drawn deeply inside each other are bodies the solver will fire apart.
`Physics.colliderManifest()` gives the same reading as data when you need the
entity id rather than the picture.

## 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 a time collage — `mode = "collage"` with
`duration` — because a single frame of motion proves nothing.

A collage is a grid, and what varies across its cells is the argument you pass:
`duration` for a cell per sample over time, `passes` for a cell per render pass,
`viewpoints` for a cell per named view of one subject, `setups` for a cell per
whole camera set-up — a shot list, each entry aimed the way a single capture is.
`setups` and `viewpoints` both say where the camera stands, so a request names
one of them; either crosses with `passes`. `passes = { "final", "albedo",
"normal", "depth" }` puts one frame under four questions at once, which is how a
material problem separates from a geometry one without four calls to compare
from memory.

## 5. Ask about a specific object

Whether an object is built right is a different question from whether the scene
looks right, and it has its own arguments:

- **`viewpoint`** names where the camera stands: `front`, `back`, `left`,
  `right`, `top`, `bottom`, `iso`. It comes with **`basis`**, which says whether
  the name is measured against the subject's own axes (`"local"` — `front` is
  the side it faces however it is turned) or the world's (`"world"`). With a
  subject there is no default, because for anything rotated those are different
  pictures.
- **`projection = "orthographic"`** covers a fixed world-space height at every
  distance, so parallel edges stay parallel and two equal-size objects at
  different depths cover equal pixels. Perspective convergence hides both, which
  is why a side read under it proves less than it appears to.
- **`isolate = true`** draws the subject without the other geometry, so a prop
  behind a wall can be looked at where it stands. It changes only that: lighting
  and sky are untouched, and excluded geometry still casts shadows onto the
  subject. Reach for it on any object-focused shot: naming an `entity` fits the
  FRAME to that subject but does not decide what is drawn in it, and a scene's
  default player spawn sits at the world origin — exactly where a new prop tends
  to be built, and large enough to hide it.

Combined, they make the shot that answers "is this asset correct" in one call —
its six sides, each an orthographic elevation, nothing else in frame:

```
capture { entity = "crate", mode = "collage", viewpoints = "sides",
          basis = "local", projection = "orthographic", isolate = true }
```

Every cell comes back labelled with the viewpoint or pass that produced it, so a
grid is read from the response rather than guessed from the pictures. 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.

## 6. Ask what the editor action actually committed

"I dragged it and it didn't go where I put it", "I pressed the menu item and
nothing happened", "the delete key removed something else" — every editor action
publishes a record when it closes, and the `editor` toolbox's **observe** tool reads them — every action's last
record.

The drag record separates three quantities a single "it didn't move" conflates:
`pointerAsked` (where the pointer put it), `applied` (what the gizmo handed the
engine after `snapping` quantised it), and each entity's `after` — the transform
the engine **holds**, read back from the engine rather than recomputed. Each
entity row carries `before` / `requested` / `after` and, when it is not a clean
commit, one `reason`: `writeDiverged` when the engine committed something else,
`writeRefused` with the error when the write raised, `entityMissing` when it was
despawned mid-drag. The whole drag reads `cancelled` / `userCancelled` for an Esc
and `cancelled` / `pivotLost` when the selection emptied under it — three
terminal states that otherwise all look like "nothing happened". A press that
landed on a handle and began no drag at all is the fourth: it publishes a `grab`
record naming `pointerBlocked` — the UI layer held pointer focus, so the press
never reached the handle.

The same read answers a menu action that did nothing: a command's record tells
`commandMissing` from `commandDisabled` from `predicateRaised` from `bodyRaised`,
and `commands.run` returns that reason to its caller as well as logging the
error. `delete` and `duplicate` records carry the ids removed and created, so
the id of a fresh copy comes from the return value rather than the log.

The editor's own per-frame cost is named alongside it —
`script.editor.gizmo.tick`, `script.editor.viewport.select`,
`script.editor.selection.highlight` and `script.editor.panel.<id>` in
`profiler.stats()` — so a slow editor is attributable to the piece that is slow.

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

One line is meant to be wrong: a test that calls an API incorrectly to
assert it raises. Declare that with `--!expect-error [code,...]` on the line
above, and the errors it names are consumed. The declaration is checked in
both directions — when the guarded line checks clean, `expect-error-unfulfilled`
reports at the directive, so an API that stops rejecting the input surfaces
at the assertion that outlived it.

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