Skip to main content

Saving and migration

Saving​

A world is saved as one record, only at a minute's boundary, when all of that minute's work is done: the world's fields; every instance's memory, state, started timers and random numbers; closed bindings and their errors; the clock and the order counters.

A save carries the revision of the definitions it was made with: the compiled scripts, the properties' values, the objects' contracts, the events used and the versions of the rules. Definitions are stored by their content, so an old save can always be opened with its own definitions and its inputs replayed.

Play works the same way: a session runs with the definitions it opened with, and later changes do not reach it. Rerun starts a new session with the current ones.

Loading​

Loading with the same revision goes on where the world was; no handler runs. Opening a world with changed definitions is not loading but a migration, and it never happens on its own.

Migration​

A migration is explicit: a report is shown first; once approved, it is applied to the whole world at once and a new save is written with the new revision. If anything fails, including an on migrate, nothing is applied: the world goes on with the old revision, and no binding closes. The previous save stays as a backup.

It is one moment, in this order:

  1. the old instances run a migration transaction each, in instance order;
  2. new bindings, and bindings an error had closed, start as new instances;
  3. the events raised are delivered after all of them.

An instance's migration transaction:

  1. Memory matches by name and type. was "oldName" carries a rename; int → float widens; new memory takes its initial value. A value that does not match is not carried over; the report shows it with its old type and value, and it stays in the previous save.
  2. State matches by name. An instance in a state that is gone moves to the auto state (or the empty state) and its on enter runs.
  3. Declared timers follow the new definition: if opening moved from 18:00 to 20:00, the next firing is 20:00. A started timer keeps its due minute if its name still has a handler; otherwise it is dropped and reported.
  4. If the header's version went up, on migrate(from: int) runs once, with the old version as from. It can repair memory, fields and the state.

To change the type of a value and keep it, hold the old value under another name with its old type, raise version, and convert it in on migrate:

version 2

// Version 1 kept `memory level: int`; version 2 keeps a word.
memory level: string = "low" "Level"
memory oldLevel: int = 0 was "level"

on migrate(from: int)
if from < 2
level = if oldLevel > 1 then "high" else "low"
end
end

An instance whose definition or object is gone is dropped and reported. Property values that components gave under an old name are moved by the editor (was).