149 lines
4.7 KiB
Markdown
149 lines
4.7 KiB
Markdown
# 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
|
|
|
|
```c
|
|
extern save_t SAVE;
|
|
// SAVE.files[SAVE_FILE_COUNT_MAX] -- one per slot
|
|
// SAVE.platform -- platform-specific state
|
|
```
|
|
|
|
## API
|
|
|
|
```c
|
|
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.
|
|
|
|
```c
|
|
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.
|
|
|
|
```c
|
|
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
|
|
|
|
```c
|
|
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.
|