Files
dusk/.claude/save.md
T
2026-06-16 10:15:59 -05:00

4.7 KiB

Save System

Source: src/dusk/save/, platform layers in src/dusk<platform>/save/

Overview

The save system provides multi-slot persistent storage. Each slot maps to one savefile_t. Platform implementations handle the actual read/write (memory card on GameCube/Wii, EEPROM/flash on PSP, filesystem on Linux).

Global state

extern save_t SAVE;
// SAVE.files[SAVE_FILE_COUNT_MAX]  -- one per slot
// SAVE.platform                    -- platform-specific state

API

errorret_t saveInit(void);
errorret_t saveDispose(void);

errorret_t saveLoad(uint8_t slot);    // read slot from storage -> SAVE.files[slot]
errorret_t saveWrite(uint8_t slot);   // write SAVE.files[slot] -> storage
errorret_t saveDelete(uint8_t slot);  // delete slot from storage

bool_t     saveExists(uint8_t slot);  // true if a save file is present

savefile_t *saveGet(uint8_t slot);    // pointer to the in-memory slot data

Slot indices are 0-based, range [0, SAVE_FILE_COUNT_MAX - 1].

Save file structure (savefile.h)

savefile_t is a plain struct written verbatim to storage. Keep it small and use fixed-width integer types (uint8_t, int32_t, etc.) to ensure cross-platform binary compatibility.

Endianness: storage is always written in little-endian byte order. Use the endian.h utilities when reading fields on big-endian targets (GameCube, Wii). See .claude/util.md.

Versioning: include a version field at the start of savefile_t. Check it on load and handle mismatches gracefully (reset to defaults rather than crashing on corrupt data).

Save stream (savestream.h)

savestream_t is a cursor-based reader/writer used to serialize savefile_t to/from a raw byte buffer. Platform implementations use it to abstract the I/O layer.

typedef struct {
  bool_t found;
  uint32_t checksum;
  uint32_t expectedChecksum;
  saveplatformstream_t platform;
} savestream_t;

Typed read/write macros

Use the saveFile* macros inside saveFileLoad and saveFileWrite. All multi-byte values are stored in little-endian order; endian conversion is handled automatically.

saveFileReadHeader(stream, headerBuf)
saveFileWriteHeader(stream, headerBuf)

saveFileReadVersion(stream, &version)
saveFileWriteVersion(stream, &version)

saveFileReadBool(stream, &boolField)
saveFileWriteBool(stream, &boolField)

saveFileReadInt8(stream, &i8)    saveFileWriteInt8(stream, &i8)
saveFileReadUInt8(stream, &u8)   saveFileWriteUInt8(stream, &u8)
saveFileReadInt16(stream, &i16)  saveFileWriteInt16(stream, &i16)
saveFileReadUInt16(stream, &u16) saveFileWriteUInt16(stream, &u16)
saveFileReadInt32(stream, &i32)  saveFileWriteInt32(stream, &i32)
saveFileReadUInt32(stream, &u32) saveFileWriteUInt32(stream, &u32)
saveFileReadInt64(stream, &i64)  saveFileWriteInt64(stream, &i64)
saveFileReadUInt64(stream, &u64) saveFileWriteUInt64(stream, &u64)
saveFileReadFloat(stream, &f)    saveFileWriteFloat(stream, &f)

saveFileReadString(stream, buf, maxLen)
saveFileWriteString(stream, str, maxLen)

saveFileReadDate(stream, &epoch)
saveFileWriteDate(stream, &epoch)

Each macro expands to errorChain(saveStreamRead/WriteXxxImpl(...)). A failing read/write propagates the error up from saveFileLoad / saveFileWrite.

Typical saveFileLoad / saveFileWrite pattern

errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) {
  char_t header[SAVE_FILE_HEADER_SIZE];
  saveFileReadHeader(stream, header);
  saveFileReadVersion(stream, &file->version);
  saveFileReadInt32(stream, &file->score);
  // ... remaining fields ...
  errorOk();
}

errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) {
  char_t header[SAVE_FILE_HEADER_SIZE] = SAVE_FILE_HEADER;
  saveFileWriteHeader(stream, header);
  saveFileWriteVersion(stream, &file->version);
  saveFileWriteInt32(stream, &file->score);
  // ... remaining fields ...
  errorOk();
}

After saveFileWrite completes, the platform layer calls saveStreamFinalizeWriteImpl which seeks back and writes the CRC32. After saveFileLoad, the platform calls saveStreamVerifyChecksumImpl to confirm the CRC matches.

Platform notes

Platform Storage mechanism
Linux File in user home / working directory
Knulli File on filesystem
PSP EEPROM / memory stick via sceIo
GameCube Memory Card via libogc CARD_* API
Wii NAND filesystem via libogc or SD card

Platform-specific save implementations go in src/dusk<platform>/save/ and are wired in via save/saveplatform.h macros.

PSP note

PSP save dialogs are OS-level UI shown via sceUtility. When a dialog is open, systemGetActiveDialogType() returns a blocking type so the engine pauses the main loop. Never call save functions directly from game code without going through the engine's dialog guard.