Save file update (incomplete)

This commit is contained in:
2026-05-10 11:20:09 -05:00
parent d7f515575a
commit a8fd55cb38
42 changed files with 2678 additions and 1 deletions
+1
View File
@@ -76,5 +76,6 @@ add_subdirectory(system)
add_subdirectory(time)
add_subdirectory(ui)
add_subdirectory(network)
add_subdirectory(save)
add_subdirectory(util)
add_subdirectory(thread)
+3
View File
@@ -25,6 +25,7 @@
#include "system/system.h"
#include "console/console.h"
#include "item/backpack.h"
#include "save/save.h"
double jerry_port_current_time(void) {
dusktimeepoch_t epoch = timeGetEpoch();
@@ -50,6 +51,7 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) {
consoleInit();
errorChain(inputInit());
errorChain(assetInit());
errorChain(saveInit());
errorChain(localeManagerInit());
errorChain(scriptManagerInit());
errorChain(displayInit());
@@ -105,6 +107,7 @@ errorret_t engineDispose(void) {
uiDispose();
consoleDispose();
errorChain(displayDispose());
errorChain(saveDispose());
errorChain(assetDispose());
errorOk();
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
save.c
savestream.c
)
+108
View File
@@ -0,0 +1,108 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/save.h"
#include "save/savestream.h"
#include "util/memory.h"
#include "assert/assert.h"
save_t SAVE;
errorret_t saveInit(void) {
memoryZero(&SAVE, sizeof(save_t));
#ifdef saveInitPlatform
errorChain(saveInitPlatform());
#endif
errorOk();
}
errorret_t saveDispose(void) {
#ifdef saveDisposePlatform
errorChain(saveDisposePlatform());
#endif
errorOk();
}
errorret_t saveLoad(const uint8_t slot) {
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
savefile_t *file = &SAVE.files[slot];
file->exists = false;
savestream_t stream;
memoryZero(&stream, sizeof(savestream_t));
#ifdef saveStreamOpenReadPlatform
errorChain(saveStreamOpenReadPlatform(&stream, slot));
#endif
if(!stream.found) errorOk();
errorret_t ret = saveFileLoad(&stream, file);
#ifdef saveStreamClosePlatform
saveStreamClosePlatform(&stream);
#endif
if(ret.code != ERROR_OK) return ret;
errorChain(saveStreamVerifyChecksumImpl(&stream, slot));
file->exists = true;
errorOk();
}
errorret_t saveWrite(const uint8_t slot) {
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
savefile_t *file = &SAVE.files[slot];
savestream_t stream;
memoryZero(&stream, sizeof(savestream_t));
#ifdef saveStreamOpenWritePlatform
errorChain(saveStreamOpenWritePlatform(&stream, slot));
#endif
errorret_t ret = saveFileWrite(&stream, file);
if(ret.code == ERROR_OK) {
ret = saveStreamFinalizeWriteImpl(&stream);
}
#ifdef saveStreamClosePlatform
saveStreamClosePlatform(&stream);
#endif
if(ret.code != ERROR_OK) return ret;
file->exists = true;
errorOk();
}
errorret_t saveDelete(const uint8_t slot) {
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
#ifdef saveDeletePlatform
errorChain(saveDeletePlatform(slot));
#endif
SAVE.files[slot].exists = false;
errorOk();
}
bool_t saveExists(const uint8_t slot) {
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
return SAVE.files[slot].exists;
}
savefile_t * saveGet(const uint8_t slot) {
assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX");
return &SAVE.files[slot];
}
+74
View File
@@ -0,0 +1,74 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "savefile.h"
#include "save/saveplatform.h"
typedef struct {
/** Per-slot save file data; indexed 0 to SAVE_FILE_COUNT_MAX - 1. */
savefile_t files[SAVE_FILE_COUNT_MAX];
/** Platform-specific save system state (paths, card handles, etc.). */
saveplatform_t platform;
} save_t;
extern save_t SAVE;
/**
* Initializes the save system.
*
* @return An error code if initialization fails.
*/
errorret_t saveInit(void);
/**
* Disposes of the save system.
*
* @return An error code if disposal fails.
*/
errorret_t saveDispose(void);
/**
* Loads the save file for a given slot from persistent storage.
*
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
* @return An error code if the load fails.
*/
errorret_t saveLoad(const uint8_t slot);
/**
* Writes the save file for a given slot to persistent storage.
*
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
* @return An error code if the write fails.
*/
errorret_t saveWrite(const uint8_t slot);
/**
* Deletes the save file for a given slot from persistent storage.
*
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
* @return An error code if the delete fails.
*/
errorret_t saveDelete(const uint8_t slot);
/**
* Checks whether a save file exists for a given slot.
*
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
* @return true if a save file exists for the slot, false otherwise.
*/
bool_t saveExists(const uint8_t slot);
/**
* Gets a pointer to the save file data for a given slot.
*
* @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1).
* @return A pointer to the savefile_t for the given slot.
*/
savefile_t * saveGet(const uint8_t slot);
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
/** Format version written into every save file. Increment on breaking change. */
#define SAVE_FILE_VERSION 1
/** Magic bytes that identify a Dusk save file. */
#define SAVE_FILE_HEADER "DSK"
/** Byte length of the magic header (excludes the null terminator). */
#define SAVE_FILE_HEADER_SIZE (sizeof(SAVE_FILE_HEADER) - 1)
/** Maximum number of independent save slots supported. */
#define SAVE_FILE_COUNT_MAX 3
typedef struct {
/** Magic header bytes read from the file; must equal SAVE_FILE_HEADER. */
char_t header[SAVE_FILE_HEADER_SIZE];
/** Format version read from the file; used to branch on older layouts. */
uint32_t version;
/** Runtime flag — true if this slot was successfully loaded or written. */
bool_t exists;
} savefile_t;
+336
View File
@@ -0,0 +1,336 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/savestream.h"
#include "util/crypt.h"
#include "util/endian.h"
#include "util/string.h"
#include "util/memory.h"
errorret_t saveStreamReadBytesRawImpl(
savestream_t *stream, void *buf, const size_t len
) {
#ifdef saveStreamReadBytesPlatform
errorChain(saveStreamReadBytesPlatform(stream, buf, len));
#endif
errorOk();
}
errorret_t saveStreamWriteBytesRawImpl(
savestream_t *stream, const void *buf, const size_t len
) {
#ifdef saveStreamWriteBytesPlatform
errorChain(saveStreamWriteBytesPlatform(stream, buf, len));
#endif
errorOk();
}
errorret_t saveStreamReadBytesImpl(
savestream_t *stream, void *buf, const size_t len
) {
errorChain(saveStreamReadBytesRawImpl(stream, buf, len));
cryptCRC32Update(&stream->checksum, buf, len);
errorOk();
}
errorret_t saveStreamWriteBytesImpl(
savestream_t *stream, const void *buf, const size_t len
) {
cryptCRC32Update(&stream->checksum, buf, len);
errorChain(saveStreamWriteBytesRawImpl(stream, buf, len));
errorOk();
}
errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream) {
uint32_t finalCRC = cryptCRC32End(stream->checksum);
uint32_t leChecksum = endianLittleToHost32(finalCRC);
#ifdef saveStreamSeekPlatform
errorChain(saveStreamSeekPlatform(stream, SAVE_FILE_HEADER_SIZE));
#endif
errorChain(saveStreamWriteBytesRawImpl(
stream, &leChecksum, sizeof(uint32_t)
));
errorOk();
}
errorret_t saveStreamVerifyChecksumImpl(
savestream_t *stream, const uint8_t slot
) {
uint32_t computed = cryptCRC32End(stream->checksum);
if(computed != stream->expectedChecksum) {
errorThrow("Save slot %u has invalid checksum", (uint32_t)slot);
}
errorOk();
}
errorret_t saveStreamReadHeaderImpl(
savestream_t *stream, char_t header[SAVE_FILE_HEADER_SIZE]
) {
errorChain(saveStreamReadBytesRawImpl(stream, header, SAVE_FILE_HEADER_SIZE));
if(
header[0] != SAVE_FILE_HEADER[0] ||
header[1] != SAVE_FILE_HEADER[1] ||
header[2] != SAVE_FILE_HEADER[2]
) {
errorThrow("Save file has invalid header");
}
uint32_t leChecksum;
errorChain(saveStreamReadBytesRawImpl(stream, &leChecksum, sizeof(uint32_t)));
stream->expectedChecksum = endianLittleToHost32(leChecksum);
stream->checksum = cryptCRC32Begin();
errorOk();
}
errorret_t saveStreamWriteHeaderImpl(
savestream_t *stream, const char_t header[SAVE_FILE_HEADER_SIZE]
) {
errorChain(saveStreamWriteBytesRawImpl(
stream, header, SAVE_FILE_HEADER_SIZE
));
uint32_t placeholder = 0;
errorChain(saveStreamWriteBytesRawImpl(stream, &placeholder, sizeof(uint32_t)));
stream->checksum = cryptCRC32Begin();
errorOk();
}
errorret_t saveStreamReadVersionImpl(savestream_t *stream, uint32_t *out) {
uint32_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
*out = endianLittleToHost32(raw);
errorOk();
}
errorret_t saveStreamWriteVersionImpl(
savestream_t *stream, const uint32_t *input
) {
uint32_t raw = endianLittleToHost32(*input);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
errorOk();
}
// ---------------------------------------------------------------------------
// Bool
// ---------------------------------------------------------------------------
errorret_t saveStreamReadBoolImpl(savestream_t *stream, bool_t *out) {
uint8_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint8_t)));
*out = (bool_t)(raw != 0);
errorOk();
}
errorret_t saveStreamWriteBoolImpl(savestream_t *stream, const bool_t *input) {
uint8_t raw = *input ? 1 : 0;
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint8_t)));
errorOk();
}
// ---------------------------------------------------------------------------
// Integers
// ---------------------------------------------------------------------------
errorret_t saveStreamReadInt8Impl(savestream_t *stream, int8_t *out) {
errorChain(saveStreamReadBytesImpl(stream, out, sizeof(int8_t)));
errorOk();
}
errorret_t saveStreamWriteInt8Impl(savestream_t *stream, const int8_t *input) {
errorChain(saveStreamWriteBytesImpl(stream, input, sizeof(int8_t)));
errorOk();
}
errorret_t saveStreamReadUInt8Impl(savestream_t *stream, uint8_t *out) {
errorChain(saveStreamReadBytesImpl(stream, out, sizeof(uint8_t)));
errorOk();
}
errorret_t saveStreamWriteUInt8Impl(savestream_t *stream, const uint8_t *input) {
errorChain(saveStreamWriteBytesImpl(stream, input, sizeof(uint8_t)));
errorOk();
}
errorret_t saveStreamReadInt16Impl(savestream_t *stream, int16_t *out) {
uint16_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint16_t)));
uint16_t host = endianLittleToHost16(raw);
memoryCopy(out, &host, sizeof(int16_t));
errorOk();
}
errorret_t saveStreamWriteInt16Impl(savestream_t *stream, const int16_t *input) {
uint16_t raw;
memoryCopy(&raw, input, sizeof(int16_t));
raw = endianLittleToHost16(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint16_t)));
errorOk();
}
errorret_t saveStreamReadUInt16Impl(savestream_t *stream, uint16_t *out) {
uint16_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint16_t)));
*out = endianLittleToHost16(raw);
errorOk();
}
errorret_t saveStreamWriteUInt16Impl(
savestream_t *stream, const uint16_t *input
) {
uint16_t raw = endianLittleToHost16(*input);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint16_t)));
errorOk();
}
errorret_t saveStreamReadInt32Impl(savestream_t *stream, int32_t *out) {
uint32_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
uint32_t host = endianLittleToHost32(raw);
memoryCopy(out, &host, sizeof(int32_t));
errorOk();
}
errorret_t saveStreamWriteInt32Impl(savestream_t *stream, const int32_t *input) {
uint32_t raw;
memoryCopy(&raw, input, sizeof(int32_t));
raw = endianLittleToHost32(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
errorOk();
}
errorret_t saveStreamReadUInt32Impl(savestream_t *stream, uint32_t *out) {
uint32_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t)));
*out = endianLittleToHost32(raw);
errorOk();
}
errorret_t saveStreamWriteUInt32Impl(
savestream_t *stream, const uint32_t *input
) {
uint32_t raw = endianLittleToHost32(*input);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_t)));
errorOk();
}
errorret_t saveStreamReadInt64Impl(savestream_t *stream, int64_t *out) {
uint64_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
uint64_t host = endianLittleToHost64(raw);
memoryCopy(out, &host, sizeof(int64_t));
errorOk();
}
errorret_t saveStreamWriteInt64Impl(savestream_t *stream, const int64_t *input) {
uint64_t raw;
memoryCopy(&raw, input, sizeof(int64_t));
raw = endianLittleToHost64(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
errorOk();
}
errorret_t saveStreamReadUInt64Impl(savestream_t *stream, uint64_t *out) {
uint64_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
*out = endianLittleToHost64(raw);
errorOk();
}
errorret_t saveStreamWriteUInt64Impl(
savestream_t *stream, const uint64_t *input
) {
uint64_t raw = endianLittleToHost64(*input);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
errorOk();
}
// ---------------------------------------------------------------------------
// Float
// ---------------------------------------------------------------------------
errorret_t saveStreamReadFloatImpl(savestream_t *stream, float_t *out) {
float_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(float_t)));
*out = endianLittleToHostFloat(raw);
errorOk();
}
errorret_t saveStreamWriteFloatImpl(savestream_t *stream, const float_t *input) {
float_t raw = endianLittleToHostFloat(*input);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(float_t)));
errorOk();
}
// ---------------------------------------------------------------------------
// String
// ---------------------------------------------------------------------------
errorret_t saveStreamReadStringImpl(
savestream_t *stream, char_t *out, const size_t maxLen
) {
for(size_t i = 0; i < maxLen; i++) {
errorChain(saveStreamReadBytesImpl(stream, &out[i], sizeof(char_t)));
if(out[i] == '\0') errorOk();
}
out[maxLen - 1] = '\0';
errorOk();
}
errorret_t saveStreamWriteStringImpl(
savestream_t *stream, const char_t *input, const size_t maxLen
) {
size_t len = strlen(input);
if(len >= maxLen) len = maxLen - 1;
errorChain(saveStreamWriteBytesImpl(stream, input, len + 1));
errorOk();
}
// ---------------------------------------------------------------------------
// Date (dusktimeepoch_t — stored as three little-endian 64-bit bit patterns)
// ---------------------------------------------------------------------------
errorret_t saveStreamReadDateImpl(savestream_t *stream, dusktimeepoch_t *out) {
uint64_t raw;
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
raw = endianLittleToHost64(raw);
memoryCopy(&out->time, &raw, sizeof(double_t));
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
raw = endianLittleToHost64(raw);
memoryCopy(&out->timeZone, &raw, sizeof(double_t));
errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint64_t)));
raw = endianLittleToHost64(raw);
memoryCopy(&out->offsetTime, &raw, sizeof(double_t));
errorOk();
}
errorret_t saveStreamWriteDateImpl(
savestream_t *stream, const dusktimeepoch_t *input
) {
uint64_t raw;
memoryCopy(&raw, &input->time, sizeof(double_t));
raw = endianLittleToHost64(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
memoryCopy(&raw, &input->timeZone, sizeof(double_t));
raw = endianLittleToHost64(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
memoryCopy(&raw, &input->offsetTime, sizeof(double_t));
raw = endianLittleToHost64(raw);
errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t)));
errorOk();
}
+491
View File
@@ -0,0 +1,491 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "savefile.h"
#include "save/saveplatform.h"
#include "time/timeepoch.h"
/**
* Active I/O context for a single save slot read or write pass.
*
* Platform code fills/drains `platform`. The stream layer maintains the
* running CRC32 accumulator so header/checksum bytes are excluded
* automatically from the digest.
*/
typedef struct {
/** Set true by the platform open-read function if the file exists. */
bool_t found;
/** Running CRC32 accumulator, updated by each CRC-covered read/write. */
uint32_t checksum;
/** CRC32 value read from the file header; verified after loading. */
uint32_t expectedChecksum;
/** Platform-specific stream state (file handle, buffer, etc.). */
saveplatformstream_t platform;
} savestream_t;
// ---------------------------------------------------------------------------
// Internal functions — do not call directly; use the macros below.
// ---------------------------------------------------------------------------
/**
* Reads bytes from the platform stream without updating the CRC.
*
* @param stream Active stream.
* @param buf Destination buffer.
* @param len Number of bytes to read.
* @return An error if the read fails.
*/
errorret_t saveStreamReadBytesRawImpl(
savestream_t *stream, void *buf, const size_t len
);
/**
* Writes bytes to the platform stream without updating the CRC.
*
* @param stream Active stream.
* @param buf Source buffer.
* @param len Number of bytes to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteBytesRawImpl(
savestream_t *stream, const void *buf, const size_t len
);
/**
* Reads bytes from the platform stream and accumulates them into the CRC.
*
* @param stream Active stream.
* @param buf Destination buffer.
* @param len Number of bytes to read.
* @return An error if the read fails.
*/
errorret_t saveStreamReadBytesImpl(
savestream_t *stream, void *buf, const size_t len
);
/**
* Updates the CRC then writes bytes to the platform stream.
*
* @param stream Active stream.
* @param buf Source buffer.
* @param len Number of bytes to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteBytesImpl(
savestream_t *stream, const void *buf, const size_t len
);
/**
* Finalizes a write stream: computes the final CRC32, seeks to the
* checksum field in the header, and writes it in little-endian order.
*
* @param stream Active write stream.
* @return An error if the seek or write fails.
*/
errorret_t saveStreamFinalizeWriteImpl(savestream_t *stream);
/**
* Verifies that the CRC32 accumulated during loading matches the value
* stored in the file header.
*
* @param stream Active read stream (loading must be complete).
* @param slot Slot index used in the error message on mismatch.
* @return An error if the checksum does not match.
*/
errorret_t saveStreamVerifyChecksumImpl(
savestream_t *stream, const uint8_t slot
);
/**
* Reads and validates the magic header, then reads the stored CRC32 and
* resets the running accumulator.
*
* @param stream Active read stream.
* @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to receive the header.
* @return An error if the header is missing or invalid.
*/
errorret_t saveStreamReadHeaderImpl(
savestream_t *stream, char_t header[SAVE_FILE_HEADER_SIZE]
);
/**
* Writes the magic header and a zero CRC32 placeholder, then resets the
* running accumulator.
*
* @param stream Active write stream.
* @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteHeaderImpl(
savestream_t *stream,
const char_t header[SAVE_FILE_HEADER_SIZE]
);
/**
* Reads a little-endian uint32 version field from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadVersionImpl(savestream_t *stream, uint32_t *out);
/**
* Writes a uint32 version field to the stream in little-endian order.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteVersionImpl(
savestream_t *stream, const uint32_t *input
);
/**
* Reads a single byte as a boolean (0 = false, non-zero = true).
*
* @param stream Active read stream.
* @param out Receives the boolean value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadBoolImpl(savestream_t *stream, bool_t *out);
/**
* Writes a boolean as a single byte (true = 1, false = 0).
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteBoolImpl(savestream_t *stream, const bool_t *input);
/**
* Reads a signed 8-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadInt8Impl(savestream_t *stream, int8_t *out);
/**
* Writes a signed 8-bit integer to the stream.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteInt8Impl(savestream_t *stream, const int8_t *input);
/**
* Reads an unsigned 8-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadUInt8Impl(savestream_t *stream, uint8_t *out);
/**
* Writes an unsigned 8-bit integer to the stream.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteUInt8Impl(savestream_t *stream, const uint8_t *input);
/**
* Reads a little-endian signed 16-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadInt16Impl(savestream_t *stream, int16_t *out);
/**
* Writes a signed 16-bit integer to the stream in little-endian order.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteInt16Impl(
savestream_t *stream, const int16_t *input
);
/**
* Reads a little-endian unsigned 16-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadUInt16Impl(savestream_t *stream, uint16_t *out);
/**
* Writes an unsigned 16-bit integer to the stream in little-endian order.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteUInt16Impl(
savestream_t *stream, const uint16_t *input
);
/**
* Reads a little-endian signed 32-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadInt32Impl(savestream_t *stream, int32_t *out);
/**
* Writes a signed 32-bit integer to the stream in little-endian order.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteInt32Impl(
savestream_t *stream, const int32_t *input
);
/**
* Reads a little-endian unsigned 32-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadUInt32Impl(savestream_t *stream, uint32_t *out);
/**
* Writes an unsigned 32-bit integer to the stream in little-endian order.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteUInt32Impl(
savestream_t *stream, const uint32_t *input
);
/**
* Reads a little-endian signed 64-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadInt64Impl(savestream_t *stream, int64_t *out);
/**
* Writes a signed 64-bit integer to the stream in little-endian order.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteInt64Impl(
savestream_t *stream, const int64_t *input
);
/**
* Reads a little-endian unsigned 64-bit integer from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadUInt64Impl(savestream_t *stream, uint64_t *out);
/**
* Writes an unsigned 64-bit integer to the stream in little-endian order.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteUInt64Impl(
savestream_t *stream, const uint64_t *input
);
/**
* Reads a little-endian float from the stream.
*
* @param stream Active read stream.
* @param out Receives the host-order value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadFloatImpl(savestream_t *stream, float_t *out);
/**
* Writes a float to the stream in little-endian order.
*
* @param stream Active write stream.
* @param input Value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteFloatImpl(
savestream_t *stream, const float_t *input
);
/**
* Reads a null-terminated string from the stream up to maxLen bytes
* (including the terminator). Always null-terminates the output buffer.
*
* @param stream Active read stream.
* @param out Destination buffer of at least maxLen bytes.
* @param maxLen Maximum bytes to read, including the null terminator.
* @return An error if the read fails.
*/
errorret_t saveStreamReadStringImpl(
savestream_t *stream, char_t *out, const size_t maxLen
);
/**
* Writes a null-terminated string to the stream, truncating to maxLen-1
* characters and always appending a null terminator.
*
* @param stream Active write stream.
* @param input Source string.
* @param maxLen Maximum bytes to write, including the null terminator.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteStringImpl(
savestream_t *stream, const char_t *input, const size_t maxLen
);
/**
* Reads a dusktimeepoch_t as three little-endian 64-bit IEEE 754 doubles
* (time, timeZone, offsetTime).
*
* @param stream Active read stream.
* @param out Receives the epoch value.
* @return An error if the read fails.
*/
errorret_t saveStreamReadDateImpl(
savestream_t *stream, dusktimeepoch_t *out
);
/**
* Writes a dusktimeepoch_t as three little-endian 64-bit IEEE 754 doubles
* (time, timeZone, offsetTime).
*
* @param stream Active write stream.
* @param input Epoch value to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteDateImpl(
savestream_t *stream, const dusktimeepoch_t *input
);
// ---------------------------------------------------------------------------
// User-facing macros — these embed errorChain so errors propagate
// automatically from saveFileLoad / saveFileWrite.
// ---------------------------------------------------------------------------
#define saveFileReadHeader(stream, header) \
errorChain(saveStreamReadHeaderImpl(stream, header))
#define saveFileWriteHeader(stream, header) \
errorChain(saveStreamWriteHeaderImpl(stream, header))
#define saveFileReadVersion(stream, out) \
errorChain(saveStreamReadVersionImpl(stream, out))
#define saveFileWriteVersion(stream, input) \
errorChain(saveStreamWriteVersionImpl(stream, input))
#define saveFileReadBool(stream, out) \
errorChain(saveStreamReadBoolImpl(stream, out))
#define saveFileWriteBool(stream, input) \
errorChain(saveStreamWriteBoolImpl(stream, input))
#define saveFileReadInt8(stream, out) \
errorChain(saveStreamReadInt8Impl(stream, out))
#define saveFileWriteInt8(stream, input) \
errorChain(saveStreamWriteInt8Impl(stream, input))
#define saveFileReadUInt8(stream, out) \
errorChain(saveStreamReadUInt8Impl(stream, out))
#define saveFileWriteUInt8(stream, input) \
errorChain(saveStreamWriteUInt8Impl(stream, input))
#define saveFileReadInt16(stream, out) \
errorChain(saveStreamReadInt16Impl(stream, out))
#define saveFileWriteInt16(stream, input) \
errorChain(saveStreamWriteInt16Impl(stream, input))
#define saveFileReadUInt16(stream, out) \
errorChain(saveStreamReadUInt16Impl(stream, out))
#define saveFileWriteUInt16(stream, input) \
errorChain(saveStreamWriteUInt16Impl(stream, input))
#define saveFileReadInt32(stream, out) \
errorChain(saveStreamReadInt32Impl(stream, out))
#define saveFileWriteInt32(stream, input) \
errorChain(saveStreamWriteInt32Impl(stream, input))
#define saveFileReadUInt32(stream, out) \
errorChain(saveStreamReadUInt32Impl(stream, out))
#define saveFileWriteUInt32(stream, input) \
errorChain(saveStreamWriteUInt32Impl(stream, input))
#define saveFileReadInt64(stream, out) \
errorChain(saveStreamReadInt64Impl(stream, out))
#define saveFileWriteInt64(stream, input) \
errorChain(saveStreamWriteInt64Impl(stream, input))
#define saveFileReadUInt64(stream, out) \
errorChain(saveStreamReadUInt64Impl(stream, out))
#define saveFileWriteUInt64(stream, input) \
errorChain(saveStreamWriteUInt64Impl(stream, input))
#define saveFileReadFloat(stream, out) \
errorChain(saveStreamReadFloatImpl(stream, out))
#define saveFileWriteFloat(stream, input) \
errorChain(saveStreamWriteFloatImpl(stream, input))
#define saveFileReadString(stream, out, maxLen) \
errorChain(saveStreamReadStringImpl(stream, out, maxLen))
#define saveFileWriteString(stream, input, maxLen) \
errorChain(saveStreamWriteStringImpl(stream, input, maxLen))
#define saveFileReadDate(stream, out) \
errorChain(saveStreamReadDateImpl(stream, out))
#define saveFileWriteDate(stream, input) \
errorChain(saveStreamWriteDateImpl(stream, input))
// ---------------------------------------------------------------------------
// Game code must implement these two functions.
// ---------------------------------------------------------------------------
/**
* Reads the contents of a save slot from the stream into the save file
* struct. Use saveFileRead* macros to deserialize fields one at a time.
*
* @param stream Active read stream for this slot.
* @param file Save file struct to populate.
* @return An error code if loading fails.
*/
extern errorret_t saveFileLoad(savestream_t *stream, savefile_t *file);
/**
* Writes the contents of the save file struct into the stream.
* Use saveFileWrite* macros to serialize fields one at a time.
*
* @param stream Active write stream for this slot.
* @param file Save file struct to serialize.
* @return An error code if writing fails.
*/
extern errorret_t saveFileWrite(savestream_t *stream, savefile_t *file);
+2
View File
@@ -28,6 +28,7 @@
#include "script/module/event/moduleEvent.h"
#include "script/module/ui/moduletextbox.h"
#include "script/module/ui/modulefullbox.h"
#include "script/module/save/modulesave.h"
static void moduleRegister(void) {
moduleInclude();
@@ -51,4 +52,5 @@ static void moduleRegister(void) {
moduleStory();
moduleTextbox();
moduleFullbox();
moduleSave();
}
+16 -1
View File
@@ -175,7 +175,7 @@ static void moduleBaseEval(const char_t *script) {
/**
* Throw a type error from a module function.
*
*
* @param message The error message to throw.
* @return A JerryScript error value.
*/
@@ -184,6 +184,21 @@ static jerry_value_t moduleBaseThrow(const char_t *message) {
return jerry_throw_sz(JERRY_ERROR_TYPE, message);
}
/**
* Converts a C errorret_t into a JS exception, forwarding the error message
* so that try/catch in JS sees the real error text. Clears the C error state.
*
* @param err The errorret_t returned by a failing C function.
* @return A JerryScript error value carrying the C error message.
*/
static jerry_value_t moduleBaseThrowError(const errorret_t err) {
assertNotNull(err.state, "Error state must not be NULL");
assertNotNull(err.state->message, "Error message must not be NULL");
jerry_value_t jsErr = jerry_throw_sz(JERRY_ERROR_TYPE, err.state->message);
errorCatch(err);
return jsErr;
}
/**
* Set a global string constant.
*/
+93
View File
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "script/module/save/modulesaveslot.h"
#include "save/save.h"
static scriptproto_t MODULE_SAVE_PROTO;
// Static Properties
moduleBaseFunction(moduleSaveGetCount) {
return jerry_number((double)SAVE_FILE_COUNT_MAX);
}
// Static Methods
moduleBaseFunction(moduleSaveLoad) {
if(argc < 1) return moduleBaseThrow("Save.load: expected (slot)");
moduleBaseRequireNumber(0);
const uint8_t slot = (uint8_t)jerry_value_as_number(args[0]);
if(slot >= SAVE_FILE_COUNT_MAX) {
return moduleBaseThrow("Save.load: slot out of range");
}
errorret_t err = saveLoad(slot);
if(err.code != ERROR_OK) return moduleBaseThrowError(err);
return jerry_undefined();
}
moduleBaseFunction(moduleSaveWrite) {
if(argc < 1) return moduleBaseThrow("Save.write: expected (slot)");
moduleBaseRequireNumber(0);
const uint8_t slot = (uint8_t)jerry_value_as_number(args[0]);
if(slot >= SAVE_FILE_COUNT_MAX) {
return moduleBaseThrow("Save.write: slot out of range");
}
errorret_t err = saveWrite(slot);
if(err.code != ERROR_OK) return moduleBaseThrowError(err);
return jerry_undefined();
}
moduleBaseFunction(moduleSaveDelete) {
if(argc < 1) return moduleBaseThrow("Save.delete: expected (slot)");
moduleBaseRequireNumber(0);
const uint8_t slot = (uint8_t)jerry_value_as_number(args[0]);
if(slot >= SAVE_FILE_COUNT_MAX) {
return moduleBaseThrow("Save.delete: slot out of range");
}
errorret_t err = saveDelete(slot);
if(err.code != ERROR_OK) return moduleBaseThrowError(err);
return jerry_undefined();
}
moduleBaseFunction(moduleSaveExists) {
if(argc < 1) return moduleBaseThrow("Save.exists: expected (slot)");
moduleBaseRequireNumber(0);
const uint8_t slot = (uint8_t)jerry_value_as_number(args[0]);
if(slot >= SAVE_FILE_COUNT_MAX) {
return moduleBaseThrow("Save.exists: slot out of range");
}
return jerry_boolean(saveExists(slot));
}
moduleBaseFunction(moduleSaveGet) {
if(argc < 1) return moduleBaseThrow("Save.get: expected (slot)");
moduleBaseRequireNumber(0);
const uint8_t slot = (uint8_t)jerry_value_as_number(args[0]);
if(slot >= SAVE_FILE_COUNT_MAX) {
return moduleBaseThrow("Save.get: slot out of range");
}
return moduleSaveSlotCreate(slot);
}
static void moduleSave(void) {
moduleSaveSlot();
scriptProtoInit(&MODULE_SAVE_PROTO, "Save", sizeof(uint8_t), NULL);
scriptProtoDefineStaticProp(&MODULE_SAVE_PROTO, "count",
moduleSaveGetCount, NULL
);
scriptProtoDefineStaticFunc(&MODULE_SAVE_PROTO, "load", moduleSaveLoad);
scriptProtoDefineStaticFunc(&MODULE_SAVE_PROTO, "write", moduleSaveWrite);
scriptProtoDefineStaticFunc(&MODULE_SAVE_PROTO, "delete", moduleSaveDelete);
scriptProtoDefineStaticFunc(&MODULE_SAVE_PROTO, "exists", moduleSaveExists);
scriptProtoDefineStaticFunc(&MODULE_SAVE_PROTO, "get", moduleSaveGet);
}
@@ -0,0 +1,94 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "script/module/modulebase.h"
#include "script/scriptproto.h"
#include "save/save.h"
typedef struct {
uint8_t slot;
} saveslotscript_t;
static scriptproto_t MODULE_SAVE_SLOT_PROTO;
static inline saveslotscript_t * moduleSaveSlotGet(
const jerry_call_info_t *callInfo
) {
return (saveslotscript_t *)scriptProtoGetValue(
&MODULE_SAVE_SLOT_PROTO, callInfo->this_value
);
}
// Properties
moduleBaseFunction(moduleSaveSlotGetSlot) {
saveslotscript_t *s = moduleSaveSlotGet(callInfo);
if(!s) return jerry_undefined();
return jerry_number((double)s->slot);
}
moduleBaseFunction(moduleSaveSlotGetExists) {
saveslotscript_t *s = moduleSaveSlotGet(callInfo);
if(!s) return jerry_boolean(false);
return jerry_boolean(saveExists(s->slot));
}
moduleBaseFunction(moduleSaveSlotGetVersion) {
saveslotscript_t *s = moduleSaveSlotGet(callInfo);
if(!s) return jerry_undefined();
return jerry_number((double)saveGet(s->slot)->version);
}
// Methods
moduleBaseFunction(moduleSaveSlotLoad) {
saveslotscript_t *s = moduleSaveSlotGet(callInfo);
if(!s) return jerry_undefined();
errorret_t err = saveLoad(s->slot);
if(err.code != ERROR_OK) return moduleBaseThrowError(err);
return jerry_undefined();
}
moduleBaseFunction(moduleSaveSlotWrite) {
saveslotscript_t *s = moduleSaveSlotGet(callInfo);
if(!s) return jerry_undefined();
errorret_t err = saveWrite(s->slot);
if(err.code != ERROR_OK) return moduleBaseThrowError(err);
return jerry_undefined();
}
moduleBaseFunction(moduleSaveSlotDelete) {
saveslotscript_t *s = moduleSaveSlotGet(callInfo);
if(!s) return jerry_undefined();
errorret_t err = saveDelete(s->slot);
if(err.code != ERROR_OK) return moduleBaseThrowError(err);
return jerry_undefined();
}
jerry_value_t moduleSaveSlotCreate(const uint8_t slot) {
saveslotscript_t s = { .slot = slot };
return scriptProtoCreateValue(&MODULE_SAVE_SLOT_PROTO, &s);
}
static void moduleSaveSlot(void) {
scriptProtoInit(&MODULE_SAVE_SLOT_PROTO, "SaveSlot", sizeof(saveslotscript_t), NULL);
scriptProtoDefineProp(&MODULE_SAVE_SLOT_PROTO, "slot",
moduleSaveSlotGetSlot, NULL
);
scriptProtoDefineProp(&MODULE_SAVE_SLOT_PROTO, "exists",
moduleSaveSlotGetExists, NULL
);
scriptProtoDefineProp(&MODULE_SAVE_SLOT_PROTO, "version",
moduleSaveSlotGetVersion, NULL
);
scriptProtoDefineFunc(&MODULE_SAVE_SLOT_PROTO, "load", moduleSaveSlotLoad);
scriptProtoDefineFunc(&MODULE_SAVE_SLOT_PROTO, "write", moduleSaveSlotWrite);
scriptProtoDefineFunc(&MODULE_SAVE_SLOT_PROTO, "delete", moduleSaveSlotDelete);
}
+1
View File
@@ -7,6 +7,7 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
array.c
crypt.c
endian.c
memory.c
string.c
+36
View File
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "util/crypt.h"
static void _cryptCRC32Step(uint32_t *crc, const uint8_t byte) {
*crc ^= byte;
for(int j = 0; j < 8; j++) {
*crc = (*crc >> 1) ^ (0xEDB88320u & -(*crc & 1u));
}
}
uint32_t cryptCRC32Begin(void) {
return 0xFFFFFFFF;
}
void cryptCRC32Update(uint32_t *crc, const void *data, const size_t size) {
const uint8_t *bytes = (const uint8_t *)data;
for(size_t i = 0; i < size; i++) {
_cryptCRC32Step(crc, bytes[i]);
}
}
uint32_t cryptCRC32End(const uint32_t crc) {
return ~crc;
}
uint32_t cryptCRC32(const void *data, const size_t size) {
uint32_t crc = cryptCRC32Begin();
cryptCRC32Update(&crc, data, size);
return cryptCRC32End(crc);
}
+42
View File
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "dusk.h"
/**
* Computes a CRC32 checksum over a block of data.
*
* @param data Pointer to the data to checksum.
* @param size Number of bytes to checksum.
* @return The CRC32 checksum.
*/
uint32_t cryptCRC32(const void *data, const size_t size);
/**
* Returns the initial CRC32 accumulator value.
*
* @return The initial accumulator.
*/
uint32_t cryptCRC32Begin(void);
/**
* Feeds bytes into a running CRC32 accumulator.
*
* @param crc Pointer to the current accumulator (updated in place).
* @param data Pointer to the data to feed in.
* @param size Number of bytes to process.
*/
void cryptCRC32Update(uint32_t *crc, const void *data, const size_t size);
/**
* Finalizes a running CRC32 accumulator and returns the checksum.
*
* @param crc The current accumulator value.
* @return The final CRC32 checksum.
*/
uint32_t cryptCRC32End(const uint32_t crc);
+1
View File
@@ -20,5 +20,6 @@ add_subdirectory(log)
add_subdirectory(display)
add_subdirectory(input)
add_subdirectory(network)
add_subdirectory(save)
add_subdirectory(system)
add_subdirectory(time)
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savedolphin.c
savestreamdolphin.c
)
+134
View File
@@ -0,0 +1,134 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/save.h"
#include "util/string.h"
static void _saveGetFileName(const uint8_t slot, char_t *out, const size_t max) {
snprintf(out, max, "%s_%u", SAVE_DOLPHIN_GAME_CODE, (uint32_t)slot);
}
errorret_t saveInitDolphin(void) {
SAVE.platform.mounted = false;
int32_t result = CARD_Mount(
SAVE_DOLPHIN_CHANNEL,
SAVE.platform.cardBuffer,
NULL
);
if(result < 0) {
errorThrow("Failed to mount memory card (error %d)", result);
}
SAVE.platform.mounted = true;
errorOk();
}
errorret_t saveDisposeDolphin(void) {
if(SAVE.platform.mounted) {
CARD_Unmount(SAVE_DOLPHIN_CHANNEL);
SAVE.platform.mounted = false;
}
errorOk();
}
errorret_t saveLoadDolphin(const uint8_t slot, savefile_t *file) {
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
_saveGetFileName(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
int32_t result = CARD_Open(SAVE_DOLPHIN_CHANNEL, fileName, &SAVE.platform.cardFile);
if(result == CARD_ERROR_NOFILE) {
file->exists = false;
errorOk();
}
if(result < 0) {
file->exists = false;
errorThrow("Failed to open memory card file for slot %u (error %d)",
(uint32_t)slot, result
);
}
void *buffer = memalign(32, SAVE_DOLPHIN_SECTOR_SIZE);
if(!buffer) {
CARD_Close(&SAVE.platform.cardFile);
errorThrow("Failed to allocate memory card read buffer");
}
result = CARD_Read(&SAVE.platform.cardFile, buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0);
CARD_Close(&SAVE.platform.cardFile);
if(result < 0) {
free(buffer);
file->exists = false;
errorThrow("Failed to read memory card data for slot %u (error %d)",
(uint32_t)slot, result
);
}
memoryCopy(file, buffer, sizeof(savefile_t));
free(buffer);
file->exists = true;
errorOk();
}
errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file) {
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
_saveGetFileName(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
void *buffer = memalign(32, SAVE_DOLPHIN_SECTOR_SIZE);
if(!buffer) {
errorThrow("Failed to allocate memory card write buffer");
}
memset(buffer, 0, SAVE_DOLPHIN_SECTOR_SIZE);
memoryCopy(buffer, file, sizeof(savefile_t));
// Try open existing file first; create if absent.
int32_t result = CARD_Open(SAVE_DOLPHIN_CHANNEL, fileName, &SAVE.platform.cardFile);
if(result == CARD_ERROR_NOFILE) {
result = CARD_Create(
SAVE_DOLPHIN_CHANNEL,
fileName,
SAVE_DOLPHIN_SECTOR_SIZE,
&SAVE.platform.cardFile
);
}
if(result < 0) {
free(buffer);
errorThrow("Failed to open/create memory card file for slot %u (error %d)",
(uint32_t)slot, result
);
}
result = CARD_Write(&SAVE.platform.cardFile, buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0);
CARD_Close(&SAVE.platform.cardFile);
free(buffer);
if(result < 0) {
errorThrow("Failed to write memory card data for slot %u (error %d)",
(uint32_t)slot, result
);
}
errorOk();
}
errorret_t saveDeleteDolphin(const uint8_t slot) {
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
_saveGetFileName(slot, fileName, SAVE_DOLPHIN_FILE_NAME_MAX);
int32_t result = CARD_Delete(SAVE_DOLPHIN_CHANNEL, fileName);
if(result < 0 && result != CARD_ERROR_NOFILE) {
errorThrow("Failed to delete memory card file for slot %u (error %d)",
(uint32_t)slot, result
);
}
errorOk();
}
+68
View File
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "save/savefile.h"
#include <gccore.h>
#define SAVE_DOLPHIN_FILE_NAME_MAX 32
#define SAVE_DOLPHIN_SECTOR_SIZE 8192
#ifndef SAVE_DOLPHIN_GAME_CODE
#define SAVE_DOLPHIN_GAME_CODE "DUSK"
#endif
#ifndef SAVE_DOLPHIN_CHANNEL
#define SAVE_DOLPHIN_CHANNEL CARD_SLOTA
#endif
typedef struct {
card_file cardFile;
uint8_t cardBuffer[CARD_WORKAREA] __attribute__((aligned(32)));
bool_t mounted;
} savedolphin_t;
/**
* Initializes the save system on GameCube (memory card slot A by default).
*
* @return An error code if initialization fails.
*/
errorret_t saveInitDolphin(void);
/**
* Disposes of the save system on GameCube.
*
* @return An error code if disposal fails.
*/
errorret_t saveDisposeDolphin(void);
/**
* Loads a save file from the memory card for the given slot.
*
* @param slot The save slot index.
* @param file Output save file data.
* @return An error code if the load fails.
*/
errorret_t saveLoadDolphin(const uint8_t slot, savefile_t *file);
/**
* Writes a save file to the memory card for the given slot.
*
* @param slot The save slot index.
* @param file Save file data to write.
* @return An error code if the write fails.
*/
errorret_t saveWriteDolphin(const uint8_t slot, const savefile_t *file);
/**
* Deletes the save file for the given slot from the memory card.
*
* @param slot The save slot index.
* @return An error code if the delete fails.
*/
errorret_t saveDeleteDolphin(const uint8_t slot);
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/savedolphin.h"
#include "save/savestreamdolphin.h"
typedef savedolphin_t saveplatform_t;
typedef savestreamdolphin_t saveplatformstream_t;
#define saveInitPlatform saveInitDolphin
#define saveDisposePlatform saveDisposeDolphin
#define saveDeletePlatform saveDeleteDolphin
#define saveStreamOpenReadPlatform(stream, slot) \
saveStreamOpenReadDolphin(&(stream)->platform, &(stream)->found, slot)
#define saveStreamOpenWritePlatform(stream, slot) \
saveStreamOpenWriteDolphin(&(stream)->platform, slot)
#define saveStreamClosePlatform(stream) \
saveStreamCloseDolphin(&(stream)->platform)
#define saveStreamReadBytesPlatform(stream, buf, len) \
saveStreamReadBytesDolphin(&(stream)->platform, buf, len)
#define saveStreamWriteBytesPlatform(stream, buf, len) \
saveStreamWriteBytesDolphin(&(stream)->platform, buf, len)
#define saveStreamSeekPlatform(stream, pos) \
saveStreamSeekDolphin(&(stream)->platform, pos)
+112
View File
@@ -0,0 +1,112 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/save.h"
#include "save/savestreamdolphin.h"
#include "util/memory.h"
#include "util/string.h"
static void _saveStreamGetFileName(
char_t *out, const size_t max, const uint8_t slot
) {
snprintf(out, max, "%s_%u", SAVE_DOLPHIN_GAME_CODE, (uint32_t)slot);
}
errorret_t saveStreamOpenReadDolphin(
savestreamdolphin_t *p, bool_t *found, const uint8_t slot
) {
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
_saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, slot);
int32_t result = CARD_Open(
SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile
);
if(result == CARD_ERROR_NOFILE) {
*found = false;
p->position = 0;
p->writing = false;
errorOk();
}
if(result < 0) {
*found = false;
errorThrow("Failed to open memory card file for slot %u (error %d)",
(uint32_t)slot, result
);
}
result = CARD_Read(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0);
CARD_Close(&p->cardFile);
if(result < 0) {
*found = false;
errorThrow("Failed to read memory card data for slot %u (error %d)",
(uint32_t)slot, result
);
}
*found = true;
p->position = 0;
p->writing = false;
p->slot = slot;
errorOk();
}
errorret_t saveStreamOpenWriteDolphin(
savestreamdolphin_t *p, const uint8_t slot
) {
memoryZero(p->buffer, SAVE_DOLPHIN_SECTOR_SIZE);
p->position = 0;
p->writing = true;
p->slot = slot;
errorOk();
}
void saveStreamCloseDolphin(savestreamdolphin_t *p) {
if(!p->writing) return;
char_t fileName[SAVE_DOLPHIN_FILE_NAME_MAX];
_saveStreamGetFileName(fileName, SAVE_DOLPHIN_FILE_NAME_MAX, p->slot);
int32_t result = CARD_Open(SAVE_DOLPHIN_CHANNEL, fileName, &p->cardFile);
if(result == CARD_ERROR_NOFILE) {
CARD_Create(
SAVE_DOLPHIN_CHANNEL, fileName, SAVE_DOLPHIN_SECTOR_SIZE, &p->cardFile
);
}
CARD_Write(&p->cardFile, p->buffer, SAVE_DOLPHIN_SECTOR_SIZE, 0);
CARD_Close(&p->cardFile);
}
errorret_t saveStreamReadBytesDolphin(
savestreamdolphin_t *p, void *buf, const size_t len
) {
if(p->position + len > SAVE_DOLPHIN_SECTOR_SIZE) {
errorThrow("Save stream read exceeds sector size");
}
memoryCopy(buf, p->buffer + p->position, len);
p->position += len;
errorOk();
}
errorret_t saveStreamWriteBytesDolphin(
savestreamdolphin_t *p, const void *buf, const size_t len
) {
if(p->position + len > SAVE_DOLPHIN_SECTOR_SIZE) {
errorThrow("Save stream write exceeds sector size");
}
memoryCopy(p->buffer + p->position, buf, len);
p->position += len;
errorOk();
}
errorret_t saveStreamSeekDolphin(savestreamdolphin_t *p, const size_t pos) {
if(pos >= SAVE_DOLPHIN_SECTOR_SIZE) {
errorThrow("Save stream seek out of range");
}
p->position = pos;
errorOk();
}
+91
View File
@@ -0,0 +1,91 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "save/savedolphin.h"
#include <gccore.h>
#include <stddef.h>
typedef struct {
/** libogc memory card file handle. */
card_file cardFile;
/** In-memory sector buffer; all reads and writes operate on this. */
uint8_t buffer[SAVE_DOLPHIN_SECTOR_SIZE] __attribute__((aligned(32)));
/** Current read/write position within buffer. */
size_t position;
/** True when opened for writing; flushes buffer to card on close. */
bool_t writing;
/** Slot index stored at open time so Close can derive the filename. */
uint8_t slot;
} savestreamdolphin_t;
/**
* Opens a memory card slot for reading by loading its sector into buffer.
*
* @param p Stream to initialize.
* @param found Set to true if the file exists, false if it does not.
* @param slot Save slot index.
* @return An error if reading the card fails for a reason other than
* missing file.
*/
errorret_t saveStreamOpenReadDolphin(
savestreamdolphin_t *p, bool_t *found, const uint8_t slot
);
/**
* Opens a memory card slot for writing by zeroing the sector buffer.
* The buffer is flushed to the card when savestreamCloseDolphin is called.
*
* @param p Stream to initialize.
* @param slot Save slot index.
* @return An error if initialization fails.
*/
errorret_t saveStreamOpenWriteDolphin(
savestreamdolphin_t *p, const uint8_t slot
);
/**
* Flushes the sector buffer to the memory card (write mode only) and
* releases the card file handle.
*
* @param p Stream to close.
*/
void saveStreamCloseDolphin(savestreamdolphin_t *p);
/**
* Copies len bytes from the sector buffer at the current position into buf.
*
* @param p Active stream.
* @param buf Destination buffer.
* @param len Number of bytes to read.
* @return An error if the read would exceed the sector size.
*/
errorret_t saveStreamReadBytesDolphin(
savestreamdolphin_t *p, void *buf, const size_t len
);
/**
* Copies len bytes from buf into the sector buffer at the current position.
*
* @param p Active stream.
* @param buf Source buffer.
* @param len Number of bytes to write.
* @return An error if the write would exceed the sector size.
*/
errorret_t saveStreamWriteBytesDolphin(
savestreamdolphin_t *p, const void *buf, const size_t len
);
/**
* Sets the current read/write position within the sector buffer.
*
* @param p Active stream.
* @param pos Target byte offset from the start of the sector.
* @return An error if pos is out of range.
*/
errorret_t saveStreamSeekDolphin(savestreamdolphin_t *p, const size_t pos);
+1
View File
@@ -14,5 +14,6 @@ add_subdirectory(asset)
add_subdirectory(log)
add_subdirectory(input)
add_subdirectory(network)
add_subdirectory(save)
add_subdirectory(system)
add_subdirectory(time)
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savelinux.c
savestreamlinux.c
)
+84
View File
@@ -0,0 +1,84 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/save.h"
#include "util/string.h"
#include <stdio.h>
#include <sys/stat.h>
#include <errno.h>
errorret_t saveInitLinux(void) {
stringCopy(SAVE.platform.savePath, SAVE_LINUX_PATH, SAVE_LINUX_PATH_MAX);
if(mkdir(SAVE.platform.savePath, 0755) != 0 && errno != EEXIST) {
errorThrow("Failed to create save directory: %s", SAVE.platform.savePath);
}
errorOk();
}
errorret_t saveDisposeLinux(void) {
errorOk();
}
errorret_t saveLoadLinux(const uint8_t slot, savefile_t *file) {
char_t path[SAVE_LINUX_PATH_MAX];
snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT,
SAVE.platform.savePath, (uint32_t)slot
);
FILE *f = fopen(path, "rb");
if(!f) {
file->exists = false;
errorOk();
}
size_t read = fread(file, sizeof(savefile_t), 1, f);
fclose(f);
if(read != 1) {
file->exists = false;
errorThrow("Failed to read save data for slot %u", (uint32_t)slot);
}
file->exists = true;
errorOk();
}
errorret_t saveWriteLinux(const uint8_t slot, const savefile_t *file) {
char_t path[SAVE_LINUX_PATH_MAX];
snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT,
SAVE.platform.savePath, (uint32_t)slot
);
FILE *f = fopen(path, "wb");
if(!f) {
errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot);
}
size_t written = fwrite(file, sizeof(savefile_t), 1, f);
fclose(f);
if(written != 1) {
errorThrow("Failed to write save data for slot %u", (uint32_t)slot);
}
errorOk();
}
errorret_t saveDeleteLinux(const uint8_t slot) {
char_t path[SAVE_LINUX_PATH_MAX];
snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT,
SAVE.platform.savePath, (uint32_t)slot
);
if(remove(path) != 0 && errno != ENOENT) {
errorThrow("Failed to delete save file for slot %u", (uint32_t)slot);
}
errorOk();
}
+61
View File
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "save/savefile.h"
#define SAVE_LINUX_PATH_MAX FILENAME_MAX
#define SAVE_LINUX_FILE_FORMAT "%s/save_%u.dat"
#ifndef SAVE_LINUX_PATH
#define SAVE_LINUX_PATH "./saves"
#endif
typedef struct {
char_t savePath[SAVE_LINUX_PATH_MAX];
} savelinux_t;
/**
* Initializes the save system on Linux.
*
* @return An error code if initialization fails.
*/
errorret_t saveInitLinux(void);
/**
* Disposes of the save system on Linux.
*
* @return An error code if disposal fails.
*/
errorret_t saveDisposeLinux(void);
/**
* Loads a save file from disk for the given slot.
*
* @param slot The save slot index.
* @param file Output save file data.
* @return An error code if the load fails.
*/
errorret_t saveLoadLinux(const uint8_t slot, savefile_t *file);
/**
* Writes a save file to disk for the given slot.
*
* @param slot The save slot index.
* @param file Save file data to write.
* @return An error code if the write fails.
*/
errorret_t saveWriteLinux(const uint8_t slot, const savefile_t *file);
/**
* Deletes the save file for the given slot from disk.
*
* @param slot The save slot index.
* @return An error code if the delete fails.
*/
errorret_t saveDeleteLinux(const uint8_t slot);
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/savelinux.h"
#include "save/savestreamlinux.h"
typedef savelinux_t saveplatform_t;
typedef savestreamlinux_t saveplatformstream_t;
#define saveInitPlatform saveInitLinux
#define saveDisposePlatform saveDisposeLinux
#define saveDeletePlatform saveDeleteLinux
#define saveStreamOpenReadPlatform(stream, slot) \
saveStreamOpenReadLinux(&(stream)->platform, &(stream)->found, slot)
#define saveStreamOpenWritePlatform(stream, slot) \
saveStreamOpenWriteLinux(&(stream)->platform, slot)
#define saveStreamClosePlatform(stream) \
saveStreamCloseLinux(&(stream)->platform)
#define saveStreamReadBytesPlatform(stream, buf, len) \
saveStreamReadBytesLinux(&(stream)->platform, buf, len)
#define saveStreamWriteBytesPlatform(stream, buf, len) \
saveStreamWriteBytesLinux(&(stream)->platform, buf, len)
#define saveStreamSeekPlatform(stream, pos) \
saveStreamSeekLinux(&(stream)->platform, pos)
+75
View File
@@ -0,0 +1,75 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/save.h"
#include "save/savestreamlinux.h"
#include "util/string.h"
#include <sys/stat.h>
#include <errno.h>
static void _saveStreamGetPath(
char_t *out, const size_t max, const uint8_t slot
) {
snprintf(
out, max, SAVE_LINUX_FILE_FORMAT,
SAVE.platform.savePath, (uint32_t)slot
);
}
errorret_t saveStreamOpenReadLinux(
savestreamlinux_t *p, bool_t *found, const uint8_t slot
) {
char_t path[SAVE_LINUX_PATH_MAX];
_saveStreamGetPath(path, SAVE_LINUX_PATH_MAX, slot);
p->file = fopen(path, "rb");
*found = (p->file != NULL);
errorOk();
}
errorret_t saveStreamOpenWriteLinux(savestreamlinux_t *p, const uint8_t slot) {
char_t path[SAVE_LINUX_PATH_MAX];
_saveStreamGetPath(path, SAVE_LINUX_PATH_MAX, slot);
p->file = fopen(path, "wb");
if(!p->file) {
errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot);
}
errorOk();
}
void saveStreamCloseLinux(savestreamlinux_t *p) {
if(p->file) {
fclose(p->file);
p->file = NULL;
}
}
errorret_t saveStreamReadBytesLinux(
savestreamlinux_t *p, void *buf, const size_t len
) {
if(fread(buf, 1, len, p->file) != len) {
errorThrow("Unexpected end of save file");
}
errorOk();
}
errorret_t saveStreamWriteBytesLinux(
savestreamlinux_t *p, const void *buf, const size_t len
) {
if(fwrite(buf, 1, len, p->file) != len) {
errorThrow("Failed to write save data");
}
errorOk();
}
errorret_t saveStreamSeekLinux(savestreamlinux_t *p, const size_t pos) {
if(fseek(p->file, (long)pos, SEEK_SET) != 0) {
errorThrow("Failed to seek in save file");
}
errorOk();
}
+78
View File
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include <stdio.h>
#include <stddef.h>
typedef struct {
FILE *file;
} savestreamlinux_t;
/**
* Opens a save slot file for reading.
*
* @param p Stream to initialize.
* @param found Set to true if the file exists, false if it does not.
* @param slot Save slot index.
* @return An error if the open fails for a reason other than missing file.
*/
errorret_t saveStreamOpenReadLinux(
savestreamlinux_t *p, bool_t *found, const uint8_t slot
);
/**
* Opens a save slot file for writing, creating or truncating it.
*
* @param p Stream to initialize.
* @param slot Save slot index.
* @return An error if the file cannot be opened for writing.
*/
errorret_t saveStreamOpenWriteLinux(
savestreamlinux_t *p, const uint8_t slot
);
/**
* Closes the file handle held by the stream.
*
* @param p Stream to close.
*/
void saveStreamCloseLinux(savestreamlinux_t *p);
/**
* Reads len bytes from the stream into buf.
*
* @param p Active stream.
* @param buf Destination buffer.
* @param len Number of bytes to read.
* @return An error if fewer than len bytes are available.
*/
errorret_t saveStreamReadBytesLinux(
savestreamlinux_t *p, void *buf, const size_t len
);
/**
* Writes len bytes from buf into the stream.
*
* @param p Active stream.
* @param buf Source buffer.
* @param len Number of bytes to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteBytesLinux(
savestreamlinux_t *p, const void *buf, const size_t len
);
/**
* Seeks to an absolute byte position within the stream.
*
* @param p Active stream.
* @param pos Target byte offset from the start of the file.
* @return An error if the seek fails.
*/
errorret_t saveStreamSeekLinux(savestreamlinux_t *p, const size_t pos);
+1
View File
@@ -19,5 +19,6 @@ add_subdirectory(asset)
add_subdirectory(input)
add_subdirectory(log)
add_subdirectory(network)
add_subdirectory(save)
add_subdirectory(system)
add_subdirectory(time)
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savepsp.c
savestreampsp.c
)
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/savepsp.h"
#include "save/savestreampsp.h"
typedef savepsp_t saveplatform_t;
typedef savestreampsp_t saveplatformstream_t;
#define saveInitPlatform saveInitPSP
#define saveDisposePlatform saveDisposePSP
#define saveDeletePlatform saveDeletePSP
#define saveStreamOpenReadPlatform(stream, slot) \
saveStreamOpenReadPSP(&(stream)->platform, &(stream)->found, slot)
#define saveStreamOpenWritePlatform(stream, slot) \
saveStreamOpenWritePSP(&(stream)->platform, slot)
#define saveStreamClosePlatform(stream) \
saveStreamClosePSP(&(stream)->platform)
#define saveStreamReadBytesPlatform(stream, buf, len) \
saveStreamReadBytesPSP(&(stream)->platform, buf, len)
#define saveStreamWriteBytesPlatform(stream, buf, len) \
saveStreamWriteBytesPSP(&(stream)->platform, buf, len)
#define saveStreamSeekPlatform(stream, pos) \
saveStreamSeekPSP(&(stream)->platform, pos)
+81
View File
@@ -0,0 +1,81 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/save.h"
errorret_t saveInitPSP(void) {
errorOk();
}
errorret_t saveDisposePSP(void) {
errorOk();
}
errorret_t saveLoadPSP(const uint8_t slot, savefile_t *file) {
char_t path[SAVE_PSP_PATH_MAX];
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
SAVE_PSP_TITLE_ID, (uint32_t)slot
);
SceUID fd = sceIoOpen(path, PSP_O_RDONLY, 0);
if(fd < 0) {
file->exists = false;
errorOk();
}
int32_t read = sceIoRead(fd, file, sizeof(savefile_t));
sceIoClose(fd);
if(read != (int32_t)sizeof(savefile_t)) {
file->exists = false;
errorThrow("Failed to read save data for slot %u", (uint32_t)slot);
}
file->exists = true;
errorOk();
}
errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file) {
char_t dir[SAVE_PSP_PATH_MAX];
snprintf(dir, SAVE_PSP_PATH_MAX, SAVE_PSP_DIR_FORMAT,
SAVE_PSP_TITLE_ID, (uint32_t)slot
);
sceIoMkdir(dir, 0777);
char_t path[SAVE_PSP_PATH_MAX];
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
SAVE_PSP_TITLE_ID, (uint32_t)slot
);
SceUID fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);
if(fd < 0) {
errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot);
}
int32_t written = sceIoWrite(fd, file, sizeof(savefile_t));
sceIoClose(fd);
if(written != (int32_t)sizeof(savefile_t)) {
errorThrow("Failed to write save data for slot %u", (uint32_t)slot);
}
errorOk();
}
errorret_t saveDeletePSP(const uint8_t slot) {
char_t path[SAVE_PSP_PATH_MAX];
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
SAVE_PSP_TITLE_ID, (uint32_t)slot
);
int32_t result = sceIoRemove(path);
if(result < 0 && result != (int32_t)0x80010002) {
errorThrow("Failed to delete save file for slot %u", (uint32_t)slot);
}
errorOk();
}
+63
View File
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include "save/savefile.h"
#include <pspiofilemgr.h>
#define SAVE_PSP_PATH_MAX 256
#define SAVE_PSP_FILE_FORMAT "ms0:/PSP/SAVEDATA/%s%02u/save.dat"
#define SAVE_PSP_DIR_FORMAT "ms0:/PSP/SAVEDATA/%s%02u"
#ifndef SAVE_PSP_TITLE_ID
#define SAVE_PSP_TITLE_ID "DUSK00001"
#endif
typedef struct {
uint8_t unused;
} savepsp_t;
/**
* Initializes the save system on PSP.
*
* @return An error code if initialization fails.
*/
errorret_t saveInitPSP(void);
/**
* Disposes of the save system on PSP.
*
* @return An error code if disposal fails.
*/
errorret_t saveDisposePSP(void);
/**
* Loads a save file from PSP save data for the given slot.
*
* @param slot The save slot index.
* @param file Output save file data.
* @return An error code if the load fails.
*/
errorret_t saveLoadPSP(const uint8_t slot, savefile_t *file);
/**
* Writes a save file to PSP save data for the given slot.
*
* @param slot The save slot index.
* @param file Save file data to write.
* @return An error code if the write fails.
*/
errorret_t saveWritePSP(const uint8_t slot, const savefile_t *file);
/**
* Deletes the save file for the given slot from PSP save data.
*
* @param slot The save slot index.
* @return An error code if the delete fails.
*/
errorret_t saveDeletePSP(const uint8_t slot);
+77
View File
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/save.h"
#include "save/savestreampsp.h"
errorret_t saveStreamOpenReadPSP(
savestreampsp_t *p, bool_t *found, const uint8_t slot
) {
char_t path[SAVE_PSP_PATH_MAX];
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
SAVE_PSP_TITLE_ID, (uint32_t)slot
);
p->fd = sceIoOpen(path, PSP_O_RDONLY, 0);
*found = (p->fd >= 0);
errorOk();
}
errorret_t saveStreamOpenWritePSP(savestreampsp_t *p, const uint8_t slot) {
char_t dir[SAVE_PSP_PATH_MAX];
snprintf(dir, SAVE_PSP_PATH_MAX, SAVE_PSP_DIR_FORMAT,
SAVE_PSP_TITLE_ID, (uint32_t)slot
);
sceIoMkdir(dir, 0777);
char_t path[SAVE_PSP_PATH_MAX];
snprintf(path, SAVE_PSP_PATH_MAX, SAVE_PSP_FILE_FORMAT,
SAVE_PSP_TITLE_ID, (uint32_t)slot
);
p->fd = sceIoOpen(path, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);
if(p->fd < 0) {
errorThrow(
"Failed to open PSP save file for writing: slot %u", (uint32_t)slot
);
}
errorOk();
}
void saveStreamClosePSP(savestreampsp_t *p) {
if(p->fd >= 0) {
sceIoClose(p->fd);
p->fd = -1;
}
}
errorret_t saveStreamReadBytesPSP(
savestreampsp_t *p, void *buf, const size_t len
) {
int32_t read = sceIoRead(p->fd, buf, (SceSize)len);
if(read != (int32_t)len) {
errorThrow("Unexpected end of PSP save file");
}
errorOk();
}
errorret_t saveStreamWriteBytesPSP(
savestreampsp_t *p, const void *buf, const size_t len
) {
int32_t written = sceIoWrite(p->fd, buf, (SceSize)len);
if(written != (int32_t)len) {
errorThrow("Failed to write PSP save data");
}
errorOk();
}
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos) {
if(sceIoLseek(p->fd, (SceOff)pos, PSP_SEEK_SET) < 0) {
errorThrow("Failed to seek in PSP save file");
}
errorOk();
}
+77
View File
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include <pspiofilemgr.h>
#include <stddef.h>
typedef struct {
SceUID fd;
} savestreampsp_t;
/**
* Opens a PSP save data file for reading.
*
* @param p Stream to initialize.
* @param found Set to true if the file exists, false if it does not.
* @param slot Save slot index.
* @return An error if the open fails for a reason other than missing file.
*/
errorret_t saveStreamOpenReadPSP(
savestreampsp_t *p, bool_t *found, const uint8_t slot
);
/**
* Opens a PSP save data file for writing, creating or truncating it.
* Creates the save data directory if it does not already exist.
*
* @param p Stream to initialize.
* @param slot Save slot index.
* @return An error if the file cannot be opened for writing.
*/
errorret_t saveStreamOpenWritePSP(savestreampsp_t *p, const uint8_t slot);
/**
* Closes the file descriptor held by the stream.
*
* @param p Stream to close.
*/
void saveStreamClosePSP(savestreampsp_t *p);
/**
* Reads len bytes from the stream into buf.
*
* @param p Active stream.
* @param buf Destination buffer.
* @param len Number of bytes to read.
* @return An error if fewer than len bytes are available.
*/
errorret_t saveStreamReadBytesPSP(
savestreampsp_t *p, void *buf, const size_t len
);
/**
* Writes len bytes from buf into the stream.
*
* @param p Active stream.
* @param buf Source buffer.
* @param len Number of bytes to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteBytesPSP(
savestreampsp_t *p, const void *buf, const size_t len
);
/**
* Seeks to an absolute byte position within the stream.
*
* @param p Active stream.
* @param pos Target byte offset from the start of the file.
* @return An error if the seek fails.
*/
errorret_t saveStreamSeekPSP(savestreampsp_t *p, const size_t pos);
+1
View File
@@ -18,3 +18,4 @@ target_sources(${DUSK_BINARY_TARGET_NAME}
add_subdirectory(asset)
add_subdirectory(input)
add_subdirectory(log)
add_subdirectory(save)
+11
View File
@@ -0,0 +1,11 @@
# Copyright (c) 2026 Dominic Masters
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT
# Sources
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
savevita.c
savestreamvita.c
)
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/savevita.h"
#include "save/savestreamvita.h"
typedef savevita_t saveplatform_t;
typedef savestreamvita_t saveplatformstream_t;
#define saveInitPlatform saveInitVita
#define saveDisposePlatform saveDisposeVita
#define saveDeletePlatform saveDeleteVita
#define saveStreamOpenReadPlatform(stream, slot) \
saveStreamOpenReadVita(&(stream)->platform, &(stream)->found, slot)
#define saveStreamOpenWritePlatform(stream, slot) \
saveStreamOpenWriteVita(&(stream)->platform, slot)
#define saveStreamClosePlatform(stream) \
saveStreamCloseVita(&(stream)->platform)
#define saveStreamReadBytesPlatform(stream, buf, len) \
saveStreamReadBytesVita(&(stream)->platform, buf, len)
#define saveStreamWriteBytesPlatform(stream, buf, len) \
saveStreamWriteBytesVita(&(stream)->platform, buf, len)
#define saveStreamSeekPlatform(stream, pos) \
saveStreamSeekVita(&(stream)->platform, pos)
+77
View File
@@ -0,0 +1,77 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "save/save.h"
#include "save/savestreamvita.h"
errorret_t saveStreamOpenReadVita(
savestreamvita_t *p, bool_t *found, const uint8_t slot
) {
char_t path[SAVE_VITA_PATH_MAX];
snprintf(path, SAVE_VITA_PATH_MAX, SAVE_VITA_FILE_FORMAT,
SAVE_VITA_TITLE_ID, (uint32_t)slot
);
p->fd = sceIoOpen(path, SCE_O_RDONLY, 0);
*found = (p->fd >= 0);
errorOk();
}
errorret_t saveStreamOpenWriteVita(savestreamvita_t *p, const uint8_t slot) {
char_t dir[SAVE_VITA_PATH_MAX];
snprintf(dir, SAVE_VITA_PATH_MAX, SAVE_VITA_DIR_FORMAT,
SAVE_VITA_TITLE_ID, (uint32_t)slot
);
sceIoMkdir(dir, 0777);
char_t path[SAVE_VITA_PATH_MAX];
snprintf(path, SAVE_VITA_PATH_MAX, SAVE_VITA_FILE_FORMAT,
SAVE_VITA_TITLE_ID, (uint32_t)slot
);
p->fd = sceIoOpen(path, SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC, 0777);
if(p->fd < 0) {
errorThrow(
"Failed to open Vita save file for writing: slot %u", (uint32_t)slot
);
}
errorOk();
}
void saveStreamCloseVita(savestreamvita_t *p) {
if(p->fd >= 0) {
sceIoClose(p->fd);
p->fd = -1;
}
}
errorret_t saveStreamReadBytesVita(
savestreamvita_t *p, void *buf, const size_t len
) {
int32_t read = sceIoRead(p->fd, buf, (SceSize)len);
if(read != (int32_t)len) {
errorThrow("Unexpected end of Vita save file");
}
errorOk();
}
errorret_t saveStreamWriteBytesVita(
savestreamvita_t *p, const void *buf, const size_t len
) {
int32_t written = sceIoWrite(p->fd, buf, (SceSize)len);
if(written != (int32_t)len) {
errorThrow("Failed to write Vita save data");
}
errorOk();
}
errorret_t saveStreamSeekVita(savestreamvita_t *p, const size_t pos) {
if(sceIoLseek(p->fd, (SceOff)pos, SCE_SEEK_SET) < 0) {
errorThrow("Failed to seek in Vita save file");
}
errorOk();
}
+78
View File
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "error/error.h"
#include <psp2/io/fcntl.h>
#include <psp2/io/stat.h>
#include <stddef.h>
typedef struct {
SceUID fd;
} savestreamvita_t;
/**
* Opens a Vita save data file for reading.
*
* @param p Stream to initialize.
* @param found Set to true if the file exists, false if it does not.
* @param slot Save slot index.
* @return An error if the open fails for a reason other than missing file.
*/
errorret_t saveStreamOpenReadVita(
savestreamvita_t *p, bool_t *found, const uint8_t slot
);
/**
* Opens a Vita save data file for writing, creating or truncating it.
* Creates the save data directory if it does not already exist.
*
* @param p Stream to initialize.
* @param slot Save slot index.
* @return An error if the file cannot be opened for writing.
*/
errorret_t saveStreamOpenWriteVita(savestreamvita_t *p, const uint8_t slot);
/**
* Closes the file descriptor held by the stream.
*
* @param p Stream to close.
*/
void saveStreamCloseVita(savestreamvita_t *p);
/**
* Reads len bytes from the stream into buf.
*
* @param p Active stream.
* @param buf Destination buffer.
* @param len Number of bytes to read.
* @return An error if fewer than len bytes are available.
*/
errorret_t saveStreamReadBytesVita(
savestreamvita_t *p, void *buf, const size_t len
);
/**
* Writes len bytes from buf into the stream.
*
* @param p Active stream.
* @param buf Source buffer.
* @param len Number of bytes to write.
* @return An error if the write fails.
*/
errorret_t saveStreamWriteBytesVita(
savestreamvita_t *p, const void *buf, const size_t len
);
/**
* Seeks to an absolute byte position within the stream.
*
* @param p Active stream.
* @param pos Target byte offset from the start of the file.
* @return An error if the seek fails.
*/
errorret_t saveStreamSeekVita(savestreamvita_t *p, const size_t pos);