77#include " port/mods/achievements/Achievements.h"
88
99#include < nlohmann/json.hpp>
10+ #include < cstring>
1011
1112NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE (ShipSaveFeatures, achievements, rando)
1213NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE (RandoSaveCheck, randoItemId, randoAct, obtained, skipped)
@@ -15,7 +16,9 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(RandoSaveData, randoSaveChecks, randoSaveEntr
1516
1617using json = nlohmann::json;
1718
18- #define SAVE_FILE_VERSION 1
19+ #define SAVE_FILE_VERSION 2
20+ #define SAVE_FILE_VERSION_LEGACY 1
21+ #define SAVE_FILE_VERSION_ACHIEVEMENT_MIGRATION 2
1922
2023static std::string entries[] = {
2124 " BOB" , " WF" , " JRB" , " CCM" , " BBH" , " HMC" , " LLL" , " SSL" , " DDD" , " SL" ,
@@ -42,6 +45,24 @@ T GetSafeEntry(const json& node, const std::string& key, const T& def) {
4245 return node.at (key).get <T>();
4346}
4447
48+ // Reconstruct achievement data from legacy save file state
49+ inline void ReconstructAchievementData (const SaveFile& save, AchievementSaveData& achievementData) {
50+ achievementData.cheated = false ;
51+ achievementData.capStars = 0 ;
52+
53+ // Infer coins from highest coin score across all courses
54+ int32_t maxCoins = 0 ;
55+ for (size_t i = 0 ; i < COURSE_STAGES_COUNT ; i++) {
56+ if (save.courseCoinScores [i] > maxCoins) {
57+ maxCoins = save.courseCoinScores [i];
58+ }
59+ }
60+ achievementData.coins = maxCoins;
61+
62+ // Note: capStars cannot be reliably reconstructed without knowing HOW each star was obtained
63+ // (metal cap vs no metal cap), so we leave it at 0 and let players re-earn if needed
64+ }
65+
4566inline void from_json (const json& j, AchievementSaveEntry& entry) {
4667 const auto id = GetSafeEntry<std::string>(j, " id" );
4768 const auto progress = GetSafeEntry<int32_t >(j, " progress" );
@@ -61,12 +82,14 @@ inline void to_json(json& j, const AchievementSaveEntry& entry) {
6182
6283inline void from_json (const json& j, AchievementSaveData& data) {
6384 data.cheated = j.at (" cheated" ).get <bool >();
64- data.capStars = j.at (" capStars" ).get <int >();
65- data.coins = j.at (" coins" ).get <int >();
66-
67- auto entriesJson = j.at (" entries" );
68- for (size_t i = 0 ; i < entriesJson.size (); i++) {
69- data.entries [i] = entriesJson.at (i).get <AchievementSaveEntry>();
85+ data.capStars = GetSafeEntry (j, " capStars" , 0 );
86+ data.coins = GetSafeEntry (j, " coins" , 0 );
87+
88+ if (j.contains (" entries" )) {
89+ auto entriesJson = j.at (" entries" );
90+ for (size_t i = 0 ; i < entriesJson.size (); i++) {
91+ data.entries [i] = entriesJson.at (i).get <AchievementSaveEntry>();
92+ }
7093 }
7194}
7295
@@ -91,8 +114,13 @@ inline void from_json(const json& j, ShipSaveData& save) {
91114 j[" randoSaveData" ].get_to (save.randoSaveData );
92115 }
93116
94- if (save. features . achievements ) {
117+ if (j. contains ( " achievementSaveData " ) ) {
95118 j[" achievementSaveData" ].get_to (save.achievementSaveData );
119+ } else if (save.features .achievements ) {
120+ memset (&save.achievementSaveData , 0 , sizeof (AchievementSaveData));
121+ save.achievementSaveData .cheated = false ;
122+ save.achievementSaveData .capStars = 0 ;
123+ save.achievementSaveData .coins = 0 ;
96124 }
97125}
98126
@@ -137,7 +165,39 @@ inline void to_json(json& j, const SaveFile& save) {
137165 };
138166}
139167
140- inline void LoadSaveFileV1 (const json& j, SaveFile& save) {
168+ inline void LoadSaveFileLegacy (const json& j, SaveFile& save) {
169+ json capPosJson = GetSafeEntry<json>(j, " capPos" , json::object ());
170+ save.capLevel = GetSafeEntry (j, " capLevel" , 0 );
171+ save.capArea = GetSafeEntry (j, " capArea" , 0 );
172+ save.capPos [0 ] = GetSafeEntry (capPosJson, " x" , 0 );
173+ save.capPos [1 ] = GetSafeEntry (capPosJson, " y" , 0 );
174+ save.capPos [2 ] = GetSafeEntry (capPosJson, " z" , 0 );
175+ save.flags = GetSafeEntry (j, " flags" , 0 );
176+
177+ json starsJson = GetSafeEntry<json>(j, " courseStars" , json::object ());
178+ for (size_t i = 0 ; i < COURSE_COUNT ; i++) {
179+ save.courseStars [i] = GetSafeEntry<u8 >(starsJson, entries[i], static_cast <u8 >(0 ));
180+ }
181+
182+ json coinsJson = GetSafeEntry<json>(j, " courseCoinScores" , json::object ());
183+ for (size_t i = 0 ; i < COURSE_STAGES_COUNT ; i++) {
184+ save.courseCoinScores [i] = GetSafeEntry<u8 >(coinsJson, entries[i], static_cast <u8 >(0 ));
185+ }
186+
187+ memset (&save.shipSaveData , 0 , sizeof (ShipSaveData));
188+ if (j.contains (" shipSaveData" )) {
189+ j[" shipSaveData" ].get_to (save.shipSaveData );
190+
191+ // Migrate old saves that don't have achievement data
192+ bool hasMissingAchievementData = save.shipSaveData .features .achievements &&
193+ !j[" shipSaveData" ].contains (" achievementSaveData" );
194+ if (hasMissingAchievementData) {
195+ ReconstructAchievementData (save, save.shipSaveData .achievementSaveData );
196+ }
197+ }
198+ }
199+
200+ inline void LoadSaveFileV2 (const json& j, SaveFile& save) {
141201 json capPosJson = GetSafeEntry<json>(j, " capPos" , json::object ());
142202 save.capLevel = GetSafeEntry (j, " capLevel" , 0 );
143203 save.capArea = GetSafeEntry (j, " capArea" , 0 );
@@ -156,6 +216,7 @@ inline void LoadSaveFileV1(const json& j, SaveFile& save) {
156216 save.courseCoinScores [i] = GetSafeEntry<u8 >(coinsJson, entries[i], static_cast <u8 >(0 ));
157217 }
158218
219+ memset (&save.shipSaveData , 0 , sizeof (ShipSaveData));
159220 if (j.contains (" shipSaveData" )) {
160221 j[" shipSaveData" ].get_to (save.shipSaveData );
161222 }
@@ -164,7 +225,9 @@ inline void LoadSaveFileV1(const json& j, SaveFile& save) {
164225inline void from_json (const json& j, SaveFile& save) {
165226 uint32_t version = GetSafeEntry<uint32_t >(j, " version" , 1u );
166227 if (version == 1 ) {
167- LoadSaveFileV1 (j, save);
228+ LoadSaveFileLegacy (j, save);
229+ } else if (version == 2 ) {
230+ LoadSaveFileV2 (j, save);
168231 }
169232}
170233
0 commit comments