Right now every room is a sandcastle — restart the server and it's gone. Persistent worlds let a room opt in to surviving: its terrain, the things built and broken in it, and (optionally) the stuff standing around, all reloaded exactly as they were, the way Minecraft or Terraria keep a save. The good news is you've already built most of the machinery. Here's what's there, what's left, and the calls worth making up front.
Persistence usually means inventing a serialization layer from scratch. Meme Wars doesn't need to — three existing seams do the heavy lifting:
terrain.edits() / applyEdits()). Every player
build and every blown-up tile lives in that overlay — it's tiny, and it already round-trips to
clients over the wire.{ pos:{x,y}, sprite:{…}, campfire:{} }). A world dumps to JSON by walking its
component tables — no reflection, no special-casing.The one genuinely new thing is a place to put the bytes (a file per room) and a rule for when to write them.
This is the fork everything else hangs off. More persistence = more fidelity but more to serialise, version, and rebuild. The layers stack — each includes the ones before it.
Your builds and the holes you blew. Already serialised — nearly free.
Campfires, crops, dropped loot — the entities players deliberately made.
Enemies mid-charge, fish flopping, shots in flight. Rarely what you want back.
Persist the edit overlay. Builds and destruction survive; the world reloads as the shape you left it, empty of actors. This is what "my base is still here" means to most players.
edits() already exists.Also persist a curated set of entities — campfires, ripening crops, dropped items — the things a player put there on purpose. Skip transient actors (enemies, projectiles).
A full world snapshot: every entity and component, enemies and in-flight state included. Complete, but mostly captures things you'd rather see reset on load.
A persistent room writes a single JSON file (data/rooms/<id>.json). It carries
what a room needs to be re-created from cold: its identity + config, the terrain overlay, and — for
Layer 2+ — the persisted entities. Everything in it already has a serialised form today.
// data/rooms/homestead.json — everything needed to rebuild the room from cold { "id": "homestead", "name": "Homestead", "persistent": true, // how the base world is generated — identical seed => identical base terrain "seed": 88213, "gen": "one ground", "floor": "lava", "mode": "coop", "config": { "plugins": ["agriculture", "cooking"], "pluginConfig": { "hunger": { "rate": "slow" } } }, // LAYER 1 — the edit overlay, exactly what terrain.edits() returns: [tcx, tri, packedTile] "terrain": [ [12,4,3], [12,5,3], [13,4,0] ], // LAYER 2 — persisted entities: component data only (physics bodies rebuild on load) "entities": [ { "pos": {"x":640,"y":128}, "sprite": {"shape":"cooking.campfire"}, "campfire": {} }, { "pos": {"x":712,"y":120}, "crop": {"stage":2}, "sprite": {"shape":"crop"} } ] }
Note what's not here: no players (they
reconnect), no enemies or projectiles (transient), no matter-js bodies (rebuilt from
pos/body when the entity is restored). The base terrain isn't stored either
— the seed regenerates it, and only the overlay of changes is kept.
Terrain edits and placed entities change slowly (a fight doesn't touch them), so this can be cheap. Three options, not exclusive:
An edit / placement marks the room dirty; flush a few seconds later. Most durable — a crash loses seconds, not a session.
Flush every dirty room every N minutes. Simple, bounded I/O, predictable. What most games do as their backbone.
Write on a clean stop (SIGINT/SIGTERM). Cheapest, but a hard crash or power loss saves nothing.
Recommendation: B + C — a periodic autosave of dirty rooms, plus a flush on clean shutdown. Add A's debounce later if a lost few minutes ever stings. Only dirty rooms write, so an idle persistent world costs nothing.
Persistence is one flag plus two hooks on the lifecycle a room already has.
Wizard checkbox "persist this world" sets persistent:true — a plugin config field, same path as the rest.
Edits/placements mark it dirty. The autosave writes dirty persistent rooms to their file.
The idle reaper SKIPS persistent rooms (the existing never-reaped path). It stays.
On startup the server scans data/rooms/ and re-creates each — seed + config, then applyEdits + restore entities.
None of these block Layer 1, but Layer 2 needs answers and they shape the format, so better to settle them before the entity work.
pos/body when an entity is restored, exactly as spawning already does. Terrain colliders rebuild from the overlay via the existing rev counter. No new physics code, just ordering."v" field in the file from day one. Terrain is a stable tile-op list; entity component shapes will drift, so a version lets a loader migrate or skip unknown components rather than crash.Each layer is useful and shippable on its own, and Layer 1 alone delivers the headline feature — "my world is still there."
A persistent room flag (wizard checkbox) → exempt from reaping, autosave edits() to a per-room file, reload with applyEdits on boot. Builds + destruction survive. Nearly all reused code.
A persist tag on blueprints; dump/restore tagged entities in the same file; rebuild physics on load. Campfires, crops, dropped loot come back.
Only if a use-case wants enemies/state frozen and thawed. Same file, wider net. Probably never the default.
Do Layer 1 now: it's a room flag, a file, an autosave tick, and a startup scan — almost entirely seams that already exist — and it's the thing players actually mean by "persistent." Settle the five decisions above, then Layer 2 (placed entities) is the natural follow-up. Stamp a version in the file from the first write.