Add shared battleFighterParse/Serialize, use it everywhere a fighter is (de)serialized
Replaces three independently hand-rolled battlefighter_t JSON readers/ writers (party.jsonc roster loading, BATTLE_START enemy definitions, and the v1 save-file party member format) with one generic pair in battlefighter.c: battleFighterParse(fighter, obj) and its inverse battleFighterSerialize(fighter, doc). Covers team/controller/status (optional, default ally/player/normal), the 7 base stats (required), current health/mp (optional, default to max), and an optional abilities name array. - party.c: partyInit now parses each roster member via battleFighterParse directly, dropping the old partyParseStats/partyParseStatField. - cutscenebattlestart.c/.h: cutscenebattlestartenemy_t now embeds a full battlefighter_t, parsed once at Load time; Start just copies it into the enemy pool instead of re-initializing. battletest.jsonc's enemies are now flat (dropped the "stats" wrapper) to match. - saveslotver1.c/.h: rewritten in place to use the shared parser/ serializer, changing the v1 on-disk field names/shape (no migration - acceptable pre-release, documented in saveslotver1.h). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
@@ -4,18 +4,14 @@
|
||||
"type": "BATTLE_START",
|
||||
"enemies": [
|
||||
{
|
||||
"stats": {
|
||||
"attack": 8, "defense": 4, "magic": 0, "speed": 5, "luck": 2,
|
||||
"healthMax": 30, "mpMax": 0
|
||||
},
|
||||
"healthMax": 30, "mpMax": 0,
|
||||
"abilities": ["fire"]
|
||||
},
|
||||
{
|
||||
"stats": {
|
||||
"attack": 6, "defense": 3, "magic": 0, "speed": 7, "luck": 2,
|
||||
"healthMax": 20, "mpMax": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -9,6 +9,161 @@
|
||||
#include "assert/assert.h"
|
||||
#include "util/memory.h"
|
||||
|
||||
errorret_t battleFighterParseStatField(
|
||||
yyjson_val *obj,
|
||||
const char_t *key,
|
||||
uint16_t *out
|
||||
) {
|
||||
yyjson_val *val = yyjson_obj_get(obj, key);
|
||||
if(!yyjson_is_int(val)) {
|
||||
errorThrow("Fighter: missing/invalid '%s'", key);
|
||||
}
|
||||
|
||||
const int64_t num = yyjson_get_sint(val);
|
||||
if(num < 0 || num > UINT16_MAX) {
|
||||
errorThrow("Fighter: '%s' must be between 0 and %u", key, UINT16_MAX);
|
||||
}
|
||||
|
||||
*out = (uint16_t)num;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t battleFighterParseOptionalIntField(
|
||||
yyjson_val *obj,
|
||||
const char_t *key,
|
||||
const int32_t defaultValue,
|
||||
int32_t *out
|
||||
) {
|
||||
yyjson_val *val = yyjson_obj_get(obj, key);
|
||||
if(val == NULL) {
|
||||
*out = defaultValue;
|
||||
errorOk();
|
||||
}
|
||||
|
||||
if(!yyjson_is_int(val)) errorThrow("Fighter: invalid '%s'", key);
|
||||
*out = (int32_t)yyjson_get_sint(val);
|
||||
errorOk();
|
||||
}
|
||||
|
||||
errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj) {
|
||||
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||
assertNotNull(obj, "JSON object cannot be NULL");
|
||||
|
||||
int32_t teamNum;
|
||||
errorChain(battleFighterParseOptionalIntField(
|
||||
obj, "team", BATTLE_FIGHTER_TEAM_ALLY, &teamNum
|
||||
));
|
||||
if(teamNum < 0 || teamNum >= BATTLE_FIGHTER_TEAM_COUNT) {
|
||||
errorThrow("Fighter: invalid 'team'");
|
||||
}
|
||||
|
||||
int32_t controllerNum;
|
||||
errorChain(battleFighterParseOptionalIntField(
|
||||
obj, "controller", BATTLE_FIGHTER_CONTROLLER_PLAYER, &controllerNum
|
||||
));
|
||||
if(controllerNum < 0 || controllerNum >= BATTLE_FIGHTER_CONTROLLER_COUNT) {
|
||||
errorThrow("Fighter: invalid 'controller'");
|
||||
}
|
||||
|
||||
battlefighterstats_t stats;
|
||||
errorChain(battleFighterParseStatField(obj, "attack", &stats.attack));
|
||||
errorChain(battleFighterParseStatField(obj, "defense", &stats.defense));
|
||||
errorChain(battleFighterParseStatField(obj, "magic", &stats.magic));
|
||||
errorChain(battleFighterParseStatField(obj, "speed", &stats.speed));
|
||||
errorChain(battleFighterParseStatField(obj, "luck", &stats.luck));
|
||||
errorChain(battleFighterParseStatField(obj, "healthMax", &stats.healthMax));
|
||||
errorChain(battleFighterParseStatField(obj, "mpMax", &stats.mpMax));
|
||||
|
||||
battleFighterInit(
|
||||
fighter, (battlefighterteam_t)teamNum,
|
||||
(battlefightercontroller_t)controllerNum, stats
|
||||
);
|
||||
|
||||
int32_t statusNum;
|
||||
errorChain(battleFighterParseOptionalIntField(
|
||||
obj, "status", BATTLE_FIGHTER_STATUS_NORMAL, &statusNum
|
||||
));
|
||||
if(statusNum < 0 || statusNum >= BATTLE_FIGHTER_STATUS_COUNT) {
|
||||
errorThrow("Fighter: invalid 'status'");
|
||||
}
|
||||
fighter->status = (battlefighterstatus_t)statusNum;
|
||||
|
||||
int32_t health;
|
||||
errorChain(
|
||||
battleFighterParseOptionalIntField(obj, "health", stats.healthMax, &health)
|
||||
);
|
||||
if(health < 0 || health > stats.healthMax) {
|
||||
errorThrow("Fighter: 'health' out of range");
|
||||
}
|
||||
fighter->stats.health = (uint16_t)health;
|
||||
|
||||
int32_t mp;
|
||||
errorChain(battleFighterParseOptionalIntField(obj, "mp", stats.mpMax, &mp));
|
||||
if(mp < 0 || mp > stats.mpMax) {
|
||||
errorThrow("Fighter: 'mp' out of range");
|
||||
}
|
||||
fighter->stats.mp = (uint16_t)mp;
|
||||
|
||||
yyjson_val *abilities = yyjson_obj_get(obj, "abilities");
|
||||
if(abilities != NULL) {
|
||||
if(
|
||||
!yyjson_is_arr(abilities) ||
|
||||
yyjson_arr_size(abilities) > BATTLE_FIGHTER_ABILITY_COUNT_MAX
|
||||
) {
|
||||
errorThrow(
|
||||
"Fighter: 'abilities' must have 0-%u entries",
|
||||
BATTLE_FIGHTER_ABILITY_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
yyjson_arr_iter iter = yyjson_arr_iter_with(abilities);
|
||||
yyjson_val *abilityVal;
|
||||
while((abilityVal = yyjson_arr_iter_next(&iter)) != NULL) {
|
||||
const char_t *name = yyjson_get_str(abilityVal);
|
||||
battlemoveid_t ability =
|
||||
name == NULL ? BATTLE_MOVE_ID_NULL : battleMoveGetIdByName(name);
|
||||
if(ability == BATTLE_MOVE_ID_NULL) {
|
||||
errorThrow("Fighter: invalid entry in 'abilities'");
|
||||
}
|
||||
battleFighterAbilityAdd(fighter, ability);
|
||||
}
|
||||
}
|
||||
|
||||
errorOk();
|
||||
}
|
||||
|
||||
yyjson_mut_val *battleFighterSerialize(
|
||||
const battlefighter_t *fighter,
|
||||
yyjson_mut_doc *doc
|
||||
) {
|
||||
assertNotNull(fighter, "Fighter cannot be NULL");
|
||||
assertNotNull(doc, "Doc cannot be NULL");
|
||||
|
||||
yyjson_mut_val *obj = yyjson_mut_obj(doc);
|
||||
|
||||
yyjson_mut_obj_add_uint(doc, obj, "team", fighter->team);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "controller", fighter->controller);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "status", fighter->status);
|
||||
|
||||
yyjson_mut_obj_add_uint(doc, obj, "attack", fighter->stats.attack);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "defense", fighter->stats.defense);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "magic", fighter->stats.magic);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "speed", fighter->stats.speed);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "luck", fighter->stats.luck);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "healthMax", fighter->stats.healthMax);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "mpMax", fighter->stats.mpMax);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "health", fighter->stats.health);
|
||||
yyjson_mut_obj_add_uint(doc, obj, "mp", fighter->stats.mp);
|
||||
|
||||
yyjson_mut_val *abilities = yyjson_mut_arr(doc);
|
||||
for(uint8_t i = 0; i < fighter->abilityCount; i++) {
|
||||
yyjson_mut_arr_add_str(doc, abilities, BATTLE_MOVES[fighter->abilities[i]].name);
|
||||
}
|
||||
yyjson_mut_obj_add_val(doc, obj, "abilities", abilities);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
void battleFighterInit(
|
||||
battlefighter_t *fighter,
|
||||
const battlefighterteam_t team,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
#include "error/error.h"
|
||||
#include "yyjson.h"
|
||||
#include "rpg/battle/move/battlemove.h"
|
||||
#include "rpg/battle/fighter/battlefighterstatus.h"
|
||||
#include "rpg/battle/fighter/battlefighterteam.h"
|
||||
@@ -45,6 +46,42 @@ void battleFighterInit(
|
||||
const battlefighterstats_t stats
|
||||
);
|
||||
|
||||
/**
|
||||
* Generic battlefighter_t parser, shared by every place that builds a
|
||||
* fighter from JSON (party.jsonc roster members, BATTLE_START enemy
|
||||
* definitions, save-file party members) - see battleFighterSerialize for
|
||||
* the inverse. Reads 'team'/'controller'/'status' as ints (each optional,
|
||||
* defaulting to ALLY/PLAYER/NORMAL respectively - a caller that needs a
|
||||
* different team/controller, e.g. BATTLE_START's enemies, sets those
|
||||
* fields on fighter after this returns), 'attack'/'defense'/'magic'/
|
||||
* 'speed'/'luck'/'healthMax'/'mpMax' as required ints in [0, UINT16_MAX],
|
||||
* 'health'/'mp' as optional ints defaulting to healthMax/mpMax (a fresh
|
||||
* fighter), and an optional 'abilities' array of move name strings. Does
|
||||
* not touch fighter->id - set it (or leave it) before calling, same as
|
||||
* battleFighterInit.
|
||||
*
|
||||
* @param fighter Pointer to the fighter to fill in.
|
||||
* @param obj The JSON object to read fields from.
|
||||
* @return Error if a required field is missing/invalid, or an abilities
|
||||
* entry doesn't name a real move.
|
||||
*/
|
||||
errorret_t battleFighterParse(battlefighter_t *fighter, yyjson_val *obj);
|
||||
|
||||
/**
|
||||
* Inverse of battleFighterParse: builds a new JSON object (not yet
|
||||
* attached anywhere - the caller attaches it, e.g. via
|
||||
* yyjson_mut_arr_add_val/yyjson_mut_obj_add_val) with every field
|
||||
* battleFighterParse reads except id (see its doc comment for why).
|
||||
*
|
||||
* @param fighter The fighter to serialize.
|
||||
* @param doc The mutable document to allocate the object in.
|
||||
* @return The newly built object.
|
||||
*/
|
||||
yyjson_mut_val *battleFighterSerialize(
|
||||
const battlefighter_t *fighter,
|
||||
yyjson_mut_doc *doc
|
||||
);
|
||||
|
||||
/**
|
||||
* Returns true if the fighter is in a state where it can still act (i.e.
|
||||
* is not dead).
|
||||
|
||||
@@ -13,43 +13,6 @@
|
||||
#include "asset/asset.h"
|
||||
#include "asset/loader/assetloader.h"
|
||||
|
||||
errorret_t partyParseStatField(
|
||||
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");
|
||||
|
||||
@@ -96,21 +59,15 @@ errorret_t partyInit(party_t *party) {
|
||||
);
|
||||
}
|
||||
|
||||
battlefighterstats_t stats;
|
||||
errorret_t statsResult = partyParseStats(memberObj, &stats);
|
||||
if(errorIsNotOk(statsResult)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(statsResult);
|
||||
}
|
||||
|
||||
stringCopy(party->keys[idx], yyjson_get_str(key), PARTY_KEY_LENGTH_MAX - 1);
|
||||
|
||||
battlefighter_t *member = &party->members[idx];
|
||||
member->id = idx;
|
||||
battleFighterInit(
|
||||
member, BATTLE_FIGHTER_TEAM_ALLY, BATTLE_FIGHTER_CONTROLLER_PLAYER,
|
||||
stats
|
||||
);
|
||||
errorret_t parseResult = battleFighterParse(member, memberObj);
|
||||
if(errorIsNotOk(parseResult)) {
|
||||
assetUnlockEntry(entry);
|
||||
errorChain(parseResult);
|
||||
}
|
||||
|
||||
idx++;
|
||||
}
|
||||
|
||||
@@ -25,12 +25,12 @@ typedef struct {
|
||||
* Initializes party with an empty order, then seeds every roster slot
|
||||
* directly from party.jsonc's "members" object (each key/value pair fills
|
||||
* the next members[]/keys[] slot in the object's iteration order, key
|
||||
* copied into keys[] and value parsed 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 (the first member starts active
|
||||
* in the first party position). Replace this default once there's a real
|
||||
* starting-party/character-creation flow.
|
||||
* copied into keys[] and value parsed via battleFighterParse) - 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 (the first member 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, defines more members
|
||||
@@ -58,19 +58,6 @@ uint8_t partyGetMemberIndexByKey(const char_t *key);
|
||||
*/
|
||||
const char_t *partyGetMemberKey(const uint8_t memberIndex);
|
||||
|
||||
/**
|
||||
* Parses a set of base combat stats (attack/defense/magic/speed/luck/
|
||||
* 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).
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
errorret_t partyParseStats(yyjson_val *obj, battlefighterstats_t *out);
|
||||
|
||||
/**
|
||||
* Checks whether memberIndex currently occupies a slot in the current save
|
||||
* slot's party order (i.e. is actively in the party right now).
|
||||
|
||||
@@ -34,17 +34,8 @@ void cutsceneBattleStartStart(
|
||||
sizeof(CUTSCENE_BATTLE_START_ENEMY_POOL)
|
||||
);
|
||||
for(uint8_t i = 0; i < config->enemyCount; i++) {
|
||||
const cutscenebattlestartenemy_t *enemyConfig = &config->enemies[i];
|
||||
battlefighter_t *enemy = &CUTSCENE_BATTLE_START_ENEMY_POOL[i];
|
||||
|
||||
battleFighterInit(
|
||||
enemy, BATTLE_FIGHTER_TEAM_ENEMY, BATTLE_FIGHTER_CONTROLLER_AI,
|
||||
enemyConfig->stats
|
||||
);
|
||||
for(uint8_t a = 0; a < enemyConfig->abilityCount; a++) {
|
||||
battleFighterAbilityAdd(enemy, enemyConfig->abilities[a]);
|
||||
}
|
||||
|
||||
*enemy = config->enemies[i].fighter;
|
||||
fighters[i] = enemy;
|
||||
}
|
||||
|
||||
@@ -88,54 +79,14 @@ errorret_t cutsceneBattleStartLoad(
|
||||
while((enemyVal = yyjson_arr_iter_next(&iter)) != NULL) {
|
||||
cutscenebattlestartenemy_t *enemy = &item->battleStart.enemies[e];
|
||||
|
||||
yyjson_val *stats = yyjson_obj_get(enemyVal, "stats");
|
||||
if(!yyjson_is_obj(stats)) {
|
||||
errorThrow(
|
||||
"Cutscene item (BATTLE_START): enemies[%u] is missing 'stats'",
|
||||
(uint32_t)e
|
||||
);
|
||||
}
|
||||
enemy->stats.attack = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "attack"));
|
||||
enemy->stats.defense = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "defense"));
|
||||
enemy->stats.magic = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "magic"));
|
||||
enemy->stats.speed = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "speed"));
|
||||
enemy->stats.luck = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "luck"));
|
||||
enemy->stats.healthMax = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "healthMax"));
|
||||
enemy->stats.mpMax = (uint16_t)yyjson_get_int(yyjson_obj_get(stats, "mpMax"));
|
||||
errorret_t parseResult = battleFighterParse(&enemy->fighter, enemyVal);
|
||||
if(errorIsNotOk(parseResult)) errorChain(parseResult);
|
||||
|
||||
yyjson_val *abilities = yyjson_obj_get(enemyVal, "abilities");
|
||||
if(abilities != NULL) {
|
||||
size_t abilityCount = yyjson_arr_size(abilities);
|
||||
if(
|
||||
!yyjson_is_arr(abilities) ||
|
||||
abilityCount > BATTLE_FIGHTER_ABILITY_COUNT_MAX
|
||||
) {
|
||||
errorThrow(
|
||||
"Cutscene item (BATTLE_START): enemies[%u].abilities must have "
|
||||
"0-%u entries",
|
||||
(uint32_t)e, (uint32_t)BATTLE_FIGHTER_ABILITY_COUNT_MAX
|
||||
);
|
||||
}
|
||||
|
||||
yyjson_arr_iter abilityIter = yyjson_arr_iter_with(abilities);
|
||||
yyjson_val *abilityVal;
|
||||
size_t a = 0;
|
||||
while((abilityVal = yyjson_arr_iter_next(&abilityIter)) != NULL) {
|
||||
const char_t *name = yyjson_get_str(abilityVal);
|
||||
battlemoveid_t ability = name == NULL
|
||||
? BATTLE_MOVE_ID_NULL : battleMoveGetIdByName(name);
|
||||
if(ability == BATTLE_MOVE_ID_NULL) {
|
||||
errorThrow(
|
||||
"Cutscene item (BATTLE_START): enemies[%u].abilities[%u] is "
|
||||
"invalid",
|
||||
(uint32_t)e, (uint32_t)a
|
||||
);
|
||||
}
|
||||
enemy->abilities[a] = ability;
|
||||
a++;
|
||||
}
|
||||
enemy->abilityCount = (uint8_t)abilityCount;
|
||||
}
|
||||
// Enemy JSON never specifies team/controller (battleFighterParse just
|
||||
// defaulted them to ALLY/PLAYER) - BATTLE_START enemies are always
|
||||
// ENEMY/AI.
|
||||
enemy->fighter.team = BATTLE_FIGHTER_TEAM_ENEMY;
|
||||
enemy->fighter.controller = BATTLE_FIGHTER_CONTROLLER_AI;
|
||||
|
||||
e++;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
#define CUTSCENE_BATTLE_START_ENEMY_COUNT_MAX 4
|
||||
|
||||
typedef struct {
|
||||
battlefighterstats_t stats;
|
||||
battlemoveid_t abilities[BATTLE_FIGHTER_ABILITY_COUNT_MAX];
|
||||
uint8_t abilityCount;
|
||||
// Fully parsed at Load time via battleFighterParse (team/controller
|
||||
// forced to ENEMY/AI right after, since enemy JSON only ever supplies
|
||||
// stats/abilities) - Start splices this directly into
|
||||
// CUTSCENE_BATTLE_START_ENEMY_POOL, no further init needed.
|
||||
battlefighter_t fighter;
|
||||
} cutscenebattlestartenemy_t;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -26,21 +26,9 @@ errorret_t saveSlotVer1WriteJSON(
|
||||
|
||||
yyjson_mut_val *partyMembersArr = yyjson_mut_arr(doc);
|
||||
for(uint8_t i = 0; i < PARTY_MEMBER_COUNT_MAX; i++) {
|
||||
battlefighter_t *member = &slot->partyMembers[i];
|
||||
writeArrayObject(partyMembersArr);
|
||||
writeUInt32("id", member->id);
|
||||
writeUInt32("status", member->status);
|
||||
writeUInt32("team", member->team);
|
||||
writeUInt32("controller", member->controller);
|
||||
writeUInt32("health", member->stats.health);
|
||||
writeUInt32("healthMax", member->stats.healthMax);
|
||||
writeUInt32("mp", member->stats.mp);
|
||||
writeUInt32("mpMax", member->stats.mpMax);
|
||||
writeUInt32("statAttack", member->stats.attack);
|
||||
writeUInt32("statDefense", member->stats.defense);
|
||||
writeUInt32("statMagic", member->stats.magic);
|
||||
writeUInt32("statSpeed", member->stats.speed);
|
||||
writeUInt32("statLuck", member->stats.luck);
|
||||
yyjson_mut_val *memberObj =
|
||||
battleFighterSerialize(&slot->partyMembers[i], doc);
|
||||
yyjson_mut_arr_add_val(partyMembersArr, memberObj);
|
||||
}
|
||||
writeValue("partyMembers", partyMembersArr);
|
||||
|
||||
@@ -84,24 +72,8 @@ errorret_t saveSlotVer1ReadJSON(saveslotver1_t *slot, yyjson_val *object) {
|
||||
partyMembersArr, memberIdx, memberLen, memberObj
|
||||
) {
|
||||
battlefighter_t *member = &slot->partyMembers[memberIdx];
|
||||
// Shadows the outer `object` so readUInt32() below (which hardcodes
|
||||
// that name) reads from this member's object instead.
|
||||
yyjson_val *object = memberObj;
|
||||
readUInt32("id", member->id, 0);
|
||||
readUInt32("status", member->status, BATTLE_FIGHTER_STATUS_NULL);
|
||||
readUInt32("team", member->team, BATTLE_FIGHTER_TEAM_ALLY);
|
||||
readUInt32(
|
||||
"controller", member->controller, BATTLE_FIGHTER_CONTROLLER_PLAYER
|
||||
);
|
||||
readUInt32("health", member->stats.health, 0);
|
||||
readUInt32("healthMax", member->stats.healthMax, 0);
|
||||
readUInt32("mp", member->stats.mp, 0);
|
||||
readUInt32("mpMax", member->stats.mpMax, 0);
|
||||
readUInt32("statAttack", member->stats.attack, 0);
|
||||
readUInt32("statDefense", member->stats.defense, 0);
|
||||
readUInt32("statMagic", member->stats.magic, 0);
|
||||
readUInt32("statSpeed", member->stats.speed, 0);
|
||||
readUInt32("statLuck", member->stats.luck, 0);
|
||||
member->id = (uint8_t)memberIdx;
|
||||
errorChain(battleFighterParse(member, memberObj));
|
||||
}
|
||||
|
||||
readUInt8Array(
|
||||
|
||||
@@ -24,6 +24,16 @@
|
||||
// keyed by flag name (not position), so its on-disk shape is decoupled from
|
||||
// STORY_FLAG_COUNT_MAX entirely; a save simply loses whatever flags no
|
||||
// longer resolve by name (see storyGetFlagByName) if that cap ever shrinks.
|
||||
//
|
||||
// partyMembers is a THIRD exception, deliberately broken pre-release: its
|
||||
// per-member shape now comes from battleFighterSerialize/battleFighterParse
|
||||
// (rpg/battle/fighter/battlefighter.h) - the shared fighter JSON format
|
||||
// used everywhere else (party.jsonc, BATTLE_START enemies) - instead of its
|
||||
// own hand-rolled field set. This is a real on-disk break (different key
|
||||
// names, no more "statAttack" etc.) with no migration - an old v1 save
|
||||
// simply reads back as corrupt. Acceptable only because nothing has
|
||||
// shipped yet; don't repeat this once real saves exist without a version
|
||||
// bump instead.
|
||||
#define SAVE_SLOT_VER1_NAME_LENGTH 8
|
||||
#define SAVE_SLOT_VER1_MAP_NAME_MAX 10
|
||||
|
||||
|
||||
Reference in New Issue
Block a user