Documentation
¶
Overview ¶
Package save provides typed, versioned save slots backed by Godot's FileAccess. Save files live under `user://` — a per-OS userdata directory that gogogd users don't have to compute themselves.
Typical use:
type V1 struct {
Version int `json:"version"`
Score int `json:"score"`
}
type V2 struct {
Version int `json:"version"`
Score int `json:"score"`
Level int `json:"level"` // added in v2
}
func migrate1to2(v V1) V2 {
return V2{Version: 2, Score: v.Score, Level: 1}
}
func slot(name string) *save.Slot[V2] {
s := save.New[V2](name)
save.Migrate(s, 1, 2, migrate1to2)
return s
}
// Write:
slot("autosave").Write(V2{Version: 2, Score: 100, Level: 3})
// Read with auto-migration of older versions:
state, err := slot("autosave").Read()
Lifetime: a Slot is a lightweight descriptor (slot name + migrations). Hold one in a package-level var or recreate cheaply per call. The actual disk hit happens only on Read / Write.
Corruption: a malformed file returns ErrCorrupt without panicking. The caller decides whether to wipe-and-restart or surface the error to the player.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrCorrupt = errors.New("save: slot file is corrupt or unrecoverable")
ErrCorrupt is returned when the slot file exists but cannot be parsed into the slot's target type — JSON unmarshal failure, missing required fields, etc. Callers typically log + offer "wipe and restart" UX.
var ErrNotFound = errors.New("save: slot file not found")
ErrNotFound is returned by Slot.Read when the slot's file doesn't exist. Distinct from ErrCorrupt so callers can branch ("no save yet" vs "save exists but is broken").
Functions ¶
func Migrate ¶
Migrate registers a migration from version from to version to. fn takes a `From` value (the older shape) and returns a `To` value (the next shape).
Use freestanding Migrate rather than a method on Slot because Go's method-generics support is too weak to carry the From/To types separately:
save.Migrate(slot, 1, 2, func(v V1) V2 { return V2{...} })
Migration steps must form an unbroken chain from every supported on-disk version up to T. A missing link causes Read to return the partially- migrated form (likely ErrCorrupt when unmarshaling fails).
Types ¶
type Slot ¶
type Slot[T any] struct { // contains filtered or unexported fields }
Slot is a typed save slot. Construct with New; register migrations with Migrate. The type parameter T is the latest version's struct shape — Read auto-migrates older on-disk versions through the chain before returning.
Slots are inexpensive to construct; safe to recreate per call. The migrations slice is rebuilt each time, which is fine for the dozen or so migrations a real game accumulates.
func New ¶
New constructs a Slot for the file at `user://<name>.json`. The slot has no migrations registered — call Migrate for each step you support.
The name is the bare slot identifier ("autosave", "slot1", "options"), not a path. Slashes are not allowed; if you want subdirectories, build the path on the caller's side and use FileAccess directly.
func (*Slot[T]) Delete ¶
Delete removes the slot file. Returns nil if the file didn't exist — "delete a save that isn't there" is not an error.
func (*Slot[T]) Exists ¶
Exists reports whether the slot's file is present on disk. Cheap — no read or parse. Use to gate "Continue" buttons or to choose between New Game and Resume flows.
func (*Slot[T]) Path ¶
Path returns the user:// path the slot reads/writes. Useful for logging or for "show me where the save is" debug UI.
func (*Slot[T]) Read ¶
Read loads the slot from disk, auto-migrating older on-disk versions through the registered migration chain.
Returns:
- (T, nil) on success.
- (zero, ErrNotFound) if the file doesn't exist.
- (zero, ErrCorrupt) if the file exists but can't be parsed.
- (zero, err) for unexpected I/O errors.
Migration: Read peeks at the on-disk JSON's "version" field, walks the registered migration chain from that version up to the current T's version, and finally unmarshals the migrated bytes into T. Each migration step gets the previous step's serialized bytes and returns the next step's bytes — fully type-erased to keep the chain readable.