Watching stored values
Available since 0.19.0.
Storable reports every write, so a host that reacts to a stored value can listen instead of polling: unlock an achievement when gold reaches 100, mirror a flag into an editor panel, or keep a HUD in sync.
const storable = game.getLiveGame().getStorable();
// every change, anywhere
storable.onChange(({namespace, key, previous, next}) => {
console.log(`${namespace}.${key}: ${previous} -> ${next}`);
});
// one namespace
storable.onChange("persistent:player", ({key, next}) => {
console.log(key, next);
});
// one key
const token = storable.onChange("persistent:player", "gold", ({next}) => {
if (next === 100) achievements.unlock("rich");
});
token.cancel();The payload is StorableChange. Its namespace is the key the namespace is registered under, the same string getNamespace takes and the one a save file carries, not the human-readable name. A namespace declared as new Persistent("player", ...) is registered as "persistent:player"; a scene's local store is registered as "local:" plus the scene's name.
The listener runs after the new value is readable, so it can read the rest of the namespace and see a consistent state. assign reports one change per key. reset reports the return to each default, and reports a key written after construction as changing to undefined.
Subscribe on the Storable, not on a Namespace
newGame() and loading a save both rebuild every namespace from scratch: the store is cleared and each registered Persistent constructs a fresh Namespace object. The Storable is created once, with the LiveGame, and is never replaced. A subscription registered on the store survives both; a subscription bound to a namespace object goes quiet after the first load, with no error to explain it.
For the same reason, do not cache the object returned by getNamespace across a newGame() or a load. Call getNamespace again. A held reference points at a namespace that is no longer registered, so reads see stale values and writes report nothing.
Writes that do not change the value
Writing a value equal to the one already there reports nothing.
Equality is structural, not by reference. A stored value is serializable by definition (a primitive, a Date, or a plain object or array of those), and the ordinary authoring idioms rebuild the container even when nothing inside it moved:
namespace.assign({gold: 10}); // reports once
namespace.assign({gold: 10}); // reports nothing
namespace.set("bag", v => ({...v, gold: v.gold})); // new object, same contents: nothingDatevalues compare by timestamp, not by identity.- A value outside the serializable domain (a class instance, or a function, which
setwarns about but still stores) is only equal to itself, so it always reports a change.
Loading a save
A bulk application fires onRestore once, naming the namespaces involved, and no onChange at all:
storable.onRestore(({namespaces}) => {
rereadMyDerivedView();
});This covers deserialize, which fires one event for the whole save, and rewinding a single namespace to a snapshot, which is how a scene's locals are undone and fires one event naming that namespace. Ordinary play still reports per-key changes.
A host needs both signals. onChange is silent during a load, so a listener watching one key does not fire when a loaded save arrives already at the interesting value. Re-check the value on onRestore as well.
const storable = game.getLiveGame().getStorable();
function readGold() {
return storable.getNamespace("persistent:player").get("gold");
}
// evolving values
storable.onChange("persistent:player", "gold", ({next}) => render(next));
// the reload discontinuity
storable.onRestore(({namespaces}) => {
if (namespaces.includes("persistent:player")) render(readGold());
});Raw events
onChange and onRestore are filters over one dispatcher, exposed as storable.events for a host that attaches to it directly:
| Event | Payload |
|---|---|
event:storable.change | StorableChange |
event:storable.restore | StorableRestore |
Prefer onChange, which filters by namespace and key.