Reference · Verse

Verse rules that actually matter

Not a language tour. Everything here was learned by hitting it in a shipped island: the subset that breaks builds, and the patterns worth reusing. Anything marked unconfirmed needs checking before you rely on it.

Rule 1

An if condition must be able to fail

The single most common compile error when writing Verse from memory. A bare logic value is not a condition.

failable conditions
# ✗ Error: "Expected an expression that can fail in the 'if' condition clause"
if (Hidden):
if (IsActive(Idx)):

# ✓ Compare explicitly
if (Hidden = true):
if (Hidden = false):
if (IsActive(Idx) = true):

Valid failable conditions: comparisons (=, <>, >, <), decides calls written with square brackets, optional unwraps, and set expressions.

Rule 2

set inside if is the normal idiom

Assigning into a map can fail, so it's wrapped and the result discarded. The empty block is not a mistake: it's how you say "I don't care about the failure case".

set idiom
if (set Balance[Player] = 100) {}
Rule 3

Optionals

optionals
var Look : ?post_process_device = false    # empty optional
set Look = option{SomeDevice}              # filled
if (L := Look?):                           # unwrap, only runs when present

false as an empty optional reads strangely, but it's correct.

Rule 4

Specifiers you will need

SpecifierUse
<suspends>Function can sleep or await. Required for Sleep, Await, and loop with waits
<transacts>Pure-ish query, callable from inside conditions
<override>On OnBegin
<concrete>On data classes used in @editable arrays
<final><persistable>On save-data classes
Rule 5

Concurrency

spawn, race, loop
spawn { SomeLoop() }        # fire and forget, does not block

race:                        # first one to finish wins, other is cancelled
    Sleep(RotateSeconds)
    WaitForCollect(Idx, Slot)

loop:                        # infinite until break
    Sleep(1.0)
    if (Condition = false):
        break

race is the cleanest way to express "whichever happens first": a timer versus a player action, for example.

Rule 6

Iteration

for
for (Item : Array):              # values
for (Idx -> Item : Array):       # index and value
Rule 7

Odds and ends

  • String interpolation: "Collected {Amount} coins"
  • Int[SomeFloat] converts and can fail, so it needs unwrapping.
  • Min, Max, Floor, Mod[], GetRandomInt(Low, High) are available.
  • Billboard text takes a message, not a string. Keep a small StringToMessage helper.
Persistence

Save data is append-only

A <persistable> class is append-only from the moment the island is first published. Adding a field is safe. Removing or renaming one fails publish validation, because live save data still references it.

real error
Missing definition in source package that corresponds to published
definition (/…/player_progress:)XP
verse_validate_publish_error

The fix is to put the field back, even unused, with a comment saying it can never leave. Treat every persisted field as permanent on the day you add it.

the shape
player_save := class<final><persistable>:
    Currency : int = 0
    Level    : int = 0

# Verse allows only a small number of weak_map variables per island
# (4 in the project this was learned in). Spend them deliberately --
# one map holding one save class is usually enough.
var PlayerSave : weak_map(player, player_save) = map{}

The bug everybody writes

An autosave loop spawned per player does not stop when that player leaves. It keeps ticking against a now-invalid key and throws:

real error
ErrRuntime_WeakMapInvalidKey: Invalid key used to access persistent
`var` `weak_map`.
guard every iteration
SaveLoop(Player : player)<suspends> : void =
    loop:
        Sleep(SaveIntervalSecs)
        if (IsConnected(Player) = false):
            break
        Save(Player)

IsConnected(Player : player)<transacts> : logic =
    for (P : GetPlayspace().GetPlayers()):
        if (P = Player):
            return true
    return false

Full walkthrough: Player progress that survives leaving.

Hard platform limits

There is no player-name API

Verse cannot read a player's display name. Not through player, not through agent, not through any devices module. GetDisplayName, player_name, Nameplate, EpicAccountId: all return nothing in the digests.

Anything you build in the world that should show who someone is, a name above a head or on a board, can't be built from Verse. Design around it from the start.

There is no timezone or DST handling

GetSecondsSinceEpoch() gives you UTC. Converting a local wall-clock time to that is your problem, and a fixed offset is only correct for half the year.

timezone offset
# Amsterdam: 1 in winter, 2 during summer time.
# No automatic DST -- this gets flipped by hand at each clock change.
TimeZoneOffsetHours : int = 2
Symptom

The event fires exactly one hour off the time you typed.

Patterns worth reusing

Rotating active nodes

Of N total nodes, only a few are awake; each rotates on a timer or on first collection. Full guide: Keep players moving.

rotation pattern
loop:
    if (Idx := PickSleepingNode()?, Node := Nodes[Idx]):
        # ... enable trigger, beacon, start fill loop ...
        race:
            Sleep(RotateSeconds)
            WaitForCollect(Idx, Node)
        # ... disable, free the slot ...
    else:
        Sleep(1.0)

Cue-driven timeline

Model a timed show as an array of <concrete> cue structs, each with a timestamp plus everything that fires at it. Keep the end of the show as its own field. Tying it to the cue list cut a live show off mid-song.

show end pattern
Remaining := ShowDurationSeconds - Elapsed
if (Remaining > 0.0):
    Sleep(Remaining)
OnShowEnd()

One-time-per-player gate

Native devices have no per-player use limits. Keep the memory yourself. This lives in memory, not save data.

gate pattern
var HasUsed : [player]logic = map{}

OnEnter(Agent : agent) : void =
    if (Player := player[Agent]):
        Used := if (U := HasUsed[Player]) then U else false
        if (Used = false):
            if (set HasUsed[Player] = true) {}
            Destination.Teleport(Player)

Pausing a background loop from outside

When one system must suppress another temporarily, a flag checked inside the loop beats trying to kill the loop. Without it, the loop re-enables things a few seconds later and the bug looks random.

suppression flag
var SuppressMarkers : logic = false

SetMarkersHidden(Hidden : logic) : void =
    set SuppressMarkers = Hidden
    if (Hidden = true):
        for (Slot : Slots):
            Slot.Beacon.Disable()
    else:
        for (Idx -> Slot : Slots):
            if (IsActive(Idx) = true):
                Slot.Beacon.Enable()

# ...and inside the rotation loop:
if (SuppressMarkers = false):
    Slot.Beacon.Enable()

Teleporting everyone

mass teleport
TeleportEveryoneTo(Marker : creative_prop) : void =
    Where := Marker.GetTransform()
    for (Player : GetPlayspace().GetPlayers()):
        if (Char := Player.GetFortCharacter[]):
            if (Char.TeleportTo[Position := Where.Translation, Rotation := Where.Rotation]) {}
Know this

GetFortCharacter[] fails silently for anyone mid-respawn or with a momentary network issue, and that player is skipped with no error. "Some players but not others" isn't a selection, it's timing.