Add versioned save slot/settings schemas with strict version checks

Splits saveslot_t/savesettings_t into a stable, version-independent
in-memory struct (save/slot/, save/settings/) and a separate frozen
per-schema-version struct (saveslotver1_t/savesettingsver1_t) used only
for JSON serialization, converted between manually. Every write now
stamps a "version" field (SAVE_SLOT_CURRENT/SAVE_SETTINGS_CURRENT), and
every read requires it to match exactly, rejecting anything else -
laying the groundwork for a real migration path instead of silently
misreading old/incompatible save data.
This commit is contained in:
2026-09-08 12:49:06 -05:00
parent 327c2bec70
commit 8139a0ea54
28 changed files with 634 additions and 115 deletions
+4 -2
View File
@@ -8,6 +8,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
save.c
savedevice.c
saveslot.c
savesettings.c
)
# Subdirs
add_subdirectory(slot)
add_subdirectory(settings)
+2 -2
View File
@@ -7,8 +7,8 @@
#pragma once
#include "savedevice.h"
#include "saveslot.h"
#include "savesettings.h"
#include "save/slot/saveslot.h"
#include "save/settings/savesettings.h"
typedef struct {
// File state
+2 -2
View File
@@ -12,8 +12,8 @@
#if defined(SAVE_DEVICE_DATA_RAW)
#include "yyjson.h"
#include "save/savejson.h"
#include "save/saveslot.h"
#include "save/savesettings.h"
#include "save/slot/saveslot.h"
#include "save/settings/savesettings.h"
#include "util/crypt.h"
#include "util/endian.h"
#include <zlib.h>
+4 -4
View File
@@ -164,10 +164,10 @@ errorret_t saveDeviceDispose(savedevice_t *device);
#if defined(SAVE_DEVICE_DATA_RAW)
// Mirrors the same guarded default in saveslot.h - can't #include that
// header here to get it directly, since saveslot.h itself includes this
// header (savedevice.h), and relying on include order to resolve the cycle
// correctly would be fragile.
// Mirrors the same guarded default in save/slot/saveslot.h - can't
// #include that header here to get it directly, since it itself includes
// this header (savedevice.h), and relying on include order to resolve the
// cycle correctly would be fragile.
#ifndef SAVE_SLOT_COUNT
#define SAVE_SLOT_COUNT 3
#endif
+27
View File
@@ -76,6 +76,33 @@
(dest) = hasInt32(key) ? \
(int32_t)yyjson_get_int(yyjson_obj_get(object, key)) : (int32_t)(def)
/**
* Writes a schema version into the current JSON object's "version" key.
* Pair with requireVersion() on the read side.
*
* @param version The version constant to write (e.g. SAVE_SLOT_CURRENT).
*/
#define writeVersion(version) writeInt32("version", (version))
/**
* Requires the current JSON object's "version" key to exist and exactly
* match the given version constant, throwing otherwise. Used to reject a
* save file outright rather than trying to migrate an old schema.
*
* @param version The expected version constant (e.g. SAVE_SLOT_CURRENT).
*/
#define requireVersion(version) { \
requireInt32("version"); \
int32_t saveJsonVersion; \
readInt32("version", saveJsonVersion, 0); \
if(saveJsonVersion != (version)) { \
errorThrow( \
"Save JSON version mismatch (expected %d, got %d)", \
(int)(version), (int)saveJsonVersion \
); \
} \
}
/**
* Checks if the given key exists on the current JSON object.
*
+12
View File
@@ -0,0 +1,12 @@
# 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
savesettings.c
savesettingscurrent.c
savesettingsver1.c
)
+38
View File
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "savesettings.h"
#include "save/settings/savesettingscurrent.h"
#include "assert/assert.h"
#include "util/memory.h"
void saveSettingsInit(savesettings_t *settings) {
assertNotNull(settings, "Settings cannot be null");
memorySet(settings, 0, sizeof(savesettings_t));
settings->audioMasterVolume = 1.0f;
audiomixerchannel_t channel = 0;
while(channel < AUDIO_MIXER_CHANNEL_COUNT) {
settings->audioChannelVolume[channel] = 1.0f;
channel++;
}
}
errorret_t saveSettingsWriteJSON(
savesettings_t *settings,
yyjson_mut_doc *doc,
yyjson_mut_val *object
) {
errorChain(saveSettingsCurrentWriteJSON(settings, doc, object));
errorOk();
}
errorret_t saveSettingsReadJSON(savesettings_t *settings, yyjson_val *object) {
errorChain(saveSettingsCurrentReadJSON(settings, object));
errorOk();
}
@@ -1,19 +1,24 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "time/timeepoch.h"
#include "savedevice.h"
#include "yyjson.h"
#include "save/savedevice.h"
#include "audio/mixer/audiomixerchannel.h"
#include "yyjson.h"
// The stable, version-independent in-memory shape of save settings - what
// the rest of the engine actually reads/writes, using live engine constants
// (AUDIO_MIXER_CHANNEL_COUNT) freely. Every save schema version
// (savesettingsver1_t, ...) redefines its own frozen copy of these fields
// for its JSON wire format; saveSettingsCurrentWriteJSON()/ReadJSON()
// (savesettingscurrent.c) manually copy field-by-field between this and
// whichever version is current, so a version's on-disk shape can never
// drift just because a live engine constant changes.
typedef struct savesettings_s {
int32_t someSetting;
// Persisted mixer volume preferences - see audiomixerchannelstate_t's own
// comment on why these live here rather than on the mixer itself.
float_t audioMasterVolume;
@@ -24,7 +29,7 @@ typedef struct savesettings_s {
* Inits the save settings with the default state, this is functionally "new
* game" but will not set the player name, as that is what we use to determine
* if a save slot is "in use" or not.
*
*
* @param settings The save settings to init.
*/
void saveSettingsInit(savesettings_t *settings);
@@ -50,4 +55,4 @@ errorret_t saveSettingsWriteJSON(
* @param object The JSON object to read from.
* @return Error state if any.
*/
errorret_t saveSettingsReadJSON(savesettings_t *settings, yyjson_val *object);
errorret_t saveSettingsReadJSON(savesettings_t *settings, yyjson_val *object);
@@ -0,0 +1,66 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "savesettingscurrent.h"
#include "assert/assert.h"
errorret_t saveSettingsCurrentWriteJSON(
savesettings_t *settings,
yyjson_mut_doc *doc,
yyjson_mut_val *object
) {
assertNotNull(settings, "Settings cannot be null");
assertNotNull(doc, "Doc cannot be null");
assertNotNull(object, "Object cannot be null");
savesettingsver1_t ver1;
ver1.audioMasterVolume = settings->audioMasterVolume;
// Bounded by the smaller of the two counts, in case a future live channel
// count ever diverges from this frozen v1 schema's count.
uint8_t channelCount = AUDIO_MIXER_CHANNEL_COUNT;
if(SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT < channelCount) {
channelCount = SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT;
}
for(uint8_t i = 0; i < channelCount; i++) {
ver1.audioChannelVolume[i] = settings->audioChannelVolume[i];
}
for(
uint8_t i = channelCount; i < SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT; i++
) {
ver1.audioChannelVolume[i] = 1.0f;
}
errorChain(saveSettingsVer1WriteJSON(&ver1, doc, object));
errorOk();
}
errorret_t saveSettingsCurrentReadJSON(
savesettings_t *settings,
yyjson_val *object
) {
assertNotNull(settings, "Settings cannot be null");
assertNotNull(object, "Object cannot be null");
savesettingsver1_t ver1;
errorChain(saveSettingsVer1ReadJSON(&ver1, object));
settings->audioMasterVolume = ver1.audioMasterVolume;
uint8_t channelCount = AUDIO_MIXER_CHANNEL_COUNT;
if(SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT < channelCount) {
channelCount = SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT;
}
for(uint8_t i = 0; i < channelCount; i++) {
settings->audioChannelVolume[i] = ver1.audioChannelVolume[i];
}
for(uint8_t i = channelCount; i < AUDIO_MIXER_CHANNEL_COUNT; i++) {
settings->audioChannelVolume[i] = 1.0f;
}
errorOk();
}
@@ -0,0 +1,40 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/settings/savesettings.h"
#include "save/settings/savesettingsver1.h"
/**
* Converts the given save settings to whichever schema SAVE_SETTINGS_CURRENT
* (savesettingsver.h) selects and writes it out to the given JSON object.
* Update this (only) when bumping to a new SAVE_SETTINGS_VER_N.
*
* @param settings The save settings to write.
* @param doc The mutable JSON document that owns the object.
* @param object The mutable JSON object to write into.
* @return Error state if any.
*/
errorret_t saveSettingsCurrentWriteJSON(
savesettings_t *settings,
yyjson_mut_doc *doc,
yyjson_mut_val *object
);
/**
* Reads whichever schema SAVE_SETTINGS_CURRENT (savesettingsver.h) selects
* from the given JSON object and converts it into the given save settings.
* Update this (only) when bumping to a new SAVE_SETTINGS_VER_N.
*
* @param settings The save settings to read into.
* @param object The JSON object to read from.
* @return Error state if any.
*/
errorret_t saveSettingsCurrentReadJSON(
savesettings_t *settings,
yyjson_val *object
);
+15
View File
@@ -0,0 +1,15 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "time/timeepoch.h"
#include "save/savedevice.h"
#include "yyjson.h"
#define SAVE_SETTINGS_VER_1 1
#define SAVE_SETTINGS_CURRENT SAVE_SETTINGS_VER_1
@@ -1,30 +1,16 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "savesettings.h"
#include "savesettingsver1.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "savejson.h"
#include "save/savejson.h"
void saveSettingsInit(savesettings_t *settings) {
assertNotNull(settings, "Settings cannot be null");
memorySet(settings, 0, sizeof(savesettings_t));
settings->audioMasterVolume = 1.0f;
audiomixerchannel_t channel = 0;
while(channel < AUDIO_MIXER_CHANNEL_COUNT) {
settings->audioChannelVolume[channel] = 1.0f;
channel++;
}
}
errorret_t saveSettingsWriteJSON(
savesettings_t *settings,
errorret_t saveSettingsVer1WriteJSON(
savesettingsver1_t *settings,
yyjson_mut_doc *doc,
yyjson_mut_val *object
) {
@@ -32,26 +18,30 @@ errorret_t saveSettingsWriteJSON(
assertNotNull(doc, "Doc cannot be null");
assertNotNull(object, "Object cannot be null");
writeInt32("someSetting", settings->someSetting);
writeVersion(SAVE_SETTINGS_CURRENT);
writeFloat("audioMasterVolume", settings->audioMasterVolume);
writeFloatArray(
"audioChannelVolume", settings->audioChannelVolume,
AUDIO_MIXER_CHANNEL_COUNT
SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT
);
errorOk();
}
errorret_t saveSettingsReadJSON(savesettings_t *settings, yyjson_val *object) {
errorret_t saveSettingsVer1ReadJSON(
savesettingsver1_t *settings,
yyjson_val *object
) {
assertNotNull(settings, "Settings cannot be null");
assertNotNull(object, "Object cannot be null");
readInt32("someSetting", settings->someSetting, 0);
requireVersion(SAVE_SETTINGS_CURRENT);
readFloat("audioMasterVolume", settings->audioMasterVolume, 1.0f);
readFloatArray(
"audioChannelVolume", settings->audioChannelVolume,
AUDIO_MIXER_CHANNEL_COUNT, 1.0f
SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT, 1.0f
);
errorOk();
}
}
+55
View File
@@ -0,0 +1,55 @@
/**
* 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/settings/savesettingsver.h"
#include "yyjson.h"
// This schema's own frozen channel count - never reference
// AUDIO_MIXER_CHANNEL_COUNT (or any other live constant) here. Once
// shipped, this file's on-disk shape must never change underneath old save
// files, even if the live engine constant does.
#define SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT 7
// Independent from savesettings_t (save/settings/savesettings.h) by design
// - this is what version 1 actually put on disk, frozen forever.
// saveSettingsCurrentWriteJSON()/ReadJSON() (savesettingscurrent.c) manually
// copy field-by-field between the two, since they're separate types that
// merely happen to currently look alike.
typedef struct savesettingsver1_s {
float_t audioMasterVolume;
float_t audioChannelVolume[SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT];
} savesettingsver1_t;
/**
* Writes the given v1 save settings data out to the given JSON object,
* stamping SAVE_SETTINGS_CURRENT as the "version" field.
*
* @param settings The v1 save settings data to write.
* @param doc The mutable JSON document that owns the object.
* @param object The mutable JSON object to write into.
* @return Error state if any.
*/
errorret_t saveSettingsVer1WriteJSON(
savesettingsver1_t *settings,
yyjson_mut_doc *doc,
yyjson_mut_val *object
);
/**
* Reads v1 save settings data in from the given JSON object. Throws if the
* JSON's "version" is missing or doesn't match SAVE_SETTINGS_CURRENT.
*
* @param settings The v1 save settings data to read into.
* @param object The JSON object to read from.
* @return Error state if any.
*/
errorret_t saveSettingsVer1ReadJSON(
savesettingsver1_t *settings,
yyjson_val *object
);
+12
View File
@@ -0,0 +1,12 @@
# 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
saveslot.c
saveslotcurrent.c
saveslotver1.c
)
@@ -1,15 +1,14 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "saveslot.h"
#include "save/slot/saveslotcurrent.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "savejson.h"
#include "time/time.h"
void saveSlotInit(saveslot_t *slot) {
assertNotNull(slot, "Slot cannot be null");
@@ -33,29 +32,11 @@ errorret_t saveSlotWriteJSON(
yyjson_mut_doc *doc,
yyjson_mut_val *object
) {
assertNotNull(slot, "Slot cannot be null");
assertNotNull(doc, "Doc cannot be null");
assertNotNull(object, "Object cannot be null");
// Update time
slot->cachedData.time = timeGetEpoch();
writeString("name", slot->cachedData.name);
writeTime("time", slot->cachedData.time);
writeInt32("playerLevel", slot->cachedData.playerLevel);
errorChain(saveSlotCurrentWriteJSON(slot, doc, object));
errorOk();
}
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object) {
assertNotNull(slot, "Slot cannot be null");
assertNotNull(object, "Object cannot be null");
char_t saveJsonStringBuffer[SAVE_JSON_STRING_BUFFER_SIZE];
readString("name", slot->cachedData.name, "", SAVE_SLOT_NAME_LENGTH);
readTime("time", slot->cachedData.time);
readInt32("playerLevel", slot->cachedData.playerLevel, 1);
errorChain(saveSlotCurrentReadJSON(slot, object));
errorOk();
}
}
@@ -1,13 +1,14 @@
/**
* Copyright (c) 2026 Dominic Masters
*
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/savedevice.h"
#include "rpg/overworld/map.h"
#include "time/timeepoch.h"
#include "savedevice.h"
#include "yyjson.h"
#define SAVE_SLOT_NAME_LENGTH 8
@@ -15,10 +16,18 @@
#define SAVE_SLOT_COUNT 3
#endif
// The stable, version-independent in-memory shape of a save slot - what the
// rest of the engine actually reads/writes. Every save schema version
// (saveslotver1_t, ...) redefines its own copy of these same fields for its
// JSON wire format; saveSlotCurrentWriteJSON()/ReadJSON() (saveslotcurrent.c)
// manually copy field-by-field between this and whichever version is
// current, so a version's frozen on-disk shape can never drift just because
// this stable struct (or a live engine constant like MAP_NAME_MAX) changes.
typedef struct {
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
dusktimeepoch_t time;
int32_t playerLevel;
char_t mapName[MAP_NAME_MAX];
} saveslotcache_t;
typedef struct saveslot_s {
@@ -32,7 +41,7 @@ typedef struct saveslot_s {
* Inits the save slot with the default state, this is functionally "new game"
* but will not set the player name, as that is what we use to determine if a
* save slot is "in use" or not.
*
*
* @param slot The save slot to init.
*/
void saveSlotInit(saveslot_t *slot);
@@ -40,7 +49,7 @@ void saveSlotInit(saveslot_t *slot);
/**
* Checks if the save slot is in use, this is determined by checking if the
* player name is set or not.
*
*
* @param slot The save slot to check.
* @return True if the save slot is in use, false otherwise.
*/
@@ -48,7 +57,7 @@ bool_t saveSlotInUse(saveslotcache_t *slot);
/**
* Returns whether or not the given save slot has ever saved (has an epoc non 0)
*
*
* @param slot The save slot to check.
* @return True if the save slot has ever saved, false otherwise.
*/
@@ -75,4 +84,4 @@ errorret_t saveSlotWriteJSON(
* @param object The JSON object to read from.
* @return Error state if any.
*/
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object);
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object);
+51
View File
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "saveslotcurrent.h"
#include "assert/assert.h"
#include "util/string.h"
#include "time/time.h"
errorret_t saveSlotCurrentWriteJSON(
saveslot_t *slot,
yyjson_mut_doc *doc,
yyjson_mut_val *object
) {
assertNotNull(slot, "Slot cannot be null");
assertNotNull(doc, "Doc cannot be null");
assertNotNull(object, "Object cannot be null");
// Stamped on the stable struct (not the throwaway ver1 copy below) so the
// fresh timestamp survives the write - saveSlotHasSaved() depends on it.
slot->cachedData.time = timeGetEpoch();
saveslotver1_t ver1;
stringCopy(ver1.name, slot->cachedData.name, sizeof(ver1.name));
ver1.time = slot->cachedData.time;
ver1.playerLevel = slot->cachedData.playerLevel;
stringCopy(ver1.mapName, slot->cachedData.mapName, sizeof(ver1.mapName));
errorChain(saveSlotVer1WriteJSON(&ver1, doc, object));
errorOk();
}
errorret_t saveSlotCurrentReadJSON(saveslot_t *slot, yyjson_val *object) {
assertNotNull(slot, "Slot cannot be null");
assertNotNull(object, "Object cannot be null");
saveslotver1_t ver1;
errorChain(saveSlotVer1ReadJSON(&ver1, object));
stringCopy(slot->cachedData.name, ver1.name, sizeof(slot->cachedData.name));
slot->cachedData.time = ver1.time;
slot->cachedData.playerLevel = ver1.playerLevel;
stringCopy(
slot->cachedData.mapName, ver1.mapName, sizeof(slot->cachedData.mapName)
);
errorOk();
}
+37
View File
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
#include "save/slot/saveslot.h"
#include "save/slot/saveslotver1.h"
/**
* Converts the given save slot to whichever schema SAVE_SLOT_CURRENT
* (saveslotver.h) selects and writes it out to the given JSON object.
* Update this (only) when bumping to a new SAVE_SLOT_VER_N.
*
* @param slot The save slot to write.
* @param doc The mutable JSON document that owns the object.
* @param object The mutable JSON object to write into.
* @return Error state if any.
*/
errorret_t saveSlotCurrentWriteJSON(
saveslot_t *slot,
yyjson_mut_doc *doc,
yyjson_mut_val *object
);
/**
* Reads whichever schema SAVE_SLOT_CURRENT (saveslotver.h) selects from the
* given JSON object and converts it into the given save slot. Update this
* (only) when bumping to a new SAVE_SLOT_VER_N.
*
* @param slot The save slot to read into.
* @param object The JSON object to read from.
* @return Error state if any.
*/
errorret_t saveSlotCurrentReadJSON(saveslot_t *slot, yyjson_val *object);
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#pragma once
// One constant per save slot JSON schema this codebase has ever shipped.
// Never remove or renumber an existing entry - old save files on disk may
// still declare it.
#define SAVE_SLOT_VER_1 1
// The schema saveSlotCurrentWriteJSON()/saveSlotCurrentReadJSON() use today.
// Bump this to a new SAVE_SLOT_VER_N when the slot JSON shape changes.
#define SAVE_SLOT_CURRENT SAVE_SLOT_VER_1
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2026 Dominic Masters
*
* This software is released under the MIT License.
* https://opensource.org/licenses/MIT
*/
#include "saveslotver1.h"
#include "assert/assert.h"
#include "save/savejson.h"
errorret_t saveSlotVer1WriteJSON(
saveslotver1_t *slot,
yyjson_mut_doc *doc,
yyjson_mut_val *object
) {
assertNotNull(slot, "Slot cannot be null");
assertNotNull(doc, "Doc cannot be null");
assertNotNull(object, "Object cannot be null");
writeVersion(SAVE_SLOT_CURRENT);
writeString("name", slot->name);
writeTime("time", slot->time);
writeInt32("playerLevel", slot->playerLevel);
writeString("mapName", slot->mapName);
errorOk();
}
errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object) {
assertNotNull(slot, "Slot cannot be null");
assertNotNull(object, "Object cannot be null");
char_t saveJsonStringBuffer[SAVE_JSON_STRING_BUFFER_SIZE];
requireVersion(SAVE_SLOT_CURRENT);
readString("name", slot->name, "", SAVE_SLOT_VER1_NAME_LENGTH);
readTime("time", slot->time);
readInt32("playerLevel", slot->playerLevel, 1);
readString(
"mapName", slot->mapName, "", SAVE_SLOT_VER1_MAP_NAME_MAX - 1
);
errorOk();
}
+56
View File
@@ -0,0 +1,56 @@
/**
* 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 "time/timeepoch.h"
#include "save/slot/saveslotver.h"
#include "yyjson.h"
// This schema's own frozen copies of save/slot/saveslot.h's field sizes -
// never reference SAVE_SLOT_NAME_LENGTH/MAP_NAME_MAX (or any other live
// constant) here. Once shipped, this file's on-disk shape must never change
// underneath old save files, even if the live engine constants do.
#define SAVE_SLOT_VER1_NAME_LENGTH 8
#define SAVE_SLOT_VER1_MAP_NAME_MAX 10
// Independent from saveslotcache_t (save/slot/saveslot.h) by design - this
// is what version 1 actually put on disk, frozen forever. saveSlotCurrent
// WriteJSON()/ReadJSON() (saveslotcurrent.c) manually copy field-by-field
// between the two, since they're separate types that merely happen to
// currently look alike.
typedef struct saveslotver1_s {
char_t name[SAVE_SLOT_VER1_NAME_LENGTH + 1];
dusktimeepoch_t time;
int32_t playerLevel;
char_t mapName[SAVE_SLOT_VER1_MAP_NAME_MAX];
} saveslotver1_t;
/**
* Writes the given v1 save slot data out to the given JSON object, stamping
* SAVE_SLOT_CURRENT as the "version" field.
*
* @param slot The v1 save slot data to write.
* @param doc The mutable JSON document that owns the object.
* @param object The mutable JSON object to write into.
* @return Error state if any.
*/
errorret_t saveSlotVer1WriteJSON(
saveslotver1_t *slot,
yyjson_mut_doc *doc,
yyjson_mut_val *object
);
/**
* Reads v1 save slot data in from the given JSON object. Throws if the
* JSON's "version" is missing or doesn't match SAVE_SLOT_CURRENT.
*
* @param slot The v1 save slot data to read into.
* @param object The JSON object to read from.
* @return Error state if any.
*/
errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object);
+1 -1
View File
@@ -11,7 +11,7 @@
#include "ui/widget/uimenu.h"
#include "ui/widget/uiscrolling.h"
#include "ui/dialog/save/uisaveslot.h"
#include "save/saveslot.h"
#include "save/slot/saveslot.h"
#define UI_SELECT_SAVE_TITLE_TEXT_MAX 64
#define UI_SELECT_SAVE_TITLE_SPRITES_MAX UI_SELECT_SAVE_TITLE_TEXT_MAX
+2 -2
View File
@@ -7,8 +7,8 @@
#include "save/savedevice.h"
#include "save/savedevicelinux.h"
#include "save/saveslot.h"
#include "save/savesettings.h"
#include "save/slot/saveslot.h"
#include "save/settings/savesettings.h"
#include "save/savejson.h"
#include "asset/asset.h"
#include "assert/assert.h"