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:
@@ -8,6 +8,8 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
|||||||
PUBLIC
|
PUBLIC
|
||||||
save.c
|
save.c
|
||||||
savedevice.c
|
savedevice.c
|
||||||
saveslot.c
|
|
||||||
savesettings.c
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Subdirs
|
||||||
|
add_subdirectory(slot)
|
||||||
|
add_subdirectory(settings)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "savedevice.h"
|
#include "savedevice.h"
|
||||||
#include "saveslot.h"
|
#include "save/slot/saveslot.h"
|
||||||
#include "savesettings.h"
|
#include "save/settings/savesettings.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
// File state
|
// File state
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||||
#include "yyjson.h"
|
#include "yyjson.h"
|
||||||
#include "save/savejson.h"
|
#include "save/savejson.h"
|
||||||
#include "save/saveslot.h"
|
#include "save/slot/saveslot.h"
|
||||||
#include "save/savesettings.h"
|
#include "save/settings/savesettings.h"
|
||||||
#include "util/crypt.h"
|
#include "util/crypt.h"
|
||||||
#include "util/endian.h"
|
#include "util/endian.h"
|
||||||
#include <zlib.h>
|
#include <zlib.h>
|
||||||
|
|||||||
@@ -164,10 +164,10 @@ errorret_t saveDeviceDispose(savedevice_t *device);
|
|||||||
|
|
||||||
#if defined(SAVE_DEVICE_DATA_RAW)
|
#if defined(SAVE_DEVICE_DATA_RAW)
|
||||||
|
|
||||||
// Mirrors the same guarded default in saveslot.h - can't #include that
|
// Mirrors the same guarded default in save/slot/saveslot.h - can't
|
||||||
// header here to get it directly, since saveslot.h itself includes this
|
// #include that header here to get it directly, since it itself includes
|
||||||
// header (savedevice.h), and relying on include order to resolve the cycle
|
// this header (savedevice.h), and relying on include order to resolve the
|
||||||
// correctly would be fragile.
|
// cycle correctly would be fragile.
|
||||||
#ifndef SAVE_SLOT_COUNT
|
#ifndef SAVE_SLOT_COUNT
|
||||||
#define SAVE_SLOT_COUNT 3
|
#define SAVE_SLOT_COUNT 3
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -76,6 +76,33 @@
|
|||||||
(dest) = hasInt32(key) ? \
|
(dest) = hasInt32(key) ? \
|
||||||
(int32_t)yyjson_get_int(yyjson_obj_get(object, key)) : (int32_t)(def)
|
(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.
|
* Checks if the given key exists on the current JSON object.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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
|
* Copyright (c) 2026 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "time/timeepoch.h"
|
#include "save/savedevice.h"
|
||||||
#include "savedevice.h"
|
|
||||||
#include "yyjson.h"
|
|
||||||
#include "audio/mixer/audiomixerchannel.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 {
|
typedef struct savesettings_s {
|
||||||
int32_t someSetting;
|
|
||||||
|
|
||||||
// Persisted mixer volume preferences - see audiomixerchannelstate_t's own
|
// Persisted mixer volume preferences - see audiomixerchannelstate_t's own
|
||||||
// comment on why these live here rather than on the mixer itself.
|
// comment on why these live here rather than on the mixer itself.
|
||||||
float_t audioMasterVolume;
|
float_t audioMasterVolume;
|
||||||
@@ -24,7 +29,7 @@ typedef struct savesettings_s {
|
|||||||
* Inits the save settings with the default state, this is functionally "new
|
* 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
|
* 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.
|
* if a save slot is "in use" or not.
|
||||||
*
|
*
|
||||||
* @param settings The save settings to init.
|
* @param settings The save settings to init.
|
||||||
*/
|
*/
|
||||||
void saveSettingsInit(savesettings_t *settings);
|
void saveSettingsInit(savesettings_t *settings);
|
||||||
@@ -50,4 +55,4 @@ errorret_t saveSettingsWriteJSON(
|
|||||||
* @param object The JSON object to read from.
|
* @param object The JSON object to read from.
|
||||||
* @return Error state if any.
|
* @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
|
||||||
|
);
|
||||||
@@ -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
|
* Copyright (c) 2026 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "savesettings.h"
|
#include "savesettingsver1.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "save/savejson.h"
|
||||||
#include "savejson.h"
|
|
||||||
|
|
||||||
void saveSettingsInit(savesettings_t *settings) {
|
errorret_t saveSettingsVer1WriteJSON(
|
||||||
assertNotNull(settings, "Settings cannot be null");
|
savesettingsver1_t *settings,
|
||||||
|
|
||||||
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_doc *doc,
|
||||||
yyjson_mut_val *object
|
yyjson_mut_val *object
|
||||||
) {
|
) {
|
||||||
@@ -32,26 +18,30 @@ errorret_t saveSettingsWriteJSON(
|
|||||||
assertNotNull(doc, "Doc cannot be null");
|
assertNotNull(doc, "Doc cannot be null");
|
||||||
assertNotNull(object, "Object cannot be null");
|
assertNotNull(object, "Object cannot be null");
|
||||||
|
|
||||||
writeInt32("someSetting", settings->someSetting);
|
writeVersion(SAVE_SETTINGS_CURRENT);
|
||||||
writeFloat("audioMasterVolume", settings->audioMasterVolume);
|
writeFloat("audioMasterVolume", settings->audioMasterVolume);
|
||||||
writeFloatArray(
|
writeFloatArray(
|
||||||
"audioChannelVolume", settings->audioChannelVolume,
|
"audioChannelVolume", settings->audioChannelVolume,
|
||||||
AUDIO_MIXER_CHANNEL_COUNT
|
SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT
|
||||||
);
|
);
|
||||||
|
|
||||||
errorOk();
|
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(settings, "Settings cannot be null");
|
||||||
assertNotNull(object, "Object cannot be null");
|
assertNotNull(object, "Object cannot be null");
|
||||||
|
|
||||||
readInt32("someSetting", settings->someSetting, 0);
|
requireVersion(SAVE_SETTINGS_CURRENT);
|
||||||
|
|
||||||
readFloat("audioMasterVolume", settings->audioMasterVolume, 1.0f);
|
readFloat("audioMasterVolume", settings->audioMasterVolume, 1.0f);
|
||||||
readFloatArray(
|
readFloatArray(
|
||||||
"audioChannelVolume", settings->audioChannelVolume,
|
"audioChannelVolume", settings->audioChannelVolume,
|
||||||
AUDIO_MIXER_CHANNEL_COUNT, 1.0f
|
SAVE_SETTINGS_VER1_AUDIO_CHANNEL_COUNT, 1.0f
|
||||||
);
|
);
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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
|
* Copyright (c) 2026 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "saveslot.h"
|
#include "saveslot.h"
|
||||||
|
#include "save/slot/saveslotcurrent.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "savejson.h"
|
|
||||||
#include "time/time.h"
|
|
||||||
|
|
||||||
void saveSlotInit(saveslot_t *slot) {
|
void saveSlotInit(saveslot_t *slot) {
|
||||||
assertNotNull(slot, "Slot cannot be null");
|
assertNotNull(slot, "Slot cannot be null");
|
||||||
@@ -33,29 +32,11 @@ errorret_t saveSlotWriteJSON(
|
|||||||
yyjson_mut_doc *doc,
|
yyjson_mut_doc *doc,
|
||||||
yyjson_mut_val *object
|
yyjson_mut_val *object
|
||||||
) {
|
) {
|
||||||
assertNotNull(slot, "Slot cannot be null");
|
errorChain(saveSlotCurrentWriteJSON(slot, doc, object));
|
||||||
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);
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object) {
|
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object) {
|
||||||
assertNotNull(slot, "Slot cannot be null");
|
errorChain(saveSlotCurrentReadJSON(slot, object));
|
||||||
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);
|
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2026 Dominic Masters
|
* Copyright (c) 2026 Dominic Masters
|
||||||
*
|
*
|
||||||
* This software is released under the MIT License.
|
* This software is released under the MIT License.
|
||||||
* https://opensource.org/licenses/MIT
|
* https://opensource.org/licenses/MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include "save/savedevice.h"
|
||||||
|
#include "rpg/overworld/map.h"
|
||||||
#include "time/timeepoch.h"
|
#include "time/timeepoch.h"
|
||||||
#include "savedevice.h"
|
|
||||||
#include "yyjson.h"
|
#include "yyjson.h"
|
||||||
|
|
||||||
#define SAVE_SLOT_NAME_LENGTH 8
|
#define SAVE_SLOT_NAME_LENGTH 8
|
||||||
@@ -15,10 +16,18 @@
|
|||||||
#define SAVE_SLOT_COUNT 3
|
#define SAVE_SLOT_COUNT 3
|
||||||
#endif
|
#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 {
|
typedef struct {
|
||||||
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
|
char_t name[SAVE_SLOT_NAME_LENGTH + 1];// 8 characters + null terminator
|
||||||
dusktimeepoch_t time;
|
dusktimeepoch_t time;
|
||||||
int32_t playerLevel;
|
int32_t playerLevel;
|
||||||
|
char_t mapName[MAP_NAME_MAX];
|
||||||
} saveslotcache_t;
|
} saveslotcache_t;
|
||||||
|
|
||||||
typedef struct saveslot_s {
|
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"
|
* 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
|
* but will not set the player name, as that is what we use to determine if a
|
||||||
* save slot is "in use" or not.
|
* save slot is "in use" or not.
|
||||||
*
|
*
|
||||||
* @param slot The save slot to init.
|
* @param slot The save slot to init.
|
||||||
*/
|
*/
|
||||||
void saveSlotInit(saveslot_t *slot);
|
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
|
* Checks if the save slot is in use, this is determined by checking if the
|
||||||
* player name is set or not.
|
* player name is set or not.
|
||||||
*
|
*
|
||||||
* @param slot The save slot to check.
|
* @param slot The save slot to check.
|
||||||
* @return True if the save slot is in use, false otherwise.
|
* @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)
|
* Returns whether or not the given save slot has ever saved (has an epoc non 0)
|
||||||
*
|
*
|
||||||
* @param slot The save slot to check.
|
* @param slot The save slot to check.
|
||||||
* @return True if the save slot has ever saved, false otherwise.
|
* @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.
|
* @param object The JSON object to read from.
|
||||||
* @return Error state if any.
|
* @return Error state if any.
|
||||||
*/
|
*/
|
||||||
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object);
|
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object);
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
#include "ui/widget/uimenu.h"
|
#include "ui/widget/uimenu.h"
|
||||||
#include "ui/widget/uiscrolling.h"
|
#include "ui/widget/uiscrolling.h"
|
||||||
#include "ui/dialog/save/uisaveslot.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_TEXT_MAX 64
|
||||||
#define UI_SELECT_SAVE_TITLE_SPRITES_MAX UI_SELECT_SAVE_TITLE_TEXT_MAX
|
#define UI_SELECT_SAVE_TITLE_SPRITES_MAX UI_SELECT_SAVE_TITLE_TEXT_MAX
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
|
|
||||||
#include "save/savedevice.h"
|
#include "save/savedevice.h"
|
||||||
#include "save/savedevicelinux.h"
|
#include "save/savedevicelinux.h"
|
||||||
#include "save/saveslot.h"
|
#include "save/slot/saveslot.h"
|
||||||
#include "save/savesettings.h"
|
#include "save/settings/savesettings.h"
|
||||||
#include "save/savejson.h"
|
#include "save/savejson.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
|
|||||||
+10
-10
@@ -57,7 +57,7 @@ static void test_saveInit_setsDefaults(void **state) {
|
|||||||
assert_int_equal(SAVE.slotCurrent, 0xFF);
|
assert_int_equal(SAVE.slotCurrent, 0xFF);
|
||||||
assert_false(SAVE.findingAvailableDevice);
|
assert_false(SAVE.findingAvailableDevice);
|
||||||
assert_false(SAVE.noAvailableDeviceFound);
|
assert_false(SAVE.noAvailableDeviceFound);
|
||||||
assert_int_equal(SAVE.settings.someSetting, 0);
|
assert_true(SAVE.settings.audioMasterVolume == 1.0f);
|
||||||
assert_int_equal(SAVE.devices[0].state, SAVE_DEVICE_STATE_UNKNOWN);
|
assert_int_equal(SAVE.devices[0].state, SAVE_DEVICE_STATE_UNKNOWN);
|
||||||
|
|
||||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
@@ -229,7 +229,7 @@ static void test_saveSaveSettings_notDirty_isNoop(void **state) {
|
|||||||
// if someone changes the no-op condition without meaning to.
|
// if someone changes the no-op condition without meaning to.
|
||||||
makeDeviceAvailable();
|
makeDeviceAvailable();
|
||||||
assert_false(SAVE.settingsDirty);
|
assert_false(SAVE.settingsDirty);
|
||||||
SAVE.settings.someSetting = 123;
|
SAVE.settings.audioMasterVolume = 12.0f;
|
||||||
|
|
||||||
errorret_t ret = saveSaveSettings();
|
errorret_t ret = saveSaveSettings();
|
||||||
assert_true(errorIsOk(ret));
|
assert_true(errorIsOk(ret));
|
||||||
@@ -238,7 +238,7 @@ static void test_saveSaveSettings_notDirty_isNoop(void **state) {
|
|||||||
saveSettingsInit(&onDisk);
|
saveSettingsInit(&onDisk);
|
||||||
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
||||||
assert_true(errorIsOk(readRet));
|
assert_true(errorIsOk(readRet));
|
||||||
assert_int_equal(onDisk.someSetting, 0);// never actually written
|
assert_true(onDisk.audioMasterVolume == 1.0f);// never actually written
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_saveSaveSettings_dirtyForced_writesAndClearsFlag(
|
static void test_saveSaveSettings_dirtyForced_writesAndClearsFlag(
|
||||||
@@ -246,7 +246,7 @@ static void test_saveSaveSettings_dirtyForced_writesAndClearsFlag(
|
|||||||
) {
|
) {
|
||||||
makeDeviceAvailable();
|
makeDeviceAvailable();
|
||||||
SAVE.settingsDirty = true;// nothing in real code sets this - forced here
|
SAVE.settingsDirty = true;// nothing in real code sets this - forced here
|
||||||
SAVE.settings.someSetting = 55;
|
SAVE.settings.audioMasterVolume = 5.5f;
|
||||||
|
|
||||||
errorret_t ret = saveSaveSettings();
|
errorret_t ret = saveSaveSettings();
|
||||||
assert_true(errorIsOk(ret));
|
assert_true(errorIsOk(ret));
|
||||||
@@ -256,7 +256,7 @@ static void test_saveSaveSettings_dirtyForced_writesAndClearsFlag(
|
|||||||
saveSettingsInit(&onDisk);
|
saveSettingsInit(&onDisk);
|
||||||
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
||||||
assert_true(errorIsOk(readRet));
|
assert_true(errorIsOk(readRet));
|
||||||
assert_int_equal(onDisk.someSetting, 55);
|
assert_true(onDisk.audioMasterVolume == 5.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_saveLoadSettings_noCurrentDeviceAsserts(void **state) {
|
static void test_saveLoadSettings_noCurrentDeviceAsserts(void **state) {
|
||||||
@@ -268,16 +268,16 @@ static void test_saveLoadSettings_readsAndClearsDirtyFlag(void **state) {
|
|||||||
|
|
||||||
savesettings_t onDisk;
|
savesettings_t onDisk;
|
||||||
saveSettingsInit(&onDisk);
|
saveSettingsInit(&onDisk);
|
||||||
onDisk.someSetting = 99;
|
onDisk.audioMasterVolume = 9.25f;
|
||||||
errorret_t writeRet = saveDeviceSettingsWrite(&SAVE.devices[0], &onDisk);
|
errorret_t writeRet = saveDeviceSettingsWrite(&SAVE.devices[0], &onDisk);
|
||||||
assert_true(errorIsOk(writeRet));
|
assert_true(errorIsOk(writeRet));
|
||||||
|
|
||||||
SAVE.settingsDirty = true;
|
SAVE.settingsDirty = true;
|
||||||
SAVE.settings.someSetting = 0;
|
SAVE.settings.audioMasterVolume = 0.0f;
|
||||||
|
|
||||||
errorret_t ret = saveLoadSettings();
|
errorret_t ret = saveLoadSettings();
|
||||||
assert_true(errorIsOk(ret));
|
assert_true(errorIsOk(ret));
|
||||||
assert_int_equal(SAVE.settings.someSetting, 99);
|
assert_true(SAVE.settings.audioMasterVolume == 9.25f);
|
||||||
assert_false(SAVE.settingsDirty);
|
assert_false(SAVE.settingsDirty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,7 +470,7 @@ static void test_saveUpdate_foundDevice_rewritesSettingsUnconditionally(
|
|||||||
// it, regardless of settingsDirty - independent of (and unguarded by) the
|
// it, regardless of settingsDirty - independent of (and unguarded by) the
|
||||||
// same dirty-flag check saveSaveSettings() itself honors.
|
// same dirty-flag check saveSaveSettings() itself honors.
|
||||||
resetFindCallbackState();
|
resetFindCallbackState();
|
||||||
SAVE.settings.someSetting = 321;
|
SAVE.settings.audioMasterVolume = 3.25f;
|
||||||
assert_false(SAVE.settingsDirty);
|
assert_false(SAVE.settingsDirty);
|
||||||
|
|
||||||
saveFindAvailableDevice(findAvailableCallback, NULL);
|
saveFindAvailableDevice(findAvailableCallback, NULL);
|
||||||
@@ -482,7 +482,7 @@ static void test_saveUpdate_foundDevice_rewritesSettingsUnconditionally(
|
|||||||
saveSettingsInit(&onDisk);
|
saveSettingsInit(&onDisk);
|
||||||
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
errorret_t readRet = saveDeviceSettingsRead(&SAVE.devices[0], &onDisk);
|
||||||
assert_true(errorIsOk(readRet));
|
assert_true(errorIsOk(readRet));
|
||||||
assert_int_equal(onDisk.someSetting, 321);
|
assert_true(onDisk.audioMasterVolume == 3.25f);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
#include "dusktest.h"
|
#include "dusktest.h"
|
||||||
#include "savetestfixture.h"
|
#include "savetestfixture.h"
|
||||||
#include "save/savedevice.h"
|
#include "save/savedevice.h"
|
||||||
#include "save/saveslot.h"
|
#include "save/slot/saveslot.h"
|
||||||
#include "save/savesettings.h"
|
#include "save/settings/savesettings.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
|
|
||||||
@@ -224,7 +224,7 @@ static void test_saveDeviceSettings_dispatchesToPlatform(void **state) {
|
|||||||
|
|
||||||
savesettings_t written;
|
savesettings_t written;
|
||||||
saveSettingsInit(&written);
|
saveSettingsInit(&written);
|
||||||
written.someSetting = 88;
|
written.audioMasterVolume = 0.25f;// exactly representable, safe to compare
|
||||||
|
|
||||||
errorret_t writeRet = saveDeviceSettingsWrite(&device, &written);
|
errorret_t writeRet = saveDeviceSettingsWrite(&device, &written);
|
||||||
assert_true(errorIsOk(writeRet));
|
assert_true(errorIsOk(writeRet));
|
||||||
@@ -234,7 +234,7 @@ static void test_saveDeviceSettings_dispatchesToPlatform(void **state) {
|
|||||||
errorret_t readRet = saveDeviceSettingsRead(&device, &read);
|
errorret_t readRet = saveDeviceSettingsRead(&device, &read);
|
||||||
assert_true(errorIsOk(readRet));
|
assert_true(errorIsOk(readRet));
|
||||||
|
|
||||||
assert_int_equal(read.someSetting, 88);
|
assert_true(read.audioMasterVolume == 0.25f);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
#include "savetestfixture.h"
|
#include "savetestfixture.h"
|
||||||
#include "save/savedevice.h"
|
#include "save/savedevice.h"
|
||||||
#include "save/savedevicelinux.h"
|
#include "save/savedevicelinux.h"
|
||||||
#include "save/saveslot.h"
|
#include "save/slot/saveslot.h"
|
||||||
#include "save/savesettings.h"
|
#include "save/settings/savesettings.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -298,7 +298,7 @@ static void test_settingsWriteRead_roundTrip(void **state) {
|
|||||||
|
|
||||||
savesettings_t written;
|
savesettings_t written;
|
||||||
saveSettingsInit(&written);
|
saveSettingsInit(&written);
|
||||||
written.someSetting = 314;
|
written.audioMasterVolume = 3.25f;// exactly representable, safe to compare
|
||||||
|
|
||||||
errorret_t writeRet = saveDeviceLinuxSettingsWrite(&device, &written);
|
errorret_t writeRet = saveDeviceLinuxSettingsWrite(&device, &written);
|
||||||
assert_true(errorIsOk(writeRet));
|
assert_true(errorIsOk(writeRet));
|
||||||
@@ -308,7 +308,7 @@ static void test_settingsWriteRead_roundTrip(void **state) {
|
|||||||
|
|
||||||
errorret_t readRet = saveDeviceLinuxSettingsRead(&device, &read);
|
errorret_t readRet = saveDeviceLinuxSettingsRead(&device, &read);
|
||||||
assert_true(errorIsOk(readRet));
|
assert_true(errorIsOk(readRet));
|
||||||
assert_int_equal(read.someSetting, 314);
|
assert_true(read.audioMasterVolume == 3.25f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_settingsRead_noFileYet_leavesUntouched(void **state) {
|
static void test_settingsRead_noFileYet_leavesUntouched(void **state) {
|
||||||
@@ -317,11 +317,11 @@ static void test_settingsRead_noFileYet_leavesUntouched(void **state) {
|
|||||||
|
|
||||||
savesettings_t settings;
|
savesettings_t settings;
|
||||||
saveSettingsInit(&settings);
|
saveSettingsInit(&settings);
|
||||||
settings.someSetting = 777;
|
settings.audioMasterVolume = 7.75f;
|
||||||
|
|
||||||
errorret_t ret = saveDeviceLinuxSettingsRead(&device, &settings);
|
errorret_t ret = saveDeviceLinuxSettingsRead(&device, &settings);
|
||||||
assert_true(errorIsOk(ret));
|
assert_true(errorIsOk(ret));
|
||||||
assert_int_equal(settings.someSetting, 777);
|
assert_true(settings.audioMasterVolume == 7.75f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_settingsRead_corruptFile_errors(void **state) {
|
static void test_settingsRead_corruptFile_errors(void **state) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "dusktest.h"
|
#include "dusktest.h"
|
||||||
#include "save/savesettings.h"
|
#include "save/settings/savesettings.h"
|
||||||
#include "save/savejson.h"
|
#include "save/savejson.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -51,14 +51,14 @@ static errorret_t settingsFromJSON(
|
|||||||
// saveSettingsInit
|
// saveSettingsInit
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static void test_saveSettingsInit_zeroesSomeSetting(void **state) {
|
static void test_saveSettingsInit_resetsAudioMasterVolume(void **state) {
|
||||||
savesettings_t settings;
|
savesettings_t settings;
|
||||||
memoryZero(&settings, sizeof(settings));
|
memoryZero(&settings, sizeof(settings));
|
||||||
settings.someSetting = 42;
|
settings.audioMasterVolume = 42.0f;
|
||||||
|
|
||||||
saveSettingsInit(&settings);
|
saveSettingsInit(&settings);
|
||||||
|
|
||||||
assert_int_equal(settings.someSetting, 0);
|
assert_true(settings.audioMasterVolume == 1.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_saveSettingsInit_nullAsserts(void **state) {
|
static void test_saveSettingsInit_nullAsserts(void **state) {
|
||||||
@@ -85,7 +85,6 @@ static void test_saveSettingsReadJSON_nullAsserts(void **state) {
|
|||||||
savesettings_t settings;
|
savesettings_t settings;
|
||||||
saveSettingsInit(&settings);
|
saveSettingsInit(&settings);
|
||||||
writeInit();
|
writeInit();
|
||||||
yyjson_mut_obj_add_int(doc, object, "someSetting", 1);
|
|
||||||
|
|
||||||
size_t len;
|
size_t len;
|
||||||
char_t *json = yyjson_mut_write(doc, 0, &len);
|
char_t *json = yyjson_mut_write(doc, 0, &len);
|
||||||
@@ -105,7 +104,7 @@ static void test_saveSettingsReadJSON_nullAsserts(void **state) {
|
|||||||
static void test_saveSettingsWriteReadJSON_roundTrip(void **state) {
|
static void test_saveSettingsWriteReadJSON_roundTrip(void **state) {
|
||||||
savesettings_t written;
|
savesettings_t written;
|
||||||
saveSettingsInit(&written);
|
saveSettingsInit(&written);
|
||||||
written.someSetting = 12345;
|
written.audioMasterVolume = 2.5f;
|
||||||
|
|
||||||
char_t *json;
|
char_t *json;
|
||||||
size_t len;
|
size_t len;
|
||||||
@@ -114,35 +113,59 @@ static void test_saveSettingsWriteReadJSON_roundTrip(void **state) {
|
|||||||
|
|
||||||
savesettings_t read;
|
savesettings_t read;
|
||||||
saveSettingsInit(&read);
|
saveSettingsInit(&read);
|
||||||
read.someSetting = -1;// sentinel, should be overwritten
|
read.audioMasterVolume = -1.0f;// sentinel, should be overwritten
|
||||||
|
|
||||||
errorret_t readRet = settingsFromJSON(&read, json);
|
errorret_t readRet = settingsFromJSON(&read, json);
|
||||||
assert_true(errorIsOk(readRet));
|
assert_true(errorIsOk(readRet));
|
||||||
assert_int_equal(read.someSetting, 12345);
|
assert_true(read.audioMasterVolume == 2.5f);
|
||||||
|
|
||||||
free(json);
|
free(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_saveSettingsReadJSON_missingKeyDefaultsToZero(void **state) {
|
static void test_saveSettingsReadJSON_missingKeyUsesDefault(void **state) {
|
||||||
|
savesettings_t settings;
|
||||||
|
saveSettingsInit(&settings);
|
||||||
|
settings.audioMasterVolume = 999.0f;// sentinel
|
||||||
|
|
||||||
|
errorret_t ret = settingsFromJSON(&settings, "{\"version\":1}");
|
||||||
|
assert_true(errorIsOk(ret));
|
||||||
|
assert_true(settings.audioMasterVolume == 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// saveSettingsReadJSON - version enforcement
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void test_saveSettingsReadJSON_versionMissing_errors(void **state) {
|
||||||
savesettings_t settings;
|
savesettings_t settings;
|
||||||
saveSettingsInit(&settings);
|
saveSettingsInit(&settings);
|
||||||
settings.someSetting = 999;// sentinel
|
|
||||||
|
|
||||||
errorret_t ret = settingsFromJSON(&settings, "{}");
|
errorret_t ret = settingsFromJSON(&settings, "{}");
|
||||||
assert_true(errorIsOk(ret));
|
assert_true(errorIsNotOk(ret));
|
||||||
assert_int_equal(settings.someSetting, 0);
|
errorCatch(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSettingsReadJSON_versionMismatch_errors(void **state) {
|
||||||
|
savesettings_t settings;
|
||||||
|
saveSettingsInit(&settings);
|
||||||
|
|
||||||
|
errorret_t ret = settingsFromJSON(&settings, "{\"version\":999}");
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
assertInit();
|
assertInit();
|
||||||
const struct CMUnitTest tests[] = {
|
const struct CMUnitTest tests[] = {
|
||||||
cmocka_unit_test(test_saveSettingsInit_zeroesSomeSetting),
|
cmocka_unit_test(test_saveSettingsInit_resetsAudioMasterVolume),
|
||||||
cmocka_unit_test(test_saveSettingsInit_nullAsserts),
|
cmocka_unit_test(test_saveSettingsInit_nullAsserts),
|
||||||
|
|
||||||
cmocka_unit_test(test_saveSettingsWriteJSON_nullAsserts),
|
cmocka_unit_test(test_saveSettingsWriteJSON_nullAsserts),
|
||||||
cmocka_unit_test(test_saveSettingsReadJSON_nullAsserts),
|
cmocka_unit_test(test_saveSettingsReadJSON_nullAsserts),
|
||||||
cmocka_unit_test(test_saveSettingsWriteReadJSON_roundTrip),
|
cmocka_unit_test(test_saveSettingsWriteReadJSON_roundTrip),
|
||||||
cmocka_unit_test(test_saveSettingsReadJSON_missingKeyDefaultsToZero),
|
cmocka_unit_test(test_saveSettingsReadJSON_missingKeyUsesDefault),
|
||||||
|
cmocka_unit_test(test_saveSettingsReadJSON_versionMissing_errors),
|
||||||
|
cmocka_unit_test(test_saveSettingsReadJSON_versionMismatch_errors),
|
||||||
};
|
};
|
||||||
|
|
||||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "dusktest.h"
|
#include "dusktest.h"
|
||||||
#include "save/saveslot.h"
|
#include "save/slot/saveslot.h"
|
||||||
#include "save/savejson.h"
|
#include "save/savejson.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
@@ -174,7 +174,9 @@ static void test_saveSlotReadJSON_missingFieldsUseDefaults(void **state) {
|
|||||||
slot.cachedData.playerLevel = 999;
|
slot.cachedData.playerLevel = 999;
|
||||||
|
|
||||||
errorret_t ret = slotFromJSON(
|
errorret_t ret = slotFromJSON(
|
||||||
&slot, "{\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
&slot,
|
||||||
|
"{\"version\":1,"
|
||||||
|
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||||
);
|
);
|
||||||
assert_true(errorIsOk(ret));
|
assert_true(errorIsOk(ret));
|
||||||
|
|
||||||
@@ -186,7 +188,9 @@ static void test_saveSlotReadJSON_missingTimeKey_errors(void **state) {
|
|||||||
saveslot_t slot;
|
saveslot_t slot;
|
||||||
saveSlotInit(&slot);
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
errorret_t ret = slotFromJSON(&slot, "{\"name\":\"Hero\",\"playerLevel\":5}");
|
errorret_t ret = slotFromJSON(
|
||||||
|
&slot, "{\"version\":1,\"name\":\"Hero\",\"playerLevel\":5}"
|
||||||
|
);
|
||||||
assert_true(errorIsNotOk(ret));
|
assert_true(errorIsNotOk(ret));
|
||||||
errorCatch(ret);
|
errorCatch(ret);
|
||||||
}
|
}
|
||||||
@@ -197,7 +201,7 @@ static void test_saveSlotReadJSON_nameTooLong_errors(void **state) {
|
|||||||
|
|
||||||
errorret_t ret = slotFromJSON(
|
errorret_t ret = slotFromJSON(
|
||||||
&slot,
|
&slot,
|
||||||
"{\"name\":\"WayTooLongAName\","
|
"{\"version\":1,\"name\":\"WayTooLongAName\","
|
||||||
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||||
);
|
);
|
||||||
assert_true(errorIsNotOk(ret));
|
assert_true(errorIsNotOk(ret));
|
||||||
@@ -209,12 +213,43 @@ static void test_saveSlotReadJSON_nonObjectRoot_errors(void **state) {
|
|||||||
saveSlotInit(&slot);
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
// A non-object root has no keys, so every field falls back to its
|
// A non-object root has no keys, so every field falls back to its
|
||||||
// default - except "time", which is not optional and errors instead.
|
// default - except "version" and "time", which are required and error
|
||||||
|
// instead.
|
||||||
errorret_t ret = slotFromJSON(&slot, "[]");
|
errorret_t ret = slotFromJSON(&slot, "[]");
|
||||||
assert_true(errorIsNotOk(ret));
|
assert_true(errorIsNotOk(ret));
|
||||||
errorCatch(ret);
|
errorCatch(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// saveSlotReadJSON - version enforcement
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
static void test_saveSlotReadJSON_versionMissing_errors(void **state) {
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
|
errorret_t ret = slotFromJSON(
|
||||||
|
&slot,
|
||||||
|
"{\"name\":\"Hero\","
|
||||||
|
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||||
|
);
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_saveSlotReadJSON_versionMismatch_errors(void **state) {
|
||||||
|
saveslot_t slot;
|
||||||
|
saveSlotInit(&slot);
|
||||||
|
|
||||||
|
errorret_t ret = slotFromJSON(
|
||||||
|
&slot,
|
||||||
|
"{\"version\":999,\"name\":\"Hero\","
|
||||||
|
"\"time\":{\"time\":1.0,\"timeZone\":0.0,\"offsetTime\":0.0}}"
|
||||||
|
);
|
||||||
|
assert_true(errorIsNotOk(ret));
|
||||||
|
errorCatch(ret);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
assertInit();
|
assertInit();
|
||||||
const struct CMUnitTest tests[] = {
|
const struct CMUnitTest tests[] = {
|
||||||
@@ -234,6 +269,8 @@ int main(void) {
|
|||||||
cmocka_unit_test(test_saveSlotReadJSON_missingTimeKey_errors),
|
cmocka_unit_test(test_saveSlotReadJSON_missingTimeKey_errors),
|
||||||
cmocka_unit_test(test_saveSlotReadJSON_nameTooLong_errors),
|
cmocka_unit_test(test_saveSlotReadJSON_nameTooLong_errors),
|
||||||
cmocka_unit_test(test_saveSlotReadJSON_nonObjectRoot_errors),
|
cmocka_unit_test(test_saveSlotReadJSON_nonObjectRoot_errors),
|
||||||
|
cmocka_unit_test(test_saveSlotReadJSON_versionMissing_errors),
|
||||||
|
cmocka_unit_test(test_saveSlotReadJSON_versionMismatch_errors),
|
||||||
};
|
};
|
||||||
|
|
||||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||||
|
|||||||
Reference in New Issue
Block a user