Unify save system into one save.h/.c; diverge storage format per platform

Renames savefile_t to saveslot_t and folds last session's standalone
settings.h/.c module back in as savemeta_t, so there's one save system
(SAVE.slots[] + SAVE.meta) instead of two parallel ones - while letting
each platform pick its own physical format for the two concepts:

- Linux now writes human-editable JSON (slot0.json, settings.json, ...)
  via yyjson's mutable writer API, so players can hand-fix a bad setting.
- PSP folds meta into the same sceUtilitySavedata binary payload as its
  one save slot (SAVE_SLOT_COUNT_MAX=1 there - a future save picker will
  let players manage multiple named saves via the OS's own browser).
- GameCube consolidates the 3 per-slot memory card files and the separate
  settings file into one combined card file.

Also fixes two bugs surfaced while building this: the CRC finalize step
seeked to a hardcoded offset (only safe for one section per file, breaks
once meta+slots share a buffer), and save.c's async/sync dispatch left an
unconditional fallback call that doesn't exist on PSP-only platforms.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-04 18:35:06 -05:00
parent 7f7be39230
commit aa0180571e
59 changed files with 1142 additions and 2071 deletions
+147
View File
@@ -0,0 +1,147 @@
/**
* 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 "yyjson.h"
/**
* Small helper around a yyjson mutable document, used to build up a save
* file's fields one at a time before writing it out. Schema-agnostic -
* knows nothing about saveslot_t/savemeta_t; the caller supplies field
* names and values.
*/
typedef struct {
yyjson_mut_doc *doc;
yyjson_mut_val *root;
} savejsonwriterlinux_t;
/**
* Creates a new mutable JSON document with an empty root object.
*
* @param writer Writer to initialize.
* @return An error if the document can't be allocated.
*/
errorret_t saveJsonWriterInitLinux(savejsonwriterlinux_t *writer);
/**
* Adds a "version": <uint> field to the root object.
*/
void saveJsonWriterAddUInt32Linux(
savejsonwriterlinux_t *writer, const char_t *key, const uint32_t value
);
/**
* Adds a float field to the root object (written as a JSON number).
*/
void saveJsonWriterAddFloatLinux(
savejsonwriterlinux_t *writer, const char_t *key, const float_t value
);
/**
* Adds a string field to the root object. The value is copied into the
* document, so the caller's buffer doesn't need to outlive the call.
*/
void saveJsonWriterAddStringLinux(
savejsonwriterlinux_t *writer, const char_t *key, const char_t *value
);
/**
* Adds a JSON array of booleans as a field on the root object.
*/
void saveJsonWriterAddBoolArrayLinux(
savejsonwriterlinux_t *writer, const char_t *key, const bool_t *values,
const size_t count
);
/**
* Adds a JSON array of unsigned 8-bit integers as a field on the root
* object.
*/
void saveJsonWriterAddUInt8ArrayLinux(
savejsonwriterlinux_t *writer, const char_t *key, const uint8_t *values,
const size_t count
);
/**
* Pretty-prints the document to the given file path, creating or
* truncating it.
*
* @param writer Writer holding the document to write.
* @param path Destination file path.
* @return An error if the write fails.
*/
errorret_t saveJsonWriterSaveLinux(
savejsonwriterlinux_t *writer, const char_t *path
);
/**
* Frees the document. Safe to call even if saveJsonWriterInitLinux()
* failed partway.
*
* @param writer Writer to dispose.
*/
void saveJsonWriterDisposeLinux(savejsonwriterlinux_t *writer);
/**
* Reads and parses a JSON file, returning its root object.
*
* @param path File path to read.
* @param outDoc Receives the parsed document (must be freed via
* yyjson_doc_free() once done, regardless of found/error outcome).
* @param outRoot Receives the root object, or NULL if not found.
* @param found Set to true if the file exists, false if it does not
* (not finding the file is not an error).
* @return An error if the file exists but fails to parse.
*/
errorret_t saveJsonReaderOpenLinux(
const char_t *path, yyjson_doc **outDoc, yyjson_val **outRoot,
bool_t *found
);
/**
* Reads a uint32 field, falling back to defaultValue if the key is
* missing or not a number - a hand-edited file shouldn't hard-fail the
* whole load over one bad/missing field.
*/
uint32_t saveJsonReadUInt32Linux(
yyjson_val *root, const char_t *key, const uint32_t defaultValue
);
/**
* Reads a float field, falling back to defaultValue if the key is
* missing or not a number.
*/
float_t saveJsonReadFloatLinux(
yyjson_val *root, const char_t *key, const float_t defaultValue
);
/**
* Reads a string field into out, falling back to defaultValue if the key
* is missing or not a string. Always null-terminates.
*/
void saveJsonReadStringLinux(
yyjson_val *root, const char_t *key, char_t *out, const size_t maxLen,
const char_t *defaultValue
);
/**
* Reads a JSON array of booleans into out, up to count entries. Missing
* key, non-array value, or a shorter array all leave the remaining/all
* entries untouched (caller should zero the buffer first).
*/
void saveJsonReadBoolArrayLinux(
yyjson_val *root, const char_t *key, bool_t *out, const size_t count
);
/**
* Reads a JSON array of unsigned 8-bit integers into out, up to count
* entries. Same forgiving semantics as saveJsonReadBoolArrayLinux().
*/
void saveJsonReadUInt8ArrayLinux(
yyjson_val *root, const char_t *key, uint8_t *out, const size_t count
);