Add order-based party roster, back-navigation in battle selection, fix win-state cutscene leak
- Battle selection menu can now go back a fighter to re-decide their move, and battle win/loss no longer leaks the round's remaining queued move cutscenes into the ending state (cutsceneSystemDiscardQueued). - Party roster is now driven by party.jsonc: every defined member always exists (partyInit), while party->order alone determines who's currently active - partyAddMember/partyRemoveMember (and their cutscene items) now activate/deactivate an existing roster member by index instead of creating/destroying one, with removal compacting order to stay packed. - New PARTY_ADD_MEMBER/PARTY_REMOVE_MEMBER/PARTY_HEAL_MEMBER/PARTY_HEAL_ALL cutscene items. - saveSlotInit/partyInit now return errorret_t and take their target struct directly (saveslot_t*/party_t*) instead of implicitly operating on SAVE.slot; saveSlotReadJSON resets through saveSlotInit rather than duplicating the reset logic. - Test coverage: stub the JSON asset loader in save test fixtures so partyInit's real party.jsonc load runs against an in-memory doc instead of hanging with no asset system initialized. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -8,6 +8,7 @@ include(dusktest)
|
||||
# Tests
|
||||
dusktest(test_savesettings.c)
|
||||
dusktest(test_saveslot.c)
|
||||
target_sources(test_saveslot PRIVATE savetestfixture.c)
|
||||
|
||||
dusktest(test_savedevicelinux.c)
|
||||
target_sources(test_savedevicelinux PRIVATE savetestfixture.c)
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
|
||||
#include "savetestfixture.h"
|
||||
#include "assert/assert.h"
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
#include "asset/loader/assetentry.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include <ftw.h>
|
||||
@@ -22,6 +25,51 @@ static char_t SAVE_TEST_FIXTURE_HOME[PATH_MAX];
|
||||
static char_t SAVE_TEST_FIXTURE_REAL_HOME[PATH_MAX];
|
||||
static bool_t SAVE_TEST_FIXTURE_HAD_REAL_HOME;
|
||||
|
||||
static const char_t *SAVE_TEST_FIXTURE_PARTY_JSON =
|
||||
"{\"members\":[{"
|
||||
"\"attack\":10,\"defense\":5,\"magic\":0,\"speed\":10,\"luck\":0,"
|
||||
"\"healthMax\":30,\"mpMax\":10"
|
||||
"}]}";
|
||||
|
||||
static yyjson_doc *SAVE_TEST_FIXTURE_PARTY_DOC = NULL;
|
||||
|
||||
errorret_t saveTestFixturePartyJsonLoadSyncStub(assetloading_t *loading) {
|
||||
loading->entry->data.json = SAVE_TEST_FIXTURE_PARTY_DOC;
|
||||
loading->entry->state = ASSET_ENTRY_STATE_LOADED;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t saveTestFixturePartyJsonDisposeStub(assetentry_t *entry) {
|
||||
errorOk();
|
||||
}
|
||||
|
||||
void saveTestFixtureStubPartyAsset(void) {
|
||||
memoryZero(&ASSET, sizeof(ASSET));
|
||||
for(size_t i = 0; i < ASSET_LOADING_COUNT_MAX; i++) {
|
||||
threadMutexInit(&ASSET.loading[i].mutex);
|
||||
}
|
||||
|
||||
if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) {
|
||||
SAVE_TEST_FIXTURE_PARTY_DOC = yyjson_read(
|
||||
SAVE_TEST_FIXTURE_PARTY_JSON, strlen(SAVE_TEST_FIXTURE_PARTY_JSON), 0
|
||||
);
|
||||
assertNotNull(
|
||||
SAVE_TEST_FIXTURE_PARTY_DOC, "Failed to parse party JSON fixture"
|
||||
);
|
||||
}
|
||||
|
||||
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].loadSync =
|
||||
saveTestFixturePartyJsonLoadSyncStub;
|
||||
ASSET_LOADER_CALLBACKS[ASSET_LOADER_TYPE_JSON].dispose =
|
||||
saveTestFixturePartyJsonDisposeStub;
|
||||
}
|
||||
|
||||
void saveTestFixtureUnstubPartyAsset(void) {
|
||||
if(SAVE_TEST_FIXTURE_PARTY_DOC == NULL) return;
|
||||
yyjson_doc_free(SAVE_TEST_FIXTURE_PARTY_DOC);
|
||||
SAVE_TEST_FIXTURE_PARTY_DOC = NULL;
|
||||
}
|
||||
|
||||
static int_t saveTestFixtureRemoveEntry(
|
||||
const char *path,
|
||||
const struct stat *statBuffer,
|
||||
@@ -49,6 +97,8 @@ int saveTestFixtureSetup(void **state) {
|
||||
|
||||
assertTrue(setenv("HOME", SAVE_TEST_FIXTURE_HOME, 1) == 0, "setenv failed");
|
||||
|
||||
saveTestFixtureStubPartyAsset();
|
||||
|
||||
errorret_t ret = saveInit();
|
||||
assertTrue(errorIsOk(ret), "saveInit failed in test fixture");
|
||||
|
||||
@@ -56,6 +106,8 @@ int saveTestFixtureSetup(void **state) {
|
||||
}
|
||||
|
||||
int saveTestFixtureTeardown(void **state) {
|
||||
saveTestFixtureUnstubPartyAsset();
|
||||
|
||||
nftw(
|
||||
SAVE_TEST_FIXTURE_HOME, saveTestFixtureRemoveEntry, 16,
|
||||
FTW_DEPTH | FTW_PHYS
|
||||
|
||||
@@ -35,3 +35,21 @@ int saveTestFixtureTeardown(void **state);
|
||||
* filesystem permission tricks. Must be called after saveTestFixtureSetup.
|
||||
*/
|
||||
void saveTestFixtureBlockSaveDirectory(void);
|
||||
|
||||
/**
|
||||
* Stubs the JSON asset loader's sync callback to hand back a small
|
||||
* in-memory party.jsonc-shaped doc instead of doing a real file/zip read -
|
||||
* saveInit()/saveSlotInit() seed the party roster via partyInit(), which
|
||||
* loads party.jsonc through the real asset system, and there's no real
|
||||
* asset bundle available under the test harness. No thread, no real I/O.
|
||||
* Resets ASSET to a fresh, otherwise-empty state each call, so it's safe
|
||||
* to call once per test. Called automatically by saveTestFixtureSetup.
|
||||
*/
|
||||
void saveTestFixtureStubPartyAsset(void);
|
||||
|
||||
/**
|
||||
* Frees the in-memory doc created by saveTestFixtureStubPartyAsset. Safe
|
||||
* to call even if that function was never called. Called automatically
|
||||
* by saveTestFixtureTeardown.
|
||||
*/
|
||||
void saveTestFixtureUnstubPartyAsset(void);
|
||||
|
||||
@@ -298,7 +298,7 @@ static void test_saveSaveSlot_invalidSlotIndexAsserts(void **state) {
|
||||
static void test_saveSaveSlot_success(void **state) {
|
||||
makeDeviceAvailable();
|
||||
SAVE.slotCurrent = 1;
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
stringCopy(
|
||||
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
|
||||
);
|
||||
@@ -336,7 +336,7 @@ static void test_saveLoadSlot_success(void **state) {
|
||||
// ever operates on SAVE.slot now), then reset it again below - the
|
||||
// write and the load-under-test don't overlap in time.
|
||||
SAVE.slotCurrent = 2;
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
stringCopy(
|
||||
SAVE.slot.cachedData.name, "Zelda", sizeof(SAVE.slot.cachedData.name)
|
||||
);
|
||||
@@ -345,7 +345,7 @@ static void test_saveLoadSlot_success(void **state) {
|
||||
assert_true(errorIsOk(writeRet));
|
||||
|
||||
SAVE.slotCurrent = 2;
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
SAVE.slotDirty = true;
|
||||
|
||||
errorret_t ret = saveLoadSlot();
|
||||
@@ -371,7 +371,7 @@ static void test_saveLoadSlot_deviceReadFails_cacheStaysStale(void **state) {
|
||||
fclose(file);
|
||||
|
||||
SAVE.slotCurrent = 0;
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
SAVE.caches[0].playerLevel = 77;// sentinel
|
||||
|
||||
errorret_t ret = saveLoadSlot();
|
||||
@@ -391,7 +391,7 @@ static void test_saveLoadAllSlots_loadsAllAndEndsAtLastIndex(void **state) {
|
||||
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
SAVE.slotCurrent = i;
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
SAVE.slot.cachedData.playerLevel = (int32_t)(i + 1);
|
||||
errorret_t saveRet = saveSaveSlot();
|
||||
assert_true(errorIsOk(saveRet));
|
||||
@@ -410,7 +410,7 @@ static void test_saveLoadAllSlots_missingFileLeavesInitDefaults(void **state) {
|
||||
makeDeviceAvailable();
|
||||
|
||||
SAVE.slotCurrent = 0;
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
SAVE.slot.cachedData.playerLevel = 5;
|
||||
errorret_t saveRet = saveSaveSlot();
|
||||
assert_true(errorIsOk(saveRet));
|
||||
@@ -433,7 +433,7 @@ static void test_saveLoadAllSlots_middleSlotCorrupt_stopsAtFailingIndex(
|
||||
makeDeviceAvailable();
|
||||
|
||||
SAVE.slotCurrent = 0;
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
errorret_t saveRet = saveSaveSlot();
|
||||
assert_true(errorIsOk(saveRet));
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ static void test_saveDeviceSlot_dispatchesToPlatform(void **state) {
|
||||
|
||||
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
|
||||
// write source here.
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
SAVE.slot.cachedData.playerLevel = 21;
|
||||
|
||||
errorret_t writeRet = saveDeviceSlotWrite(&device, &SAVE.slot, 0);
|
||||
|
||||
@@ -206,7 +206,7 @@ static void test_slotWriteRead_roundTrip(void **state) {
|
||||
|
||||
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
|
||||
// write source here.
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
stringCopy(
|
||||
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
|
||||
);
|
||||
|
||||
@@ -9,11 +9,25 @@
|
||||
#include "save/slot/saveslot.h"
|
||||
#include "save/save.h"
|
||||
#include "save/savejson.h"
|
||||
#include "savetestfixture.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// saveSlotInit() seeds the party roster via partyInit(), which loads
|
||||
// party.jsonc through the real asset system - stub it out (see
|
||||
// savetestfixture.h) rather than needing a real asset bundle here.
|
||||
static int assetGroupSetup(void **state) {
|
||||
saveTestFixtureStubPartyAsset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int assetGroupTeardown(void **state) {
|
||||
saveTestFixtureUnstubPartyAsset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Helpers - wrap the writeInit/readInit macros (which can only be used
|
||||
// inside a function returning errorret_t) so tests can call them plainly.
|
||||
@@ -52,7 +66,7 @@ static errorret_t slotFromJSON(saveslot_t *slot, const char_t *json) {
|
||||
static void test_saveSlotInit_defaults(void **state) {
|
||||
memorySet(&SAVE.slot, 0xFF, sizeof(SAVE.slot));
|
||||
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
|
||||
assert_int_equal(SAVE.slot.version, 1);
|
||||
assert_int_equal(SAVE.slot.dataType, 0);
|
||||
@@ -66,7 +80,7 @@ static void test_saveSlotInit_defaults(void **state) {
|
||||
// ============================================================
|
||||
|
||||
static void test_saveSlotInUse(void **state) {
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
|
||||
assert_false(saveSlotInUse(&SAVE.slot.cachedData));
|
||||
|
||||
@@ -81,7 +95,7 @@ static void test_saveSlotInUse_nullAsserts(void **state) {
|
||||
}
|
||||
|
||||
static void test_saveSlotHasSaved(void **state) {
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
|
||||
assert_false(saveSlotHasSaved(&SAVE.slot.cachedData));
|
||||
|
||||
@@ -136,7 +150,7 @@ static void test_saveSlotReadJSON_nullAsserts(void **state) {
|
||||
static void test_saveSlotReadJSON_roundTrip(void **state) {
|
||||
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
|
||||
// write source here.
|
||||
saveSlotInit();
|
||||
saveSlotInit(&SAVE.slot);
|
||||
SAVE.slot.version = 7;// deliberately non-default, not part of the JSON schema
|
||||
stringCopy(
|
||||
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
|
||||
@@ -296,5 +310,5 @@ int main(void) {
|
||||
cmocka_unit_test(test_saveSlotReadJSON_versionMismatch_errors),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
return cmocka_run_group_tests(tests, assetGroupSetup, assetGroupTeardown);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user