diff --git a/assets/init.js b/assets/init.js index da9b32ea..1c3947fb 100644 --- a/assets/init.js +++ b/assets/init.js @@ -75,3 +75,19 @@ if (typeof PSP !== 'undefined') { } Scene.set('scenes/cube.js'); + +Console.print("Testing save stuff;"); +Console.print("Save Count: " + Save.count); +Console.print("Save 0 exists? " + Save.exists(0)); +try { + Save.load(0); + Console.print("Successfully loaded save 0."); +} catch (e) { + Console.print("Error loading save 0: " + e); + Save.delete(0); +} +Console.print("Save 0 exists? " + Save.exists(0)); +Console.print("Writing..."); +Save.write(0); +Console.print("Save 0 exists? " + Save.exists(0)); +Console.print("Save 0 data: " + Save.load(0)); \ No newline at end of file diff --git a/src/dusk/CMakeLists.txt b/src/dusk/CMakeLists.txt index 8d110da8..589caa4e 100644 --- a/src/dusk/CMakeLists.txt +++ b/src/dusk/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index ce528bf1..bbe07a77 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -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(); diff --git a/src/dusk/save/CMakeLists.txt b/src/dusk/save/CMakeLists.txt new file mode 100644 index 00000000..55b35252 --- /dev/null +++ b/src/dusk/save/CMakeLists.txt @@ -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 +) diff --git a/src/dusk/save/save.c b/src/dusk/save/save.c new file mode 100644 index 00000000..8fa81401 --- /dev/null +++ b/src/dusk/save/save.c @@ -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]; +} diff --git a/src/dusk/save/save.h b/src/dusk/save/save.h new file mode 100644 index 00000000..1309dc02 --- /dev/null +++ b/src/dusk/save/save.h @@ -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); diff --git a/src/dusk/save/savefile.h b/src/dusk/save/savefile.h new file mode 100644 index 00000000..d2e51986 --- /dev/null +++ b/src/dusk/save/savefile.h @@ -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; diff --git a/src/dusk/save/savestream.c b/src/dusk/save/savestream.c new file mode 100644 index 00000000..8e481521 --- /dev/null +++ b/src/dusk/save/savestream.c @@ -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(); +} diff --git a/src/dusk/save/savestream.h b/src/dusk/save/savestream.h new file mode 100644 index 00000000..257b6ef1 --- /dev/null +++ b/src/dusk/save/savestream.h @@ -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); diff --git a/src/dusk/script/module/module.h b/src/dusk/script/module/module.h index 0a03d7db..3d036b25 100644 --- a/src/dusk/script/module/module.h +++ b/src/dusk/script/module/module.h @@ -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(); } diff --git a/src/dusk/script/module/modulebase.h b/src/dusk/script/module/modulebase.h index ddb26b59..9eb82ab4 100644 --- a/src/dusk/script/module/modulebase.h +++ b/src/dusk/script/module/modulebase.h @@ -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. */ diff --git a/src/dusk/script/module/save/modulesave.h b/src/dusk/script/module/save/modulesave.h new file mode 100644 index 00000000..2e07de5b --- /dev/null +++ b/src/dusk/script/module/save/modulesave.h @@ -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); +} diff --git a/src/dusk/script/module/save/modulesaveslot.h b/src/dusk/script/module/save/modulesaveslot.h new file mode 100644 index 00000000..f272589f --- /dev/null +++ b/src/dusk/script/module/save/modulesaveslot.h @@ -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); +} diff --git a/src/dusk/util/CMakeLists.txt b/src/dusk/util/CMakeLists.txt index 304788ce..df1d282d 100644 --- a/src/dusk/util/CMakeLists.txt +++ b/src/dusk/util/CMakeLists.txt @@ -7,6 +7,7 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC array.c + crypt.c endian.c memory.c string.c diff --git a/src/dusk/util/crypt.c b/src/dusk/util/crypt.c new file mode 100644 index 00000000..cec0909b --- /dev/null +++ b/src/dusk/util/crypt.c @@ -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); +} diff --git a/src/dusk/util/crypt.h b/src/dusk/util/crypt.h new file mode 100644 index 00000000..b9484341 --- /dev/null +++ b/src/dusk/util/crypt.h @@ -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); diff --git a/src/duskdolphin/CMakeLists.txt b/src/duskdolphin/CMakeLists.txt index 78be2119..818635ee 100644 --- a/src/duskdolphin/CMakeLists.txt +++ b/src/duskdolphin/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/src/duskdolphin/save/CMakeLists.txt b/src/duskdolphin/save/CMakeLists.txt new file mode 100644 index 00000000..95b853df --- /dev/null +++ b/src/duskdolphin/save/CMakeLists.txt @@ -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 +) diff --git a/src/duskdolphin/save/savedolphin.c b/src/duskdolphin/save/savedolphin.c new file mode 100644 index 00000000..66597ed1 --- /dev/null +++ b/src/duskdolphin/save/savedolphin.c @@ -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(); +} diff --git a/src/duskdolphin/save/savedolphin.h b/src/duskdolphin/save/savedolphin.h new file mode 100644 index 00000000..1408ba89 --- /dev/null +++ b/src/duskdolphin/save/savedolphin.h @@ -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 + +#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); diff --git a/src/duskdolphin/save/saveplatform.h b/src/duskdolphin/save/saveplatform.h new file mode 100644 index 00000000..510b5d26 --- /dev/null +++ b/src/duskdolphin/save/saveplatform.h @@ -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) diff --git a/src/duskdolphin/save/savestreamdolphin.c b/src/duskdolphin/save/savestreamdolphin.c new file mode 100644 index 00000000..45411c1b --- /dev/null +++ b/src/duskdolphin/save/savestreamdolphin.c @@ -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(); +} diff --git a/src/duskdolphin/save/savestreamdolphin.h b/src/duskdolphin/save/savestreamdolphin.h new file mode 100644 index 00000000..2b351fe6 --- /dev/null +++ b/src/duskdolphin/save/savestreamdolphin.h @@ -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 +#include + +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); diff --git a/src/dusklinux/CMakeLists.txt b/src/dusklinux/CMakeLists.txt index 7771e18d..af0e8acb 100644 --- a/src/dusklinux/CMakeLists.txt +++ b/src/dusklinux/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/src/dusklinux/save/CMakeLists.txt b/src/dusklinux/save/CMakeLists.txt new file mode 100644 index 00000000..74b1dcc7 --- /dev/null +++ b/src/dusklinux/save/CMakeLists.txt @@ -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 +) diff --git a/src/dusklinux/save/savelinux.c b/src/dusklinux/save/savelinux.c new file mode 100644 index 00000000..0b672464 --- /dev/null +++ b/src/dusklinux/save/savelinux.c @@ -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 +#include +#include + +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(); +} diff --git a/src/dusklinux/save/savelinux.h b/src/dusklinux/save/savelinux.h new file mode 100644 index 00000000..1f99066e --- /dev/null +++ b/src/dusklinux/save/savelinux.h @@ -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); diff --git a/src/dusklinux/save/saveplatform.h b/src/dusklinux/save/saveplatform.h new file mode 100644 index 00000000..24f80972 --- /dev/null +++ b/src/dusklinux/save/saveplatform.h @@ -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) diff --git a/src/dusklinux/save/savestreamlinux.c b/src/dusklinux/save/savestreamlinux.c new file mode 100644 index 00000000..e4879ad4 --- /dev/null +++ b/src/dusklinux/save/savestreamlinux.c @@ -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 +#include + +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(); +} diff --git a/src/dusklinux/save/savestreamlinux.h b/src/dusklinux/save/savestreamlinux.h new file mode 100644 index 00000000..3d8a1f58 --- /dev/null +++ b/src/dusklinux/save/savestreamlinux.h @@ -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 +#include + +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); diff --git a/src/duskpsp/CMakeLists.txt b/src/duskpsp/CMakeLists.txt index 1bdee020..ca661aa5 100644 --- a/src/duskpsp/CMakeLists.txt +++ b/src/duskpsp/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/src/duskpsp/save/CMakeLists.txt b/src/duskpsp/save/CMakeLists.txt new file mode 100644 index 00000000..ee6cc44a --- /dev/null +++ b/src/duskpsp/save/CMakeLists.txt @@ -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 +) diff --git a/src/duskpsp/save/saveplatform.h b/src/duskpsp/save/saveplatform.h new file mode 100644 index 00000000..44db5a7f --- /dev/null +++ b/src/duskpsp/save/saveplatform.h @@ -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) diff --git a/src/duskpsp/save/savepsp.c b/src/duskpsp/save/savepsp.c new file mode 100644 index 00000000..2038292b --- /dev/null +++ b/src/duskpsp/save/savepsp.c @@ -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(); +} diff --git a/src/duskpsp/save/savepsp.h b/src/duskpsp/save/savepsp.h new file mode 100644 index 00000000..c498f89a --- /dev/null +++ b/src/duskpsp/save/savepsp.h @@ -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 + +#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); diff --git a/src/duskpsp/save/savestreampsp.c b/src/duskpsp/save/savestreampsp.c new file mode 100644 index 00000000..5adef148 --- /dev/null +++ b/src/duskpsp/save/savestreampsp.c @@ -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(); +} diff --git a/src/duskpsp/save/savestreampsp.h b/src/duskpsp/save/savestreampsp.h new file mode 100644 index 00000000..2574833b --- /dev/null +++ b/src/duskpsp/save/savestreampsp.h @@ -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 +#include + +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); diff --git a/src/duskvita/CMakeLists.txt b/src/duskvita/CMakeLists.txt index ab5edee9..8e31820d 100644 --- a/src/duskvita/CMakeLists.txt +++ b/src/duskvita/CMakeLists.txt @@ -18,3 +18,4 @@ target_sources(${DUSK_BINARY_TARGET_NAME} add_subdirectory(asset) add_subdirectory(input) add_subdirectory(log) +add_subdirectory(save) diff --git a/src/duskvita/save/CMakeLists.txt b/src/duskvita/save/CMakeLists.txt new file mode 100644 index 00000000..ffa8c58f --- /dev/null +++ b/src/duskvita/save/CMakeLists.txt @@ -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 +) diff --git a/src/duskvita/save/saveplatform.h b/src/duskvita/save/saveplatform.h new file mode 100644 index 00000000..47dfeb6d --- /dev/null +++ b/src/duskvita/save/saveplatform.h @@ -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) diff --git a/src/duskvita/save/savestreamvita.c b/src/duskvita/save/savestreamvita.c new file mode 100644 index 00000000..f3db7aa6 --- /dev/null +++ b/src/duskvita/save/savestreamvita.c @@ -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(); +} diff --git a/src/duskvita/save/savestreamvita.h b/src/duskvita/save/savestreamvita.h new file mode 100644 index 00000000..dcd9ec90 --- /dev/null +++ b/src/duskvita/save/savestreamvita.h @@ -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 +#include +#include + +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);