diff --git a/src/dusk/engine/engine.c b/src/dusk/engine/engine.c index 38149eb9..cb0232b7 100644 --- a/src/dusk/engine/engine.c +++ b/src/dusk/engine/engine.c @@ -17,6 +17,7 @@ #include "ui/ui.h" #include "assert/assert.h" #include "network/network.h" +#include "network/http/networkhttprequest.h" #include "system/system.h" #include "console/console.h" #include "save/save.h" @@ -45,6 +46,13 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { errorChain(networkInit()); errorChain(sceneInit()); + networkRequestConnection( + engineNetworkOnConnected, + engineNetworkOnFailed, + engineNetworkOnDisconnect, + NULL + ); + consolePrint("Engine initialized"); #ifdef DUSK_ASSERTIONS_FAKED @@ -54,7 +62,6 @@ errorret_t engineInit(const int32_t argc, const char_t **argv) { #endif sceneSet(SCENE_TYPE_OVERWORLD); - errorOk(); } @@ -63,6 +70,15 @@ errorret_t engineUpdate(void) { // Order here is important. errorChain(networkUpdate()); timeUpdate(); + + if( + ENGINE.networkDisconnectTestPending && + TIME.time >= ENGINE.networkDisconnectTestAt + ) { + ENGINE.networkDisconnectTestPending = false; + networkRequestDisconnection(engineNetworkDisconnectTestOnComplete, NULL); + } + inputUpdate(); consoleUpdate(); errorChain(rpgUpdate()); @@ -93,3 +109,63 @@ errorret_t engineDispose(void) { errorOk(); } + +void engineHttpTestOnComplete(void *params, void *user) { + networkhttprequest_t *request = (networkhttprequest_t *)params; + const networkhttpresponse_t *response = &request->response; + + consolePrint("frogfind.com response status: %d", response->status); + + for(uint32_t i = 0; i < response->headers.count; i++) { + const networkhttpheader_t *header = &response->headers.headers[i]; + consolePrint("frogfind.com header: %s: %s", header->name, header->value); + } + + consolePrint("frogfind.com body length: %d", (int32_t)response->bodyLength); + + const size_t chunkMax = CONSOLE_LINE_MAX - 32; + for(size_t offset = 0; offset < response->bodyLength; offset += chunkMax) { + size_t chunkLength = response->bodyLength - offset; + if(chunkLength > chunkMax) chunkLength = chunkMax; + + consolePrint("frogfind.com body: %.*s", + (int32_t)chunkLength, (const char_t *)response->body + offset); + } +} + +void engineHttpTestOnError(void *params, void *user) { + consolePrint("frogfind.com request failed"); +} + +void engineNetworkOnConnected(void *user) { + consolePrint("Network connected"); + + errorret_t ret = networkHttpRequest( + NETWORK_HTTP_METHOD_GET, + "http://frogfind.com", + NULL, 0, + NULL, 0, + NULL, 0, + engineHttpTestOnComplete, + engineHttpTestOnError, + NULL + ); + errorCatch(errorPrint(ret)); + + ENGINE.networkDisconnectTestAt = TIME.time + 10.0f; + ENGINE.networkDisconnectTestPending = true; +} + +void engineNetworkOnFailed(errorret_t error, void *user) { + consolePrint("Network connection failed"); + errorCatch(errorPrint(error)); +} + +void engineNetworkOnDisconnect(errorret_t error, void *user) { + consolePrint("Network disconnected"); + errorCatch(errorPrint(error)); +} + +void engineNetworkDisconnectTestOnComplete(void *user) { + consolePrint("Network disconnect test complete"); +} diff --git a/src/dusk/engine/engine.h b/src/dusk/engine/engine.h index c1de0118..4f066404 100644 --- a/src/dusk/engine/engine.h +++ b/src/dusk/engine/engine.h @@ -16,6 +16,10 @@ typedef struct { int32_t argc; const char_t **argv; const char_t *version; + + // Test: disconnects the network 10 seconds after it connects. + bool_t networkDisconnectTestPending; + float_t networkDisconnectTestAt; } engine_t; extern engine_t ENGINE; @@ -38,3 +42,51 @@ errorret_t engineUpdate(void); */ errorret_t engineDispose(void); +/** + * Logs the response status, headers and body of the frogfind.com test + * request to the console. + * + * @param params The completed networkhttprequest_t. + * @param user Unused. + */ +void engineHttpTestOnComplete(void *params, void *user); + +/** + * Logs that the frogfind.com test request failed. + * + * @param params The failed networkhttprequest_t. + * @param user Unused. + */ +void engineHttpTestOnError(void *params, void *user); + +/** + * Fires the frogfind.com test request once the network connection is + * up (some platforms, such as PSP, only bring the network stack up + * asynchronously after networkRequestConnection is called). + * + * @param user Unused. + */ +void engineNetworkOnConnected(void *user); + +/** + * Logs that the network connection could not be established. + * + * @param error The error describing why the connection failed. + * @param user Unused. + */ +void engineNetworkOnFailed(errorret_t error, void *user); + +/** + * Logs that the network connection was lost after having connected. + * + * @param error The error describing why the connection was lost. + * @param user Unused. + */ +void engineNetworkOnDisconnect(errorret_t error, void *user); + +/** + * Logs that the 10-second test disconnect completed. + * + * @param user Unused. + */ +void engineNetworkDisconnectTestOnComplete(void *user); diff --git a/src/dusk/input/input.c b/src/dusk/input/input.c index 80a5d83d..4d23c37b 100644 --- a/src/dusk/input/input.c +++ b/src/dusk/input/input.c @@ -12,6 +12,7 @@ #include "util/math.h" #include "time/time.h" #include "event/event.h" +#include "save/save.h" input_t INPUT; @@ -46,6 +47,43 @@ errorret_t inputInit(void) { errorOk(); } +errorret_t inputSaveSettings(void) { + yyjson_mut_doc *doc = yyjson_mut_doc_new(NULL); + yyjson_mut_val *root = yyjson_mut_obj(doc); + yyjson_mut_doc_set_root(doc, root); + + yyjson_mut_val *settings = yyjson_mut_obj(doc); + yyjson_mut_obj_add_val(doc, root, "settings", settings); + + yyjson_mut_val *inputSettings = yyjson_mut_obj(doc); + yyjson_mut_obj_add_val(doc, settings, "input", inputSettings); + + yyjson_mut_obj_add_real(doc, inputSettings, "deadzone", INPUT.deadzone); + + errorret_t ret = saveWrite(SAVE_SETTINGS_SLOT, doc); + yyjson_mut_doc_free(doc); + + if(errorIsNotOk(ret)) return ret; + errorOk(); +} + +errorret_t inputLoadSettings(void) { + yyjson_doc *doc = NULL; + errorChain(saveLoad(SAVE_SETTINGS_SLOT, &doc)); + + if(!doc) errorOk(); + + yyjson_val *root = yyjson_doc_get_root(doc); + yyjson_val *settings = yyjson_obj_get(root, "settings"); + yyjson_val *inputSettings = yyjson_obj_get(settings, "input"); + yyjson_val *deadzone = yyjson_obj_get(inputSettings, "deadzone"); + + if(deadzone) INPUT.deadzone = (float_t)yyjson_get_num(deadzone); + + yyjson_doc_free(doc); + errorOk(); +} + void inputUpdate(void) { #ifdef inputUpdatePlatform inputUpdatePlatform(); diff --git a/src/dusk/input/input.h b/src/dusk/input/input.h index 06093eef..29fbfe10 100644 --- a/src/dusk/input/input.h +++ b/src/dusk/input/input.h @@ -37,6 +37,23 @@ errorret_t inputInit(void); */ void inputUpdate(void); +/** + * Persists the current input settings (currently just the gamepad + * deadzone) to the save system as JSON, under settings.input. + * + * @return An error code if serialization or the write fails. + */ +errorret_t inputSaveSettings(void); + +/** + * Loads input settings (currently just the gamepad deadzone) from the + * save system's JSON payload under settings.input and applies them to + * INPUT. Leaves INPUT unchanged if no settings file exists yet. + * + * @return An error code if the load or parse fails. + */ +errorret_t inputLoadSettings(void); + /** * Gets the current value of a specific input action. * diff --git a/src/dusk/save/save.c b/src/dusk/save/save.c index 2b046807..ac0ea045 100644 --- a/src/dusk/save/save.c +++ b/src/dusk/save/save.c @@ -29,8 +29,11 @@ errorret_t saveDispose(void) { errorOk(); } -errorret_t saveLoad(const uint8_t slot) { +errorret_t saveLoad(const uint8_t slot, yyjson_doc **outDoc) { assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); + assertNotNull(outDoc, "outDoc must not be NULL"); + + *outDoc = NULL; savefile_t *file = &SAVE.files[slot]; file->exists = false; @@ -44,7 +47,8 @@ errorret_t saveLoad(const uint8_t slot) { if(!stream.found) errorOk(); - errorret_t ret = saveFileLoad(&stream, file); + yyjson_doc *doc = NULL; + errorret_t ret = saveFileLoad(&stream, file, &doc); #ifdef saveStreamClosePlatform saveStreamClosePlatform(&stream); @@ -52,14 +56,14 @@ errorret_t saveLoad(const uint8_t slot) { if(errorIsNotOk(ret)) return ret; - errorChain(saveStreamVerifyChecksumImpl(&stream, slot)); - + *outDoc = doc; file->exists = true; errorOk(); } -errorret_t saveWrite(const uint8_t slot) { +errorret_t saveWrite(const uint8_t slot, yyjson_mut_doc *doc) { assertTrue(slot < SAVE_FILE_COUNT_MAX, "slot exceeds SAVE_FILE_COUNT_MAX"); + assertNotNull(doc, "doc must not be NULL"); savefile_t *file = &SAVE.files[slot]; @@ -70,11 +74,7 @@ errorret_t saveWrite(const uint8_t slot) { errorChain(saveStreamOpenWritePlatform(&stream, slot)); #endif - errorret_t ret = saveFileWrite(&stream, file); - - if(errorIsOk(ret)) { - ret = saveStreamFinalizeWriteImpl(&stream); - } + errorret_t ret = saveFileWrite(&stream, file, doc); #ifdef saveStreamClosePlatform saveStreamClosePlatform(&stream); diff --git a/src/dusk/save/save.h b/src/dusk/save/save.h index 1309dc02..9eb9856d 100644 --- a/src/dusk/save/save.h +++ b/src/dusk/save/save.h @@ -9,9 +9,10 @@ #include "error/error.h" #include "savefile.h" #include "save/saveplatform.h" +#include "yyjson.h" typedef struct { - /** Per-slot save file data; indexed 0 to SAVE_FILE_COUNT_MAX - 1. */ + /** Per-slot save file metadata; 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; @@ -34,20 +35,30 @@ errorret_t saveInit(void); errorret_t saveDispose(void); /** - * Loads the save file for a given slot from persistent storage. + * Loads the save file for a given slot from persistent storage and parses + * its JSON payload. The payload is read and parsed fresh on every call and + * is not retained anywhere else - the caller owns the returned document + * and must release it with yyjson_doc_free() once done reading from it. * - * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). - * @return An error code if the load fails. + * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). + * @param outDoc Receives the parsed JSON document, or NULL if the slot has + * no save file on disk. + * @return An error code if the load or checksum validation fails. */ -errorret_t saveLoad(const uint8_t slot); +errorret_t saveLoad(const uint8_t slot, yyjson_doc **outDoc); /** - * Writes the save file for a given slot to persistent storage. + * Serializes the given JSON document and writes it to persistent storage + * for a given slot, alongside its byte size and CRC32 checksum. The + * document is serialized to bytes fresh on every call and is not retained + * anywhere else - the caller keeps ownership of doc and must dispose of it + * with yyjson_mut_doc_free() once done. * * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). - * @return An error code if the write fails. + * @param doc The JSON document to serialize and write. + * @return An error code if serialization or the write fails. */ -errorret_t saveWrite(const uint8_t slot); +errorret_t saveWrite(const uint8_t slot, yyjson_mut_doc *doc); /** * Deletes the save file for a given slot from persistent storage. @@ -66,7 +77,8 @@ errorret_t saveDelete(const uint8_t slot); bool_t saveExists(const uint8_t slot); /** - * Gets a pointer to the save file data for a given slot. + * Gets a pointer to the save file metadata for a given slot. This does + * not include the JSON payload itself - see saveLoad(). * * @param slot The save slot index (0 to SAVE_FILE_COUNT_MAX - 1). * @return A pointer to the savefile_t for the given slot. diff --git a/src/dusk/save/savefile.h b/src/dusk/save/savefile.h index dfef1d66..4930c75b 100644 --- a/src/dusk/save/savefile.h +++ b/src/dusk/save/savefile.h @@ -20,11 +20,21 @@ /** Maximum number of independent save slots supported. */ #define SAVE_FILE_COUNT_MAX 3 +/** + * Slot used to persist game settings. Shares the save slot range with + * game saves for now, until settings get a dedicated file of their own. + */ +#define SAVE_SETTINGS_SLOT 0 + 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; + /** Byte length of the serialized JSON payload. */ + uint32_t size; + /** CRC32 checksum of the serialized JSON payload, for validation. */ + uint32_t checksum; /** 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 index efad01aa..dd460fa7 100644 --- a/src/dusk/save/savestream.c +++ b/src/dusk/save/savestream.c @@ -8,10 +8,10 @@ #include "save/savestream.h" #include "util/crypt.h" #include "util/endian.h" -#include "util/string.h" #include "util/memory.h" +#include -errorret_t saveStreamReadBytesRawImpl( +errorret_t saveStreamReadBytesImpl( savestream_t *stream, void *buf, const size_t len ) { #ifdef saveStreamReadBytesPlatform @@ -20,7 +20,7 @@ errorret_t saveStreamReadBytesRawImpl( errorOk(); } -errorret_t saveStreamWriteBytesRawImpl( +errorret_t saveStreamWriteBytesImpl( savestream_t *stream, const void *buf, const size_t len ) { #ifdef saveStreamWriteBytesPlatform @@ -29,51 +29,10 @@ errorret_t saveStreamWriteBytesRawImpl( 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)); + errorChain(saveStreamReadBytesImpl(stream, header, SAVE_FILE_HEADER_SIZE)); if( header[0] != SAVE_FILE_HEADER[0] || @@ -83,25 +42,13 @@ errorret_t saveStreamReadHeaderImpl( 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(); + errorChain(saveStreamWriteBytesImpl(stream, header, SAVE_FILE_HEADER_SIZE)); errorOk(); } @@ -120,100 +67,14 @@ errorret_t saveStreamWriteVersionImpl( errorOk(); } -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(); -} - -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) { +errorret_t saveStreamReadSizeImpl(savestream_t *stream, uint32_t *out) { uint32_t raw; errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t))); *out = endianLittleToHost32(raw); errorOk(); } -errorret_t saveStreamWriteUInt32Impl( +errorret_t saveStreamWriteSizeImpl( savestream_t *stream, const uint32_t *input ) { uint32_t raw = endianLittleToHost32(*input); @@ -221,120 +82,83 @@ errorret_t saveStreamWriteUInt32Impl( 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)); +errorret_t saveStreamReadChecksumImpl(savestream_t *stream, uint32_t *out) { + uint32_t raw; + errorChain(saveStreamReadBytesImpl(stream, &raw, sizeof(uint32_t))); + *out = endianLittleToHost32(raw); errorOk(); } -errorret_t saveStreamWriteInt64Impl( - savestream_t *stream, const int64_t *input +errorret_t saveStreamWriteChecksumImpl( + savestream_t *stream, const uint32_t *input ) { - uint64_t raw; - memoryCopy(&raw, input, sizeof(int64_t)); - raw = endianLittleToHost64(raw); - errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t))); + uint32_t raw = endianLittleToHost32(*input); + errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint32_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 +errorret_t saveFileWritePayloadImpl( + savestream_t *stream, + const savefile_t *file, + const char_t *json, + const size_t len ) { - uint64_t raw = endianLittleToHost64(*input); - errorChain(saveStreamWriteBytesImpl(stream, &raw, sizeof(uint64_t))); - errorOk(); -} - -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(); -} - -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(); -} - -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(); -} - -errorret_t saveFileLoad(savestream_t *stream, savefile_t *file) { - saveFileReadHeader(stream, file->header); - saveFileReadVersion(stream, &file->version); - errorOk(); -} - -errorret_t saveFileWrite(savestream_t *stream, savefile_t *file) { saveFileWriteHeader(stream, file->header); saveFileWriteVersion(stream, &file->version); + saveFileWriteSize(stream, &file->size); + saveFileWriteChecksum(stream, &file->checksum); + errorChain(saveStreamWriteBytesImpl(stream, json, len)); + errorOk(); +} + +errorret_t saveFileLoad( + savestream_t *stream, savefile_t *file, yyjson_doc **outDoc +) { + saveFileReadHeader(stream, file->header); + saveFileReadVersion(stream, &file->version); + saveFileReadSize(stream, &file->size); + saveFileReadChecksum(stream, &file->checksum); + + uint8_t *buf = memoryAllocate(file->size); + errorret_t ret = saveStreamReadBytesImpl(stream, buf, file->size); + + if(errorIsNotOk(ret)) { + memoryFree(buf); + return ret; + } + + if(cryptCRC32(buf, file->size) != file->checksum) { + memoryFree(buf); + errorThrow("Save file has invalid checksum"); + } + + yyjson_doc *doc = yyjson_read((const char_t *)buf, file->size, 0); + memoryFree(buf); + + if(!doc) errorThrow("Failed to parse save file JSON"); + + *outDoc = doc; + errorOk(); +} + +errorret_t saveFileWrite( + savestream_t *stream, savefile_t *file, yyjson_mut_doc *doc +) { + size_t len = 0; + char_t *json = yyjson_mut_write(doc, 0, &len); + if(!json) errorThrow("Failed to serialize save data to JSON"); + + memoryCopy(file->header, SAVE_FILE_HEADER, SAVE_FILE_HEADER_SIZE); + file->version = SAVE_FILE_VERSION; + file->size = (uint32_t)len; + file->checksum = cryptCRC32(json, len); + + errorret_t ret = saveFileWritePayloadImpl(stream, file, json, len); + + // json comes from yyjson's own allocator (malloc), not the project + // allocator, so it must be released with free() rather than memoryFree(). + free(json); + + if(errorIsNotOk(ret)) return ret; errorOk(); } diff --git a/src/dusk/save/savestream.h b/src/dusk/save/savestream.h index 4eb6c540..a5006375 100644 --- a/src/dusk/save/savestream.h +++ b/src/dusk/save/savestream.h @@ -9,41 +9,17 @@ #include "error/error.h" #include "savefile.h" #include "save/saveplatform.h" -#include "time/timeepoch.h" +#include "yyjson.h" typedef struct { + /** True if the platform found and opened an existing save file. */ bool_t found; - uint32_t checksum; - uint32_t expectedChecksum; + /** Platform-specific stream handle (file handle, card slot, etc.). */ saveplatformstream_t platform; } savestream_t; /** - * 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. + * Reads raw bytes from the platform stream. * * @param stream Active stream. * @param buf Destination buffer. @@ -55,7 +31,7 @@ errorret_t saveStreamReadBytesImpl( ); /** - * Updates the CRC then writes bytes to the platform stream. + * Writes raw bytes to the platform stream. * * @param stream Active stream. * @param buf Source buffer. @@ -67,29 +43,7 @@ errorret_t saveStreamWriteBytesImpl( ); /** - * 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. + * Reads and validates the magic header from the stream. * * @param stream Active read stream. * @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to receive the header. @@ -100,8 +54,7 @@ errorret_t saveStreamReadHeaderImpl( ); /** - * Writes the magic header and a zero CRC32 placeholder, then resets the - * running accumulator. + * Writes the magic header to the stream. * * @param stream Active write stream. * @param header Buffer of SAVE_FILE_HEADER_SIZE bytes to write. @@ -133,268 +86,94 @@ errorret_t saveStreamWriteVersionImpl( ); /** - * 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. + * Reads a little-endian uint32 JSON payload size 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 saveStreamReadInt16Impl(savestream_t *stream, int16_t *out); +errorret_t saveStreamReadSizeImpl(savestream_t *stream, uint32_t *out); /** - * Writes a signed 16-bit integer to the stream in little-endian order. + * Writes a uint32 JSON payload size 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 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( +errorret_t saveStreamWriteSizeImpl( savestream_t *stream, const uint32_t *input ); /** - * Reads a little-endian signed 64-bit integer from the stream. + * Reads a little-endian uint32 CRC32 checksum 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 saveStreamReadInt64Impl(savestream_t *stream, int64_t *out); +errorret_t saveStreamReadChecksumImpl(savestream_t *stream, uint32_t *out); /** - * Writes a signed 64-bit integer to the stream in little-endian order. + * Writes a uint32 CRC32 checksum 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 saveStreamWriteInt64Impl( - savestream_t *stream, const int64_t *input +errorret_t saveStreamWriteChecksumImpl( + savestream_t *stream, const uint32_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 -); - -/** - * 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. - */ -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. + * Writes the header, version, size and checksum fields already populated + * on file, followed by the raw JSON payload bytes. Split out of + * saveFileWrite so the caller can free its serialized JSON buffer exactly + * once regardless of which step fails. * * @param stream Active write stream for this slot. - * @param file Save file struct to serialize. - * @return An error code if writing fails. + * @param file Save file metadata with header/version/size/checksum set. + * @param json Serialized JSON payload bytes. + * @param len Byte length of json. + * @return An error code if any write fails. */ -errorret_t saveFileWrite(savestream_t *stream, savefile_t *file); +errorret_t saveFileWritePayloadImpl( + savestream_t *stream, + const savefile_t *file, + const char_t *json, + const size_t len +); + +/** + * Reads a save slot's header, version and JSON payload from the stream, + * validating the payload against its stored size and CRC32 checksum. The + * JSON is parsed fresh into a new document each call; no payload bytes are + * kept resident beyond this call. + * + * @param stream Active read stream for this slot. + * @param file Save file metadata struct to populate. + * @param outDoc Receives the parsed JSON document, owned by the caller. + * @return An error code if loading, validation or parsing fails. + */ +errorret_t saveFileLoad( + savestream_t *stream, savefile_t *file, yyjson_doc **outDoc +); + +/** + * Serializes the given JSON document and writes the header, version, + * size, checksum and payload to the stream. The serialized bytes are not + * kept resident beyond this call. + * + * @param stream Active write stream for this slot. + * @param file Save file metadata struct to populate and serialize. + * @param doc The JSON document to serialize and write. + * @return An error code if serialization or writing fails. + */ +errorret_t saveFileWrite( + savestream_t *stream, savefile_t *file, yyjson_mut_doc *doc +); #define saveFileReadHeader(stream, header) \ errorChain(saveStreamReadHeaderImpl(stream, header)) @@ -406,63 +185,12 @@ errorret_t saveFileWrite(savestream_t *stream, savefile_t *file); #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)) +#define saveFileReadSize(stream, out) \ + errorChain(saveStreamReadSizeImpl(stream, out)) +#define saveFileWriteSize(stream, input) \ + errorChain(saveStreamWriteSizeImpl(stream, input)) +#define saveFileReadChecksum(stream, out) \ + errorChain(saveStreamReadChecksumImpl(stream, out)) +#define saveFileWriteChecksum(stream, input) \ + errorChain(saveStreamWriteChecksumImpl(stream, input))