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:
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
#include "input/input.h"
|
||||
#include "save/settings.h"
|
||||
#include "save/save.h"
|
||||
|
||||
inputbuttondata_t INPUT_BUTTON_DATA[] = {
|
||||
#ifdef DUSK_INPUT_GAMEPAD
|
||||
@@ -548,5 +548,5 @@ errorret_t inputInitLinux(void) {
|
||||
}
|
||||
|
||||
float_t inputGetDeadzoneSDL2(const inputbutton_t button) {
|
||||
return settingsGet()->deadzone;
|
||||
return saveGetMeta()->deadzone;
|
||||
}
|
||||
@@ -7,7 +7,5 @@
|
||||
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||
PUBLIC
|
||||
savelinux.c
|
||||
savestreamlinux.c
|
||||
settingslinux.c
|
||||
settingsstreamlinux.c
|
||||
savejsonlinux.c
|
||||
)
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "save/savejsonlinux.h"
|
||||
#include "util/string.h"
|
||||
#include <sys/stat.h>
|
||||
|
||||
errorret_t saveJsonWriterInitLinux(savejsonwriterlinux_t *writer) {
|
||||
writer->doc = yyjson_mut_doc_new(NULL);
|
||||
if(!writer->doc) {
|
||||
errorThrow("Failed to allocate JSON document");
|
||||
}
|
||||
writer->root = yyjson_mut_obj(writer->doc);
|
||||
yyjson_mut_doc_set_root(writer->doc, writer->root);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void saveJsonWriterAddUInt32Linux(
|
||||
savejsonwriterlinux_t *writer, const char_t *key, const uint32_t value
|
||||
) {
|
||||
yyjson_mut_obj_add_uint(writer->doc, writer->root, key, (uint64_t)value);
|
||||
}
|
||||
|
||||
void saveJsonWriterAddFloatLinux(
|
||||
savejsonwriterlinux_t *writer, const char_t *key, const float_t value
|
||||
) {
|
||||
yyjson_mut_obj_add_real(writer->doc, writer->root, key, (double)value);
|
||||
}
|
||||
|
||||
void saveJsonWriterAddStringLinux(
|
||||
savejsonwriterlinux_t *writer, const char_t *key, const char_t *value
|
||||
) {
|
||||
yyjson_mut_obj_add_strcpy(writer->doc, writer->root, key, value);
|
||||
}
|
||||
|
||||
void saveJsonWriterAddBoolArrayLinux(
|
||||
savejsonwriterlinux_t *writer, const char_t *key, const bool_t *values,
|
||||
const size_t count
|
||||
) {
|
||||
yyjson_mut_val *arr = yyjson_mut_arr(writer->doc);
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
yyjson_mut_arr_add_bool(writer->doc, arr, values[i]);
|
||||
}
|
||||
yyjson_mut_obj_add_val(writer->doc, writer->root, key, arr);
|
||||
}
|
||||
|
||||
void saveJsonWriterAddUInt8ArrayLinux(
|
||||
savejsonwriterlinux_t *writer, const char_t *key, const uint8_t *values,
|
||||
const size_t count
|
||||
) {
|
||||
yyjson_mut_val *arr = yyjson_mut_arr(writer->doc);
|
||||
for(size_t i = 0; i < count; i++) {
|
||||
yyjson_mut_arr_add_uint(writer->doc, arr, (uint64_t)values[i]);
|
||||
}
|
||||
yyjson_mut_obj_add_val(writer->doc, writer->root, key, arr);
|
||||
}
|
||||
|
||||
errorret_t saveJsonWriterSaveLinux(
|
||||
savejsonwriterlinux_t *writer, const char_t *path
|
||||
) {
|
||||
yyjson_write_err err;
|
||||
if(!yyjson_mut_write_file(
|
||||
path, writer->doc, YYJSON_WRITE_PRETTY, NULL, &err
|
||||
)) {
|
||||
errorThrow("Failed to write %s: %s", path, err.msg);
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void saveJsonWriterDisposeLinux(savejsonwriterlinux_t *writer) {
|
||||
if(writer->doc) {
|
||||
yyjson_mut_doc_free(writer->doc);
|
||||
writer->doc = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t saveJsonReaderOpenLinux(
|
||||
const char_t *path, yyjson_doc **outDoc, yyjson_val **outRoot,
|
||||
bool_t *found
|
||||
) {
|
||||
*outDoc = NULL;
|
||||
*outRoot = NULL;
|
||||
|
||||
struct stat st;
|
||||
if(stat(path, &st) != 0) {
|
||||
*found = false;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
yyjson_read_err err;
|
||||
*outDoc = yyjson_read_file(
|
||||
path, YYJSON_READ_ALLOW_COMMENTS | YYJSON_READ_ALLOW_TRAILING_COMMAS,
|
||||
NULL, &err
|
||||
);
|
||||
if(!*outDoc) {
|
||||
*found = false;
|
||||
errorThrow("Failed to parse %s: %s", path, err.msg);
|
||||
}
|
||||
|
||||
*outRoot = yyjson_doc_get_root(*outDoc);
|
||||
*found = true;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
uint32_t saveJsonReadUInt32Linux(
|
||||
yyjson_val *root, const char_t *key, const uint32_t defaultValue
|
||||
) {
|
||||
yyjson_val *val = yyjson_obj_get(root, key);
|
||||
if(!val || !yyjson_is_num(val)) return defaultValue;
|
||||
return (uint32_t)yyjson_get_uint(val);
|
||||
}
|
||||
|
||||
float_t saveJsonReadFloatLinux(
|
||||
yyjson_val *root, const char_t *key, const float_t defaultValue
|
||||
) {
|
||||
yyjson_val *val = yyjson_obj_get(root, key);
|
||||
if(!val || !yyjson_is_num(val)) return defaultValue;
|
||||
return (float_t)yyjson_get_num(val);
|
||||
}
|
||||
|
||||
void saveJsonReadStringLinux(
|
||||
yyjson_val *root, const char_t *key, char_t *out, const size_t maxLen,
|
||||
const char_t *defaultValue
|
||||
) {
|
||||
yyjson_val *val = yyjson_obj_get(root, key);
|
||||
const char_t *src = defaultValue;
|
||||
if(val && yyjson_is_str(val)) src = yyjson_get_str(val);
|
||||
stringCopy(out, src, maxLen);
|
||||
}
|
||||
|
||||
void saveJsonReadBoolArrayLinux(
|
||||
yyjson_val *root, const char_t *key, bool_t *out, const size_t count
|
||||
) {
|
||||
yyjson_val *arr = yyjson_obj_get(root, key);
|
||||
if(!arr || !yyjson_is_arr(arr)) return;
|
||||
|
||||
size_t idx, len;
|
||||
yyjson_val *elem;
|
||||
yyjson_arr_foreach(arr, idx, len, elem) {
|
||||
if(idx >= count) break;
|
||||
if(yyjson_is_bool(elem)) out[idx] = yyjson_get_bool(elem);
|
||||
}
|
||||
}
|
||||
|
||||
void saveJsonReadUInt8ArrayLinux(
|
||||
yyjson_val *root, const char_t *key, uint8_t *out, const size_t count
|
||||
) {
|
||||
yyjson_val *arr = yyjson_obj_get(root, key);
|
||||
if(!arr || !yyjson_is_arr(arr)) return;
|
||||
|
||||
size_t idx, len;
|
||||
yyjson_val *elem;
|
||||
yyjson_arr_foreach(arr, idx, len, elem) {
|
||||
if(idx >= count) break;
|
||||
if(yyjson_is_int(elem)) out[idx] = (uint8_t)yyjson_get_int(elem);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -6,8 +6,9 @@
|
||||
*/
|
||||
|
||||
#include "save/save.h"
|
||||
#include "save/savejsonlinux.h"
|
||||
#include "util/string.h"
|
||||
#include <stdio.h>
|
||||
#include "util/memory.h"
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
@@ -25,60 +26,121 @@ 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
|
||||
static void _saveSlotPathLinux(
|
||||
char_t *out, const size_t max, const uint8_t slot
|
||||
) {
|
||||
snprintf(
|
||||
out, max, SAVE_LINUX_SLOT_FILE_FORMAT, SAVE.platform.savePath,
|
||||
(uint32_t)slot
|
||||
);
|
||||
}
|
||||
|
||||
FILE *f = fopen(path, "rb");
|
||||
if(!f) {
|
||||
file->exists = false;
|
||||
errorOk();
|
||||
}
|
||||
static void _saveMetaPathLinux(char_t *out, const size_t max) {
|
||||
snprintf(out, max, SAVE_LINUX_META_FILE_FORMAT, SAVE.platform.savePath);
|
||||
}
|
||||
|
||||
size_t read = fread(file, sizeof(savefile_t), 1, f);
|
||||
fclose(f);
|
||||
errorret_t saveSlotLoadLinux(const uint8_t slot, saveslot_t *out) {
|
||||
char_t path[SAVE_LINUX_PATH_MAX];
|
||||
_saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot);
|
||||
|
||||
if(read != 1) {
|
||||
file->exists = false;
|
||||
errorThrow("Failed to read save data for slot %u", (uint32_t)slot);
|
||||
}
|
||||
yyjson_doc *doc;
|
||||
yyjson_val *root;
|
||||
bool_t found;
|
||||
errorret_t ret = saveJsonReaderOpenLinux(path, &doc, &root, &found);
|
||||
if(errorIsNotOk(ret)) { yyjson_doc_free(doc); errorChain(ret); }
|
||||
if(!found) errorOk();
|
||||
|
||||
file->exists = true;
|
||||
memoryZero(out, sizeof(saveslot_t));
|
||||
out->version = saveJsonReadUInt32Linux(root, "version", SAVE_SLOT_VERSION);
|
||||
saveJsonReadStringLinux(
|
||||
root, "playerName", out->playerName, SAVE_PLAYER_NAME_MAX, ""
|
||||
);
|
||||
saveJsonReadBoolArrayLinux(
|
||||
root, "globalItemCollected", out->globalItemCollected,
|
||||
SAVE_GLOBAL_ITEM_COUNT_MAX
|
||||
);
|
||||
saveJsonReadUInt8ArrayLinux(
|
||||
root, "storyFlags", out->storyFlags, SAVE_STORY_FLAG_COUNT_MAX
|
||||
);
|
||||
out->exists = true;
|
||||
|
||||
yyjson_doc_free(doc);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveWriteLinux(const uint8_t slot, const savefile_t *file) {
|
||||
errorret_t saveSlotWriteLinux(const uint8_t slot, saveslot_t *slotData) {
|
||||
char_t path[SAVE_LINUX_PATH_MAX];
|
||||
snprintf(path, SAVE_LINUX_PATH_MAX, SAVE_LINUX_FILE_FORMAT,
|
||||
SAVE.platform.savePath, (uint32_t)slot
|
||||
_saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot);
|
||||
|
||||
slotData->version = SAVE_SLOT_VERSION;
|
||||
|
||||
savejsonwriterlinux_t writer;
|
||||
errorChain(saveJsonWriterInitLinux(&writer));
|
||||
saveJsonWriterAddUInt32Linux(&writer, "version", slotData->version);
|
||||
saveJsonWriterAddStringLinux(&writer, "playerName", slotData->playerName);
|
||||
saveJsonWriterAddBoolArrayLinux(
|
||||
&writer, "globalItemCollected", slotData->globalItemCollected,
|
||||
SAVE_GLOBAL_ITEM_COUNT_MAX
|
||||
);
|
||||
saveJsonWriterAddUInt8ArrayLinux(
|
||||
&writer, "storyFlags", slotData->storyFlags, SAVE_STORY_FLAG_COUNT_MAX
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
errorret_t ret = saveJsonWriterSaveLinux(&writer, path);
|
||||
saveJsonWriterDisposeLinux(&writer);
|
||||
errorChain(ret);
|
||||
|
||||
slotData->exists = true;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveDeleteLinux(const uint8_t slot) {
|
||||
errorret_t saveDeleteSlotLinux(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
|
||||
);
|
||||
_saveSlotPathLinux(path, SAVE_LINUX_PATH_MAX, slot);
|
||||
|
||||
if(remove(path) != 0 && errno != ENOENT) {
|
||||
errorThrow("Failed to delete save file for slot %u", (uint32_t)slot);
|
||||
errorThrow("Failed to delete save slot %u: %s", (uint32_t)slot, path);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveMetaLoadLinux(savemeta_t *out) {
|
||||
char_t path[SAVE_LINUX_PATH_MAX];
|
||||
_saveMetaPathLinux(path, SAVE_LINUX_PATH_MAX);
|
||||
|
||||
yyjson_doc *doc;
|
||||
yyjson_val *root;
|
||||
bool_t found;
|
||||
errorret_t ret = saveJsonReaderOpenLinux(path, &doc, &root, &found);
|
||||
if(errorIsNotOk(ret)) { yyjson_doc_free(doc); errorChain(ret); }
|
||||
if(!found) errorOk();
|
||||
|
||||
out->version = saveJsonReadUInt32Linux(root, "version", SAVE_META_VERSION);
|
||||
out->deadzone = saveJsonReadFloatLinux(
|
||||
root, "deadzone", SAVE_META_DEADZONE_DEFAULT
|
||||
);
|
||||
out->exists = true;
|
||||
|
||||
yyjson_doc_free(doc);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveMetaWriteLinux(savemeta_t *meta) {
|
||||
char_t path[SAVE_LINUX_PATH_MAX];
|
||||
_saveMetaPathLinux(path, SAVE_LINUX_PATH_MAX);
|
||||
|
||||
meta->version = SAVE_META_VERSION;
|
||||
|
||||
savejsonwriterlinux_t writer;
|
||||
errorChain(saveJsonWriterInitLinux(&writer));
|
||||
saveJsonWriterAddUInt32Linux(&writer, "version", meta->version);
|
||||
saveJsonWriterAddFloatLinux(&writer, "deadzone", meta->deadzone);
|
||||
|
||||
errorret_t ret = saveJsonWriterSaveLinux(&writer, path);
|
||||
saveJsonWriterDisposeLinux(&writer);
|
||||
errorChain(ret);
|
||||
|
||||
meta->exists = true;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "save/savefile.h"
|
||||
#include "save/saveslot.h"
|
||||
#include "save/savemeta.h"
|
||||
|
||||
#define SAVE_LINUX_PATH_MAX FILENAME_MAX
|
||||
#define SAVE_LINUX_FILE_FORMAT "%s/save_%u.dat"
|
||||
#define SAVE_LINUX_SLOT_FILE_FORMAT "%s/slot%u.json"
|
||||
#define SAVE_LINUX_META_FILE_FORMAT "%s/settings.json"
|
||||
|
||||
#ifndef SAVE_LINUX_PATH
|
||||
#define SAVE_LINUX_PATH "./saves"
|
||||
@@ -21,7 +23,8 @@ typedef struct {
|
||||
} savelinux_t;
|
||||
|
||||
/**
|
||||
* Initializes the save system on Linux.
|
||||
* Initializes the save system on Linux - ensures the save directory
|
||||
* exists (shared by both save slots and meta).
|
||||
*
|
||||
* @return An error code if initialization fails.
|
||||
*/
|
||||
@@ -35,27 +38,43 @@ errorret_t saveInitLinux(void);
|
||||
errorret_t saveDisposeLinux(void);
|
||||
|
||||
/**
|
||||
* Loads a save file from disk for the given slot.
|
||||
* Loads a save slot as JSON (slotN.json) from disk, if it exists.
|
||||
*
|
||||
* @param slot The save slot index.
|
||||
* @param file Output save file data.
|
||||
* @return An error code if the load fails.
|
||||
* @param out Output slot data.
|
||||
* @return An error code if the slot exists but fails to parse.
|
||||
*/
|
||||
errorret_t saveLoadLinux(const uint8_t slot, savefile_t *file);
|
||||
errorret_t saveSlotLoadLinux(const uint8_t slot, saveslot_t *out);
|
||||
|
||||
/**
|
||||
* Writes a save file to disk for the given slot.
|
||||
* Writes a save slot as JSON (slotN.json) to disk.
|
||||
*
|
||||
* @param slot The save slot index.
|
||||
* @param file Save file data to write.
|
||||
* @param slotData Slot data to write.
|
||||
* @return An error code if the write fails.
|
||||
*/
|
||||
errorret_t saveWriteLinux(const uint8_t slot, const savefile_t *file);
|
||||
errorret_t saveSlotWriteLinux(const uint8_t slot, saveslot_t *slotData);
|
||||
|
||||
/**
|
||||
* Deletes the save file for the given slot from disk.
|
||||
* Deletes the save slot JSON file for the given index.
|
||||
*
|
||||
* @param slot The save slot index.
|
||||
* @return An error code if the delete fails.
|
||||
*/
|
||||
errorret_t saveDeleteLinux(const uint8_t slot);
|
||||
errorret_t saveDeleteSlotLinux(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Loads save meta as JSON (settings.json) from disk, if it exists.
|
||||
*
|
||||
* @param out Output meta data.
|
||||
* @return An error code if the file exists but fails to parse.
|
||||
*/
|
||||
errorret_t saveMetaLoadLinux(savemeta_t *out);
|
||||
|
||||
/**
|
||||
* Writes save meta as JSON (settings.json) to disk.
|
||||
*
|
||||
* @param meta Meta data to write.
|
||||
* @return An error code if the write fails.
|
||||
*/
|
||||
errorret_t saveMetaWriteLinux(savemeta_t *meta);
|
||||
|
||||
@@ -7,24 +7,21 @@
|
||||
|
||||
#pragma once
|
||||
#include "save/savelinux.h"
|
||||
#include "save/savestreamlinux.h"
|
||||
|
||||
typedef savelinux_t saveplatform_t;
|
||||
typedef savestreamlinux_t saveplatformstream_t;
|
||||
// Linux fully overrides every save operation with JSON I/O (see
|
||||
// savelinux.c/savejsonlinux.c) - nothing generic ever opens a
|
||||
// savestream_t here, so this only needs to exist for that shared type to
|
||||
// compile.
|
||||
typedef struct {
|
||||
uint8_t reserved;
|
||||
} 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)
|
||||
#define saveSlotDeletePlatform saveDeleteSlotLinux
|
||||
#define saveSlotLoadPlatform saveSlotLoadLinux
|
||||
#define saveSlotWritePlatform saveSlotWriteLinux
|
||||
#define saveMetaLoadPlatform saveMetaLoadLinux
|
||||
#define saveMetaWritePlatform saveMetaWriteLinux
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "save/save.h"
|
||||
#include "save/savestreamlinux.h"
|
||||
#include "util/string.h"
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
static void _saveStreamGetPath(
|
||||
char_t *out, const size_t max, const uint8_t slot
|
||||
) {
|
||||
snprintf(
|
||||
out, max, SAVE_LINUX_FILE_FORMAT,
|
||||
SAVE.platform.savePath, (uint32_t)slot
|
||||
);
|
||||
}
|
||||
|
||||
errorret_t saveStreamOpenReadLinux(
|
||||
savestreamlinux_t *p, bool_t *found, const uint8_t slot
|
||||
) {
|
||||
char_t path[SAVE_LINUX_PATH_MAX];
|
||||
_saveStreamGetPath(path, SAVE_LINUX_PATH_MAX, slot);
|
||||
|
||||
p->file = fopen(path, "rb");
|
||||
*found = (p->file != NULL);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamOpenWriteLinux(savestreamlinux_t *p, const uint8_t slot) {
|
||||
char_t path[SAVE_LINUX_PATH_MAX];
|
||||
_saveStreamGetPath(path, SAVE_LINUX_PATH_MAX, slot);
|
||||
|
||||
p->file = fopen(path, "wb");
|
||||
if(!p->file) {
|
||||
errorThrow("Failed to open save file for writing: slot %u", (uint32_t)slot);
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void saveStreamCloseLinux(savestreamlinux_t *p) {
|
||||
if(p->file) {
|
||||
fclose(p->file);
|
||||
p->file = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t saveStreamReadBytesLinux(
|
||||
savestreamlinux_t *p, void *buf, const size_t len
|
||||
) {
|
||||
if(fread(buf, 1, len, p->file) != len) {
|
||||
errorThrow("Unexpected end of save file");
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamWriteBytesLinux(
|
||||
savestreamlinux_t *p, const void *buf, const size_t len
|
||||
) {
|
||||
if(fwrite(buf, 1, len, p->file) != len) {
|
||||
errorThrow("Failed to write save data");
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveStreamSeekLinux(savestreamlinux_t *p, const size_t pos) {
|
||||
if(fseek(p->file, (long)pos, SEEK_SET) != 0) {
|
||||
errorThrow("Failed to seek in save file");
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include <stdio.h>
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct {
|
||||
FILE *file;
|
||||
} savestreamlinux_t;
|
||||
|
||||
/**
|
||||
* Opens a save slot file for reading.
|
||||
*
|
||||
* @param p Stream to initialize.
|
||||
* @param found Set to true if the file exists, false if it does not.
|
||||
* @param slot Save slot index.
|
||||
* @return An error if the open fails for a reason other than missing file.
|
||||
*/
|
||||
errorret_t saveStreamOpenReadLinux(
|
||||
savestreamlinux_t *p, bool_t *found, const uint8_t slot
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens a save slot file for writing, creating or truncating it.
|
||||
*
|
||||
* @param p Stream to initialize.
|
||||
* @param slot Save slot index.
|
||||
* @return An error if the file cannot be opened for writing.
|
||||
*/
|
||||
errorret_t saveStreamOpenWriteLinux(
|
||||
savestreamlinux_t *p, const uint8_t slot
|
||||
);
|
||||
|
||||
/**
|
||||
* Closes the file handle held by the stream.
|
||||
*
|
||||
* @param p Stream to close.
|
||||
*/
|
||||
void saveStreamCloseLinux(savestreamlinux_t *p);
|
||||
|
||||
/**
|
||||
* Reads len bytes from the stream into buf.
|
||||
*
|
||||
* @param p Active stream.
|
||||
* @param buf Destination buffer.
|
||||
* @param len Number of bytes to read.
|
||||
* @return An error if fewer than len bytes are available.
|
||||
*/
|
||||
errorret_t saveStreamReadBytesLinux(
|
||||
savestreamlinux_t *p, void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Writes len bytes from buf into the stream.
|
||||
*
|
||||
* @param p Active stream.
|
||||
* @param buf Source buffer.
|
||||
* @param len Number of bytes to write.
|
||||
* @return An error if the write fails.
|
||||
*/
|
||||
errorret_t saveStreamWriteBytesLinux(
|
||||
savestreamlinux_t *p, const void *buf, const size_t len
|
||||
);
|
||||
|
||||
/**
|
||||
* Seeks to an absolute byte position within the stream.
|
||||
*
|
||||
* @param p Active stream.
|
||||
* @param pos Target byte offset from the start of the file.
|
||||
* @return An error if the seek fails.
|
||||
*/
|
||||
errorret_t saveStreamSeekLinux(savestreamlinux_t *p, const size_t pos);
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "save/settings.h"
|
||||
#include "util/string.h"
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
|
||||
errorret_t settingsInitLinux(void) {
|
||||
stringCopy(
|
||||
SETTINGS.platform.path, SETTINGS_LINUX_PATH, SETTINGS_LINUX_PATH_MAX
|
||||
);
|
||||
|
||||
if(mkdir(SETTINGS.platform.path, 0755) != 0 && errno != EEXIST) {
|
||||
errorThrow(
|
||||
"Failed to create settings directory: %s", SETTINGS.platform.path
|
||||
);
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t settingsDisposeLinux(void) {
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* 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/settingsfile.h"
|
||||
|
||||
#define SETTINGS_LINUX_PATH_MAX FILENAME_MAX
|
||||
#define SETTINGS_LINUX_FILE_FORMAT "%s/settings.dat"
|
||||
|
||||
#ifndef SETTINGS_LINUX_PATH
|
||||
#define SETTINGS_LINUX_PATH "./saves"
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
char_t path[SETTINGS_LINUX_PATH_MAX];
|
||||
} settingslinux_t;
|
||||
|
||||
/**
|
||||
* Initializes the settings system on Linux - ensures the settings
|
||||
* directory exists, independent of whether the game-save system (see
|
||||
* savelinux.h) has been initialized.
|
||||
*
|
||||
* @return An error code if initialization fails.
|
||||
*/
|
||||
errorret_t settingsInitLinux(void);
|
||||
|
||||
/**
|
||||
* Disposes of the settings system on Linux.
|
||||
*
|
||||
* @return An error code if disposal fails.
|
||||
*/
|
||||
errorret_t settingsDisposeLinux(void);
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "save/settingslinux.h"
|
||||
#include "save/settingsstreamlinux.h"
|
||||
|
||||
typedef settingslinux_t settingsplatform_t;
|
||||
typedef settingsstreamlinux_t settingsplatformstream_t;
|
||||
|
||||
#define settingsInitPlatform settingsInitLinux
|
||||
#define settingsDisposePlatform settingsDisposeLinux
|
||||
|
||||
#define settingsStreamOpenReadPlatform(stream) \
|
||||
settingsStreamOpenReadLinux(&(stream)->platform, &(stream)->found)
|
||||
#define settingsStreamOpenWritePlatform(stream) \
|
||||
settingsStreamOpenWriteLinux(&(stream)->platform)
|
||||
#define settingsStreamClosePlatform(stream) \
|
||||
settingsStreamCloseLinux(&(stream)->platform)
|
||||
#define settingsStreamReadBytesPlatform(stream, buf, len) \
|
||||
settingsStreamReadBytesLinux(&(stream)->platform, buf, len)
|
||||
#define settingsStreamWriteBytesPlatform(stream, buf, len) \
|
||||
settingsStreamWriteBytesLinux(&(stream)->platform, buf, len)
|
||||
#define settingsStreamSeekPlatform(stream, pos) \
|
||||
settingsStreamSeekLinux(&(stream)->platform, pos)
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#include "save/settings.h"
|
||||
#include "save/settingsstreamlinux.h"
|
||||
#include "util/string.h"
|
||||
|
||||
static void _settingsStreamGetPath(char_t *out, const size_t max) {
|
||||
snprintf(out, max, SETTINGS_LINUX_FILE_FORMAT, SETTINGS.platform.path);
|
||||
}
|
||||
|
||||
errorret_t settingsStreamOpenReadLinux(
|
||||
settingsstreamlinux_t *p, bool_t *found
|
||||
) {
|
||||
char_t path[SETTINGS_LINUX_PATH_MAX];
|
||||
_settingsStreamGetPath(path, SETTINGS_LINUX_PATH_MAX);
|
||||
|
||||
p->file = fopen(path, "rb");
|
||||
*found = (p->file != NULL);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t settingsStreamOpenWriteLinux(settingsstreamlinux_t *p) {
|
||||
char_t path[SETTINGS_LINUX_PATH_MAX];
|
||||
_settingsStreamGetPath(path, SETTINGS_LINUX_PATH_MAX);
|
||||
|
||||
p->file = fopen(path, "wb");
|
||||
if(!p->file) {
|
||||
errorThrow("Failed to open settings file for writing");
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void settingsStreamCloseLinux(settingsstreamlinux_t *p) {
|
||||
if(p->file) {
|
||||
fclose(p->file);
|
||||
p->file = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
errorret_t settingsStreamReadBytesLinux(
|
||||
settingsstreamlinux_t *p, void *buf, const size_t len
|
||||
) {
|
||||
if(fread(buf, 1, len, p->file) != len) {
|
||||
errorThrow("Unexpected end of settings file");
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t settingsStreamWriteBytesLinux(
|
||||
settingsstreamlinux_t *p, const void *buf, const size_t len
|
||||
) {
|
||||
if(fwrite(buf, 1, len, p->file) != len) {
|
||||
errorThrow("Failed to write settings data");
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t settingsStreamSeekLinux(settingsstreamlinux_t *p, const size_t pos) {
|
||||
if(fseek(p->file, (long)pos, SEEK_SET) != 0) {
|
||||
errorThrow("Failed to seek in settings file");
|
||||
}
|
||||
errorOk();
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 Dominic Masters
|
||||
*
|
||||
* This software is released under the MIT License.
|
||||
* https://opensource.org/licenses/MIT
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include <stdio.h>
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct {
|
||||
FILE *file;
|
||||
} settingsstreamlinux_t;
|
||||
|
||||
/**
|
||||
* Opens the settings file for reading.
|
||||
*
|
||||
* @param p Stream to initialize.
|
||||
* @param found Set to true if the file exists, false if it does not.
|
||||
* @return An error if the open fails for a reason other than missing file.
|
||||
*/
|
||||
errorret_t settingsStreamOpenReadLinux(
|
||||
settingsstreamlinux_t *p, bool_t *found
|
||||
);
|
||||
|
||||
/**
|
||||
* Opens the settings file for writing, creating or truncating it.
|
||||
*
|
||||
* @param p Stream to initialize.
|
||||
* @return An error if the file cannot be opened for writing.
|
||||
*/
|
||||
errorret_t settingsStreamOpenWriteLinux(settingsstreamlinux_t *p);
|
||||
|
||||
/**
|
||||
* Closes the file handle held by the stream.
|
||||
*
|
||||
* @param p Stream to close.
|
||||
*/
|
||||
void settingsStreamCloseLinux(settingsstreamlinux_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 settingsStreamReadBytesLinux(
|
||||
settingsstreamlinux_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 settingsStreamWriteBytesLinux(
|
||||
settingsstreamlinux_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 settingsStreamSeekLinux(settingsstreamlinux_t *p, const size_t pos);
|
||||
Reference in New Issue
Block a user