The Vault Remembers — <persistable> and [player]int Saves
Tutorialintermediatecompiles

The Vault Remembers — <persistable> and [player]int Saves

Updated intermediateVerse FoundationsGame ModesCode verified

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:

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.

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.

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.

Comments

    Sign in to vote, comment, or suggest an edit.Sign in