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. vector3 and color work too.
  • No var fields — the record is immutable; updates store a fresh record.
  • No agent, player, or device references — those are live runtime identities, not data. You key the weak_map by 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:

  1. Guard — both functions first seed volcano_save{} if the failable lookup finds no record (a first-time player). Reading before guarding would fail.
  2. Read — Src := VolcanoSaveMap[Player] grabs the current record.
  3. Rebuild — store a fresh volcano_save: that overrides the one field (EmberBank / BestWave) and copies everything else via MakeVolcanoSave<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 = 12 is in the vault.
  • Walk (or portal) to AweShucks Town → the portal hub reads VolcanoSaveMap[Player] and lights up the Emberpeak gate because MoltenGateUnlocked says 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; no var fields, no agent/player/device references inside the record.
  • Store it in a weak_map(player, ...) at module scope — plain [player]int won't compile there.
  • Add a Version:int for 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_save when 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 lesson
Pass the quiz above to chart this quest in your Journal.

Sources

/guides/persisting-a-tycoon-state-with-persistable
Source

Biloxi Studios original lesson

Guided course

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