The Vault Remembers — <persistable> and [player]int Saves
In this lesson you'll learn to
- ✓Student can declare a persistable class + weak_map(player, save) [player]-keyed var so best-wave/ember-bank survive sessions AND carry across zone islands (the cross-zone save introduced here).
Student can declare a persistable class + weak_map(player, save) [player]-keyed var so best-wave/ember-bank survive sessions AND carry across zone islands (the cross-zone save introduced here).
📖 Reference & full walkthrough: The Vault Remembers — <persistable> and [player]int Saves
🔁 Builds on: north-jungle: classes/structs + maps
🧩 Your capstone piece: volcano_save: best wave, ember bank, unlock flags — read by AweShucks Town's portal hub
The Vault Remembers — <persistable> and [player]int Saves
Wave 14 of the Emberpeak Trials, a full ember bank, the Molten Gate finally
unlocked — and then the server cycles. If none of that is still there tomorrow,
the volcano is just a lava-flavored minigame. This lesson builds volcano_save:
the one record per player that survives session restarts and — because
persistence is scoped to the island, not the zone — is the same record
AweShucks Town's portal hub reads later. This is the cross-zone save.
The pattern is the canonical UEFN one, grounded on Epic's Santa's Toy Factory
starter project (in the Library as SampleVerse/SantaToyFactory, attributed to
Epic Games). We break its player_info.verse down, then rebuild it volcano-sized.
The player-keyed save — weak_map, not [player]int
The smallest possible save is one integer per player. Your first instinct might be a plain map at module scope:
var EmberBank:[player]int = map{} # DOES NOT COMPILE at module scope
Verse refuses: a module-scope variable outlives any single game, so the compiler
only allows the two types it knows how to persist — weak_map(session, t)
(this-session-only) and weak_map(player, t) (saved per player, reloaded every
time that player joins). The [player]int save is therefore written as:
var EmberBank:weak_map(player, int) = map{}
Reads and writes are failable ([]), because the key may be absent — a
first-time visitor has no entry yet — and because the player must be in the
current session for their data to be accessible.
One persistable record per player
One loose weak_map per stat gets messy fast. The Santa's Toy Factory sample
instead keeps ALL of a player's progress in a single class declared
class<final><persistable> in Persistence/player_info.verse:
player_info := class<final><persistable>:
Version:int = 0
Gold:int = 0
FactoryBeltInfos:[]factory_belt_info = array{factory_belt_info{}, ...}
FactoryMouldInfos:[]factory_mould_info = array{...}
OrderNumber:int = 0
OrderInfo:order_info = order_info{}
GiftOpeningTime:date_and_time = date_and_time{}
The rules for a persistable class:
<final>is required — persistable classes cannot have subclasses.- Every field must itself be persistable:
int,float,logic,string, enums/classes/structs marked<persistable>, or arrays/maps/options/tuples of those.vector3andcolorwork too. - No
varfields — the record is immutable; updates store a fresh record. - No
agent,player, or device references — those are live runtime identities, not data. You key theweak_mapby the player; you never store the player in the save.
Notice the Version:int field — a forward-compat hook so a future build can
migrate old saves (the next lesson, Versioning and Resetting Save Data, is
built on it).
Update by copy-constructor, never in place
You cannot mutate a field of a stored persistable record directly — the record
has no var fields. Instead the sample rebuilds a fresh record around the
change using a <constructor>:
MakePlayerInfo<constructor>(Src:player_info)<transacts> := player_info:
Version := Src.Version
Gold := Src.Gold
FactoryBeltInfos := Src.FactoryBeltInfos
... # copy EVERY field
GrantGold<public>(Player:player, Amount:int)<decides><transacts>:void=
CheckPlayerInfoForPlayer[Player]
SourceInfo := PlayerInfoMap[Player]
set PlayerInfoMap[Player] = player_info:
Gold := SourceInfo.Gold + Amount # the one field we change
MakePlayerInfo<constructor>(SourceInfo) # ...everything else copied
This “override one field, copy the rest via the constructor” idiom is what keeps a single-field update from silently zeroing the other fields. Forget the constructor line and every unmentioned field snaps back to its class default — a wiped ember bank with no error message.
Build volcano_save (compile-verified)
The Emberpeak version of the whole pattern — <final><persistable> record,
copy-constructor, weak_map, guarded updates. This is the capstone piece the
portal hub will read:
using { /Verse.org/Simulation }
volcano_save := class<final><persistable>:
Version:int = 0
BestWave:int = 0
EmberBank:int = 0
MoltenGateUnlocked:logic = false
MakeVolcanoSave<constructor>(Src:volcano_save)<transacts> := volcano_save:
Version := Src.Version
BestWave := Src.BestWave
EmberBank := Src.EmberBank
MoltenGateUnlocked := Src.MoltenGateUnlocked
var VolcanoSaveMap:weak_map(player, volcano_save) = map{}
BankEmbers<public>(Player:player, Amount:int)<decides><transacts>:void=
if (not VolcanoSaveMap[Player]):
set VolcanoSaveMap[Player] = volcano_save{}
Src := VolcanoSaveMap[Player]
set VolcanoSaveMap[Player] = volcano_save:
EmberBank := Src.EmberBank + Amount
MakeVolcanoSave<constructor>(Src)
RecordWave<public>(Player:player, Wave:int)<decides><transacts>:void=
if (not VolcanoSaveMap[Player]):
set VolcanoSaveMap[Player] = volcano_save{}
Src := VolcanoSaveMap[Player]
if (Wave > Src.BestWave):
set VolcanoSaveMap[Player] = volcano_save:
BestWave := Wave
MakeVolcanoSave<constructor>(Src)
Walk it through:
- Guard — both functions first seed
volcano_save{}if the failable lookup finds no record (a first-time player). Reading before guarding would fail. - Read —
Src := VolcanoSaveMap[Player]grabs the current record. - Rebuild — store a fresh
volcano_save:that overrides the one field (EmberBank/BestWave) and copies everything else viaMakeVolcanoSave<constructor>(Src).
RecordWave only writes when the new wave beats the stored best — the
comparison is just another failable expression inside the if. Call these from
a device — BankEmbers[Player, 25] after a wave clear, RecordWave[Player, WaveNumber] when the horde wipes the squad — and the vault remembers.
One save, every zone
Here is the part that makes this the cross-zone save. Verse persistence is
scoped to the published island and the module the weak_map lives in — not
to the corner of the map where you wrote it. Every zone of this experience —
Emberpeak, the shores, AweShucks Town — ships on the same island, so any Verse
code that brings VolcanoSaveMap's module into scope sees the same record:
- Survive to wave 12 at the volcano →
BestWave = 12is in the vault. - Walk (or portal) to AweShucks Town → the portal hub reads
VolcanoSaveMap[Player]and lights up the Emberpeak gate becauseMoltenGateUnlockedsays so. - Log off for a week → the engine reloads the record the moment you rejoin.
No save files, no explicit load call — the engine loads every
weak_map(player, t) module variable when the player joins, and persists your
writes. The only thing that resets it is you: a version bump or an explicit
reset, which is exactly the next lesson.
Checklist
- Persistable class is
<final><persistable>; every field is persistable; novarfields, noagent/player/device references inside the record. - Store it in a
weak_map(player, ...)at module scope — plain[player]intwon't compile there. - Add a
Version:intfor future migrations. - Update via a copy-
<constructor>, overriding only the field you change. - Guard reads/writes with the failable
[]context — seed a default record for first-time players before reading. - One island = one save scope: the record written in this zone is the record every other zone reads.
Next steps
- Continue the arc: Versioning and Resetting Save Data — what happens to
everyone's
volcano_savewhen you add a field after shipping. - Open Santa's Toy Factory — Player Persistence in the Library to see the
full production record this pattern is grounded on: factory state, orders,
timestamps, all in one
<final><persistable>class.
Keep going — free
You've read the intro. The rest of this lesson is free for members. Sign in to continue and track your progress.
Sign in free to continue this lessonSources
/guides/persisting-a-tycoon-state-with-persistableBiloxi Studios original lesson
Add this lesson to your free study plan.
🧭 The Keeper’s log
Quest complete? Chart your next heading from the 🐉 East Volcano expedition.
⛵ Take me there