diff --git a/src/dusk/rpg/rpg.c b/src/dusk/rpg/rpg.c index b5b075f2..17220401 100644 --- a/src/dusk/rpg/rpg.c +++ b/src/dusk/rpg/rpg.c @@ -33,9 +33,6 @@ errorret_t rpgInit(void) { errorChain(mapInit()); rpgCameraInit(); - // Init test world - errorChain(mapSetMap("overworld")); - // The player is the one entity that isn't sourced from map/chunk data - // every other entity (NPCs, items) and map area comes from the loaded // chunks' own spawn data (see rpg/overworld/map.c mapChunkLoaded). diff --git a/src/dusk/save/savejson.h b/src/dusk/save/savejson.h index c80d1c32..7095b6c9 100644 --- a/src/dusk/save/savejson.h +++ b/src/dusk/save/savejson.h @@ -512,12 +512,12 @@ ); \ } \ stringCopy( \ - saveJsonStringBuffer, yyjson_get_str(saveJsonStrVal), (maxLength) \ + saveJsonStringBuffer, yyjson_get_str(saveJsonStrVal), (maxLength) + 1 \ ); \ } else { \ - stringCopy(saveJsonStringBuffer, (def), (maxLength)); \ + stringCopy(saveJsonStringBuffer, (def), (maxLength) + 1); \ } \ - stringCopy((dest), saveJsonStringBuffer, (maxLength)); \ + stringCopy((dest), saveJsonStringBuffer, (maxLength) + 1); \ } /** diff --git a/src/dusk/save/slot/saveslot.c b/src/dusk/save/slot/saveslot.c index 383bdceb..113f4691 100644 --- a/src/dusk/save/slot/saveslot.c +++ b/src/dusk/save/slot/saveslot.c @@ -9,6 +9,7 @@ #include "save/slot/saveslotcurrent.h" #include "assert/assert.h" #include "util/memory.h" +#include "util/string.h" void saveSlotInit(saveslot_t *slot) { assertNotNull(slot, "Slot cannot be null"); @@ -20,6 +21,24 @@ void saveSlotInit(saveslot_t *slot) { partyInit(&slot->party); } +void saveSlotNewGame(saveslot_t *slot) { + assertNotNull(slot, "Slot cannot be null"); + + saveSlotInit(slot); + + stringCopy( + slot->cachedData.mapName, SAVE_SLOT_MAP_NAME_DEFAULT, + sizeof(slot->cachedData.mapName) + ); + + // TEMPORARY: placeholder starting party, same stats shape as + // rpg/battle/testbattle/testbattle.c's mock allies - replace once + // there's a real starting-party/character-creation flow. + const battlefighterstats_t startingStats = + { .attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0 }; + partyAddMember(&slot->party, startingStats, 30, 10); +} + bool_t saveSlotInUse(saveslotcache_t *slot) { assertNotNull(slot, "Slot cannot be null"); return slot->name[0] != '\0'; diff --git a/src/dusk/save/slot/saveslot.h b/src/dusk/save/slot/saveslot.h index 67d88adb..9a8fcde3 100644 --- a/src/dusk/save/slot/saveslot.h +++ b/src/dusk/save/slot/saveslot.h @@ -17,29 +17,19 @@ #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. +#define SAVE_SLOT_MAP_NAME_DEFAULT "overworld" + 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]; - - // Set when this slot's on-disk file failed to load (corrupt or an - // incompatible schema version) and was reset to an empty slot instead - - // see saveLoadSlot() (save/save.c). bool_t corrupt; } saveslotcache_t; typedef struct saveslot_s { uint8_t version; uint8_t dataType; - saveslotcache_t cachedData; party_t party; } saveslot_t; @@ -53,6 +43,18 @@ typedef struct saveslot_s { */ void saveSlotInit(saveslot_t *slot); +/** + * Sets up the save slot for a brand new game: calls saveSlotInit, then + * seeds it with a starting party. Use this (not saveSlotInit) whenever a + * new save is actually being created for a player - saveSlotInit alone + * leaves the party empty, which every other caller (resetting a slot + * before a load attempt, filling in an untouched slot during a raw + * device write, wiping a deleted slot) wants. + * + * @param slot The save slot to set up. + */ +void saveSlotNewGame(saveslot_t *slot); + /** * Checks if the save slot is in use, this is determined by checking if the * player name is set or not. diff --git a/src/dusk/save/slot/saveslotcurrent.c b/src/dusk/save/slot/saveslotcurrent.c index ef10f108..094d2fbb 100644 --- a/src/dusk/save/slot/saveslotcurrent.c +++ b/src/dusk/save/slot/saveslotcurrent.c @@ -43,7 +43,11 @@ errorret_t saveSlotCurrentReadJSON(saveslot_t *slot, yyjson_val *object) { assertNotNull(slot, "Slot cannot be null"); assertNotNull(object, "Object cannot be null"); + // Zeroed up front: saveSlotVer1ReadJSON returns early (leaving everything + // past "name" untouched) for a blank/never-used slot, so this must not be + // left as uninitialized stack garbage. saveslotver1_t ver1; + memoryZero(&ver1, sizeof(ver1)); errorChain(saveSlotVer1ReadJSON(&ver1, object)); stringCopy(slot->cachedData.name, ver1.name, sizeof(slot->cachedData.name)); diff --git a/src/dusk/save/slot/saveslotver1.c b/src/dusk/save/slot/saveslotver1.c index db6a95be..834aaf3b 100644 --- a/src/dusk/save/slot/saveslotver1.c +++ b/src/dusk/save/slot/saveslotver1.c @@ -58,8 +58,22 @@ errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object) { requireVersion(SAVE_SLOT_CURRENT); readString("name", slot->name, "", SAVE_SLOT_VER1_NAME_LENGTH); + if(slot->name[0] == '\0') errorOk(); + readTime("time", slot->time); readInt32("playerLevel", slot->playerLevel, 1); + + // A blank/never-used slot returned early above (see saveSlotInUse - + // keyed off "name"), so name is guaranteed non-empty here - and an + // in-use slot always has a map name too (see saveSlotNewGame). Missing, + // null, or empty means this save is corrupt. + yyjson_val *mapNameVal = yyjson_obj_get(object, "mapName"); + if( + mapNameVal == NULL || !yyjson_is_str(mapNameVal) || + yyjson_get_len(mapNameVal) == 0 + ) { + errorThrow("Save JSON 'mapName' must be a non-empty string"); + } readString( "mapName", slot->mapName, "", SAVE_SLOT_VER1_MAP_NAME_MAX - 1 ); diff --git a/src/dusk/ui/dialog/save/uiselectsave.c b/src/dusk/ui/dialog/save/uiselectsave.c index 81a8acb4..80917858 100644 --- a/src/dusk/ui/dialog/save/uiselectsave.c +++ b/src/dusk/ui/dialog/save/uiselectsave.c @@ -138,7 +138,7 @@ void uiSelectSaveNameEntered( const uint8_t index = UI_SELECT_SAVE.pendingNameIndex; saveslot_t slot; - saveSlotInit(&slot); + saveSlotNewGame(&slot); stringCopy(slot.cachedData.name, text, SAVE_SLOT_NAME_LENGTH); // No save device available - proceed with an in-memory-only slot rather diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.c b/src/dusk/ui/screen/mainmenu/uimainmenu.c index 4508b2cd..e9a7e665 100644 --- a/src/dusk/ui/screen/mainmenu/uimainmenu.c +++ b/src/dusk/ui/screen/mainmenu/uimainmenu.c @@ -20,6 +20,7 @@ #include "scene/scene.h" #include "asset/asset.h" #include "save/save.h" +#include "rpg/overworld/map.h" #define UI_MAIN_MENU_INDEX_START_GAME 0 #define UI_MAIN_MENU_INDEX_OPTIONS 1 @@ -58,6 +59,16 @@ void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) { return; } + assertStrLenMin( + SAVE.slot.cachedData.mapName, 1, "Save slot has no map name" + ); + errorret_t mapResult = mapSetMap(SAVE.slot.cachedData.mapName); + if(errorIsNotOk(mapResult)) { + errorCatch(errorPrint(mapResult)); + uiFatalErrorOpen(mapResult.state->message); + return; + } + sceneSet(SCENE_TYPE_OVERWORLD); }