Saving games: slots, versions and the cloud¶
A game's "Continue" and "Load game" menus read save slots
(kke/SaveSlots.h, Lua saves.*). A slot holds one save, plus what the menu
shows about it: a title, when it was saved, the play time, and the game's
save version. Small things that aren't a whole game (a best score, unlocked
skins) go in store.* instead (SCRIPTING.md "Saving").
From Lua¶
saves.version(2) -- the shape of what this game saves; raise it when that changes
-- Bring older saves up to date when they're read: one step per version.
saves.migrate(1, function(d) d.hp = d.health d.health = nil end) -- 1 -> 2 (changing the table is enough)
function saveGame(slot)
local ok, why = saves.write(slot, {
level = currentLevel, pos = { player.x, player.y, player.z }, items = inventory,
}, { title = levelName, playtime = playSeconds })
if not ok then print("Couldn't save: " .. why) end
end
function loadGame(slot)
local d, info = saves.read(slot) -- nil and the reason when it's empty, damaged or from a newer game
if not d then return print(info) end
startLevel(d.level, d.pos)
end
for _, s in ipairs(saves.list()) do -- newest first: the "Continue" button is saves.list()[1]
print(s.slot, s.title, os.date("%d %b %H:%M", s.time), s.playtime, s.version, s.bytes)
end
saves.remove("slot 3")
if saves.exists("auto") then ... end
| Function | Does |
|---|---|
saves.write(slot, data [, {title =, playtime =}]) → true, or false and why |
Replaces what the slot holds. data is a number, text, true/false or a table of those (up to 8 MB saved). |
saves.read(slot) → data, info; or nil and why |
Runs the saves.migrate steps from the save's version up to saves.version(). A save from a newer version of the game is refused, never read wrongly. |
saves.list() → infos |
Every slot, newest first: slot, title, time (Unix seconds), version (what it was saved as), playtime (seconds), bytes. |
saves.exists(slot), saves.remove(slot) |
An empty slot removes fine. |
saves.version([n]) → n |
What write stamps; 1 until a game sets it. |
saves.migrate(from, fn) |
fn(data) turns a version from save into version from + 1: change the table, or return new data. |
- A slot name is 1-64 letters, digits, spaces,
_,-or.("slot 1", "auto", "chapter-2"); a number is the slot of that name. A game has at most 100 slots. - A migration step that errors is a script error naming its line, and the
save on disk stays as it was:
readnever writes. Save again after a read to keep the migrated form. - A missing step is named in the reason ("no saves.migrate(2, ...)").
Where saves go¶
Into the same store as store.* (save/scripts.db next to the game, a SQLite
file: STORAGE.md), in the game's own collection
(saves.<game>). A write is one transaction: a crash halfway leaves the old
save, never half of the new one.
Each save is one self-checking file: KKSAVE, the format, the header (version,
time, play time, slot, title, size), the data and a CRC-32 of all of it. A
changed or cut byte is found, and that slot reads as damaged instead of
loading garbage. saves.list reads only the headers, so a menu with 100 big
saves still opens at once.
The cloud hook (C++)¶
Steam Cloud, a phone and PC sharing saves, or a game's own server: the engine gives the files, the platform layer moves them.
kke::SaveSlots& slots = *scriptModule.scriptStore()->saveSlots();
slots.onWritten = [&](const kke::SaveInfo& info, const std::string& file) { cloud.upload(info.slot, file); };
slots.onRemoved = [&](const std::string& slot) { cloud.remove(slot); };
// At start, or when the platform says something changed:
for (const std::string& file : cloud.downloadAll()) slots.importFile(file); // Taken, Older, Damaged or Failed
importFile keeps a downloaded save only when it is intact and saved later
than what the slot holds, so starting the game on two machines never throws
away the newest play. fileOf(slot) gives the file of a save written before
the hook was set, to upload it too.
Tests¶
tests/test_save_slots.cpp (slots, limits, every damaged or cut byte found,
the cloud hook's import rules) and ScriptSaves.* in tests/test_script_vm.cpp
(Lua: write, read, list, migrations, a newer save refused).