Worlds that last, and values every server shares
Worlds kept between sessions (Persist) and values every server shares.
The reference is "Kept worlds and shared data" in docs/engine/API.md; the SDK side is "Data that outlives a session" in the
platform guide (onceworlds.com/agents.md).
Pick the place
| You want | Use | Who changes it |
|---|---|---|
| A player's own progress (level, unlocks, settings) | defineSave + Save(...) (onceworlds.data.player) |
that player's page |
| A server's world: a base, a farm, a town, a sandbox | Persist(), or platform.data.server by hand |
the room's host only |
| One value for the whole game: a community goal, a wall, a record, today's event | "data" in onceworlds.json + platform.data.game |
anyone, only by the declared operation, within its bounds |
Server data lasts in private servers and named rooms. A friends game (Flow.dropIn({ friends: true }), or
game.join({ private: true })) is the same private server each time a player starts it, so their world is there tomorrow, on
any device. A public server's data ends with the server: Persist says so and keeps nothing.
Every page is untrusted. Server data trusts the host's page, as every host rule does: other players send the host requests
(net.rpc, net.claim), and the host checks them before changing the world. Game data trusts nobody: the platform applies
only the operation the key declares, within its bounds, a few times a minute per player. Declare the tightest bounds the game
needs, and give out nothing valuable for game data.
Tycoon progress
A player's own factory that keeps growing while they're away. Machines are entities, cash is a resource: both kept.
import { createGame, defineComponent, defineResource, t, Transform } from '@onceworlds/engine';
import { Flow, Persist, persistOf, Sprite } from '@onceworlds/engine/modules';
const Machine = defineComponent('Machine', { kind: t.enum(['press', 'oven']), level: t.u8(1, { min: 1, max: 20 }) }, { save: true });
const Bank = defineResource('Bank', { cash: t.u32(), at: t.u32() }, { save: true }); // at: platform seconds
const game = createGame({
modules: [
Flow.dropIn({ friends: true }), // each player's own private server
Persist({
rebuild: { Machine: (m) => [Sprite({ image: `assets/${m.kind}.png` })] },
fresh: (world) => void world.spawn([Transform({ position: [0, 0, 0] }), Machine({ kind: 'press' })], { owner: 'host' }),
}),
],
});
game.start();
// Away earnings: the platform's clock is the same for everyone (not the device's), so a page can't skip ahead.
persistOf(game)!.ready.then(() => {
const bank = game.world.resource(Bank);
const now = Math.floor(game.platform.now() / 1000);
if (bank.at > 0) bank.cash += Math.floor(Math.min(now - bank.at, 8 * 3600) * incomePerSecond(game.world));
bank.at = now;
});
- Keep progress in fields marked
save; keep what can be worked out again (sprites, colliders, sounds) out, and give it back withrebuild. - Pick number types for how big they get:
t.f32is exact for whole numbers only up to 16,777,216,t.u32up to about 4.29 billion; keep anything bigger as a string, or in thousands. - Raise
versionwith amigratestep when a kept component changes shape. An old page that meets a newer world leaves it alone.
Sandbox world
A shared build in a named room (or a friends server): players place blocks, the host checks and writes them, everyone sees them, and the next session finds them.
import { createGame, defineComponent, t, Transform } from '@onceworlds/engine';
import { Flow, Net, netOf, Persist } from '@onceworlds/engine/modules';
// Kept and sent: the next host can only save what reached its page.
const Block = defineComponent('Block', { kind: t.u8(0, { max: 15 }) }, { save: true, net: { replicate: true } });
const game = createGame({
modules: [
Net({ components: [Block] }),
Flow.none({ join: { name: 'build-island' } }), // a named room: kept like a private server
Persist({ key: 'island', rebuild: { Block: (b) => blockLook(b.kind) } }),
],
});
game.start();
const place = netOf(game)!.rpc('place', {
args: { x: t.i32(0, { min: -64, max: 64 }), y: t.i32(0, { min: 0, max: 64 }), kind: t.u8(0, { max: 15 }) },
to: 'host',
rate: 8,
validate: (_from, a) => !occupied(game.world, a.x, a.y) || 'taken',
run: (_from, a) => void game.world.spawn([Transform({ position: [a.x, a.y, 0] }), Block({ kind: a.kind })], { owner: 'host' }),
});
place({ x: 3, y: 1, kind: 2 });
- Blocks are the host's entities (
owner: 'host'): Net sends them to everyone, Persist keeps them. A player's own entities (their character) are never kept in a world. - A world must fit twice in a server's 512 KB: about 240 KB of JSON. Thousands of blocks fit; for more, keep a block layer as one
entity with a
t.listof packed numbers, or split the map into areas by hand withplatform.data.serverkeys.
Survival base
A base that survives the night, written by hand with server data (no Persist): the host owns the truth, and saves once a night and when a wall changes.
const data = game.platform.data.server;
// Everyone: the base came with the room, no waiting.
const base = data.get('base') ?? { walls: [], chests: [], night: 1 };
buildBase(game.world, base);
data.on('change', (key, value) => key === 'base' && buildBase(game.world, value));
// The host: write when something changed, not every frame (true once the room has it).
async function keep(): Promise<void> {
if (!data.canWrite) return;
if (!(await data.set('base', baseOf(game.world)))) console.warn('base not kept yet: retrying at the next change');
}
game.room?.on('matchend', keep);
- A key holds 16 KB of JSON; a server keeps 512 KB in 128 keys. Split by area (
base-north,base-south) when one key is too small. - Only the host writes. A player who builds sends the host a request; the host checks materials and position, then writes.
- A private server's owner can start the world over (
data.reset(), or the platform menu): every page reloads into an empty world.
Game-wide meter
A goal every server fills together, starting over each day. Declare it in onceworlds.json:
"data": {
"meter": { "type": "counter", "max": 1000, "add": 1, "reset": "daily" },
"heroes": { "type": "list", "max": 10, "bytes": 64 }
}
const shared = game.platform.data.game;
// A player finished a delivery: one point, at most what "add" allows.
const total = await shared.add('meter', 1);
if (total === 1000) await shared.push('heroes', { name: me.name });
// Show it: read every few seconds, not every frame (reads are a few seconds old anyway).
setInterval(async () => {
const now = await shared.get('meter');
if (now !== null) meterBar.set(now / 1000);
}, 5000);
- A counter stays within
min..max, and oneaddis withinadd(1: 0 to 1;[1, 5]: 1 to 5). A cheater can add the mostaddallows, about 30 times a minute: that is the worst they can do, so choose bounds where that is harmless. recordkeeps one best score with who set it,maphands out fields (plots, seats) to one player each,valueis set by a room's host (today's event, chosen by a host's page).- Lists and values are screened like chat. Game data is shown to everyone: names, not secrets.
- The dashboard's Data tab shows every key and every kept world, and can clear or start them over.