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:
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// Starting party roster, loaded by partyInit into a fresh save slot.
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"attack": 10, "defense": 5, "magic": 0, "speed": 10, "luck": 0,
|
||||||
|
"healthMax": 30, "mpMax": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attack": 8, "defense": 6, "magic": 4, "speed": 8, "luck": 1,
|
||||||
|
"healthMax": 25, "mpMax": 15
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+152
-28
@@ -9,49 +9,134 @@
|
|||||||
#include "assert/assert.h"
|
#include "assert/assert.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
|
#include "asset/asset.h"
|
||||||
|
#include "asset/loader/assetloader.h"
|
||||||
|
|
||||||
void partyInit(void) {
|
errorret_t partyParseStatField(
|
||||||
party_t *party = &SAVE.slot.party;
|
yyjson_val *obj,
|
||||||
|
const char_t *key,
|
||||||
|
const uint16_t min,
|
||||||
|
uint16_t *out
|
||||||
|
) {
|
||||||
|
yyjson_val *val = yyjson_obj_get(obj, key);
|
||||||
|
if(!yyjson_is_int(val)) {
|
||||||
|
errorThrow("Party stats: missing/invalid '%s'", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const int64_t num = yyjson_get_sint(val);
|
||||||
|
if(num < min || num > UINT16_MAX) {
|
||||||
|
errorThrow(
|
||||||
|
"Party stats: '%s' must be between %u and %u", key, min, UINT16_MAX
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = (uint16_t)num;
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t partyParseStats(yyjson_val *obj, battlefighterstats_t *out) {
|
||||||
|
assertNotNull(obj, "JSON object cannot be NULL");
|
||||||
|
assertNotNull(out, "Destination stats cannot be NULL");
|
||||||
|
|
||||||
|
errorChain(partyParseStatField(obj, "attack", 0, &out->attack));
|
||||||
|
errorChain(partyParseStatField(obj, "defense", 0, &out->defense));
|
||||||
|
errorChain(partyParseStatField(obj, "magic", 0, &out->magic));
|
||||||
|
errorChain(partyParseStatField(obj, "speed", 0, &out->speed));
|
||||||
|
errorChain(partyParseStatField(obj, "luck", 0, &out->luck));
|
||||||
|
errorChain(partyParseStatField(obj, "healthMax", 1, &out->healthMax));
|
||||||
|
errorChain(partyParseStatField(obj, "mpMax", 0, &out->mpMax));
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t partyInit(party_t *party) {
|
||||||
|
assertNotNull(party, "Party cannot be NULL");
|
||||||
|
|
||||||
memoryZero(party, sizeof(party_t));
|
memoryZero(party, sizeof(party_t));
|
||||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
|
||||||
party->members[i].id = i;
|
|
||||||
}
|
|
||||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||||
party->order[i] = PARTY_ORDER_EMPTY;
|
party->order[i] = PARTY_ORDER_EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assetentry_t *entry = assetLock("party.jsonc", ASSET_LOADER_TYPE_JSON, NULL);
|
||||||
|
errorret_t result = assetRequireLoaded(entry);
|
||||||
|
if(errorIsNotOk(result)) {
|
||||||
|
assetUnlockEntry(entry);
|
||||||
|
errorChain(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t partyGetAvailableMember(void) {
|
yyjson_val *root = yyjson_doc_get_root(entry->data.json);
|
||||||
const party_t *party = &SAVE.slot.party;
|
yyjson_val *members = yyjson_obj_get(root, "members");
|
||||||
|
if(!yyjson_is_arr(members)) {
|
||||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
assetUnlockEntry(entry);
|
||||||
if(party->members[i].status == BATTLE_FIGHTER_STATUS_NULL) return i;
|
errorThrow("party.jsonc: missing/invalid 'members' array");
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0xFF;
|
const size_t memberCount = yyjson_arr_size(members);
|
||||||
}
|
if(memberCount > PARTY_MEMBER_COUNT_MAX) {
|
||||||
|
assetUnlockEntry(entry);
|
||||||
battlefighter_t *partyAddMember(
|
errorThrow(
|
||||||
const battlefighterstats_t stats
|
"party.jsonc: defines more than PARTY_MEMBER_COUNT_MAX (%u) members",
|
||||||
) {
|
PARTY_MEMBER_COUNT_MAX
|
||||||
party_t *party = &SAVE.slot.party;
|
|
||||||
|
|
||||||
const uint8_t index = partyGetAvailableMember();
|
|
||||||
if(index == 0xFF) return NULL;
|
|
||||||
|
|
||||||
battlefighter_t *member = &party->members[index];
|
|
||||||
battleFighterInit(
|
|
||||||
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER, stats
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t idx, max;
|
||||||
|
yyjson_val *memberObj;
|
||||||
|
yyjson_arr_foreach(members, idx, max, memberObj) {
|
||||||
|
battlefighterstats_t stats;
|
||||||
|
errorret_t statsResult = partyParseStats(memberObj, &stats);
|
||||||
|
if(errorIsNotOk(statsResult)) {
|
||||||
|
assetUnlockEntry(entry);
|
||||||
|
errorChain(statsResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
battlefighter_t *member = &party->members[idx];
|
||||||
|
member->id = (uint8_t)idx;
|
||||||
|
battleFighterInit(
|
||||||
|
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||||
|
stats
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assetUnlockEntry(entry);
|
||||||
|
|
||||||
|
// TEST: only the first defined member starts active, in the first party
|
||||||
|
// position - replace once there's a real starting-party/character-
|
||||||
|
// creation flow. Deliberately separate from the roster seeding above:
|
||||||
|
// order (not a member's presence in the roster) is what determines who's
|
||||||
|
// currently in the party.
|
||||||
|
if(memberCount > 0) party->order[0] = 0;
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t partyIsMemberActive(const uint8_t memberIndex) {
|
||||||
|
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||||
|
|
||||||
|
const party_t *party = &SAVE.slot.party;
|
||||||
|
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||||
|
if(party->order[i] == memberIndex) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void partyAddMember(const uint8_t memberIndex) {
|
||||||
|
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||||
|
|
||||||
|
party_t *party = &SAVE.slot.party;
|
||||||
|
assertTrue(
|
||||||
|
party->members[memberIndex].status != BATTLE_FIGHTER_STATUS_NULL,
|
||||||
|
"Member does not exist in the roster"
|
||||||
|
);
|
||||||
|
|
||||||
|
if(partyIsMemberActive(memberIndex)) return;
|
||||||
|
|
||||||
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||||
if(party->order[i] != PARTY_ORDER_EMPTY) continue;
|
if(party->order[i] != PARTY_ORDER_EMPTY) continue;
|
||||||
party->order[i] = index;
|
party->order[i] = memberIndex;
|
||||||
break;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return member;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
battlefighter_t *partyGetOrderMember(const uint8_t slot) {
|
battlefighter_t *partyGetOrderMember(const uint8_t slot) {
|
||||||
@@ -72,3 +157,42 @@ void partySetOrder(const uint8_t slot, const uint8_t memberIndex) {
|
|||||||
|
|
||||||
SAVE.slot.party.order[slot] = memberIndex;
|
SAVE.slot.party.order[slot] = memberIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void partyRemoveMember(const uint8_t memberIndex) {
|
||||||
|
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||||
|
|
||||||
|
party_t *party = &SAVE.slot.party;
|
||||||
|
|
||||||
|
uint8_t removedSlot = PARTY_ORDER_EMPTY;
|
||||||
|
for(uint8_t i = 0; i < PARTY_ACTIVE_SIZE_MAX; i++) {
|
||||||
|
if(party->order[i] != memberIndex) continue;
|
||||||
|
removedSlot = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(removedSlot == PARTY_ORDER_EMPTY) return;
|
||||||
|
|
||||||
|
for(uint8_t i = removedSlot; i < PARTY_ACTIVE_SIZE_MAX - 1; i++) {
|
||||||
|
party->order[i] = party->order[i + 1];
|
||||||
|
}
|
||||||
|
party->order[PARTY_ACTIVE_SIZE_MAX - 1] = PARTY_ORDER_EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void partyHealMember(const uint8_t memberIndex) {
|
||||||
|
assertTrue(memberIndex < PARTY_MEMBER_COUNT_MAX, "Invalid party member index");
|
||||||
|
|
||||||
|
battlefighter_t *member = &SAVE.slot.party.members[memberIndex];
|
||||||
|
if(member->status == BATTLE_FIGHTER_STATUS_NULL) return;
|
||||||
|
|
||||||
|
member->status = BATTLE_FIGHTER_STATUS_NORMAL;
|
||||||
|
member->stats.health = member->stats.healthMax;
|
||||||
|
member->stats.mp = member->stats.mpMax;
|
||||||
|
}
|
||||||
|
|
||||||
|
void partyHealAll(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) continue;
|
||||||
|
partyHealMember(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+69
-17
@@ -7,44 +7,69 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "rpg/battle/fighter/battlefighter.h"
|
#include "rpg/battle/fighter/battlefighter.h"
|
||||||
|
#include "yyjson.h"
|
||||||
|
|
||||||
#define PARTY_MEMBER_COUNT_MAX 4
|
#define PARTY_MEMBER_COUNT_MAX 4
|
||||||
#define PARTY_ACTIVE_SIZE_MAX 3
|
#define PARTY_ACTIVE_SIZE_MAX 3
|
||||||
#define PARTY_ORDER_EMPTY 0xFF
|
#define PARTY_ORDER_EMPTY 0xFF
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
// Every member party.jsonc defines always exists here (seeded by
|
||||||
|
// partyInit), regardless of whether it's currently active - order (not
|
||||||
|
// this array, and not a member's status) is what determines who's
|
||||||
|
// currently in the party.
|
||||||
battlefighter_t members[PARTY_MEMBER_COUNT_MAX];
|
battlefighter_t members[PARTY_MEMBER_COUNT_MAX];
|
||||||
uint8_t order[PARTY_ACTIVE_SIZE_MAX];
|
uint8_t order[PARTY_ACTIVE_SIZE_MAX];
|
||||||
// TODO: Whether party members are "front" or "back".
|
// TODO: Whether party members are "front" or "back".
|
||||||
} party_t;
|
} party_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the current save slot's party (SAVE.slot.party) with an
|
* Initializes party with an empty order, then seeds every roster slot
|
||||||
* empty roster and order.
|
* directly from party.jsonc's "members" array (index i of the array fills
|
||||||
|
* members[i] via partyParseStats/battleFighterInit) - unlike
|
||||||
|
* partyAddMember, this never touches order itself except for one
|
||||||
|
* placeholder default: if party.jsonc defines at least one member,
|
||||||
|
* order[0] is set to 0 (member 0 starts active in the first party
|
||||||
|
* position). Replace this default once there's a real starting-party/
|
||||||
|
* character-creation flow.
|
||||||
|
*
|
||||||
|
* @param party The party to initialize.
|
||||||
|
* @return Error if party.jsonc fails to load/parse, or if it defines more
|
||||||
|
* members than PARTY_MEMBER_COUNT_MAX has room for.
|
||||||
*/
|
*/
|
||||||
void partyInit(void);
|
errorret_t partyInit(party_t *party);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets an available (unused) member slot index in the current save slot's
|
* Parses a set of base combat stats (attack/defense/magic/speed/luck/
|
||||||
* party.
|
* healthMax/mpMax) out of a JSON object. Every field is required - there
|
||||||
|
* is no implicit default - and must be an integer in [0, UINT16_MAX],
|
||||||
|
* except healthMax which must be at least 1 (a fighter with 0 max health
|
||||||
|
* makes no sense).
|
||||||
*
|
*
|
||||||
* @return The index of an available slot, or 0xFF if the party is full.
|
* @param obj The JSON object to read fields from.
|
||||||
|
* @param out Destination stats, filled in on success.
|
||||||
|
* @return Error if a field is missing, not an integer, or out of range.
|
||||||
*/
|
*/
|
||||||
uint8_t partyGetAvailableMember(void);
|
errorret_t partyParseStats(yyjson_val *obj, battlefighterstats_t *out);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a member to the current save slot's party roster in the next
|
* Checks whether memberIndex currently occupies a slot in the current save
|
||||||
* available slot. Party members are always allies controlled by the
|
* slot's party order (i.e. is actively in the party right now).
|
||||||
* player. If there is a free active order slot, the new member is placed
|
|
||||||
* into it.
|
|
||||||
*
|
*
|
||||||
* @param stats The member's base combat stats, including healthMax/mpMax.
|
* @param memberIndex The roster member index to check.
|
||||||
* @return Pointer to the newly added party member, or NULL if the party is
|
* @return True if memberIndex appears anywhere in order.
|
||||||
* already full.
|
|
||||||
*/
|
*/
|
||||||
battlefighter_t *partyAddMember(
|
bool_t partyIsMemberActive(const uint8_t memberIndex);
|
||||||
const battlefighterstats_t stats
|
|
||||||
);
|
/**
|
||||||
|
* Adds an existing roster member (see partyInit - every roster slot
|
||||||
|
* always exists once initialized) to the current save slot's active party
|
||||||
|
* order, in the first free order slot. No-op if memberIndex is already
|
||||||
|
* active, or if there is no free order slot.
|
||||||
|
*
|
||||||
|
* @param memberIndex The roster member index to activate.
|
||||||
|
*/
|
||||||
|
void partyAddMember(const uint8_t memberIndex);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the roster member currently occupying an active order slot in the
|
* Gets the roster member currently occupying an active order slot in the
|
||||||
@@ -66,3 +91,30 @@ battlefighter_t *partyGetOrderMember(const uint8_t slot);
|
|||||||
* PARTY_ORDER_EMPTY to clear the slot.
|
* PARTY_ORDER_EMPTY to clear the slot.
|
||||||
*/
|
*/
|
||||||
void partySetOrder(const uint8_t slot, const uint8_t memberIndex);
|
void partySetOrder(const uint8_t slot, const uint8_t memberIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes memberIndex from the current save slot's active party order -
|
||||||
|
* the roster member itself is untouched (it still exists, per partyInit;
|
||||||
|
* it's simply no longer active) - shifting every later order entry down
|
||||||
|
* one slot to close the gap, so order stays packed with no holes between
|
||||||
|
* active members. No-op if memberIndex isn't currently active.
|
||||||
|
*
|
||||||
|
* @param memberIndex The roster member index to deactivate.
|
||||||
|
*/
|
||||||
|
void partyRemoveMember(const uint8_t memberIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fully restores a roster member's health/mp to their max and clears a
|
||||||
|
* dead status back to normal - unlike battleFighterHeal, this can revive
|
||||||
|
* a dead member (there's no combat context here to keep them down). No-op
|
||||||
|
* if the slot is empty.
|
||||||
|
*
|
||||||
|
* @param memberIndex The roster member index to heal.
|
||||||
|
*/
|
||||||
|
void partyHealMember(const uint8_t memberIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls partyHealMember for every occupied slot in the current save
|
||||||
|
* slot's party roster.
|
||||||
|
*/
|
||||||
|
void partyHealAll(void);
|
||||||
|
|||||||
@@ -115,6 +115,28 @@ void battleStateSelectionPlayerAttack(const uint8_t targetIndex) {
|
|||||||
BATTLE.stateData.selection.selectionIndex++;
|
BATTLE.stateData.selection.selectionIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void battleStateSelectionGoBack(void) {
|
||||||
|
battlestateselection_t *state = &BATTLE.stateData.selection;
|
||||||
|
uint8_t index = state->selectionIndex;
|
||||||
|
|
||||||
|
while(index > 0) {
|
||||||
|
index--;
|
||||||
|
|
||||||
|
const uint8_t fighterIndex = BATTLE.fighterOrder[index];
|
||||||
|
battlefighter_t *fighter = BATTLE.fighters[fighterIndex];
|
||||||
|
if(fighter == NULL) continue;
|
||||||
|
if(fighter->controller != BATTLE_FIGHTER_CONTROLLER_PLAYER) continue;
|
||||||
|
if(!battleStateSelectionFighterHasDecided(fighterIndex)) continue;
|
||||||
|
|
||||||
|
battlemoveaction_t *action = &BATTLE.actionsPending[fighterIndex];
|
||||||
|
action->move = BATTLE_MOVE_ID_NULL;
|
||||||
|
BATTLE.actionsPendingCount--;
|
||||||
|
|
||||||
|
state->selectionIndex = index;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void battleStateSelectionPlayerFlee(void) {
|
void battleStateSelectionPlayerFlee(void) {
|
||||||
battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
|
battlefighter_t *fighter = battleStateSelectionGetCurrentFighter();
|
||||||
if(fighter == NULL) return;
|
if(fighter == NULL) return;
|
||||||
|
|||||||
@@ -109,3 +109,12 @@ void battleStateSelectionPlayerAttack(const uint8_t targetIndex);
|
|||||||
* BATTLE_STATE_SELECTION and awaiting a decision.
|
* BATTLE_STATE_SELECTION and awaiting a decision.
|
||||||
*/
|
*/
|
||||||
void battleStateSelectionPlayerFlee(void);
|
void battleStateSelectionPlayerFlee(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewinds the selection cursor to the nearest earlier player-controlled
|
||||||
|
* fighter that has already decided a move this round, clearing its
|
||||||
|
* pending action so it can be re-decided. AI-decided fighters along the
|
||||||
|
* way are left alone (skipped over, not rewound onto). No-ops if there's
|
||||||
|
* no such fighter (e.g. already at the first decision of the round).
|
||||||
|
*/
|
||||||
|
void battleStateSelectionGoBack(void);
|
||||||
@@ -125,6 +125,17 @@ void cutsceneSystemInsertCutscene(const cutscene_t *cutscene) {
|
|||||||
cutsceneSystemInsertItems(cutscene->items, cutscene->itemCount);
|
cutsceneSystemInsertItems(cutscene->items, cutscene->itemCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void cutsceneSystemDiscardQueued(void) {
|
||||||
|
assertNotNull(
|
||||||
|
CUTSCENE_SYSTEM.scene,
|
||||||
|
"cutsceneSystemDiscardQueued called with no cutscene running"
|
||||||
|
);
|
||||||
|
|
||||||
|
// currentItem is always 0 once a cutscene has started (see its doc
|
||||||
|
// comment) - keep just that one item, drop everything queued after it.
|
||||||
|
CUTSCENE_SYSTEM.scene->itemCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
void cutsceneSystemLoad(const char_t *file) {
|
void cutsceneSystemLoad(const char_t *file) {
|
||||||
assertNotNull(file, "File cannot be NULL");
|
assertNotNull(file, "File cannot be NULL");
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,17 @@ void cutsceneSystemInsertItems(
|
|||||||
*/
|
*/
|
||||||
void cutsceneSystemInsertCutscene(const cutscene_t *cutscene);
|
void cutsceneSystemInsertCutscene(const cutscene_t *cutscene);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discards every item still queued behind the currently-running one,
|
||||||
|
* without disturbing that item's own progress - e.g. so a callback that
|
||||||
|
* decides the rest of a batch-spliced sequence is now moot (a battle
|
||||||
|
* ending mid-round, with other fighters' still-queued move cutscenes and
|
||||||
|
* the round's trailing state transition left behind) can drop it before
|
||||||
|
* splicing in its own follow-up items, instead of leaving it to run
|
||||||
|
* afterward against torn-down state.
|
||||||
|
*/
|
||||||
|
void cutsceneSystemDiscardQueued(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads and immediately starts a cutscene asset by file name.
|
* Loads and immediately starts a cutscene asset by file name.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -21,3 +21,4 @@ add_subdirectory(save)
|
|||||||
add_subdirectory(audio)
|
add_subdirectory(audio)
|
||||||
add_subdirectory(json)
|
add_subdirectory(json)
|
||||||
add_subdirectory(story)
|
add_subdirectory(story)
|
||||||
|
add_subdirectory(party)
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ void cutsceneBattlePostMoveStart(
|
|||||||
|
|
||||||
BATTLE.result = allyAlive ? BATTLE_RESULT_WIN : BATTLE_RESULT_LOSS;
|
BATTLE.result = allyAlive ? BATTLE_RESULT_WIN : BATTLE_RESULT_LOSS;
|
||||||
|
|
||||||
|
// Drop the rest of this round's already-spliced items (other fighters'
|
||||||
|
// still-pending move cutscenes, the trailing BATTLE_SET_STATE(PRE_ROUND) -
|
||||||
|
// see battleStateExecutingInit) - the battle is over, so none of that
|
||||||
|
// should run against the WON/LOST/ENDING transition about to replace it.
|
||||||
|
cutsceneSystemDiscardQueued();
|
||||||
|
|
||||||
const cutsceneitem_t items[] = {
|
const cutsceneitem_t items[] = {
|
||||||
{
|
{
|
||||||
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
.type = CUTSCENE_ITEM_TYPE_BATTLE_SET_STATE,
|
||||||
|
|||||||
@@ -371,6 +371,35 @@ cutsceneitemcallbacks_t CUTSCENE_ITEM_CALLBACKS[CUTSCENE_ITEM_TYPE_COUNT] = {
|
|||||||
.update = cutsceneStoryWaitFlagUpdate,
|
.update = cutsceneStoryWaitFlagUpdate,
|
||||||
.jsonName = "STORY_WAIT_FLAG",
|
.jsonName = "STORY_WAIT_FLAG",
|
||||||
.load = cutsceneStoryWaitFlagLoad
|
.load = cutsceneStoryWaitFlagLoad
|
||||||
|
},
|
||||||
|
|
||||||
|
// Party
|
||||||
|
[CUTSCENE_ITEM_TYPE_PARTY_ADD_MEMBER] = {
|
||||||
|
.init = cutscenePartyAddMemberStart,
|
||||||
|
.update = cutscenePartyAddMemberUpdate,
|
||||||
|
.jsonName = "PARTY_ADD_MEMBER",
|
||||||
|
.load = cutscenePartyAddMemberLoad
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_PARTY_REMOVE_MEMBER] = {
|
||||||
|
.init = cutscenePartyRemoveMemberStart,
|
||||||
|
.update = cutscenePartyRemoveMemberUpdate,
|
||||||
|
.jsonName = "PARTY_REMOVE_MEMBER",
|
||||||
|
.load = cutscenePartyRemoveMemberLoad
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_PARTY_HEAL_MEMBER] = {
|
||||||
|
.init = cutscenePartyHealMemberStart,
|
||||||
|
.update = cutscenePartyHealMemberUpdate,
|
||||||
|
.jsonName = "PARTY_HEAL_MEMBER",
|
||||||
|
.load = cutscenePartyHealMemberLoad
|
||||||
|
},
|
||||||
|
|
||||||
|
[CUTSCENE_ITEM_TYPE_PARTY_HEAL_ALL] = {
|
||||||
|
.init = cutscenePartyHealAllStart,
|
||||||
|
.update = cutscenePartyHealAllUpdate,
|
||||||
|
.jsonName = "PARTY_HEAL_ALL",
|
||||||
|
.load = cutsceneItemLoadNoop
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,10 @@
|
|||||||
#include "audio/cutsceneaudioset.h"
|
#include "audio/cutsceneaudioset.h"
|
||||||
#include "story/cutscenestorysetflag.h"
|
#include "story/cutscenestorysetflag.h"
|
||||||
#include "story/cutscenestorywaitflag.h"
|
#include "story/cutscenestorywaitflag.h"
|
||||||
|
#include "party/cutscenepartyaddmember.h"
|
||||||
|
#include "party/cutscenepartyremovemember.h"
|
||||||
|
#include "party/cutscenepartyhealmember.h"
|
||||||
|
#include "party/cutscenepartyhealall.h"
|
||||||
|
|
||||||
typedef struct cutscene_s cutscene_t;
|
typedef struct cutscene_s cutscene_t;
|
||||||
|
|
||||||
@@ -144,6 +148,9 @@ struct cutsceneitem_s {
|
|||||||
cutsceneuishow_t uiShow;
|
cutsceneuishow_t uiShow;
|
||||||
cutscenestorysetflag_t storySetFlag;
|
cutscenestorysetflag_t storySetFlag;
|
||||||
cutscenestorywaitflag_t storyWaitFlag;
|
cutscenestorywaitflag_t storyWaitFlag;
|
||||||
|
cutscenepartyaddmember_t partyAddMember;
|
||||||
|
cutscenepartyremovemember_t partyRemoveMember;
|
||||||
|
cutscenepartyhealmember_t partyHealMember;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ typedef enum {
|
|||||||
CUTSCENE_ITEM_TYPE_STORY_SET_FLAG,
|
CUTSCENE_ITEM_TYPE_STORY_SET_FLAG,
|
||||||
CUTSCENE_ITEM_TYPE_STORY_WAIT_FLAG,
|
CUTSCENE_ITEM_TYPE_STORY_WAIT_FLAG,
|
||||||
|
|
||||||
|
// Party
|
||||||
|
CUTSCENE_ITEM_TYPE_PARTY_ADD_MEMBER,
|
||||||
|
CUTSCENE_ITEM_TYPE_PARTY_REMOVE_MEMBER,
|
||||||
|
CUTSCENE_ITEM_TYPE_PARTY_HEAL_MEMBER,
|
||||||
|
CUTSCENE_ITEM_TYPE_PARTY_HEAL_ALL,
|
||||||
|
|
||||||
CUTSCENE_ITEM_TYPE_COUNT
|
CUTSCENE_ITEM_TYPE_COUNT
|
||||||
} cutsceneitemtype_t;
|
} cutsceneitemtype_t;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Copyright (c) 2026 Dominic Masters
|
||||||
|
#
|
||||||
|
# This software is released under the MIT License.
|
||||||
|
# https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
|
PUBLIC
|
||||||
|
cutscenepartyaddmember.c
|
||||||
|
cutscenepartyremovemember.c
|
||||||
|
cutscenepartyhealmember.c
|
||||||
|
cutscenepartyhealall.c
|
||||||
|
)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/battle/party.h"
|
||||||
|
#include "rpg/cutscene/item/json/cutscenejsonfield.h"
|
||||||
|
|
||||||
|
void cutscenePartyAddMemberStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
partyAddMember(item->partyAddMember.memberIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutscenePartyAddMemberUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t cutscenePartyAddMemberLoad(
|
||||||
|
yyjson_val *itemObj,
|
||||||
|
cutsceneitem_t *item
|
||||||
|
) {
|
||||||
|
const int32_t memberIndex = cutsceneJsonGetInt(itemObj, "memberIndex", -1);
|
||||||
|
if(memberIndex < 0 || memberIndex >= PARTY_MEMBER_COUNT_MAX) {
|
||||||
|
errorThrow(
|
||||||
|
"Cutscene item (PARTY_ADD_MEMBER): invalid/missing 'memberIndex'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
item->partyAddMember.memberIndex = (uint8_t)memberIndex;
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t memberIndex;
|
||||||
|
} cutscenepartyaddmember_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a PARTY_ADD_MEMBER item, activating the configured existing
|
||||||
|
* roster member into the current save slot's active party order (see
|
||||||
|
* partyAddMember). No-op if already active or there's no free order slot.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutscenePartyAddMemberStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a PARTY_ADD_MEMBER item (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutscenePartyAddMemberUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a PARTY_ADD_MEMBER cutscene item's JSON fields into item:
|
||||||
|
* 'memberIndex', a required integer roster index.
|
||||||
|
*
|
||||||
|
* @param itemObj The item's JSON object.
|
||||||
|
* @param item Destination item, already zeroed by the caller.
|
||||||
|
* @return Error code indicating success or failure of the parse.
|
||||||
|
*/
|
||||||
|
errorret_t cutscenePartyAddMemberLoad(
|
||||||
|
yyjson_val *itemObj,
|
||||||
|
cutsceneitem_t *item
|
||||||
|
);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/battle/party.h"
|
||||||
|
|
||||||
|
void cutscenePartyHealAllStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
partyHealAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutscenePartyHealAllUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a PARTY_HEAL_ALL item, fully restoring every occupied slot in
|
||||||
|
* the current save slot's party roster (see partyHealAll).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutscenePartyHealAllStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a PARTY_HEAL_ALL item (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutscenePartyHealAllUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/battle/party.h"
|
||||||
|
#include "rpg/cutscene/item/json/cutscenejsonfield.h"
|
||||||
|
|
||||||
|
void cutscenePartyHealMemberStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
partyHealMember(item->partyHealMember.memberIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutscenePartyHealMemberUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t cutscenePartyHealMemberLoad(
|
||||||
|
yyjson_val *itemObj,
|
||||||
|
cutsceneitem_t *item
|
||||||
|
) {
|
||||||
|
const int32_t memberIndex = cutsceneJsonGetInt(itemObj, "memberIndex", -1);
|
||||||
|
if(memberIndex < 0 || memberIndex >= PARTY_MEMBER_COUNT_MAX) {
|
||||||
|
errorThrow(
|
||||||
|
"Cutscene item (PARTY_HEAL_MEMBER): invalid/missing 'memberIndex'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
item->partyHealMember.memberIndex = (uint8_t)memberIndex;
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t memberIndex;
|
||||||
|
} cutscenepartyhealmember_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a PARTY_HEAL_MEMBER item, fully restoring the configured member's
|
||||||
|
* health/mp and reviving it if dead (see partyHealMember).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutscenePartyHealMemberStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a PARTY_HEAL_MEMBER item (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutscenePartyHealMemberUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a PARTY_HEAL_MEMBER cutscene item's JSON fields into item:
|
||||||
|
* 'memberIndex', a required integer roster index.
|
||||||
|
*
|
||||||
|
* @param itemObj The item's JSON object.
|
||||||
|
* @param item Destination item, already zeroed by the caller.
|
||||||
|
* @return Error code indicating success or failure of the parse.
|
||||||
|
*/
|
||||||
|
errorret_t cutscenePartyHealMemberLoad(
|
||||||
|
yyjson_val *itemObj,
|
||||||
|
cutsceneitem_t *item
|
||||||
|
);
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "rpg/cutscene/item/cutsceneitem.h"
|
||||||
|
#include "rpg/battle/party.h"
|
||||||
|
#include "rpg/cutscene/item/json/cutscenejsonfield.h"
|
||||||
|
|
||||||
|
void cutscenePartyRemoveMemberStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
partyRemoveMember(item->partyRemoveMember.memberIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool_t cutscenePartyRemoveMemberUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorret_t cutscenePartyRemoveMemberLoad(
|
||||||
|
yyjson_val *itemObj,
|
||||||
|
cutsceneitem_t *item
|
||||||
|
) {
|
||||||
|
const int32_t memberIndex = cutsceneJsonGetInt(itemObj, "memberIndex", -1);
|
||||||
|
if(memberIndex < 0 || memberIndex >= PARTY_MEMBER_COUNT_MAX) {
|
||||||
|
errorThrow(
|
||||||
|
"Cutscene item (PARTY_REMOVE_MEMBER): invalid/missing 'memberIndex'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
item->partyRemoveMember.memberIndex = (uint8_t)memberIndex;
|
||||||
|
|
||||||
|
errorOk();
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 Dominic Masters
|
||||||
|
*
|
||||||
|
* This software is released under the MIT License.
|
||||||
|
* https://opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include "rpg/cutscene/item/cutsceneitembase.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t memberIndex;
|
||||||
|
} cutscenepartyremovemember_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a PARTY_REMOVE_MEMBER item, deactivating the configured member
|
||||||
|
* from the current save slot's active party order (see partyRemoveMember)
|
||||||
|
* - the roster member itself still exists, just no longer active.
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
*/
|
||||||
|
void cutscenePartyRemoveMemberStart(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a PARTY_REMOVE_MEMBER item (always completes immediately).
|
||||||
|
*
|
||||||
|
* @param item The cutscene item.
|
||||||
|
* @param data Runtime data storage.
|
||||||
|
* @returns true always.
|
||||||
|
*/
|
||||||
|
bool_t cutscenePartyRemoveMemberUpdate(
|
||||||
|
const cutsceneitem_t *item,
|
||||||
|
cutsceneitemdata_t *data
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a PARTY_REMOVE_MEMBER cutscene item's JSON fields into item:
|
||||||
|
* 'memberIndex', a required integer roster index.
|
||||||
|
*
|
||||||
|
* @param itemObj The item's JSON object.
|
||||||
|
* @param item Destination item, already zeroed by the caller.
|
||||||
|
* @return Error code indicating success or failure of the parse.
|
||||||
|
*/
|
||||||
|
errorret_t cutscenePartyRemoveMemberLoad(
|
||||||
|
yyjson_val *itemObj,
|
||||||
|
cutsceneitem_t *item
|
||||||
|
);
|
||||||
@@ -33,7 +33,6 @@ errorret_t rpgInit(void) {
|
|||||||
errorChain(itemInit());
|
errorChain(itemInit());
|
||||||
errorChain(battleMoveInit());
|
errorChain(battleMoveInit());
|
||||||
backpackInit();
|
backpackInit();
|
||||||
partyInit();
|
|
||||||
cutsceneSystemInit();
|
cutsceneSystemInit();
|
||||||
errorChain(mapInit());
|
errorChain(mapInit());
|
||||||
rpgCameraInit();
|
rpgCameraInit();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ errorret_t saveInit() {
|
|||||||
|
|
||||||
// Initialize the slots and settings
|
// Initialize the slots and settings
|
||||||
saveSettingsInit(&SAVE.settings);
|
saveSettingsInit(&SAVE.settings);
|
||||||
saveSlotInit();
|
errorChain(saveSlotInit(&SAVE.slot));
|
||||||
|
|
||||||
// Update caches to match default data.
|
// Update caches to match default data.
|
||||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
@@ -226,7 +226,7 @@ errorret_t saveLoadSlot() {
|
|||||||
|
|
||||||
if(SAVE.deviceCurrent == 0xFF) {
|
if(SAVE.deviceCurrent == 0xFF) {
|
||||||
// No save device available, so we assume an empty slot.
|
// No save device available, so we assume an empty slot.
|
||||||
saveSlotInit();
|
errorChain(saveSlotInit(&SAVE.slot));
|
||||||
SAVE.caches[SAVE.slotCurrent] = SAVE.slot.cachedData;
|
SAVE.caches[SAVE.slotCurrent] = SAVE.slot.cachedData;
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
@@ -245,7 +245,7 @@ errorret_t saveLoadSlot() {
|
|||||||
"Save slot %u could not be loaded, treating as empty.\n",
|
"Save slot %u could not be loaded, treating as empty.\n",
|
||||||
(uint32_t)SAVE.slotCurrent
|
(uint32_t)SAVE.slotCurrent
|
||||||
);
|
);
|
||||||
saveSlotInit();
|
errorChain(saveSlotInit(&SAVE.slot));
|
||||||
SAVE.slot.cachedData.corrupt = true;
|
SAVE.slot.cachedData.corrupt = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,7 +260,7 @@ errorret_t saveLoadSlot() {
|
|||||||
errorret_t saveLoadAllSlots() {
|
errorret_t saveLoadAllSlots() {
|
||||||
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
SAVE.slotCurrent = i;
|
SAVE.slotCurrent = i;
|
||||||
saveSlotInit();
|
errorChain(saveSlotInit(&SAVE.slot));
|
||||||
errorChain(saveLoadSlot());// Load slot updates the cache.
|
errorChain(saveLoadSlot());// Load slot updates the cache.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,33 +12,33 @@
|
|||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
|
|
||||||
void saveSlotInit(void) {
|
errorret_t saveSlotInit(saveslot_t *slot) {
|
||||||
memorySet(&SAVE.slot, 0, sizeof(saveslot_t));
|
assertNotNull(slot, "Slot cannot be null");
|
||||||
|
|
||||||
SAVE.slot.version = 1;
|
// Zero the save slot.
|
||||||
|
memoryZero(slot, sizeof(saveslot_t));
|
||||||
|
slot->version = 1;
|
||||||
|
|
||||||
|
// Set the default story flags.
|
||||||
for(storyflag_t flag = 0; flag < STORY_FLAG_COUNT; flag++) {
|
for(storyflag_t flag = 0; flag < STORY_FLAG_COUNT; flag++) {
|
||||||
SAVE.slot.storyFlags[flag] = STORY_FLAG_MAP[flag].initial;
|
slot->storyFlags[flag] = STORY_FLAG_MAP[flag].initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
partyInit();
|
// Set the default party.
|
||||||
|
errorChain(partyInit(&slot->party));
|
||||||
|
|
||||||
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveSlotNewGame(void) {
|
errorret_t saveSlotNewGame(void) {
|
||||||
saveSlotInit();
|
errorChain(saveSlotInit(&SAVE.slot));
|
||||||
|
|
||||||
stringCopy(
|
stringCopy(
|
||||||
SAVE.slot.cachedData.mapName, SAVE_SLOT_MAP_NAME_DEFAULT,
|
SAVE.slot.cachedData.mapName, SAVE_SLOT_MAP_NAME_DEFAULT,
|
||||||
sizeof(SAVE.slot.cachedData.mapName)
|
sizeof(SAVE.slot.cachedData.mapName)
|
||||||
);
|
);
|
||||||
|
|
||||||
// TEMPORARY: placeholder starting party stats - replace once there's a
|
errorOk();
|
||||||
// real starting-party/character-creation flow.
|
|
||||||
const battlefighterstats_t startingStats = {
|
|
||||||
.attack = 10, .defense = 5, .magic = 0, .speed = 10, .luck = 0,
|
|
||||||
.healthMax = 30, .mpMax = 10
|
|
||||||
};
|
|
||||||
partyAddMember(startingStats);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool_t saveSlotInUse(saveslotcache_t *slot) {
|
bool_t saveSlotInUse(saveslotcache_t *slot) {
|
||||||
@@ -64,14 +64,13 @@ errorret_t saveSlotReadJSON(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");
|
||||||
|
|
||||||
// A plain reset, not saveSlotInit() - this may be reading into a slot
|
// saveSlotInit() now takes slot directly, so this can reset through it
|
||||||
// other than SAVE.slot (e.g. a save-device round trip in a test), and
|
// rather than duplicating the memSet/version stub - saveSlotCurrent
|
||||||
// saveSlotInit() only ever operates on SAVE.slot now. saveSlotCurrent
|
// ReadJSON() fully overwrites every field it sets anyway (including
|
||||||
// ReadJSON() fully overwrites every field below anyway (including
|
// party.members/order), so seeding a fresh party.jsonc roster here only
|
||||||
// party.members/order), so this only needs to reset the bookkeeping
|
// to replace it is wasted work, but keeps this in one reset path rather
|
||||||
// fields (version/dataType) that aren't part of the JSON wire format.
|
// than two subtly different ones.
|
||||||
memorySet(slot, 0, sizeof(saveslot_t));
|
errorChain(saveSlotInit(slot));
|
||||||
slot->version = 1;
|
|
||||||
|
|
||||||
errorChain(saveSlotCurrentReadJSON(slot, object));
|
errorChain(saveSlotCurrentReadJSON(slot, object));
|
||||||
errorOk();
|
errorOk();
|
||||||
|
|||||||
@@ -37,22 +37,27 @@ typedef struct saveslot_s {
|
|||||||
} saveslot_t;
|
} saveslot_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inits the current save slot (SAVE.slot) with the default state, this is
|
* Inits slot with the default state, this is functionally "new game" but
|
||||||
* functionally "new game" but will not set the player name, as that is
|
* will not set the player name, as that is what we use to determine if a
|
||||||
* what we use to determine if a save slot is "in use" or not.
|
* save slot is "in use" or not. Seeds the party roster from party.jsonc
|
||||||
|
* (see partyInit).
|
||||||
|
*
|
||||||
|
* @param slot The save slot to initialize.
|
||||||
|
* @return Error if the starting party roster (party.jsonc) fails to
|
||||||
|
* load/parse.
|
||||||
*/
|
*/
|
||||||
void saveSlotInit(void);
|
errorret_t saveSlotInit(saveslot_t *slot);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the current save slot (SAVE.slot) for a brand new game: calls
|
* 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 (which seeds the starting party roster) and sets the
|
||||||
* saveSlotInit) whenever a new save is actually being created for a
|
* default starting map. Use this (not saveSlotInit directly) whenever a
|
||||||
* player - saveSlotInit alone leaves the party empty, which every other
|
* new save is actually being created for a player. Callers creating a
|
||||||
* caller (resetting the slot before a load attempt, wiping a deleted
|
* new save at a specific index should set SAVE.slotCurrent first.
|
||||||
* slot) wants. Callers creating a new save at a specific index should set
|
*
|
||||||
* SAVE.slotCurrent first.
|
* @return Error if saveSlotInit fails.
|
||||||
*/
|
*/
|
||||||
void saveSlotNewGame(void);
|
errorret_t saveSlotNewGame(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
|||||||
@@ -107,7 +107,12 @@ void uiSelectSaveDeleteConfirmed(const bool_t result, void *user) {
|
|||||||
// uimainmenu.c), so it's safe to reuse SAVE.slot as scratch for whichever
|
// 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.
|
// index is being written here - nothing else is relying on it.
|
||||||
SAVE.slotCurrent = index;
|
SAVE.slotCurrent = index;
|
||||||
saveSlotInit();
|
errorret_t initResult = saveSlotInit(&SAVE.slot);
|
||||||
|
if(errorIsNotOk(initResult)) {
|
||||||
|
errorCatch(errorPrint(initResult));
|
||||||
|
uiFatalErrorOpen(initResult.state->message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t writeResult = saveDeviceSlotWrite(
|
errorret_t writeResult = saveDeviceSlotWrite(
|
||||||
&SAVE.devices[SAVE.deviceCurrent], &SAVE.slot, index
|
&SAVE.devices[SAVE.deviceCurrent], &SAVE.slot, index
|
||||||
@@ -144,7 +149,12 @@ void uiSelectSaveNameEntered(
|
|||||||
// uimainmenu.c), so it's safe to reuse SAVE.slot as scratch for whichever
|
// 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.
|
// index is being written here - nothing else is relying on it.
|
||||||
SAVE.slotCurrent = index;
|
SAVE.slotCurrent = index;
|
||||||
saveSlotNewGame();
|
errorret_t newGameResult = saveSlotNewGame();
|
||||||
|
if(errorIsNotOk(newGameResult)) {
|
||||||
|
errorCatch(errorPrint(newGameResult));
|
||||||
|
uiFatalErrorOpen(newGameResult.state->message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
stringCopy(SAVE.slot.cachedData.name, text, SAVE_SLOT_NAME_LENGTH);
|
stringCopy(SAVE.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
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ void uiBattleMenuOpenTargets(void) {
|
|||||||
uiMenuOpen(&UI_BATTLE_MENU.targetMenu);
|
uiMenuOpen(&UI_BATTLE_MENU.targetMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool_t uiBattleMenuActionCancel(const uimenu_t *menu) {
|
||||||
|
battleStateSelectionGoBack();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void uiBattleMenuActionSelected(
|
void uiBattleMenuActionSelected(
|
||||||
const uimenu_t *menu,
|
const uimenu_t *menu,
|
||||||
const uint8_t index,
|
const uint8_t index,
|
||||||
@@ -112,6 +117,8 @@ errorret_t uiBattleMenuInit(void) {
|
|||||||
MENU_BUTTON(UI_BATTLE_MENU.fleeLabel);
|
MENU_BUTTON(UI_BATTLE_MENU.fleeLabel);
|
||||||
MENU_END(UI_BATTLE_MENU.actionItems, 1);
|
MENU_END(UI_BATTLE_MENU.actionItems, 1);
|
||||||
|
|
||||||
|
uiMenuSetCancelCallback(&UI_BATTLE_MENU.actionMenu, uiBattleMenuActionCancel);
|
||||||
|
|
||||||
errorOk();
|
errorOk();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,13 @@ void uiMainMenuSelectSaveResult(const uint8_t slotIndex, void *user) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SAVE.slotCurrent = slotIndex;
|
SAVE.slotCurrent = slotIndex;
|
||||||
saveSlotInit();
|
errorret_t initResult = saveSlotInit(&SAVE.slot);
|
||||||
|
if(errorIsNotOk(initResult)) {
|
||||||
|
errorCatch(errorPrint(initResult));
|
||||||
|
uiFatalErrorOpen(initResult.state->message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
errorret_t result = saveLoadSlot();
|
errorret_t result = saveLoadSlot();
|
||||||
if(errorIsNotOk(result)) {
|
if(errorIsNotOk(result)) {
|
||||||
errorCatch(errorPrint(result));
|
errorCatch(errorPrint(result));
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ include(dusktest)
|
|||||||
# Tests
|
# Tests
|
||||||
dusktest(test_savesettings.c)
|
dusktest(test_savesettings.c)
|
||||||
dusktest(test_saveslot.c)
|
dusktest(test_saveslot.c)
|
||||||
|
target_sources(test_saveslot PRIVATE savetestfixture.c)
|
||||||
|
|
||||||
dusktest(test_savedevicelinux.c)
|
dusktest(test_savedevicelinux.c)
|
||||||
target_sources(test_savedevicelinux PRIVATE savetestfixture.c)
|
target_sources(test_savedevicelinux PRIVATE savetestfixture.c)
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
|
|
||||||
#include "savetestfixture.h"
|
#include "savetestfixture.h"
|
||||||
#include "assert/assert.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/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include <ftw.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 char_t SAVE_TEST_FIXTURE_REAL_HOME[PATH_MAX];
|
||||||
static bool_t SAVE_TEST_FIXTURE_HAD_REAL_HOME;
|
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(
|
static int_t saveTestFixtureRemoveEntry(
|
||||||
const char *path,
|
const char *path,
|
||||||
const struct stat *statBuffer,
|
const struct stat *statBuffer,
|
||||||
@@ -49,6 +97,8 @@ int saveTestFixtureSetup(void **state) {
|
|||||||
|
|
||||||
assertTrue(setenv("HOME", SAVE_TEST_FIXTURE_HOME, 1) == 0, "setenv failed");
|
assertTrue(setenv("HOME", SAVE_TEST_FIXTURE_HOME, 1) == 0, "setenv failed");
|
||||||
|
|
||||||
|
saveTestFixtureStubPartyAsset();
|
||||||
|
|
||||||
errorret_t ret = saveInit();
|
errorret_t ret = saveInit();
|
||||||
assertTrue(errorIsOk(ret), "saveInit failed in test fixture");
|
assertTrue(errorIsOk(ret), "saveInit failed in test fixture");
|
||||||
|
|
||||||
@@ -56,6 +106,8 @@ int saveTestFixtureSetup(void **state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int saveTestFixtureTeardown(void **state) {
|
int saveTestFixtureTeardown(void **state) {
|
||||||
|
saveTestFixtureUnstubPartyAsset();
|
||||||
|
|
||||||
nftw(
|
nftw(
|
||||||
SAVE_TEST_FIXTURE_HOME, saveTestFixtureRemoveEntry, 16,
|
SAVE_TEST_FIXTURE_HOME, saveTestFixtureRemoveEntry, 16,
|
||||||
FTW_DEPTH | FTW_PHYS
|
FTW_DEPTH | FTW_PHYS
|
||||||
|
|||||||
@@ -35,3 +35,21 @@ int saveTestFixtureTeardown(void **state);
|
|||||||
* filesystem permission tricks. Must be called after saveTestFixtureSetup.
|
* filesystem permission tricks. Must be called after saveTestFixtureSetup.
|
||||||
*/
|
*/
|
||||||
void saveTestFixtureBlockSaveDirectory(void);
|
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) {
|
static void test_saveSaveSlot_success(void **state) {
|
||||||
makeDeviceAvailable();
|
makeDeviceAvailable();
|
||||||
SAVE.slotCurrent = 1;
|
SAVE.slotCurrent = 1;
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
stringCopy(
|
stringCopy(
|
||||||
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
|
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
|
// ever operates on SAVE.slot now), then reset it again below - the
|
||||||
// write and the load-under-test don't overlap in time.
|
// write and the load-under-test don't overlap in time.
|
||||||
SAVE.slotCurrent = 2;
|
SAVE.slotCurrent = 2;
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
stringCopy(
|
stringCopy(
|
||||||
SAVE.slot.cachedData.name, "Zelda", sizeof(SAVE.slot.cachedData.name)
|
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));
|
assert_true(errorIsOk(writeRet));
|
||||||
|
|
||||||
SAVE.slotCurrent = 2;
|
SAVE.slotCurrent = 2;
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
SAVE.slotDirty = true;
|
SAVE.slotDirty = true;
|
||||||
|
|
||||||
errorret_t ret = saveLoadSlot();
|
errorret_t ret = saveLoadSlot();
|
||||||
@@ -371,7 +371,7 @@ static void test_saveLoadSlot_deviceReadFails_cacheStaysStale(void **state) {
|
|||||||
fclose(file);
|
fclose(file);
|
||||||
|
|
||||||
SAVE.slotCurrent = 0;
|
SAVE.slotCurrent = 0;
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
SAVE.caches[0].playerLevel = 77;// sentinel
|
SAVE.caches[0].playerLevel = 77;// sentinel
|
||||||
|
|
||||||
errorret_t ret = saveLoadSlot();
|
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++) {
|
for(uint8_t i = 0; i < SAVE_SLOT_COUNT; i++) {
|
||||||
SAVE.slotCurrent = i;
|
SAVE.slotCurrent = i;
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
SAVE.slot.cachedData.playerLevel = (int32_t)(i + 1);
|
SAVE.slot.cachedData.playerLevel = (int32_t)(i + 1);
|
||||||
errorret_t saveRet = saveSaveSlot();
|
errorret_t saveRet = saveSaveSlot();
|
||||||
assert_true(errorIsOk(saveRet));
|
assert_true(errorIsOk(saveRet));
|
||||||
@@ -410,7 +410,7 @@ static void test_saveLoadAllSlots_missingFileLeavesInitDefaults(void **state) {
|
|||||||
makeDeviceAvailable();
|
makeDeviceAvailable();
|
||||||
|
|
||||||
SAVE.slotCurrent = 0;
|
SAVE.slotCurrent = 0;
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
SAVE.slot.cachedData.playerLevel = 5;
|
SAVE.slot.cachedData.playerLevel = 5;
|
||||||
errorret_t saveRet = saveSaveSlot();
|
errorret_t saveRet = saveSaveSlot();
|
||||||
assert_true(errorIsOk(saveRet));
|
assert_true(errorIsOk(saveRet));
|
||||||
@@ -433,7 +433,7 @@ static void test_saveLoadAllSlots_middleSlotCorrupt_stopsAtFailingIndex(
|
|||||||
makeDeviceAvailable();
|
makeDeviceAvailable();
|
||||||
|
|
||||||
SAVE.slotCurrent = 0;
|
SAVE.slotCurrent = 0;
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
errorret_t saveRet = saveSaveSlot();
|
errorret_t saveRet = saveSaveSlot();
|
||||||
assert_true(errorIsOk(saveRet));
|
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
|
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
|
||||||
// write source here.
|
// write source here.
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
SAVE.slot.cachedData.playerLevel = 21;
|
SAVE.slot.cachedData.playerLevel = 21;
|
||||||
|
|
||||||
errorret_t writeRet = saveDeviceSlotWrite(&device, &SAVE.slot, 0);
|
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
|
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
|
||||||
// write source here.
|
// write source here.
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
stringCopy(
|
stringCopy(
|
||||||
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
|
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,11 +9,25 @@
|
|||||||
#include "save/slot/saveslot.h"
|
#include "save/slot/saveslot.h"
|
||||||
#include "save/save.h"
|
#include "save/save.h"
|
||||||
#include "save/savejson.h"
|
#include "save/savejson.h"
|
||||||
|
#include "savetestfixture.h"
|
||||||
#include "util/memory.h"
|
#include "util/memory.h"
|
||||||
#include "util/string.h"
|
#include "util/string.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.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
|
// Helpers - wrap the writeInit/readInit macros (which can only be used
|
||||||
// inside a function returning errorret_t) so tests can call them plainly.
|
// 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) {
|
static void test_saveSlotInit_defaults(void **state) {
|
||||||
memorySet(&SAVE.slot, 0xFF, sizeof(SAVE.slot));
|
memorySet(&SAVE.slot, 0xFF, sizeof(SAVE.slot));
|
||||||
|
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
|
|
||||||
assert_int_equal(SAVE.slot.version, 1);
|
assert_int_equal(SAVE.slot.version, 1);
|
||||||
assert_int_equal(SAVE.slot.dataType, 0);
|
assert_int_equal(SAVE.slot.dataType, 0);
|
||||||
@@ -66,7 +80,7 @@ static void test_saveSlotInit_defaults(void **state) {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
static void test_saveSlotInUse(void **state) {
|
static void test_saveSlotInUse(void **state) {
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
|
|
||||||
assert_false(saveSlotInUse(&SAVE.slot.cachedData));
|
assert_false(saveSlotInUse(&SAVE.slot.cachedData));
|
||||||
|
|
||||||
@@ -81,7 +95,7 @@ static void test_saveSlotInUse_nullAsserts(void **state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void test_saveSlotHasSaved(void **state) {
|
static void test_saveSlotHasSaved(void **state) {
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
|
|
||||||
assert_false(saveSlotHasSaved(&SAVE.slot.cachedData));
|
assert_false(saveSlotHasSaved(&SAVE.slot.cachedData));
|
||||||
|
|
||||||
@@ -136,7 +150,7 @@ static void test_saveSlotReadJSON_nullAsserts(void **state) {
|
|||||||
static void test_saveSlotReadJSON_roundTrip(void **state) {
|
static void test_saveSlotReadJSON_roundTrip(void **state) {
|
||||||
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
|
// saveSlotInit() only ever operates on SAVE.slot now, so that's the
|
||||||
// write source here.
|
// write source here.
|
||||||
saveSlotInit();
|
saveSlotInit(&SAVE.slot);
|
||||||
SAVE.slot.version = 7;// deliberately non-default, not part of the JSON schema
|
SAVE.slot.version = 7;// deliberately non-default, not part of the JSON schema
|
||||||
stringCopy(
|
stringCopy(
|
||||||
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
|
SAVE.slot.cachedData.name, "Hero", sizeof(SAVE.slot.cachedData.name)
|
||||||
@@ -296,5 +310,5 @@ int main(void) {
|
|||||||
cmocka_unit_test(test_saveSlotReadJSON_versionMismatch_errors),
|
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