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:
+11
-14
@@ -8,9 +8,10 @@
|
||||
#include "party.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "save/save.h"
|
||||
|
||||
void partyInit(party_t *party) {
|
||||
assertNotNull(party, "Party cannot be null");
|
||||
void partyInit(void) {
|
||||
party_t *party = &SAVE.slot.party;
|
||||
|
||||
memoryZero(party, sizeof(party_t));
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
@@ -21,8 +22,8 @@ void partyInit(party_t *party) {
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t partyGetAvailableMember(const party_t *party) {
|
||||
assertNotNull(party, "Party cannot be null");
|
||||
uint8_t partyGetAvailableMember(void) {
|
||||
const party_t *party = &SAVE.slot.party;
|
||||
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
if(party->members[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
||||
@@ -32,14 +33,13 @@ uint8_t partyGetAvailableMember(const party_t *party) {
|
||||
}
|
||||
|
||||
battlefighter_t *partyAddMember(
|
||||
party_t *party,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
) {
|
||||
assertNotNull(party, "Party cannot be null");
|
||||
party_t *party = &SAVE.slot.party;
|
||||
|
||||
const uint8_t index = partyGetAvailableMember(party);
|
||||
const uint8_t index = partyGetAvailableMember();
|
||||
if(index == 0xFF) return NULL;
|
||||
|
||||
battlefighter_t *member = &party->members[index];
|
||||
@@ -57,24 +57,21 @@ battlefighter_t *partyAddMember(
|
||||
return member;
|
||||
}
|
||||
|
||||
battlefighter_t *partyGetOrderMember(party_t *party, const uint8_t slot) {
|
||||
assertNotNull(party, "Party cannot be null");
|
||||
battlefighter_t *partyGetOrderMember(const uint8_t slot) {
|
||||
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
||||
|
||||
party_t *party = &SAVE.slot.party;
|
||||
const uint8_t index = party->order[slot];
|
||||
if(index == PARTY_ORDER_EMPTY) return NULL;
|
||||
return &party->members[index];
|
||||
}
|
||||
|
||||
void partySetOrder(
|
||||
party_t *party, const uint8_t slot, const uint8_t memberIndex
|
||||
) {
|
||||
assertNotNull(party, "Party cannot be null");
|
||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
|
||||
assertTrue(slot < PARTY_ACTIVE_SIZE_MAX, "Invalid party order slot");
|
||||
assertTrue(
|
||||
memberIndex == PARTY_ORDER_EMPTY || memberIndex < PARTY_MEMBER_COUNT_MAX,
|
||||
"Invalid party member index"
|
||||
);
|
||||
|
||||
party->order[slot] = memberIndex;
|
||||
SAVE.slot.party.order[slot] = memberIndex;
|
||||
}
|
||||
|
||||
+17
-21
@@ -23,26 +23,25 @@ typedef struct {
|
||||
} party_t;
|
||||
|
||||
/**
|
||||
* Initializes the party with an empty roster and order.
|
||||
*
|
||||
* @param party The party to initialize.
|
||||
* Initializes the current save slot's party (SAVE.slot.party) with an
|
||||
* empty roster and order.
|
||||
*/
|
||||
void partyInit(party_t *party);
|
||||
void partyInit(void);
|
||||
|
||||
/**
|
||||
* Gets an available (unused) party member slot index.
|
||||
* Gets an available (unused) member slot index in the current save slot's
|
||||
* party.
|
||||
*
|
||||
* @param party The party to query.
|
||||
* @return The index of an available slot, or 0xFF if the party is full.
|
||||
*/
|
||||
uint8_t partyGetAvailableMember(const party_t *party);
|
||||
uint8_t partyGetAvailableMember(void);
|
||||
|
||||
/**
|
||||
* Adds a member to the party roster in the next available slot. Party
|
||||
* members are always allies controlled by the player. If there is a
|
||||
* free active order slot, the new member is placed into it.
|
||||
* Adds a member to the current save slot's party roster in the next
|
||||
* available slot. Party members are always allies controlled by the
|
||||
* player. If there is a free active order slot, the new member is placed
|
||||
* into it.
|
||||
*
|
||||
* @param party The party to add the member to.
|
||||
* @param stats The member's base combat stats.
|
||||
* @param healthMax The member's maximum health.
|
||||
* @param mpMax The member's maximum mp.
|
||||
@@ -50,31 +49,28 @@ uint8_t partyGetAvailableMember(const party_t *party);
|
||||
* already full.
|
||||
*/
|
||||
battlefighter_t *partyAddMember(
|
||||
party_t *party,
|
||||
const battlefighterstats_t stats,
|
||||
const uint16_t healthMax,
|
||||
const uint16_t mpMax
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the roster member currently occupying an active order slot.
|
||||
* Gets the roster member currently occupying an active order slot in the
|
||||
* current save slot's party.
|
||||
*
|
||||
* @param party The party to query.
|
||||
* @param slot The active order slot to query.
|
||||
* @return Pointer to the member in that slot, or NULL if the slot is
|
||||
* empty.
|
||||
*/
|
||||
battlefighter_t *partyGetOrderMember(party_t *party, const uint8_t slot);
|
||||
battlefighter_t *partyGetOrderMember(const uint8_t slot);
|
||||
|
||||
/**
|
||||
* Assigns a roster member to an active order slot, replacing whatever
|
||||
* was there. Use PARTY_ORDER_EMPTY to clear a slot.
|
||||
* Assigns a roster member to an active order slot in the current save
|
||||
* slot's party, replacing whatever was there. Use PARTY_ORDER_EMPTY to
|
||||
* clear a slot.
|
||||
*
|
||||
* @param party The party to modify.
|
||||
* @param slot The active order slot to assign.
|
||||
* @param memberIndex The roster member index to place there, or
|
||||
* PARTY_ORDER_EMPTY to clear the slot.
|
||||
*/
|
||||
void partySetOrder(
|
||||
party_t *party, const uint8_t slot, const uint8_t memberIndex
|
||||
);
|
||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex);
|
||||
|
||||
@@ -22,7 +22,7 @@ void cutsceneStartBattleStart(
|
||||
|
||||
for(uint8_t i = 0; i < config->playerCount; i++) {
|
||||
battlefighter_t *member =
|
||||
partyGetOrderMember(&SAVE.slot.party, config->players[i]);
|
||||
partyGetOrderMember(config->players[i]);
|
||||
if(member == NULL) continue;
|
||||
|
||||
battlefighter_t *fighter = battleAddFighter(
|
||||
@@ -62,7 +62,7 @@ bool_t cutsceneStartBattleUpdate(
|
||||
uint8_t allySlot = 0;
|
||||
for(uint8_t i = 0; i < config->playerCount; i++) {
|
||||
battlefighter_t *member =
|
||||
partyGetOrderMember(&SAVE.slot.party, config->players[i]);
|
||||
partyGetOrderMember(config->players[i]);
|
||||
if(member == NULL) continue;
|
||||
|
||||
battlefighter_t *fighter = &BATTLE.fighters[allySlot++];
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ errorret_t rpgInit(void) {
|
||||
memoryZero(MAP_AREAS, sizeof(MAP_AREAS));
|
||||
|
||||
backpackInit();
|
||||
partyInit(&SAVE.slot.party);
|
||||
partyInit();
|
||||
cutsceneSystemInit();
|
||||
errorChain(mapInit());
|
||||
rpgCameraInit();
|
||||
|
||||
@@ -21,7 +21,7 @@ errorret_t saveInit() {
|
||||
|
||||
// Initialize the slots and settings
|
||||
saveSettingsInit(&SAVE.settings);
|
||||
saveSlotInit(&SAVE.slot);
|
||||
saveSlotInit();
|
||||
|
||||
// Update caches to match default data.
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
@@ -226,7 +226,7 @@ errorret_t saveLoadSlot() {
|
||||
|
||||
if(SAVE.deviceCurrent == 0xFF) {
|
||||
// No save device available, so we assume an empty slot.
|
||||
saveSlotInit(&SAVE.slot);
|
||||
saveSlotInit();
|
||||
SAVE.caches[SAVE.slotCurrent] = SAVE.slot.cachedData;
|
||||
errorOk();
|
||||
}
|
||||
@@ -245,7 +245,7 @@ errorret_t saveLoadSlot() {
|
||||
"Save slot %u could not be loaded, treating as empty.\n",
|
||||
(uint32_t)SAVE.slotCurrent
|
||||
);
|
||||
saveSlotInit(&SAVE.slot);
|
||||
saveSlotInit();
|
||||
SAVE.slot.cachedData.corrupt = true;
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ errorret_t saveLoadSlot() {
|
||||
errorret_t saveLoadAllSlots() {
|
||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||
SAVE.slotCurrent = i;
|
||||
saveSlotInit(&SAVE.slot);
|
||||
saveSlotInit();
|
||||
errorChain(saveLoadSlot());// Load slot updates the cache.
|
||||
}
|
||||
|
||||
|
||||
@@ -340,8 +340,15 @@ errorret_t saveDeviceRawStoreItem(
|
||||
slotItems[i].ptr = oldSpans.slots[i].ptr;
|
||||
slotItems[i].len = oldSpans.slots[i].len;
|
||||
} else {
|
||||
// A raw reset, not saveSlotInit() - this fills in a slot index the
|
||||
// current write isn't touching, and saveSlotInit() only ever
|
||||
// operates on SAVE.slot now. A blank ("" name) slot never actually
|
||||
// needs a properly-initialized party (see saveSlotReadJSON's early
|
||||
// return for an unused slot), so this is equivalent for this
|
||||
// filler-only purpose.
|
||||
saveslot_t defaultSlot;
|
||||
saveSlotInit(&defaultSlot);
|
||||
memorySet(&defaultSlot, 0, sizeof(defaultSlot));
|
||||
defaultSlot.version = 1;
|
||||
char_t *json = NULL;
|
||||
size_t len = 0;
|
||||
errorret_t result = saveDeviceRawSerializeSlot(&defaultSlot, &json, &len);
|
||||
|
||||
@@ -7,35 +7,32 @@
|
||||
|
||||
#include "saveslot.h"
|
||||
#include "save/slot/saveslotcurrent.h"
|
||||
#include "save/save.h"
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
#include "util/string.h"
|
||||
|
||||
void saveSlotInit(saveslot_t *slot) {
|
||||
assertNotNull(slot, "Slot cannot be null");
|
||||
void saveSlotInit(void) {
|
||||
memorySet(&SAVE.slot, 0, sizeof(saveslot_t));
|
||||
|
||||
memorySet(slot, 0, sizeof(saveslot_t));
|
||||
SAVE.slot.version = 1;
|
||||
|
||||
slot->version = 1;
|
||||
|
||||
partyInit(&slot->party);
|
||||
partyInit();
|
||||
}
|
||||
|
||||
void saveSlotNewGame(saveslot_t *slot) {
|
||||
assertNotNull(slot, "Slot cannot be null");
|
||||
|
||||
saveSlotInit(slot);
|
||||
void saveSlotNewGame(void) {
|
||||
saveSlotInit();
|
||||
|
||||
stringCopy(
|
||||
slot->cachedData.mapName, SAVE_SLOT_MAP_NAME_DEFAULT,
|
||||
sizeof(slot->cachedData.mapName)
|
||||
SAVE.slot.cachedData.mapName, SAVE_SLOT_MAP_NAME_DEFAULT,
|
||||
sizeof(SAVE.slot.cachedData.mapName)
|
||||
);
|
||||
|
||||
// TEMPORARY: placeholder starting party stats - 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);
|
||||
partyAddMember(startingStats, 30, 10);
|
||||
}
|
||||
|
||||
bool_t saveSlotInUse(saveslotcache_t *slot) {
|
||||
@@ -58,7 +55,17 @@ errorret_t saveSlotWriteJSON(
|
||||
}
|
||||
|
||||
errorret_t saveSlotReadJSON(saveslot_t *slot, yyjson_val *object) {
|
||||
saveSlotInit(slot);
|
||||
assertNotNull(slot, "Slot cannot be null");
|
||||
assertNotNull(object, "Object cannot be null");
|
||||
|
||||
// A plain reset, not saveSlotInit() - this may be reading into a slot
|
||||
// other than SAVE.slot (e.g. a save-device round trip in a test), and
|
||||
// saveSlotInit() only ever operates on SAVE.slot now. saveSlotCurrent
|
||||
// ReadJSON() fully overwrites every field below anyway (including
|
||||
// party.members/order), so this only needs to reset the bookkeeping
|
||||
// fields (version/dataType) that aren't part of the JSON wire format.
|
||||
memorySet(slot, 0, sizeof(saveslot_t));
|
||||
slot->version = 1;
|
||||
|
||||
errorChain(saveSlotCurrentReadJSON(slot, object));
|
||||
errorOk();
|
||||
|
||||
@@ -35,25 +35,22 @@ typedef struct saveslot_s {
|
||||
} saveslot_t;
|
||||
|
||||
/**
|
||||
* Inits the save slot with the default state, this is functionally "new game"
|
||||
* but will not set the player name, as that is what we use to determine if a
|
||||
* save slot is "in use" or not.
|
||||
*
|
||||
* @param slot The save slot to init.
|
||||
* Inits the current save slot (SAVE.slot) with the default state, this is
|
||||
* functionally "new game" but will not set the player name, as that is
|
||||
* what we use to determine if a save slot is "in use" or not.
|
||||
*/
|
||||
void saveSlotInit(saveslot_t *slot);
|
||||
void saveSlotInit(void);
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Sets up the current save slot (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 the slot before a load attempt, wiping a deleted
|
||||
* slot) wants. Callers creating a new save at a specific index should set
|
||||
* SAVE.slotCurrent first.
|
||||
*/
|
||||
void saveSlotNewGame(saveslot_t *slot);
|
||||
void saveSlotNewGame(void);
|
||||
|
||||
/**
|
||||
* Checks if the save slot is in use, this is determined by checking if the
|
||||
|
||||
@@ -103,18 +103,21 @@ void uiSelectSaveDeleteConfirmed(const bool_t result, void *user) {
|
||||
assertTrue(SAVE.deviceCurrent != 0xFF, "No current device");
|
||||
const uint8_t index = UI_SELECT_SAVE.pendingDeleteIndex;
|
||||
|
||||
saveslot_t slot;
|
||||
saveSlotInit(&slot);
|
||||
// uiSelectSaveOpen is only ever reached before any gameplay starts (see
|
||||
// uimainmenu.c), so it's safe to reuse SAVE.slot as scratch for whichever
|
||||
// index is being written here - nothing else is relying on it.
|
||||
SAVE.slotCurrent = index;
|
||||
saveSlotInit();
|
||||
|
||||
errorret_t writeResult = saveDeviceSlotWrite(
|
||||
&SAVE.devices[SAVE.deviceCurrent], &slot, index
|
||||
&SAVE.devices[SAVE.deviceCurrent], &SAVE.slot, index
|
||||
);
|
||||
if(errorIsNotOk(writeResult)) {
|
||||
errorCatch(errorPrint(writeResult));
|
||||
return;
|
||||
}
|
||||
|
||||
SAVE.caches[index] = slot.cachedData;
|
||||
SAVE.caches[index] = SAVE.slot.cachedData;
|
||||
|
||||
uiSelectSaveSwitchType(UI_SELECT_SAVE_TYPE_LOAD);
|
||||
uiSelectSaveRefreshSlot(index);
|
||||
@@ -137,16 +140,19 @@ void uiSelectSaveNameEntered(
|
||||
|
||||
const uint8_t index = UI_SELECT_SAVE.pendingNameIndex;
|
||||
|
||||
saveslot_t slot;
|
||||
saveSlotNewGame(&slot);
|
||||
stringCopy(slot.cachedData.name, text, SAVE_SLOT_NAME_LENGTH);
|
||||
// uiSelectSaveOpen is only ever reached before any gameplay starts (see
|
||||
// uimainmenu.c), so it's safe to reuse SAVE.slot as scratch for whichever
|
||||
// index is being written here - nothing else is relying on it.
|
||||
SAVE.slotCurrent = index;
|
||||
saveSlotNewGame();
|
||||
stringCopy(SAVE.slot.cachedData.name, text, SAVE_SLOT_NAME_LENGTH);
|
||||
|
||||
// No save device available - proceed with an in-memory-only slot rather
|
||||
// than writing to a device that doesn't exist (see saveLoadSlot()'s own
|
||||
// comment on the same "continue without a save device" flow).
|
||||
if(SAVE.deviceCurrent != 0xFF) {
|
||||
errorret_t writeResult = saveDeviceSlotWrite(
|
||||
&SAVE.devices[SAVE.deviceCurrent], &slot, index
|
||||
&SAVE.devices[SAVE.deviceCurrent], &SAVE.slot, index
|
||||
);
|
||||
if(errorIsNotOk(writeResult)) {
|
||||
errorCatch(errorPrint(writeResult));
|
||||
@@ -155,7 +161,7 @@ void uiSelectSaveNameEntered(
|
||||
}
|
||||
}
|
||||
|
||||
SAVE.caches[index] = slot.cachedData;
|
||||
SAVE.caches[index] = SAVE.slot.cachedData;
|
||||
uiSelectSaveRefreshSlot(index);
|
||||
|
||||
UI_SELECT_SAVE.result = index;
|
||||
|
||||
@@ -50,7 +50,7 @@ void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) {
|
||||
}
|
||||
|
||||
SAVE.slotCurrent = slotIndex;
|
||||
saveSlotInit(&SAVE.slot);
|
||||
saveSlotInit();
|
||||
errorret_t result = saveLoadSlot();
|
||||
if(errorIsNotOk(result)) {
|
||||
errorCatch(errorPrint(result));
|
||||
|
||||
Reference in New Issue
Block a user