Scripts, globals, data and saves
Scripts attached to nodes, global scripts, data resources, saved games and hot reload’s snapshots.
Files: script.odin, globals.odin, data.odin, save.odin, hot_reload.odin.
File overviews
Section titled “File overviews”script.odin
Section titled “script.odin”Scripts: game code written in Odin, attached to the nodes of a scene.
A script is a struct whose first field is using script: Script, plus the procedures the engine
calls for it. It’s registered once, by name, and scene files attach it to a node by that name
(<script name="Player">). When the scene is built, the engine allocates the struct in the
node’s memory arena (it’s freed with the node), fills in its exported fields from the scene
file, and calls its procedures:
ready once, on the first update after the node entered the scene (every node of the scene exists by then, so it can look others up)update every frame, after the node's own updatedraw after the node draws, in the node's local space (optional)destroy when the node is freed (optional; the struct's memory is freed anyway)A project keeps its scripts in one Odin package (scripts/); each file registers its script
from an @(init) procedure, so linking the package in is enough:
package scripts
import "core:log"import e "engine:."import sdl "vendor:sdl3"
Player :: struct { using script: e.Script, speed: f32 `export:"default=250 min=0"`, lives: int `export:"default=3"`, // Not exported: state the script keeps for itself velocity: [2]f32,}
@(init)register_Player :: proc "contextless" () { e.Script_Register(Player, { ready = proc(self: ^Player) { log.infof("%s is ready", self.node.name) }, update = proc(self: ^Player, events: []sdl.Event, dt: f64) { self.node.position.x += self.speed * f32(dt) }, })}Exported fields are the ones tagged export:""; they show in the editor’s inspector and are
saved in the scene. Supported types: f32, f64, int, i32, bool, string, [2]f32, enums,
sdl.Color (#rrggbb), and the asset handles of assets.odin (TextureAsset, SoundAsset,
FontAsset, DataAsset, ComponentAsset, MusicAsset, AnimationAsset), and pointers to nodes (^e.Node,
^e.Sprite, ^e.CharacterMover…). Tag options, space-separated inside the quotes:
default=<value> (else the type’s zero value; white for colors, the first value for enums),
min=<n>, max=<n>, and kind=scene for a string naming a scene (kind=<DataType> for a
DataAsset).
Node fields hold a path to another node (Node_FindPath: relative to the script’s node, as
Sword or ../Camera; from the scene’s root with a leading /). Just before ready they’re
set to that node, if it’s there and of the field’s type (or a type that embeds it: a
^e.PhysicsBody field takes a DynamicBody); otherwise they stay nil, with a warning. An empty
path leaves the field nil. Only node scripts get them (globals and data resources have no node).
Hero :: struct { using script: e.Script, sword: ^e.Area `export:"default=Sword"`, camera: ^e.Camera2D `export:"default=../Camera"`,}One script per node. The registry is global (scripts register before main).
globals.odin
Section titled “globals.odin”Global scripts: game-wide state and services that live for the whole run, across scenes (score, lives, settings, a music manager, a HUD).
Any registered script can be global: project.xml lists the globals, in the order they start, with their exported field values (the editor’s Project Settings > Globals):
<global script="GameState"> <prop name="lives" value="3"/></global>When the game starts (Game_LoadProject) each global gets its struct (fields from those values:
every run starts from them, so Play always resets the game’s state), then the scenes are built,
then the globals’ ready runs in order. Any script reaches a global by its type:
state := e.Global(GameState)state.score += 10Hooks (all optional; self.node and self.scene are nil for globals, self.game is set):
ready,update(every frame, before the scene’s nodes),destroy(when the game ends)sceneChanged(self, scene): the game switched scenesdraw(self, renderer): draws over the scene in screen space (not moved by the camera), after the scene, in the globals’ order. Leave it out for globals that draw nothing; setself.hiddento skip it for a whileloaded(self):Load_Gamejust restored its fields (save.odin)
Globals are also what Save_Game saves: their state (fields that aren’t exported, unless tagged
save:"-", and exported ones tagged save:"": save.odin).
data.odin
Section titled “data.odin”Data resources: reusable game data (enemy stats, items, levels, dialogue) as project files, separate from code.
A data type is a struct registered like a script, with exported fields:
EnemyStats :: struct { health: int `export:"default=10 min=1"`, speed: f32 `export:"default=100"`, sprite: e.TextureAsset `export:""`,}
@(init)register_EnemyStats :: proc "contextless" () { e.Data_Register(EnemyStats)}Each instance is a file, data/<name>.data.json, listed in project.xml as an asset of kind
“Data” (so exports ship it), made and edited in the editor:
{"formatVersion": 1, "type": "EnemyStats", "values": {"health": "30", "speed": "80"}}Values are text in the same forms as script fields in scene files (Script_SetField); missing
ones get their default. Data(EnemyStats, DATA_GOBLIN) loads a file the first time and returns
the same instance after that: everyone shares it, so treat it as read-only (copy it to change
it). It lives as long as the game.
save.odin
Section titled “save.odin”Saving and loading game state as JSON: what’s in the globals (globals.odin).
e.Save_Game() // into the "save" slote.Load_Game() // false if there's no save yete.Save_Game("slot2")A save holds every global’s state by field name:
- Fields that aren’t exported are the game’s state: saved, unless tagged
save:"-"(caches, pointers to nodes). - Exported fields (
export:"") are settings from the editor: not saved, so changing them in the editor reaches players who have a save. Tag onesave:""when the player changes it too (a chosen ship, a volume): then the save’s value wins. Fields that can’t be written as JSON are skipped with a warning. Numbers, bools, strings, enums (by name), arrays, slices, dynamic arrays, maps, nested structs and asset handles all work. Loading sets the fields a save has and leaves the others (fields added to a global since the save keep their values), then calls the globals’loadedhook.
{ "formatVersion": 1, "globals": { "GameState": {"score": 120, "lives": 2, "highScore": 900} }}Where saves go
- Desktop: the per-user data folder SDL picks (
SDL_GetPrefPath):<user data>/<game name>/saves/<slot>.json. Games run from the editor (--dev) use<game name> (dev), so testing never touches the saves of an exported game. - Web: the browser’s localStorage, under
<game name>/<slot>.
hot_reload.odin
Section titled “hot_reload.odin”Restarting a game where it was: what an editor’s hot reload does when scripts change (the editor rebuilds the game, then restarts it in the same state).
Game_Snapshotwrites the game’s state as JSON: the scene playing, the globals (whatSave_Gamewrites) and every node of the scene’s properties (Node_Properties: position, rotation, scale, tint, visibility, an AnimatedSprite’s animation, a state machine’s state, every script field…), by the node’s path of names (“World/Ship”).Game_RestoreSnapshotputs it back into a freshly started game: the globals at once, the nodes after the scene’s scripts’ready(so a script’s ready doesn’t undo it). Nodes the new scene doesn’t have, and properties that no longer exist (a field renamed), are skipped.- The editor asks over the live link (
snapshot <file>: live.odin) and starts the new game withplayer --restore <file>.
What isn’t carried over: nodes made at run time (bullets, spawned enemies), physics velocities, timers and tweens in flight, sounds playing, and the fields of scripts that aren’t exported and aren’t in globals. The game restarts its scene from the scene file, then those values go on top.
Script
Section titled “Script”Script :: struct { // The node it's attached to (nil for globals: globals.odin) node: ^Node, // The node's scene (a global's: nil, see globals.odin) scene: ^Scene, // The game it runs in game: ^Game, // Globals: skip `draw` while set hidden: bool,}The first field of every script struct: what the script is attached to
struct. Source: script.odin:81
ScriptHooks
Section titled “ScriptHooks”ScriptHooks :: struct($T: typeid) { // Once, on the first update after the node entered the scene (every node of it exists then) ready: proc(self: ^T), // Every frame the node may process, after the node's own update (deltaTime in seconds) update: proc(self: ^T, events: []sdl.Event, deltaTime: f64), // After the node draws, in the node's local space draw: proc(self: ^T, renderer: ^sdl.Renderer), // When the node is freed (the struct's memory is freed with the node anyway) destroy: proc(self: ^T), // Physics body nodes only: another body started / stopped touching this one (or entered / // left it, for an Area). `collision` is valid during the call collisionEnter: proc(self: ^T, collision: ^Collision), // Physics body nodes only: another body stopped touching this one (or left it, for an Area) collisionExit: proc(self: ^T, collision: ^Collision), // Physics body nodes only: this body entered / left an Area (`collision.other`) areaEnter: proc(self: ^T, collision: ^Collision), // Physics body nodes only: this body left an Area (`collision.other`) areaExit: proc(self: ^T, collision: ^Collision), // Globals only (globals.odin): the game switched to `scene`; Load_Game restored the fields sceneChanged: proc(self: ^T, scene: ^Scene), // Globals only (globals.odin): `Load_Game` restored the fields loaded: proc(self: ^T),}The procedures a script can have; all optional
struct. Source: script.odin:93
ScriptInfo
Section titled “ScriptInfo”ScriptInfo :: struct { // The struct's type name without its package ("Player"): what scene files attach name: string, // The script struct's type type: typeid, // typeid_of(^T): what JSON loading needs (save.odin) pointerType: typeid, // size_of the struct, in bytes size: int, // align_of the struct align: int, // Where Script_Register was called, so the editor can open the file file: string, // The line of that call line: int, // Its exported fields, in declaration order fields: []ScriptField, // The events it publishes: its EventSubject fields tagged `event:"..."`, named "script.<field>" // (`Script_FindEvent`; node_events.odin) events: []NodeEventInfo, // The hooks of `ScriptHooks`, taking the struct as a rawptr (nil: not given) ready: proc(self: rawptr), // ScriptHooks.update update: proc(self: rawptr, events: []sdl.Event, deltaTime: f64), // ScriptHooks.draw draw: proc(self: rawptr, renderer: ^sdl.Renderer), // ScriptHooks.destroy destroy: proc(self: rawptr), // ScriptHooks.collisionEnter collisionEnter: proc(self: rawptr, collision: ^Collision), // ScriptHooks.collisionExit collisionExit: proc(self: rawptr, collision: ^Collision), // ScriptHooks.areaEnter areaEnter: proc(self: rawptr, collision: ^Collision), // ScriptHooks.areaExit areaExit: proc(self: rawptr, collision: ^Collision), // ScriptHooks.sceneChanged sceneChanged: proc(self: rawptr, scene: ^Scene), // ScriptHooks.loaded loaded: proc(self: rawptr),}A registered script (Script_Register): its type, its exported fields and its hooks, untyped
struct. Source: script.odin:118
ScriptFieldType
Section titled “ScriptFieldType”ScriptFieldType :: enum { // f32 or f64 Float, // int or i32 Int, Bool, String, // [2]f32, as "x, y" Vector2, // sdl.Color, as `#rrggbb` or `#rrggbbaa` Color, // Any enum, by value name Enum, // Asset handles (assets.odin): the value is the asset's path Texture, Sound, Font, Data, Component, Music, // A pointer to a node (`^e.Sprite`...): the value is a path (`Node_FindPath`), resolved // before `ready` Node, // An `AnimationAsset` (an animation set's path) Animation,}What an exported field holds, as the scene file and the editor see it
enum. Source: script.odin:161
ScriptField
Section titled “ScriptField”ScriptField :: struct { // The field's name in the struct (and in `<prop name="...">`) name: string, // What kind of value it holds type: ScriptFieldType, // Its byte offset in the script struct offset: uintptr, // The field's Odin type (f32 vs f64, int vs i32, which enum) odinType: typeid, // Text form of the value used when the scene file has none defaultValue: string, // The tag's `min=`: numbers below it are raised to it (nil: no minimum) min: Maybe(f64), // The tag's `max=`: numbers above it are lowered to it (nil: no maximum) max: Maybe(f64), // "scene" for a string naming a scene; for a Data field, the data type it holds (`kind=EnemyStats`); // for a Node field, the node type it points to, without its package ("CharacterMover") hint: string, // Node fields: the node type the pointer points to (`Node`, `Sprite`...) nodeType: typeid, // Enum fields: the names, in order enumNames: []string,}An exported field of a script
struct. Source: script.odin:189
GlobalData
Section titled “GlobalData”GlobalData :: struct { // The registered script's name script: string, // Its exported field values, as text props: [dynamic]ScriptPropData,}A global in project.xml: its script and field values
struct. Source: globals.odin:38
GlobalInstance
Section titled “GlobalInstance”GlobalInstance :: struct { // The global's registered script info: ^ScriptInfo, // The global's struct, in the game's arena data: rawptr,}A running global (Game.globals)
struct. Source: globals.odin:46
DataInfo
Section titled “DataInfo”DataInfo :: struct { // The struct's type name without its package ("EnemyStats"): what data files name name: string, // The struct's type type: typeid, // size_of the struct, in bytes size: int, // align_of the struct align: int, // Where Data_Register was called, so the editor can open the file file: string, // The line of that call line: int, // Its exported fields, in declaration order fields: []ScriptField,}A registered data type (Data_Register)
struct. Source: data.odin:39
DataResourceData
Section titled “DataResourceData”DataResourceData :: struct { // The data type's name (`DataInfo.name`) type: string, // The field values as text, sorted by field name values: [dynamic]ScriptPropData,}What a data file holds (ParseDataResource): its type’s name and its field values as text
struct. Source: data.odin:57
Snapshot
Section titled “Snapshot”Snapshot :: struct { version: int, scene: string, globals: string, nodes: []struct { path: string, props: map[string]string },}A snapshot’s contents (temp allocated)
struct. Source: hot_reload.odin:85
Procedures
Section titled “Procedures”Script_Register
Section titled “Script_Register”Script_Register :: proc "contextless" ($T: typeid, hooks: ScriptHooks(T), loc := #caller_location)Registers a script type under its type name. Call it from an @(init) procedure in the script’s
file; Odin requires those to be proc "contextless", and this procedure is too, so it can be
called there directly. Registering the same name twice replaces the first. A struct whose first
field isn’t using script: Script isn’t registered (an error is printed to stderr).
Inputs
- T: The script struct; its first field must be
using script: Script - hooks: Its procedures (all optional)
- loc: Where it’s called from (kept so the editor can open the file)
Example
package scripts
import e "engine:."import sdl "vendor:sdl3"
Spinner :: struct { using script: e.Script, speed: f32 `export:"default=90"`,}
@(init)register_Spinner :: proc "contextless" () { e.Script_Register(Spinner, { update = proc(self: ^Spinner, events: []sdl.Event, dt: f64) { self.node.rotation += self.speed * f32(dt) }, })}procedure. Source: script.odin:258
Script_Find
Section titled “Script_Find”Script_Find :: proc(name: string) -> ^ScriptInfoThe script registered under a name, or nil.
Inputs
- name: The script’s name (“Player”)
Returns
- Its info, or nil when no script has that name
procedure. Source: script.odin:306
Script_All
Section titled “Script_All”Script_All :: proc() -> []^ScriptInfoEvery registered script, sorted by name (temp allocated)
procedure. Source: script.odin:311
Node_AttachScript
Section titled “Node_AttachScript”Node_AttachScript :: proc(node: ^Node, info: ^ScriptInfo, values: []ScriptPropData) -> rawptrAttaches a script to a node: allocates its struct in the node’s arena and sets its exported fields
from values (name, value text; missing ones get their default).
Inputs
- node: The node
- info: The script
- values: Field values from the scene file
Returns
- The script’s struct, or nil if the node already has one
procedure. Source: script.odin:330
Node_Scripts
Section titled “Node_Scripts”Node_Scripts :: proc(node: ^Node) -> []^NodeScriptEvery script on a node, the first one first (temp allocated).
Inputs
- node: The node
Returns
- Pointers to its scripts (valid until another is attached)
procedure. Source: script.odin:385
Node_GetScript
Section titled “Node_GetScript”Node_GetScript :: proc(node: ^Node, $T: typeid) -> ^TThe script attached to a node, as its struct type, or nil if it has none or another script.
Inputs
- node: The node (nil gives nil)
- T: The script struct’s type
Returns
- The node’s script struct, or nil
Example
Player :: struct { using script: e.Script, lives: int `export:"default=3"`,}if player := e.Node_GetScript(node, Player); player != nil do player.lives -= 1procedure. Source: script.odin:412
Script_SetField
Section titled “Script_SetField”Script_SetField :: proc(self: rawptr, field: ScriptField, text: string, allocator := context.allocator) -> boolSets a script field from its text form, as stored in scene files.
Inputs
- self: The script struct
- field: Which field
- text: The value
- allocator: For string fields
Returns
- Whether the text was a valid value for the field
procedure. Source: script.odin:431
Script_DescribeJSON
Section titled “Script_DescribeJSON”Script_DescribeJSON :: proc(allocator := context.allocator) -> stringA description of every registered script and data type, as JSON: what player --describe-scripts
prints so the editor can show exported fields in its inspector. {"scripts": [...], "data": [...]},
each with name, file, line, and for each field name, type, default, min/max (when set), hint, and
enum names.
Inputs
- allocator: Allocator for the string
Returns
- The JSON text
procedure. Source: script.odin:502
Global
Section titled “Global”Global :: proc($T: typeid, game: ^Game = nil) -> ^TThe game’s global of type T.
Inputs
- T: The global’s script type
- game: The game (default: the one playing)
Returns
- The global (the game’s, kept for its whole run), or nil if the project has none of that type
Example
package mygame
import e "engine:."
GameState :: struct { using script: e.Script, score: int,}
AddPoints :: proc(points: int) { if state := e.Global(GameState); state != nil do state.score += points}procedure. Source: globals.odin:77
Data_Register
Section titled “Data_Register”Data_Register :: proc "contextless" ($T: typeid, loc := #caller_location)Registers a data type, so data files can hold it and the editor can make and edit them. Call it
from an @(init) proc "contextless" in the type’s file, like Script_Register. Registering a
name again replaces it.
Inputs
- T: The data struct (exported fields:
export:""tags) - loc: Where it’s called from (filled in by the compiler)
Example
package mygame
import e "engine:."
EnemyStats :: struct { health: int `export:"default=10 min=1"`, speed: f32 `export:"default=100"`,}
@(init)register_EnemyStats :: proc "contextless" () { e.Data_Register(EnemyStats)}procedure. Source: data.odin:91
Data_Find
Section titled “Data_Find”Data_Find :: proc(name: string) -> ^DataInfoThe data type registered under a name.
Inputs
- name: The type’s name without its package (“EnemyStats”)
Returns
- Its info, or nil if no data type has that name
procedure. Source: data.odin:109
Data_All
Section titled “Data_All”Data_All :: proc() -> []^DataInfoEvery registered data type, sorted by name.
Returns
- The types’ info (the slice is temp allocated)
procedure. Source: data.odin:119
Data :: proc($T: typeid, asset: DataAsset, game: ^Game = nil) -> ^TA data resource, loaded the first time and shared after that.
Inputs
- T: Its type (registered with
Data_Register) - asset: The data file (a
DATA_...constant from the generated assets.odin, or an exported DataAsset field) - game: The game (default: the one playing)
Returns
- The instance (shared and kept for the game’s life: treat it as read-only), or nil (logged) if the file is missing, isn’t valid, or holds another type
Example
package mygame
import e "engine:."
EnemyStats :: struct { health: int `export:"default=10 min=1"`,}
DATA_GOBLIN :: e.DataAsset{"data/goblin.data.json"}
GoblinHealth :: proc() -> int { stats := e.Data(EnemyStats, DATA_GOBLIN) return stats != nil ? stats.health : 10}procedure. Source: data.odin:155
ParseDataResource
Section titled “ParseDataResource”ParseDataResource :: proc(text: string, allocator := context.allocator) -> (data: DataResourceData, err: ProjectError)Parses a data file’s text.
Inputs
- text: The file’s JSON
- allocator: For the result
Returns
- data: Its type name and values (allocated with
allocator; free withDataResourceData_Destroy) - err: .None; .Invalid_XML when it isn’t JSON (the error for any unreadable file); .Wrong_Root when
it isn’t an object with a “type”; .Unsupported_Version when it’s from a newer
DATA_FORMAT_VERSION
procedure. Source: data.odin:186
FormatDataResource
Section titled “FormatDataResource”FormatDataResource :: proc(type: string, values: []ScriptPropData, allocator := context.allocator) -> stringWrites the text of a data file (what the editor saves; ParseDataResource reads it back).
Values are written as JSON strings in the given order, so the output is deterministic.
Inputs
- type: The data type’s name
- values: The field values as text
- allocator: For the text
Returns
- The file’s JSON (allocated with
allocator)
procedure. Source: data.odin:228
DataResourceData_Destroy
Section titled “DataResourceData_Destroy”DataResourceData_Destroy :: proc(data: ^DataResourceData, allocator := context.allocator)Frees what ParseDataResource allocated and zeroes the data.
Inputs
- data: The parsed data
- allocator: The allocator that was given to
ParseDataResource
procedure. Source: data.odin:245
Save_Game
Section titled “Save_Game”Save_Game :: proc(slot := "save", game: ^Game = nil) -> boolSaves the game: every global’s fields, as JSON, in a slot.
Inputs
- slot: The save’s name (letters, digits, - and _ are safest: it’s a file name)
- game: The game (default: the one playing)
Returns
- Whether it was written (failures are logged); false when there’s no game
Example
if e.Save_Game() do log.info("saved")procedure. Source: save.odin:58
Load_Game
Section titled “Load_Game”Load_Game :: proc(slot := "save", game: ^Game = nil) -> boolLoads a save into the globals and calls their loaded hooks.
Inputs
- slot: The save’s name, as given to
Save_Game - game: The game (default: the one playing)
Returns
- false if there’s no such save or it can’t be read (the globals are left as they were)
Example
if e.Save_Exists() && !e.Load_Game() do log.warn("the save couldn't be loaded")procedure. Source: save.odin:84
Save_Exists
Section titled “Save_Exists”Save_Exists :: proc(slot := "save", game: ^Game = nil) -> boolWhether a save slot exists.
Inputs
- slot: The save’s name
- game: The game (default: the one playing)
Returns
- true if it exists; false too when there’s no game
procedure. Source: save.odin:108
Save_Delete
Section titled “Save_Delete”Save_Delete :: proc(slot := "save", game: ^Game = nil)Deletes a save slot (nothing happens if there’s none).
Inputs
- slot: The save’s name
- game: The game (default: the one playing)
procedure. Source: save.odin:122
Save_ToJSON
Section titled “Save_ToJSON”Save_ToJSON :: proc(game: ^Game, allocator := context.allocator) -> (string, bool)The globals as save JSON (what Save_Game writes). Fields that can’t be written as JSON are
skipped with a warning.
Inputs
- game: The game
- allocator: For the text
Returns
- The JSON text (allocated with
allocator), and true
procedure. Source: save.odin:140
Save_FromJSON
Section titled “Save_FromJSON”Save_FromJSON :: proc(game: ^Game, text: string) -> boolSets the globals’ fields from save JSON (what Load_Game reads). Fields the save doesn’t have are
left alone; globals it doesn’t name too. Doesn’t call the loaded hooks (Load_Game does).
Strings and other memory it reads go in the game’s arena.
Inputs
- game: The game
- text: The save’s JSON
Returns
- false if the text isn’t a save (or is from a newer
SAVE_FORMAT_VERSION)
procedure. Source: save.odin:173
Game_Snapshot
Section titled “Game_Snapshot”Game_Snapshot :: proc(game: ^Game) -> stringThe game’s state as JSON (temp allocated): the scene, the globals, every node’s properties.
Inputs
- game: The game
Returns
- The JSON text
procedure. Source: hot_reload.odin:39
Game_RestoreSnapshot
Section titled “Game_RestoreSnapshot”Game_RestoreSnapshot :: proc(game: ^Game, text: string) -> boolPuts a snapshot back (Game_Snapshot’s JSON): the globals now, the nodes’ properties after the
next update of the scene (its scripts’ ready). The game must be playing the snapshot’s scene
(the player’s --restore starts it there).
Inputs
- game: The game
- text: The snapshot
Returns
- false when the text isn’t a snapshot
procedure. Source: hot_reload.odin:75
Snapshot_Parse
Section titled “Snapshot_Parse”Snapshot_Parse :: proc(text: string) -> (snapshot: Snapshot, ok: bool)Reads a snapshot’s JSON (temp allocated); false when it isn’t one
procedure. Source: hot_reload.odin:93
Node_NamePath
Section titled “Node_NamePath”Node_NamePath :: proc(node: ^Node) -> stringA node’s path of names from the top of its scene (“World/Ship”), what Scene_FindNode takes
(temp allocated).
procedure. Source: hot_reload.odin:102
Constants and variables
Section titled “Constants and variables”DATA_FORMAT_VERSION
Section titled “DATA_FORMAT_VERSION”DATA_FORMAT_VERSION :: 1The data file format written by the editor
constant. Source: data.odin:65
SAVE_FORMAT_VERSION
Section titled “SAVE_FORMAT_VERSION”SAVE_FORMAT_VERSION :: 1The save format written by Save_ToJSON; saves from a newer version aren’t loaded
constant. Source: save.odin:43
GAME_SNAPSHOT_VERSION
Section titled “GAME_SNAPSHOT_VERSION”GAME_SNAPSHOT_VERSION :: 1The format Game_Snapshot writes
constant. Source: hot_reload.odin:28
