Keeping state
The game's page reloads more often than you'd expect: when Animal Jam moves a game to another node, or when a dropped connection reconnects. The client starts over each time, and anything your plugin held in Flash or in a variable is gone, without an error or a log line.
ctx.keep is state that survives that.
import { definePlugin } from '@phantomclient/plugin';
export default definePlugin({
async setup(ctx) {
const notes = ctx.keep('advertiser', { said: 0, lastRoom: '' });
await notes.ready();
ctx.log(`picking up at ${notes.value.said} said`);
notes.save({ said: notes.value.said + 1 });
},
});Compared with ctx.storage
ctx.storage is a plain key and value store. ctx.keep adds:
- Reading back before use. After
await notes.ready(),notes.valueis what was kept. - Batched writes.
savereturns at once, so a loop can call it after every step. - Per account, not per game. A game closed and opened again is a new game but the same player.
- One value for your panel too. The panel reads the same value, with no timer.
The value
notes.value | The current value. Change it in place (push to an array, add to a counter), then save. The reference never changes. |
notes.ready() | Resolves with the value once what was kept has been read back. Until then value is the starting value. |
notes.save(changes) | Writes it down, merging changes in first. Returns at once. |
notes.flush() | Waits until everything saved so far has reached the plugin's storage. |
notes.forget() | Puts it back to its starting value, and writes that down. |
notes.onChange(fn) | Runs when it changes, from this frame or another frame of the same plugin. |
The starting value is used on a first start, and fills in any field you've added since it was last saved, so you never get undefined.
What it survives
| Happens | Kept |
|---|---|
| A server switch, which loads the page | yes |
| A dropped connection, picked up again | yes |
| The player closing and reopening the game | yes |
| Phantom restarted | yes |
| The plugin turned off and on | yes |
| The player stopping an automation | see Automations |
| The app killed outright | the last half second of saves is lost |
Everything saved is flushed when your frame closes, so a plugin Phantom stops loses nothing. A process killed from outside can't flush, and writes are batched every half second, so at most that much is lost.
Reading it from a panel
Your panel is its own frame. useKept reads what your background code keeps and updates when it changes:
<script setup lang="ts">
import { useKept } from '@phantomclient/plugin/vue';
import { computed } from 'vue';
const notes = useKept('advertiser', { said: 0, lastRoom: '' });
const said = computed(() => notes.value.said);
</script>
<template>
<p>{{ said }} said</p>
</template>To change it from a panel, call ctx.keep(name, starting) there. It gives the same value, and its save goes through Phantom to the background code and every other frame of the plugin, which see it through onChange. The newest write wins. To start a run with something the panel chose, pass it to the run instead: ctx.automations.start(id, { input }).
Hearing storage change
You can listen to ctx.storage keys the same way, from any frame:
const stop = ctx.storage.onChange('routes', ({ value, removed }) =>
draw(removed ? [] : value)
);Naming it
Names are up to 64 characters. Values are kept per plugin, per name and per account, so they never collide with another plugin's.
A supervised automation's task.notes is kept the same way, under automation:<id>. A panel can read it either way:
<script setup lang="ts">
import { useAutomation, useKept } from '@phantomclient/plugin/vue';
const run = useAutomation<{ sweets: number }>('sweets');
const notes = useKept('automation:sweets', { sweets: 0 });
</script>Naming a game
A value belongs to one game's player. Code bound to a game (an each-game plugin's setup, or a panel) needs nothing extra. Code not bound to a game must name one:
for (const game of ctx.games()) {
const notes = ctx.keep('advertiser', { said: 0 }, { game: game.id });
await notes.ready();
ctx.log(`${game.screenName} is at ${notes.value.said}`);
}Two games signed in to the same account share one value.
What not to keep
It's JSON written to the plugin's storage, so keep it small: a counter, a cursor, a short list of what it has done. Don't keep long histories, anything you'd mind the player seeing, or anything you could work out again in a line of code.