---
title: "The engine"
description: "Working in Zero means working inside a live, shared world. Three things make up the engine's model, and understanding them up front saves a lot of confusion: the world is live and persistent, it runs…"
section: "Core"
slug: "core-engine"
canonical: "https://origozero.ai/docs/core-engine"
updated: "2026-09-05T16:41:46.390235878+00:00"
tags: ["documentation", "guide"]
---

# The engine

## A live, shared world

Every world is a multi-user session. Everything you do is **live** — your edits sync to everyone else in the world as you make them, and you see theirs. There is **no "save" step**: with a world bound, writes are durable the moment you make them (more on persistence next). The world's filesystem is the source of truth; "what you see is what exists."

Publishing a *playable version* for players is a separate, deliberate act (commit/push) — covered in the worlds guide. Saving and publishing are different things.

## What persists

Durability comes from the world holding `/zero/source`, so the first question is which world that is. `world.guid()` answers it: a guid when one is bound, `nil` on a bare local boot. With no world, `/zero/source` is a session buffer — a write is taken, reads back exactly as written for the rest of the run, and ends with the process. The engine says so on every such write, in the notice your tool call carries back; `world.create` / `world.swap` bind a world and end it.

With a world bound, the filesystem boundary is the whole story:

- **`/zero/source/...`** — durable. Anything you write here persists (it's stored in the backend and synced to everyone). This is where all authored content lives. No save step.
- **`/zero/runtime/...`** — live runtime state. Never persists; it's rebuilt as the world runs.
- **`/source/tmp`** — local scratch that does **not** sync to other clients. Use it for throwaway working state.

So "will this survive a reload / be seen by others?" has a simple answer: yes if a world is bound and it's under `/zero/source` (outside `tmp`), no otherwise.

## Edit vs play

`engine.mode` is `"edit"` or `"play"`, and you flip it directly:

```lua
engine.mode                 -- "edit" or "play"
engine.mode = "play"        -- enter play
engine.mode = "edit"        -- back to edit
engine.setMode("play", { strict = false })            -- enter play, waiving the script gate for this call
engine.onModeChange(function(newMode, oldMode) end)   -- react to flips
```

- **Edit** is where you author. Entering it stops the gameplay clock, so component `update(dt)` / `fixedUpdate(dt)` go idle and `editorUpdate(dt)` takes over — set `engine.paused = false` here and all three tick together (see the pause section below). Writes to `/zero/source` are durable.
- **Play** is where gameplay runs. `update` / `fixedUpdate` tick.

Entering play doesn't just "start ticking": the engine **snapshots** the edit state, **re-initialises a fresh play world** (so components re-run `awake`/`start` in play), and writes during play land on a throwaway runtime copy. Leaving play **discards** that play world and restores the edit snapshot — which is why play-mode changes can never touch your authored source. (The exact component lifecycle across the flip is in the components guide.)

Entering play is also **gated on the scripts you answer for being healthy**: while a script under `/zero/source` (library content under `/source/libs` is exempt) carries an error-severity diagnostic, `engine.mode = "play"` raises instead of flipping — `refusing to enter play — N script error(s) in your own content:` followed by the offending `path:line — message` entries. Run `lsp.checkAll({ scope = "user" })` to list them yourself, fix them, and flip again. The gate is calibrated by the `lsp.strict` world setting: `"strict"` (the default) blocks on any error, `"soft"` blocks only structural errors (syntax, unresolved `require`, broken lifecycle-callback signatures), and `"off"` logs a warning and proceeds. Returning to edit is **never** gated — you can always get back to fix the error. Neither override carries to publishing: `zm.push` refuses unconditionally while user-script errors exist, whoever wrote them (see the development guide).

You answer for what you wrote and for the world you opened — the content already there when you bound it, including your own from an earlier session. A file a **neighbour wrote while you were already running** is reported rather than gated: the flip proceeds and the diagnostics go to the engine log. Theirs to finish, since you hold a copy and correcting it would write over an edit still in flight. Read the log after a flip to see what rode along: `/zero/runtime/logs/engine` reads it as a file, and the `logs` toolbox's **search** tool narrows it to the lines another session's content raised.

To take one flip past a diagnostic without changing the gate for anyone else, pass the strict waiver on the call:

```lua
local report = engine.setMode("play", { strict = false })
for _, d in ipairs(report.bypassed) do
    print(("entered play past %s:%d — %s"):format(d.path, d.line, d.message))
end
```

`engine.setMode` is `engine.mode = ...` with the gate under your control for that one call: the world's `lsp.strict_mode` is untouched, so every other session and every later session of the world keeps the gate it had, and `report.bypassed` (mirrored into the engine log) names exactly what the call went past. `lsp.setStrictMode` is the other lever and a different one — it writes `lsp.strict_mode` into the world settings, which every session reads.

## Paused vs running

Pause is a **separate axis** from edit/play. `engine.paused` gates the gameplay tick:

```lua
engine.paused = true        -- freeze gameplay
engine.timeScale = 0.5      -- or slow it down (1.0 real, 0.0 frozen, 2.0 double)
```

When paused, `update(dt)` / `fixedUpdate(dt)` stop — but `editorUpdate(dt)` keeps firing, so the authoring loop stays live. `engine.timeScale` scales time without fully stopping it. Both are orthogonal to `engine.mode`: edit/play and paused/running are independent, which is why the pair has four states and not two. A mode flip resets the flag to that mode's default (edit paused, play running), so the two axes look like one until something writes `engine.paused` — and in edit with the clock running, `update` and `editorUpdate` fire together. `core/components` has the table.

## World lifecycle

```lua
engine.profile        -- "editor" or "runtime" (read-only; the boot profile)
engine.worldLoaded    -- true once the world's entrypoint has finished loading
engine.onWorldReady(function() end)    -- content synced into the VFS
engine.onWorldLoaded(function() end)   -- the world entrypoint has run (latched: fires immediately if already loaded)
```

A world boots: content syncs (`onWorldReady`), then `/source/.world_entrypoint.luau` runs (`onWorldLoaded`), which loads the startup scene and seeds defaults.

## Finding the rest

`engine` is an authored module — read it at `modules/api/engine/engine.module`. Publishing and the shared library are in the worlds guide; how components behave across edit/play is in the components guide; how a loaded world surfaces players and the camera is in the scenes guide.
