save

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 24, 2026 License: MIT Imports: 4 Imported by: 0

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

View Source
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.

View Source
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

func Migrate[T any, From any, To any](s *Slot[T], from, to int, fn func(From) To)

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

func New[T any](name string) *Slot[T]

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

func (s *Slot[T]) Delete() error

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

func (s *Slot[T]) Exists() bool

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

func (s *Slot[T]) Path() string

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

func (s *Slot[T]) Read() (T, error)

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.

func (*Slot[T]) Write

func (s *Slot[T]) Write(value T) error

Write serialises value as JSON and stores it in the slot file. The on-disk representation is human-readable, version-stamped JSON — debuggable without tooling, but obviously trivially editable. For anti-cheat use cases, wrap or sign the payload at the caller.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL