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());
|
||||
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).
|
||||
|
||||
@@ -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); \
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user