Load the save's map on select, seed a real new-game slot, fix readString off-by-one
New games now go through saveSlotNewGame (starting party + default map name) instead of the empty saveSlotInit, and selecting a save loads its stored map instead of the hardcoded boot-time overworld. Save loading now requires an in-use slot to have a non-empty map name, treating a blank one as corruption instead of silently faking a default. Also fixes a real off-by-one in the readString save-JSON macro: it passed its maxLength (documented as excluding the null terminator) straight through as stringCopy's destSize, which needs +1 for the terminator - so a string exactly at the length limit (e.g. "overworld", 9 chars) always asserted "src is too long". Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -33,9 +33,6 @@ errorret_t rpgInit(void) {
|
|||||||
errorChain(mapInit());
|
errorChain(mapInit());
|
||||||
rpgCameraInit();
|
rpgCameraInit();
|
||||||
|
|
||||||
// Init test world
|
|
||||||
errorChain(mapSetMap("overworld"));
|
|
||||||
|
|
||||||
// The player is the one entity that isn't sourced from map/chunk data -
|
// 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
|
// every other entity (NPCs, items) and map area comes from the loaded
|
||||||
// chunks' own spawn data (see rpg/overworld/map.c mapChunkLoaded).
|
// chunks' own spawn data (see rpg/overworld/map.c mapChunkLoaded).
|
||||||
|
|||||||
@@ -512,12 +512,12 @@
|
|||||||
); \
|
); \
|
||||||
} \
|
} \
|
||||||
stringCopy( \
|
stringCopy( \
|
||||||
saveJsonStringBuffer, yyjson_get_str(saveJsonStrVal), (maxLength) \
|
saveJsonStringBuffer, yyjson_get_str(saveJsonStrVal), (maxLength) + 1 \
|
||||||
); \
|
); \
|
||||||
} else { \
|
} else { \
|
||||||
stringCopy(saveJsonStringBuffer, (def), (maxLength)); \
|
stringCopy(saveJsonStringBuffer, (def), (maxLength) + 1); \
|
||||||
} \
|
} \
|
||||||
stringCopy((dest), saveJsonStringBuffer, (maxLength)); \
|
stringCopy((dest), saveJsonStringBuffer, (maxLength) + 1); \
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "save/slot/saveslotcurrent.h"
|
#include "save/slot/saveslotcurrent.h"
|
||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
|
#include "util/string.h"
|
||||||
|
|
||||||
void saveSlotInit(saveslot_t *slot) {
|
void saveSlotInit(saveslot_t *slot) {
|
||||||
assertNotNull(slot, "Slot cannot be null");
|
assertNotNull(slot, "Slot cannot be null");
|
||||||
@@ -20,6 +21,24 @@ void saveSlotInit(saveslot_t *slot) {
|
|||||||
partyInit(&slot->party);
|
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) {
|
bool_t saveSlotInUse(saveslotcache_t *slot) {
|
||||||
assertNotNull(slot, "Slot cannot be null");
|
assertNotNull(slot, "Slot cannot be null");
|
||||||
return slot->name[0] != '\0';
|
return slot->name[0] != '\0';
|
||||||
|
|||||||
@@ -17,29 +17,19 @@
|
|||||||
#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
|
#define SAVE_SLOT_MAP_NAME_DEFAULT "overworld"
|
||||||
// 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];
|
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;
|
bool_t corrupt;
|
||||||
} saveslotcache_t;
|
} saveslotcache_t;
|
||||||
|
|
||||||
typedef struct saveslot_s {
|
typedef struct saveslot_s {
|
||||||
uint8_t version;
|
uint8_t version;
|
||||||
uint8_t dataType;
|
uint8_t dataType;
|
||||||
|
|
||||||
saveslotcache_t cachedData;
|
saveslotcache_t cachedData;
|
||||||
party_t party;
|
party_t party;
|
||||||
} saveslot_t;
|
} saveslot_t;
|
||||||
@@ -53,6 +43,18 @@ typedef struct saveslot_s {
|
|||||||
*/
|
*/
|
||||||
void saveSlotInit(saveslot_t *slot);
|
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
|
* 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.
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ errorret_t saveSlotCurrentReadJSON(saveslot_t *slot, yyjson_val *object) {
|
|||||||
assertNotNull(slot, "Slot cannot be null");
|
assertNotNull(slot, "Slot cannot be null");
|
||||||
assertNotNull(object, "Object 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;
|
saveslotver1_t ver1;
|
||||||
|
memoryZero(&ver1, sizeof(ver1));
|
||||||
errorChain(saveSlotVer1ReadJSON(&ver1, object));
|
errorChain(saveSlotVer1ReadJSON(&ver1, object));
|
||||||
|
|
||||||
stringCopy(slot->cachedData.name, ver1.name, sizeof(slot->cachedData.name));
|
stringCopy(slot->cachedData.name, ver1.name, sizeof(slot->cachedData.name));
|
||||||
|
|||||||
@@ -58,8 +58,22 @@ errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object) {
|
|||||||
requireVersion(SAVE_SLOT_CURRENT);
|
requireVersion(SAVE_SLOT_CURRENT);
|
||||||
|
|
||||||
readString("name", slot->name, "", SAVE_SLOT_VER1_NAME_LENGTH);
|
readString("name", slot->name, "", SAVE_SLOT_VER1_NAME_LENGTH);
|
||||||
|
if(slot->name[0] == '\0') errorOk();
|
||||||
|
|
||||||
readTime("time", slot->time);
|
readTime("time", slot->time);
|
||||||
readInt32("playerLevel", slot->playerLevel, 1);
|
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(
|
readString(
|
||||||
"mapName", slot->mapName, "", SAVE_SLOT_VER1_MAP_NAME_MAX - 1
|
"mapName", slot->mapName, "", SAVE_SLOT_VER1_MAP_NAME_MAX - 1
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ void uiSelectSaveNameEntered(
|
|||||||
const uint8_t index = UI_SELECT_SAVE.pendingNameIndex;
|
const uint8_t index = UI_SELECT_SAVE.pendingNameIndex;
|
||||||
|
|
||||||
saveslot_t slot;
|
saveslot_t slot;
|
||||||
saveSlotInit(&slot);
|
saveSlotNewGame(&slot);
|
||||||
stringCopy(slot.cachedData.name, text, SAVE_SLOT_NAME_LENGTH);
|
stringCopy(slot.cachedData.name, text, SAVE_SLOT_NAME_LENGTH);
|
||||||
|
|
||||||
// No save device available - proceed with an in-memory-only slot rather
|
// No save device available - proceed with an in-memory-only slot rather
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "scene/scene.h"
|
#include "scene/scene.h"
|
||||||
#include "asset/asset.h"
|
#include "asset/asset.h"
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
|
#include "rpg/overworld/map.h"
|
||||||
|
|
||||||
#define UI_MAIN_MENU_INDEX_START_GAME 0
|
#define UI_MAIN_MENU_INDEX_START_GAME 0
|
||||||
#define UI_MAIN_MENU_INDEX_OPTIONS 1
|
#define UI_MAIN_MENU_INDEX_OPTIONS 1
|
||||||
@@ -58,6 +59,16 @@ void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) {
|
|||||||
return;
|
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);
|
sceneSet(SCENE_TYPE_OVERWORLD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user