Persistable Types
Persistable types allow you to store data that persists beyond the current game session. This is essential for saving player progress, preferences, and other game state that should be maintained across multiple play sessions.
Persistable data is stored using module-scoped weak_map(player, t) variables, where t is any persistable type. When a player joins a game, their previously saved data is automatically loaded into all module-scoped variables of type weak_map(player, t).
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
using { /Verse.org/Simulation }
# Global persistable variable storing player data
MySavedPlayerData : weak_map(player, int) = map{}
# Initialize data for a player if not already present
InitializePlayerData(Player : player) : void =
if (not MySavedPlayerData[Player]):
if (set MySavedPlayerData[Player] = 0) {}
Built-in Persistable Types
The following primitive types are persistable by default:
-
Numeric Types:
-
logic- Boolean values (true/false) int- Integer values (must fit in 64-bit signed range for persistence)-
float- Floating-point numbers -
Character Types:
-
string- Text values char- Single UTF-8 character-
char32- Single UTF-32 character -
Container Types:
-
array- Persistable if element type is persistable map- Persistable if both key and value types are persistableoption- Persistable if the wrapped type is persistabletuple- Persistable if all element types are persistable
Custom Persistable Types
You can create custom persistable types using the <persistable> specifier with classes, structs, and enums.
Classes must meet specific requirements to be persistable:
player_class := enum<persistable>:
Villager
player_profile_data := class<final><persistable>:
Version:int = 1
Class:player_class = player_class.Villager
XP:int = 0
Rank:int = 0
CompletedQuestCount:int = 0
Requirements for persistable classes:
- Must have the
<persistable>specifier - Must be
<final>(no subclasses allowed) - Cannot be
<unique> - Cannot have a superclass (including interfaces)
- Cannot be parametric (generic)
- Can only contain persistable field types
- Cannot have variable members (
varfields) - Field initializers must be effect-free (cannot use
<transacts>,<decides>, etc.)
Structs are ideal for simple data structures that won't change after publication:
coordinates := struct<persistable>:
X:float = 0.0
Y:float = 0.0
Requirements for persistable structs:
- Must have the
<persistable>specifier - Cannot be parametric (generic)
- Can only contain persistable field types (see Prohibited Field Types below)
- Field initializers must be effect-free (cannot use
<transacts>,<decides>, etc.) - Cannot be modified after island publication
Enums represent a fixed set of named values:
day := enum<persistable>:
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
Sunday
Important notes:
<closed>persistable enums cannot be changed to open after publication- Only
<open>persistable enums can have new values added after publication
Prohibited Field Types
Persistable types have strict restrictions on what field types they can contain. The following types cannot be used as fields in persistable classes or structs:
-
Abstract and Dynamic Types:
-
any- Cannot be persisted (too dynamic) comparable- Abstract interface type-
type- Type values cannot be persisted -
Non-Serializable Types:
-
rational- Exact rational numbers (not persistable) - Function types (e.g.,
int -> int) - Functions cannot be serialized weak_map- Weak references are not persistable-
Interface types - Abstract interfaces cannot be persisted
-
Non-Persistable User Types
-
Non-persistable enums - Enums without
<persistable>specifier cannot be used - Non-persistable classes - Classes without
<persistable>specifier cannot be used - Non-persistable structs - Structs without
<persistable>specifier cannot be used
Example
Initializing Player Data:
# Define a persistable player stats structure
player_stats := struct<persistable>:
Level:int = 1
Experience:int = 0
GamesPlayed:int = 0
# Global persistent storage
PlayerData : weak_map(player, player_stats) = map{}
# Initialize or retrieve player data
GetOrCreatePlayerStats(Player : player) : player_stats =
if (ExistingStats := PlayerData[Player]):
ExistingStats
else:
NewStats := player_stats{}
if (set PlayerData[Player] = NewStats):
NewStats
else:
player_stats{} # Fallback
JSON Serialization
Unreleased Feature
JSON Serialization have not yet been released and is not publicly available.
Verse provides JSON serialization functions for persistable types, enabling manual serialization and deserialization of data. While the primary persistence mechanism uses weak_map(player, t) for automatic player data, JSON serialization can be useful for debugging, data migration, or integration with external systems.
Converts a persistable value to JSON string:
# Serialize persistable data to JSON
Data := player_data{Level := 5, Score := 250}
JsonString := PersistenceModule.ToJson[Data]
# Produces: {"$package_name":"/...", "$class_name":"player_data", "x_Level":5, "x_Score":250}
Deserializes JSON string to typed value:
# Deserialize JSON to typed value
JsonString := ""
if (Restored := PersistenceModule.FromJson[JsonString, player_data]):
# Restored.Level = 10
# Restored.Score = 500
All serialized persistable objects include metadata fields:
{
"$package_name": "/SolIdeDataSources/_Verse",
"$class_name": "player_data",
"x_Level": 5,
"x_Score": 250
}
Metadata fields:
$package_name- Package path of the type$class_name- Qualified class/struct name
Field names:
- Prefixed with
x_in current format - Old format used mangled names like
i___verse_0x123_FieldName
Type-Specific Serialization
Primitives:
# Serialized as JSON number
JsonString := PersistenceModule.ToJson[int_ref{Value := 42}]
# {"$package_name":"...", "$class_name":"int_ref", "x_Value":42}
Optional types:
# None serialized as false
PersistenceModule.ToJson[optional_ref{Value := false}]
# {..., "x_Value":false}
# Some serialized as object with empty key
PersistenceModule.ToJson[optional_ref{Value := option{42}}]
# {..., "x_Value":{"":42}}
Tuples:
# Serialized as JSON array
PersistenceModule.ToJson(tuple_ref{Pair := (4, 5)})
# {..., "x_Pair":[4,5]}
# Empty tuple
PersistenceModule.ToJson(empty_tuple_ref{Empty := ()})
# {..., "x_Empty":[]}
Arrays:
PersistenceModule.ToJson[array_ref{Values := array{1, 2, 3}}]
# {..., "x_Values":[1,2,3]}
Maps:
PersistenceModule.ToJson[map_ref{Lookup := map{"a" => 1, "b" => 2}}]
# {..., "x_Lookup":[{"k":{"":"a"},"v":{"":1}}, {"k":{"":"b"},"v":{"":2}}]}
Enums:
PersistenceModule.ToJson[enum_ref{Day := day.Monday}]
# {..., "x_Day":"day::Monday"}
Default Value Handling
When deserializing, missing fields are automatically filled with their default values:
# Old JSON without NewField
OldJson := ""
# Deserializes successfully with default for NewField
if (Data := PersistenceModule.FromJson[OldJson, versioned_data]):
Data.Version = 1
Data.NewField = 0 # Uses default value
This enables forward-compatible schema evolution - new fields with defaults can be added without breaking old saved data.
Block Clauses During Deserialization
Block clauses do not execute when deserializing from JSON:
# Normal construction triggers block
Instance1 := logged_class{Value := 1}
# Deserialization does NOT trigger block
Json := PersistenceModule.ToJson(Instance1)
Instance2 := PersistenceModule.FromJson(Json, logged_class) # No print
Block clauses are only executed during normal construction, not during deserialization. This means initialization logic in blocks won't run for loaded data.
Integer Range Limitations
Verse protects against integer overflow during serialization. Integers that exceed the safe serialization range cause runtime errors:
# Safe range integers work fine
SafeData := int_ref{Value := 1000000000000000000}
PersistenceModule.ToJson[SafeData] # OK
# Very large integers may cause runtime errors during serialization
# to prevent silent precision loss
This prevents silent precision loss that could occur with floating-point representation of large integers.
Best Practices
-
Schema Stability: Design your persistable types carefully, as they cannot be easily changed after publication. Consider versioning strategies for future updates.
-
Use Structs for Simple Data: For data that won't need inheritance or complex behavior, prefer persistable structs over classes.
-
Handle Missing Data: Always check if data exists for a player before accessing it, and provide appropriate defaults.
-
Atomic Updates: When updating persistent data, create new instances rather than trying to modify existing ones (Verse uses immutable data structures).
-
Consider Memory Usage: Persistent data is loaded for all players when they join, so be mindful of the amount of data stored per player.