Scripting in DES
Use event-driven scripts to add custom material-flow behaviour that can continue from a saved run.
The examples on this page carry an AGPL header because they ship with the open-source core. A script you write yourself is part of your model. If you deliver realvirtual to others under AGPL and a script takes over code from the core or is tightly interwoven with it, the recipients must be able to get it under AGPL as well; see Licence.
Scripts keep working across a restore if every value that changes during the run is stored in one object declared with self.persist.
// SPDX-License-Identifier: AGPL-3.0-only// Snapshot contract: keep all runtime state in one JSON-only self.persist object.// No module-scope let/var. The SDK restores the timer and RNG; do not re-arm// timers in onRestore (that would duplicate the pending tick).function setup(self) { const st = self.persist({ counter: 0, history: [], timerId: 0 }); st.timerId = self.every(1, 'beat'); return { des: { on(hook) { if (hook !== 'beat') return; st.counter++; st.history.push([self.now, Math.round(self.random() * 1000000)]); if (st.history.length > 4) st.history.shift(); } }, state() { return st; }, };}The example counts a simulated-second heartbeat and keeps its last four random draws.
Persistent state
Section titled “Persistent state”- Call
self.persistonce, duringsetup, and keep the returned object. A restore refills this same object. - It holds plain JSON data only: finite numbers, strings, booleans,
null, arrays and plain objects. NoNaN,Infinity,-0,Map,Set, functions, shared references or circular references. self.propis read-only configuration. Changes to it are not saved.
Timers
Section titled “Timers”Use self.in, self.at and self.every. Pending timers are part of the snapshot and fire at the same times after a restore, so do not start them again after a restore: that would run every tick twice. self.every returns an id; keep it in the persist object if you need to cancel the timer later. setTimeout, setInterval, Date and Math.random do not exist in scripts, and timers that count down in fixedUpdate do not work in event-based DES.
Code checks
Section titled “Code checks”When you save a script, and when DES binds it, the script is checked for state that would be lost. Findings read des-script-undeclared-state: line N: …; use self.persist(...). They are warnings in the editor, but errors when the component sets DesSafe, and DES always refuses to bind such a script.
- changing variables declared outside the handlers (
let count = 0, arrays, objects,Map,Setat the top of the script or insetup) - classes declared at the top of the script or in
setup - writing to a variable of an outer function, to a global, or to properties of captured objects
- handlers created by a function call (
on: makeHandler()) instead of written out insetup - calling a helper function during
setupto build state
Constants, SDK handles such as self.drive(...), self, and scratch variables inside a handler are fine.
Converting an existing script
Section titled “Converting an existing script”// Before: state in a closure variable is lost on restorelet count = 0;function setup(self) { return { des: { on() { count++; } } };}
// After: state in self.persistfunction setup(self) { const st = self.persist({ count: 0 }); return { des: { on() { st.count++; } } };}Move every changing value into the persist object, write handlers out in place, and look up SDK handles inside the handler instead of storing them. The older onSnapshot and onRestore hooks still work, but they do not exempt a script from the check.
Random numbers and hooks
Section titled “Random numbers and hooks”Use self.random() for seeded random draws. With RNG v2, each script seed is derived from the run’s master seed, its component ID and the purpose script, so the same model and seed reproduce the sequence on each run. In v1 the default script seed comes from its component path, rather than the master seed. Draws during setup, and scripts created after a run starts, use their initial path seed (or an explicit SDK seed) until the next reset or run start.
des.on(hook, mu, data) receives scheduled hooks; the example handles beat. Its state() function returns the persistent object. The SDK also saves its own state-machine state, random position and timer IDs. onSnapshot and onRestore are a synchronous legacy facade; they do not make hidden closure state safe or replace self.persist.
Snapshot limits
Section titled “Snapshot limits”A restore refills the existing persistent object and restores pending timers. Script KPI state-time statistics are not saved. Callbacks registered after setup are not saved; register them during setup.
See Scripting for the SDK and editor, and Saving and restoring a run for model checks and snapshot limits.