Snapshots
Snapshots
Section titled “Snapshots”Sometimes you need to save the world: capture every entity’s current field values, write them to disk, and reload them the next time the server starts. golem-engine’s snapshot system handles all of that—serializing entity state, detecting when your schema changes between a save and a load, and rebuilding live entities from the stored bytes.
Saving a snapshot
Section titled “Saving a snapshot”Call SaveSnapshot on the server at any point—from inside OnTick, from an admin command, or on a schedule. The call returns immediately; the write happens in a background goroutine so the game loop never pauses:
errc := srv.SaveSnapshot(synced.SchemaFingerprint, "world.snap")
// Check the result asynchronously—or block if you're on shutdown path:if err := <-errc; err != nil { log.Printf("snapshot failed: %v", err)}synced.SchemaFingerprint is a constant generated by golem-bake from your entity schemas. It is embedded in the snapshot file header and checked on load so stale files are rejected before any entity state is touched.
The path is passed directly to the OS and resolves relative to the working directory of the running process—typically wherever you launched the server binary from. You can use any absolute or relative path. The directory must already exist; SaveSnapshot and Load do not create missing directories.
The file is written atomically: the server encodes everything in memory, writes a <path>.tmp file in the same directory, then renames it to the final path. A partial write (crash mid-way) cannot produce a corrupted snapshot.
Excluding entity types from snapshots
Section titled “Excluding entity types from snapshots”Some entities should not be persisted, such as enemies that respawn from static spawner data or short-lived projectiles. Mark the entity type in its YAML schema with persistent: false:
entity: Projectilepersistent: false
vars: speed: type: float sync: tick tag: 1The snapshot system checks this automatically and skips those entities. Entity types that do not set persistent at all default to included.
Loading a snapshot
Section titled “Loading a snapshot”If you wrap generated SyncedX types in gameplay-owned entity types, register those wrappers once during startup, typically in an init() function in the wrapper package:
func init() { synced.RegisterMonsterWrapper(func(sm *synced.SyncedMonster) golem.Entity { return &monster.Monster{SyncedMonster: sm} })}Then load the snapshot before the tick loop starts:
err := srv.LoadSnapshot("world.snap", synced.SchemaFingerprint, synced.RestoreEntity)if errors.Is(err, snapshot.ErrFingerprintMismatch) { log.Fatal("snapshot was created with an incompatible schema — delete it and start fresh")}if err != nil { log.Fatalf("could not load snapshot: %v", err)}srv.LoadSnapshot calls snapshot.Load, restores each entity via the provided function, registers it with its original ID using CreateEntity, and advances the entity ID counter past the highest restored ID — all in one call.
synced.RestoreEntity is the generated helper that reconstructs a SyncedX struct for each entity type in your schema. If you register a per-entity wrapper with synced.RegisterXWrapper(...) before loading, RestoreEntity returns that wrapper instead of the bare synced struct. Entity types not found in the current schema are silently skipped, which is the expected behavior when a type has been removed.
Wrapper registrations are package-global generated hooks, so register every wrapped entity type once during startup, ideally in init() functions alongside those wrapper types. Passing nil to RegisterXWrapper clears a previously registered wrapper.
Schema fingerprinting
Section titled “Schema fingerprinting”Every time you run golem-bake, a binary-compatibility fingerprint of your entity schemas is computed and emitted as synced.SchemaFingerprint. It covers only the things that can silently corrupt a snapshot on load: entity type names and the tag → type mapping for each variable.
When you load a snapshot, the stored fingerprint is compared to the current constant. If they differ, snapshot.Load returns snapshot.ErrFingerprintMismatch immediately—before any entity data is decoded.
What invalidates a snapshot
Section titled “What invalidates a snapshot”| Schema change | Invalidates snapshot? | Why |
|---|---|---|
| Rename a variable (same tag, same type) | No | Binary encoding is identical |
| Add a variable (new tag) | No | Absent fields load as zero values |
| Remove a variable | No | Leftover bytes for that tag are discarded |
Change sync mode | No | Doesn’t affect encoding |
Change persistent | No | Only affects future saves |
| Rename an entity type | Yes | RestoreEntity dispatches on type name |
| Change a tag number | Yes | Old bytes decode to the wrong field |
| Change a variable’s type | Yes | Same tag, wrong wire interpretation |
There is no migration support. If the fingerprint does not match, delete the snapshot and rebuild world state from scratch (or from a different source, like a seed file).
See Entity schemas for tag and persistent schema keys.