The World API
What a script's World class gives you, state, emit, query, spawn, rng and time, and how entity objects behave in JavaScript and Python.
On this page
Every script's class extends World. Inside a handler, this (JavaScript) or self (Python) is the script's world, and it is the only way a script reaches the game. This page covers each member and the entity objects that query and events give you. For signatures and parameter tables, see The neo module.
Members at a glance#
| Member | Kind | What it does |
|---|---|---|
state | property | The script's saved state, kept by the world. |
emit(event, payload) | method | Sends an event to the game's Rust systems. |
query(kind) | method | Returns every visible entity of a kind. |
spawn(kind, fields) | method | Creates an entity of a spawnable kind. |
rng | property | The world-seeded random stream: rng.random(). |
time | property | Simulation time in seconds. |
The names are the same in both languages.
state#
this.state (Python: self.state) holds the values your script keeps. Declare initial values as a class field, then read and write keys directly:
state = { opened: 0, lastOpener: null };
// in a handler
this.state.opened += 1;state = {"opened": 0, "last_opener": None}
# in a handler
self.state.opened += 1Reads give you values, not live references: nested lists and objects arrive as frozen copies, so you write a whole new list instead of pushing into one. Hot reload keeps state. All the rules are in Script state.
emit(event, payload)#
Sends an event out of the script. The game's Rust systems read it after the scripts run (as a ScriptEmitted message with the script's name, the event name and the payload).
this.emit("puzzle_solved", { by: this.state.lastOpener });self.emit("puzzle_solved", {"by": self.state.last_opener})payloadis optional; leaving it out sendsnull.- The payload must be plain data: numbers, strings, booleans,
null/None, lists, objects or dicts with string keys, and entity objects. Functions, symbols and Python sets can't be sent. - Emitted events go to the game, not to other scripts'
@onhandlers.
query(kind)#
Returns a list of every entity of kind that your script may see, in id order:
for (const lamp of this.query("Lamp")) lamp.intensity = 0.5;
const platform = this.query("Platform")[0];for lamp in self.query("Lamp"):
lamp.intensity = 0.5
platform = self.query("Platform")[0]A kind that isn't exposed to your script returns an empty list: as far as the script can tell, it doesn't exist. query takes only a kind name; filter the list yourself.
spawn(kind, fields)#
Creates an entity and returns its entity object right away:
const cube = this.spawn("Cube", { size: 0.5, position: { x: 0, y: 3, z: 0 } });
this.emit("cube_spawned", { id: cube.id });cube = self.spawn("Cube", {"size": 0.5, "position": {"x": 0, "y": 3, "z": 0}})
self.emit("cube_spawned", {"id": cube.id})- The kind must be spawnable for your script (
spawn = trueinneo.toml, or aKind:spawngrant for players), and the game must register a spawner for it. Otherwisespawnthrows (TypeError: Cube isn't spawnable by scripts). - You can only set fields your script may write, and numeric fields must be inside their declared range.
- Ids are never reused, so an id you saved can't later point at a different entity.
rng#
this.rng.random() returns a float in [0, 1) from the world's random stream. In JavaScript, Math.random() draws from the same stream. The stream is seeded from the world seed and the script's name, so the same game replays the same numbers.
if (this.rng.random() < 0.25) this.state.rolls += 1;if self.rng.random() < 0.25:
self.state.rolls += 1time#
this.time is the simulation time in seconds, as a float. It follows the game's fixed timestep, never the wall clock. See Time, sleep and async.
Entity objects#
query, spawn and event arguments give you entity objects. Each one stands for one game entity:
| You write | What happens |
|---|---|
door.id | Always readable: the entity's id (a number in JavaScript, an int in Python). |
lamp.intensity | Reads an exposed field. Nested values (such as position) are frozen copies. |
lamp.intensity = 0.5 | Writes an exposed, writable field. The game applies it right after your script's turn. |
door.open() | Calls an exposed method. |
"open" in door | true for fields and methods your script may use. |
JSON.stringify(door) | {"id":1}: only the id. |
Some details that matter:
- Same entity, same object. Two references to one entity are the same object (
a === bin JavaScript). - Positions are
{ x, y, z }. A kind declared with a transform has apositionfield. Write a whole new object:platform.position = { x: 4, y: 1, z: 0 }. Changingplatform.position.xon the copy you read fails because the copy is frozen. - Method results. In a running game, an exposed method runs right after your script's turn ends, so the call itself returns
null(Python:None). If the Rust method returns an error, it is recorded as your script's fault. - Names. JavaScript sees camelCase (
displayName); Python sees the snake_case name (display_name).
When access is denied#
Anything your script isn't allowed to use fails loudly instead of doing nothing:
| Action | JavaScript | Python |
|---|---|---|
| Read a field that isn't readable | TypeError: Platform.boosts isn't readable by scripts | PermissionError |
| Write a read-only field | TypeError: Door.open isn't writable by scripts | PermissionError |
| Write a number outside the field's range | TypeError: Lamp.intensity must be in 0..=1 | TypeError |
| Call a method more often than its rate | TypeError: ... is limited to 2 calls per second | TypeError |
| Query a kind that isn't exposed | an empty list | an empty list |
You can catch these errors. An uncaught one ends the handler with a fault. See Exposure and permissions.
Python equivalents#
The Python names are the same; only the syntax changes:
| JavaScript | Python |
|---|---|
this.state.lastOpener | self.state.last_opener |
this.emit("go", { at: this.time }) | self.emit("go", {"at": self.time}) |
this.query("Door") | self.query("Door") |
this.spawn("Cube", { size: 2 }) | self.spawn("Cube", {"size": 2}) |
this.rng.random() or Math.random() | self.rng.random() |
null | None |
Event payloads that are maps behave as dicts in Python and can also be read as attributes (player.id).