![The Vault Remembers — <persistable> and [player]int Saves](/static/guide-images/persisting-a-tycoon-state-with-persistable.png)
The Vault Remembers — <persistable> and [player]int Saves
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:
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.
Get the complete code — free
You've read the full walkthrough. The complete, copy-paste-ready Verse solution is free for members — sign in to unlock it.
Free with your BrainDead.TV / BrainDeadGuild Discord account. The walkthrough above is always free.
Check your understanding
Test yourself with an interactive quiz and track your progress + earn XP — free for members.
Turn this into a guided course
Add Persistence — the cross-zone save (East Volcano) to your free study plan — we'll suggest related pages and stitch the lot into one compile-checked, self-guided lesson with worked examples and quizzes.
References
Original tutorial generated by Verse Island from the Verse/UEFN knowledge base, with references to the Epic Games sources above. Code is validated against the knowledge base.