NEOLearn

Script state

How this.state works, initial values, what hot reload keeps, why nested values are frozen copies, and how key names map between languages.

Updated October 17, 2026

On this page

this.state (Python: self.state) is where a script keeps the values it needs from one tick to the next. It isn't an ordinary object: the world holds it, so it survives hot reloads, is hashed every tick, and only takes values that can cross into the world.

Initial values#

Declare a state class field with the starting values:

JavaScript
export default class DoorPuzzle extends World {
  state = { opened: 0, lastOpener: null, solved: false };
}

When the script loads, each key the world doesn't hold yet is set to its initial value. Keys the world already holds keep their current values. Then state becomes the world's state object for the rest of the script's life (you can't replace this.state itself, only its keys).

You don't have to declare every key up front: writing this.state.score = 10 creates the key. Declaring keys is still a good habit, because a JavaScript read of a missing key gives undefined, and it keeps the JavaScript and Python versions of a script alike.

Reading and writing#

  • Write a key by assigning to it: this.state.opened += 1.
  • Delete a key with delete this.state.key. It sets the value to null.
  • List keys with Object.keys(this.state); they come back in the order they were first written, as JavaScript names.
  • Allowed values: numbers, strings, booleans, null/None, lists, maps with string keys, and entity objects. A function, a symbol or a Python set can't be stored (TypeError). Values nested more than 32 levels deep are refused.

Every write counts as one world call toward the per-tick call limit (see Limits and determinism).

Nested values are frozen copies#

Reading a list or map from state gives you a frozen copy. Changing the copy would silently not reach the world, so it throws instead:

JavaScript
this.state.countdown.push(3);                              // TypeError: the copy is frozen
this.state.countdown = [...this.state.countdown, 3];        // write the whole list
this.state.scores = { ...this.state.scores, [name]: 10 };   // and the whole map

If you change a list several times in one handler, copy it once, change the copy, and write it back at the end:

JavaScript
const b = this.state.buckets.slice();
for (let i = 0; i < 50; i++) b[Math.floor(Math.random() * 4)] += 1;
this.state.buckets = b;

Hot reload keeps state#

When you save a running script, it reloads on the next fixed tick:

KeptReset
Every key in this.stateOther instance fields (this.cache = ...) and module-level variables
The script's simulation clockPending timers: an await sleep(...) waiting in the old version never resumes

Because existing keys keep their values, changing an initial value in the class field has no effect on a key the world already holds. Removing a key from the initial values doesn't delete it either. To reset a key during development, write it from a handler, or unload the script and load it again.

Key names across languages#

JavaScript uses camelCase; Rust, neo.toml and Python use snake_case. NEO converts names at the boundary, so both languages share the same keys:

JavaScriptStored as (and Python)
lastOpenerlast_opener
last_openerlast_opener
hpMax2hp_max2
xx

The rules:

  • A name with no capital letters is used as it is.
  • In a camelCase name, each capital letter becomes _ plus the lowercase letter.
  • A name that starts with a capital (Bad) or mixes capitals with _ (last_Opener) has no snake_case form. Writing it throws TypeError: ... isn't a script name.

The same rule applies to entity fields and methods: Rust's display_name is displayName in JavaScript. Only names are converted; keys inside a map you store (such as player ids) are kept exactly as written.

The state hash#

After every tick, NEO hashes each script's state, the entity fields it can see and what it did that tick. The Scripts panel and script.list show the first 8 bytes in hex. Two runs with the same scripts, inputs and seed give the same hash, which makes it a quick check that a change didn't alter behaviour, or that two machines agree. The tick's hash chain combines every script's hash.

Numbers are stored in a canonical form: a whole number within ±2^53 (except -0) is stored as an integer, whatever language or type it came from. So 0.5 + 0.5 stores the integer 1, and in Python self.state.x = 1.0 reads back as 1.

See also#