Saves and settings
The save family turns game state into versioned text and stores it safely. It has four layers:
| Layer | Job |
|---|---|
Codec<T> | Turns a value into plain data and back. Codecs builds them. |
SaveSchema<T> | Names the format and its current version, runs migrations, and checks versions on load. |
IByteStorage | Where the bytes go: MemoryByteStorage, FsByteStorage (files), or your own. |
SaveSlotManager<T>, SettingsStore<T> | Named save slots with backups and autosaves; one settings document. |
ts
import { Codec, Codecs, MemoryByteStorage, MigrationStep, RecordField, SaveResult, SaveSchema, SaveSlotInfo, SaveSlotManager, SaveSlotMetadata } from "gamekit";
class Profile {
name: string;
level: int;
gold: int;
constructor(name: string, level: int, gold: int) {
this.name = name;
this.level = level;
this.gold = gold;
}
}
// A codec says how a value becomes plain data and back.
const profileCodec: Codec<Profile> = Codecs.record<Profile>(
[
new RecordField("name", Codecs.string()),
new RecordField("level", Codecs.int()),
new RecordField("gold", Codecs.int(), false, 0), // optional, defaults to 0
],
(f: any): Profile => new Profile(f.name as string, f.level as int, f.gold as int));
// A schema names the format and its version. Migrations upgrade older saves.
const schema: SaveSchema<Profile> = new SaveSchema<Profile>("profile", 2, profileCodec);
schema.addMigration(new MigrationStep(1, 2, (data: any): any => {
data.gold = 0; // version 1 had no gold
return data;
}));
// MemoryByteStorage keeps bytes in memory. FsByteStorage writes files (needs the fs system).
const slots: SaveSlotManager<Profile> = new SaveSlotManager<Profile>(new MemoryByteStorage(), schema);
async function main(): Promise<void> {
const meta: SaveSlotMetadata = new SaveSlotMetadata("slot1", "profile", 2, 0, 125.0, "Forest, level 3");
const saved: SaveResult<SaveSlotMetadata> = await slots.saveSlot("slot1", new Profile("Robin", 3, 40), meta);
console.log("saved: " + saved.isOk);
const list: SaveResult<SaveSlotInfo[]> = await slots.listSlots();
if (list.isOk) {
for (const info of list.value as SaveSlotInfo[]) {
console.log("slot " + info.slotId + ": " + (info.metadata as SaveSlotMetadata).title);
}
}
const loaded: SaveResult<Profile> = await slots.loadSlot("slot1");
if (loaded.isOk) {
const p: Profile = loaded.value as Profile;
console.log(p.name + " level " + p.level + " gold " + p.gold);
} else {
console.log("load failed: " + loaded.kind + " " + loaded.message);
}
}
await main();text
saved: true
slot slot1: Forest, level 3
Robin level 3 gold 40Codecs
| Builder | Encodes |
|---|---|
Codecs.int(), float(), string(), boolean(), id() | Single values. |
Codecs.optional(codec) | A value or null. |
Codecs.array(codec) | A list. |
Codecs.record(fields, factory, extractor?) | An object, field by field. new RecordField(name, codec, required = true, default = null). |
Codecs.taggedVariant(tagField, variants, tagOf) | One of several shapes, chosen by a tag field. |
Codecs.reference(kind, idOf) | A link to another object by id, resolved through a ReferenceResolver. |
A CodecContext sets what to do with unknown fields (UnknownFieldPolicy.REJECT or PRESERVE) and missing references (ReferencePolicy.STRICT or ALLOW_NULL).
Schemas and migrations
new SaveSchema<T>(schemaId, currentVersion, codec). dump(value) writes the current version; load(text) checks the schema id and version, then runs each MigrationStep(from, to, migrate) in turn from the saved version up to the current one, with optional validation before and after each step. A save from a newer version than the schema knows is refused as INCOMPATIBLE.
Results, not exceptions
Load and save calls return a SaveResult<T>: check isOk, then read value. On failure, kind says why (SaveResultKind):
| Kind | Meaning |
|---|---|
OK | It worked. |
UNAVAILABLE | Nothing there, or the storage could not be reached. |
MALFORMED | Not valid save data. |
INCOMPATIBLE | A schema or version this game cannot read. |
CORRUPT | The data is damaged. |
Save slots
| Call | What it does |
|---|---|
saveSlot(id, data, metadata?) | Writes a temp file, validates it, backs up the old save, then renames into place. Saves to the same slot run one after another. |
loadSlot(id) | Loads the slot. If it is damaged, it falls back to the backup and restores the primary when that works. |
listSlots() | Every slot, sorted by id, as SaveSlotInfo (metadata, whether a backup exists, whether it is corrupt). |
deleteSlot(id) | Removes a slot. |
rotateAutosave(data, metadata?) | Saves to the next slot in a ring of maxAutosaves (default 3). |
latestAutosave() | The newest autosave's metadata. |
SaveSlotMetadata(slotId, schemaId, version, timestamp, playTime, title, custom) is what a load screen shows without loading the whole save. File names come from SaveSlotManagerOptions (extensions .sav, .tmp, .bak, autosave prefix autosave_). Long saves can be cancelled with a SlotCancellationToken.
Settings
new SettingsStore<T>(storage, filename, schema) keeps one document, such as volume and key bindings, separate from game saves. save, load, hasSettings and clear, with the same temp, backup and recovery steps as slots.
Saving to files
FsByteStorage writes under a folder you choose and needs the fs system in your project's systems. It is not in the package root; import it from the family folder:
ts
import { FsByteStorage } from "gamekit/src/save";
const storage: FsByteStorage = new FsByteStorage("saves");FaultInjectingByteStorage wraps another storage and fails on demand (a failed write, rename or read, a full disk, a partial write), for testing your error handling.
Saving from a scene
With a SceneCoordinator, registerSave(work) registers save work that save() runs and that quit() drains before the last scene ends. The restore recipe saves a component and loads it into a fresh world.
Reference: Codecs, SaveSchema, SaveSlotManager, SaveResult, SettingsStore, FsByteStorage.