Make party and save-slot init/new-game operate on SAVE.slot directly

party.c's functions no longer take a party_t* - they always operate on
SAVE.slot.party, since gameplay only ever has one active party. That
forced the same change on saveSlotInit/saveSlotNewGame (they called
partyInit/partyAddMember internally), so those are now parameterless
too, always resetting/seeding SAVE.slot. Callers building a save at a
specific index (new-game creation, slot deletion) now set
SAVE.slotCurrent first instead of building an independent local struct -
safe since the select-save screen is only ever reached before any
gameplay starts.

saveSlotWriteJSON/saveSlotReadJSON deliberately keep their explicit
saveslot_t* parameter - they're genuine serialization primitives used
by the save-device layer and tests against arbitrary structs, unrelated
to the single-current-party assumption. saveSlotReadJSON's internal
reset is now a plain memset instead of delegating to the now-global-only
saveSlotInit().

Updates test_save/test_savedevice/test_savedevicelinux/test_saveslot for
the new signatures. Note: test_savedevice/test_savedevicelinux/test_save
have pre-existing failures in this sandbox unrelated to this change -
savetestfixture.c swaps $HOME, but saveDeviceLinuxGetDirectory actually
derives the save path from ASSET.baseDirectory (the executable's own
location), so the fixture's sandboxing never actually applies. test_
saveslot.c (which doesn't depend on that fixture) passes 15/15.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-09-12 21:30:08 -05:00
co-authored by Claude Sonnet 5
parent c2bd761383
commit b8cbd8ff6a
14 changed files with 180 additions and 158 deletions
+42 -46
View File
@@ -7,6 +7,7 @@
#include "dusktest.h"
#include "save/slot/saveslot.h"
#include "save/save.h"
#include "save/savejson.h"
#include "util/memory.h"
#include "util/string.h"
@@ -45,24 +46,19 @@ static errorret_t slotFromJSON(saveslot_t *slot, const char_t *json) {
}
// ============================================================
// saveSlotInit
// saveSlotInit - only ever operates on SAVE.slot now.
// ============================================================
static void test_saveSlotInit_defaults(void **state) {
saveslot_t slot;
memorySet(&slot, 0xFF, sizeof(slot));
memorySet(&SAVE.slot, 0xFF, sizeof(SAVE.slot));
saveSlotInit(&slot);
saveSlotInit();
assert_int_equal(slot.version, 1);
assert_int_equal(slot.dataType, 0);
assert_int_equal(slot.cachedData.name[0], '\0');
assert_true(slot.cachedData.time.time == 0.0);
assert_int_equal(slot.cachedData.playerLevel, 0);
}
static void test_saveSlotInit_nullAsserts(void **state) {
expect_assert_failure(saveSlotInit(NULL));
assert_int_equal(SAVE.slot.version, 1);
assert_int_equal(SAVE.slot.dataType, 0);
assert_int_equal(SAVE.slot.cachedData.name[0], '\0');
assert_true(SAVE.slot.cachedData.time.time == 0.0);
assert_int_equal(SAVE.slot.cachedData.playerLevel, 0);
}
// ============================================================
@@ -70,13 +66,14 @@ static void test_saveSlotInit_nullAsserts(void **state) {
// ============================================================
static void test_saveSlotInUse(void **state) {
saveslot_t slot;
saveSlotInit(&slot);
saveSlotInit();
assert_false(saveSlotInUse(&slot.cachedData));
assert_false(saveSlotInUse(&SAVE.slot.cachedData));
stringCopy(slot.cachedData.name, "Hero", sizeof(slot.cachedData.name));
assert_true(saveSlotInUse(&slot.cachedData));
stringCopy(
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
);
assert_true(saveSlotInUse(&SAVE.slot.cachedData));
}
static void test_saveSlotInUse_nullAsserts(void **state) {
@@ -84,18 +81,17 @@ static void test_saveSlotInUse_nullAsserts(void **state) {
}
static void test_saveSlotHasSaved(void **state) {
saveslot_t slot;
saveSlotInit(&slot);
saveSlotInit();
assert_false(saveSlotHasSaved(&slot.cachedData));
assert_false(saveSlotHasSaved(&SAVE.slot.cachedData));
// Writing to JSON stamps the current time as a side effect, so even a
// pure serialize (no device write) flips "has ever saved" to true.
char_t *json;
size_t len;
errorret_t ret = slotToJSON(&slot, &json, &len);
errorret_t ret = slotToJSON(&SAVE.slot, &json, &len);
assert_true(errorIsOk(ret));
assert_true(saveSlotHasSaved(&slot.cachedData));
assert_true(saveSlotHasSaved(&SAVE.slot.cachedData));
free(json);
}
@@ -109,8 +105,9 @@ static void test_saveSlotHasSaved_nullAsserts(void **state) {
// ============================================================
static void test_saveSlotWriteJSON_nullAsserts(void **state) {
// Content is never touched - both calls assert on a NULL doc/object
// argument before slot itself would actually be read.
saveslot_t slot;
saveSlotInit(&slot);
writeInit();
expect_assert_failure(saveSlotWriteJSON(NULL, doc, object));
@@ -126,7 +123,6 @@ static void test_saveSlotWriteJSON_nullAsserts(void **state) {
static void test_saveSlotReadJSON_nullAsserts(void **state) {
saveslot_t slot;
saveSlotInit(&slot);
yyjson_doc *readDoc = yyjson_read("{}", 2, 0);
yyjson_val *readObject = yyjson_doc_get_root(readDoc);
@@ -138,45 +134,52 @@ static void test_saveSlotReadJSON_nullAsserts(void **state) {
}
static void test_saveSlotReadJSON_roundTrip(void **state) {
saveslot_t written;
saveSlotInit(&written);
written.version = 7;// deliberately non-default, not part of the JSON schema
stringCopy(written.cachedData.name, "Hero", sizeof(written.cachedData.name));
written.cachedData.playerLevel = 42;
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
// write source here.
saveSlotInit();
SAVE.slot.version = 7;// deliberately non-default, not part of the JSON schema
stringCopy(
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
);
SAVE.slot.cachedData.playerLevel = 42;
// An in-use slot (non-empty name) requires a non-empty map name too -
// see saveSlotNewGame/requireStrMin("mapName", 1) in saveSlotVer1ReadJSON.
stringCopy(
written.cachedData.mapName, "overworld",
sizeof(written.cachedData.mapName)
SAVE.slot.cachedData.mapName, "overworld",
sizeof(SAVE.slot.cachedData.mapName)
);
char_t *json;
size_t len;
errorret_t writeRet = slotToJSON(&written, &json, &len);
errorret_t writeRet = slotToJSON(&SAVE.slot, &json, &len);
assert_true(errorIsOk(writeRet));
saveslot_t read;
saveSlotInit(&read);
const dusktimeepoch_t writtenTime = SAVE.slot.cachedData.time;
// No pre-init needed - saveSlotReadJSON() fully resets/overwrites its
// destination struct regardless of prior content.
saveslot_t read;
errorret_t readRet = slotFromJSON(&read, json);
assert_true(errorIsOk(readRet));
assert_true(stringEquals(read.cachedData.name, "Hero"));
assert_int_equal(read.cachedData.playerLevel, 42);
assert_true(stringEquals(read.cachedData.mapName, "overworld"));
assert_true(read.cachedData.time.time == written.cachedData.time.time);
assert_true(read.cachedData.time.time == writtenTime.time);
// version/dataType are struct-only bookkeeping, never serialized - the
// reader keeps whatever saveSlotInit gave it regardless of the writer's
// version.
// reader resets them to the default regardless of the writer's version.
assert_int_equal(read.version, 1);
free(json);
}
static void test_saveSlotReadJSON_blankSlot_shortCircuits(void **state) {
// Deliberately not saveSlotInit()'d - saveSlotReadJSON() fully resets its
// destination struct itself before parsing, so these sentinel values are
// only here to prove they get overwritten, not preserved.
saveslot_t slot;
saveSlotInit(&slot);
memorySet(&slot, 0, sizeof(slot));
stringCopy(slot.cachedData.name, "sentinel", sizeof(slot.cachedData.name));
slot.cachedData.playerLevel = 999;
@@ -197,7 +200,6 @@ static void test_saveSlotReadJSON_blankSlot_shortCircuits(void **state) {
static void test_saveSlotReadJSON_inUseMissingMapName_errors(void **state) {
saveslot_t slot;
saveSlotInit(&slot);
// "name" is present (in use) but "mapName" is missing - a real save
// (via saveSlotNewGame) always has both, so this is corrupt.
@@ -212,7 +214,6 @@ static void test_saveSlotReadJSON_inUseMissingMapName_errors(void **state) {
static void test_saveSlotReadJSON_missingTimeKey_errors(void **state) {
saveslot_t slot;
saveSlotInit(&slot);
errorret_t ret = slotFromJSON(
&slot, "{\"version\":1,\"name\":\"Hero\",\"playerLevel\":5}"
@@ -223,7 +224,6 @@ static void test_saveSlotReadJSON_missingTimeKey_errors(void **state) {
static void test_saveSlotReadJSON_nameTooLong_errors(void **state) {
saveslot_t slot;
saveSlotInit(&slot);
errorret_t ret = slotFromJSON(
&slot,
@@ -236,7 +236,6 @@ static void test_saveSlotReadJSON_nameTooLong_errors(void **state) {
static void test_saveSlotReadJSON_nonObjectRoot_errors(void **state) {
saveslot_t slot;
saveSlotInit(&slot);
// A non-object root has no keys, so every field falls back to its
// default - except "version" and "time", which are required and error
@@ -252,7 +251,6 @@ static void test_saveSlotReadJSON_nonObjectRoot_errors(void **state) {
static void test_saveSlotReadJSON_versionMissing_errors(void **state) {
saveslot_t slot;
saveSlotInit(&slot);
errorret_t ret = slotFromJSON(
&slot,
@@ -265,7 +263,6 @@ static void test_saveSlotReadJSON_versionMissing_errors(void **state) {
static void test_saveSlotReadJSON_versionMismatch_errors(void **state) {
saveslot_t slot;
saveSlotInit(&slot);
errorret_t ret = slotFromJSON(
&slot,
@@ -280,7 +277,6 @@ int main(void) {
assertInit();
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_saveSlotInit_defaults),
cmocka_unit_test(test_saveSlotInit_nullAsserts),
cmocka_unit_test(test_saveSlotInUse),
cmocka_unit_test(test_saveSlotInUse_nullAsserts),