From d894950e6654bc75b58070336e1492b1dd6328ba Mon Sep 17 00:00:00 2001 From: KiritoDv Date: Sat, 24 Jan 2026 15:32:23 -0600 Subject: [PATCH 1/5] Implemented json saves among binary save file convertion --- .gitignore | 3 + src/game/save_file.c | 63 +++++++++++------- src/game/save_file.h | 14 ++++ src/port/data/SaveConversion.h | 117 +++++++++++++++++++++++++++++++++ src/port/data/Saves.cpp | 104 +++++++++++++++++++++++++++++ src/port/data/Saves.h | 22 +++++++ 6 files changed, 299 insertions(+), 24 deletions(-) create mode 100644 src/port/data/SaveConversion.h create mode 100644 src/port/data/Saves.cpp create mode 100644 src/port/data/Saves.h diff --git a/.gitignore b/.gitignore index c5f2109b0..4feb8aa6f 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,6 @@ modding/ node_modules /src/port/build.c /properties.h +!/src/port/mods/ +mods/ +saves/ \ No newline at end of file diff --git a/src/game/save_file.c b/src/game/save_file.c index 732f756ee..2afef5ff6 100644 --- a/src/game/save_file.c +++ b/src/game/save_file.c @@ -11,11 +11,13 @@ #include "level_table.h" #include "course_table.h" #include "rumble_init.h" +#include "port/data/Saves.h" #define MENU_DATA_MAGIC 0x4849 #define SAVE_FILE_MAGIC 0x4441 -STATIC_ASSERT(sizeof(struct SaveBuffer) == EEPROM_SIZE, "eeprom buffer size must match"); +// @port: Disabled because save files are gonna be bigger soon +// STATIC_ASSERT(sizeof(struct SaveBuffer) == EEPROM_SIZE, "eeprom buffer size must match"); extern struct SaveBuffer gSaveBuffer; @@ -148,6 +150,8 @@ static void add_save_block_signature(void *buffer, s32 size, u16 magic) { * Copy main menu data from one backup slot to the other slot. */ static void restore_main_menu_data(s32 srcSlot) { + CALL(RestoreMainMenuData, srcSlot); + s32 destSlot = srcSlot ^ 1; // Compute checksum on source data @@ -161,6 +165,8 @@ static void restore_main_menu_data(s32 srcSlot) { } static void save_main_menu_data(void) { + CALL(SaveMainMenuData); + if (gMainMenuDataModified) { // Compute checksum add_save_block_signature(&gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0]), MENU_DATA_MAGIC); @@ -234,6 +240,8 @@ static void touch_high_score_ages(s32 fileIndex) { * Copy save file data from one backup slot to the other slot. */ static void restore_save_file_data(s32 fileIndex, s32 srcSlot) { + CALL(RestoreSaveFileData, fileIndex, srcSlot); + s32 destSlot = srcSlot ^ 1; // Compute checksum on source data @@ -250,6 +258,8 @@ static void restore_save_file_data(s32 fileIndex, s32 srcSlot) { } void save_file_do_save(s32 fileIndex) { + CALL(SaveFileDoSave, fileIndex); + if (gSaveFileModified) { // Compute checksum add_save_block_signature(&gSaveBuffer.files[fileIndex][0], @@ -296,40 +306,45 @@ void save_file_load_all(void) { gSaveFileModified = FALSE; bzero(&gSaveBuffer, sizeof(gSaveBuffer)); - read_eeprom_data(&gSaveBuffer, sizeof(gSaveBuffer)); - - // Verify the main menu data and create a backup copy if only one of the slots is valid. - validSlots = verify_save_block_signature(&gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0]), MENU_DATA_MAGIC); - validSlots |= verify_save_block_signature(&gSaveBuffer.menuData[1], sizeof(gSaveBuffer.menuData[1]),MENU_DATA_MAGIC) << 1; - switch (validSlots) { - case 0: // Neither copy is correct - wipe_main_menu_data(); - break; - case 1: // Slot 0 is correct and slot 1 is incorrect - restore_main_menu_data(0); - break; - case 2: // Slot 1 is correct and slot 0 is incorrect - restore_main_menu_data(1); - break; - } - for (file = 0; file < NUM_SAVE_FILES; file++) { - // Verify the save file and create a backup copy if only one of the slots is valid. - validSlots = verify_save_block_signature(&gSaveBuffer.files[file][0], sizeof(gSaveBuffer.files[file][0]), SAVE_FILE_MAGIC); - validSlots |= verify_save_block_signature(&gSaveBuffer.files[file][1], sizeof(gSaveBuffer.files[file][1]), SAVE_FILE_MAGIC) << 1; + if(ShouldLoadOldSaveFile()) { + read_eeprom_data(&gSaveBuffer, EEPROM_SIZE); + + // Verify the main menu data and create a backup copy if only one of the slots is valid. + validSlots = verify_save_block_signature(&gSaveBuffer.menuData[0], MENU_SAVE_DATA_SIZE, MENU_DATA_MAGIC); + validSlots |= verify_save_block_signature(&gSaveBuffer.menuData[1], MENU_SAVE_DATA_SIZE, MENU_DATA_MAGIC) << 1; switch (validSlots) { case 0: // Neither copy is correct - save_file_erase(file); + wipe_main_menu_data(); break; case 1: // Slot 0 is correct and slot 1 is incorrect - restore_save_file_data(file, 0); + restore_main_menu_data(0); break; case 2: // Slot 1 is correct and slot 0 is incorrect - restore_save_file_data(file, 1); + restore_main_menu_data(1); break; } + + for (file = 0; file < NUM_SAVE_FILES; file++) { + // Verify the save file and create a backup copy if only one of the slots is valid. + validSlots = verify_save_block_signature(&gSaveBuffer.files[file][0], SAVE_FILE_SIZE, SAVE_FILE_MAGIC); + validSlots |= verify_save_block_signature(&gSaveBuffer.files[file][1], SAVE_FILE_SIZE, SAVE_FILE_MAGIC) << 1; + switch (validSlots) { + case 0: // Neither copy is correct + save_file_erase(file); + break; + case 1: // Slot 0 is correct and slot 1 is incorrect + restore_save_file_data(file, 0); + break; + case 2: // Slot 1 is correct and slot 0 is incorrect + restore_save_file_data(file, 1); + break; + } + } } + CALL(SaveFileLoadAll); + stub_save_file_1(); } diff --git a/src/game/save_file.h b/src/game/save_file.h index 12563ae57..0a8709a14 100644 --- a/src/game/save_file.h +++ b/src/game/save_file.h @@ -9,6 +9,8 @@ #include "course_table.h" #define EEPROM_SIZE 0x200 +#define SAVE_FILE_SIZE 0x38 +#define MENU_SAVE_DATA_SIZE 0x20 #define NUM_SAVE_FILES 4 struct SaveBlockSignature { @@ -34,6 +36,8 @@ struct SaveFile { u8 courseCoinScores[COURSE_STAGES_COUNT]; struct SaveBlockSignature signature; + + // @port: Custom data needs to go after this point }; enum SaveFileIndex { @@ -61,6 +65,8 @@ struct MainMenuSaveData { u8 filler[EEPROM_SIZE / 2 - SUBTRAHEND - NUM_SAVE_FILES * (4 + sizeof(struct SaveFile))]; struct SaveBlockSignature signature; + + // @port: Custom data needs to go after this point }; struct SaveBuffer { @@ -120,6 +126,10 @@ struct WarpCheckpoint { /*0x04*/ u8 warpNode; }; +#ifdef __cplusplus +extern "C" { +#endif + extern struct WarpCheckpoint gWarpCheckpoint; extern s8 gMainMenuDataModified; @@ -164,4 +174,8 @@ void eu_set_language(u16 language); u16 eu_get_language(void); #endif +#ifdef __cplusplus +} +#endif + #endif // SAVE_FILE_H diff --git a/src/port/data/SaveConversion.h b/src/port/data/SaveConversion.h new file mode 100644 index 000000000..44d19bf56 --- /dev/null +++ b/src/port/data/SaveConversion.h @@ -0,0 +1,117 @@ +#pragma once + +#include "game/save_file.h" + +#include + +using json = nlohmann::json; + +#define SAVE_FILE_VERSION 1 + +static std::string entries[] = { + "CG", "BOB", "WF", "JRB", "CCM", "BBH", "HMC", "LLL", "SSL", "DDD", "SL", + "WDW", "TTM", "THI", "TTC", "RR", "BITDW", "BITFS", "BITS", "PSS", "COTMC", + "TOTWC", "VCUTM", "WMOTR", "SA", "CAKE_END" +}; + +template +T GetSafeEntry(const json& node, const std::string& key) { + if(!node.contains(key)) { + auto dump = nlohmann::json(node).dump(4); + throw std::runtime_error("JSON missing the '" + key + "' entry\nProblematic JSON:\n" + dump); + } + + return node.at(key).get(); +} + +template +T GetSafeEntry(const json& node, const std::string& key, const T& def) { + if(!node.contains(key)) { + return def; + } + + return node.at(key).get(); +} + +inline void to_json(json& j, const SaveFile& save) { + json stars = {}; + json coins = {}; + json cap = { + { "x", save.capPos[0] }, + { "y", save.capPos[1] }, + { "z", save.capPos[2] }, + }; + for (size_t i = 0; i < COURSE_COUNT; i++) { + stars[entries[i]] = save.courseStars[i]; + } + + for (size_t i = 0; i < COURSE_STAGES_COUNT; i++) { + coins[entries[i]] = save.courseCoinScores[i]; + } + + j = json{ + { "version", SAVE_FILE_VERSION }, + { "capLevel", save.capLevel }, + { "capArea", save.capArea }, + { "capPos", cap }, + { "flags", save.flags }, + { "courseStars", stars }, + { "courseCoinScores", coins } + }; +} + +inline void LoadSaveFileV1(const json& j, SaveFile& save) { + json capPosJson = GetSafeEntry(j, "capPos"); + save.capLevel = GetSafeEntry(j, "capLevel", 0); + save.capArea = GetSafeEntry(j, "capArea", 0); + save.capPos[0] = GetSafeEntry(capPosJson, "x", 0); + save.capPos[1] = GetSafeEntry(capPosJson, "y", 0); + save.capPos[2] = GetSafeEntry(capPosJson, "z", 0); + save.flags = GetSafeEntry(j, "flags", 0); + + json starsJson = GetSafeEntry(j, "courseStars"); + for (size_t i = 0; i < COURSE_COUNT; i++) { + save.courseStars[i] = GetSafeEntry(starsJson, entries[i]); + } + + json coinsJson = GetSafeEntry(j, "courseCoinScores"); + for (size_t i = 0; i < COURSE_STAGES_COUNT; i++) { + save.courseCoinScores[i] = GetSafeEntry(coinsJson, entries[i]); + } +} + +inline void from_json(const json& j, SaveFile& save) { + uint32_t version = GetSafeEntry(j, "version"); + if(version == 1) { + LoadSaveFileV1(j, save); + } +} + +inline void to_json(json& j, const MainMenuSaveData& menu) { + json coins = json::array(); + for (size_t i = 0; i < NUM_SAVE_FILES; i++) { + coins.push_back(menu.coinScoreAges[i]); + } + + j = json{ + { "version", SAVE_FILE_VERSION }, + { "coinScoreAges", coins }, + { "soundMode", menu.soundMode } + }; +} + +inline void LoadMainMenuSaveDataV1(const json& j, MainMenuSaveData& menu) { + json coinsJson = GetSafeEntry(j, "coinScoreAges"); + for (size_t i = 0; i < NUM_SAVE_FILES; i++) { + menu.coinScoreAges[i] = coinsJson.at(i).get(); + } + + menu.soundMode = GetSafeEntry(j, "soundMode", 0); +} + +inline void from_json(const json& j, MainMenuSaveData& menu) { + uint32_t version = GetSafeEntry(j, "version"); + if(version == 1) { + LoadMainMenuSaveDataV1(j, menu); + } +} \ No newline at end of file diff --git a/src/port/data/Saves.cpp b/src/port/data/Saves.cpp new file mode 100644 index 000000000..290ee8814 --- /dev/null +++ b/src/port/data/Saves.cpp @@ -0,0 +1,104 @@ +#include "Saves.h" + +#include "port/ShipInit.hpp" +#include "port/data/SaveConversion.h" + +#include +#include +namespace fs = std::filesystem; + +extern struct SaveBuffer gSaveBuffer; + +static void Init() { + // Create saves directory if it doesn't exist + fs::path dir("saves"); + if (!fs::exists(dir)) { + fs::create_directory(dir); + } +} + +extern "C" { +void RestoreMainMenuData(int32_t srcSlot) { + int32_t destSlot = srcSlot ^ 1; + + bcopy(&gSaveBuffer.menuData[srcSlot], &gSaveBuffer.menuData[destSlot], sizeof(gSaveBuffer.menuData[destSlot])); +} + +void RestoreSaveFileData(int32_t fileIndex, int32_t srcSlot) { + int32_t destSlot = srcSlot ^ 1; + + bcopy(&gSaveBuffer.files[fileIndex][srcSlot], &gSaveBuffer.files[fileIndex][destSlot], sizeof(gSaveBuffer.files[fileIndex][destSlot])); +} + +void SaveFileDoSave(int32_t fileIndex) { + std::ofstream file(fs::path("saves/save_" + std::to_string(fileIndex) + ".json"), std::ios::out); + if (!file.is_open()) { + return; + } + + json j = gSaveBuffer.files[fileIndex][0]; + file << j.dump(4); + file.close(); +} + +bool ShouldLoadOldSaveFile(void) { + fs::path save("default.sav"); + return fs::exists(save); +} + +void SaveFileLoadAll(void) { + fs::path save("default.sav"); + if (fs::exists(save)) { + for (int32_t fileIndex = 0; fileIndex < NUM_SAVE_FILES; fileIndex++) { + SaveFileDoSave(fileIndex); + } + // Move old save files to backup + fs::rename(save, "default.sav.bak"); + return; + } + + // Read save files + for (int32_t fileIndex = 0; fileIndex < NUM_SAVE_FILES; fileIndex++) { + fs::path filepath = fs::path("saves/save_" + std::to_string(fileIndex) + ".json"); + if (!fs::exists(filepath)) { + continue; + } + + std::ifstream file(filepath, std::ios::in); + if (!file.is_open()) { + continue; + } + + json j; + file >> j; + gSaveBuffer.files[fileIndex][0] = j.get(); + file.close(); + } + + // Read global save file + fs::path globalpath = fs::path("saves/global.json"); + if (fs::exists(globalpath)) { + std::ifstream file(globalpath, std::ios::in); + if (file.is_open()) { + json j; + file >> j; + gSaveBuffer.menuData[0] = j.get(); + file.close(); + } + } +} + +void SaveMainMenuData(void) { + std::ofstream file(fs::path("saves/global.json"), std::ios::out); + if (!file.is_open()) { + return; + } + + json j = gSaveBuffer.menuData[0]; + file << j.dump(4); + file.close(); +} + +} + +static RegisterShipInitFunc initFunc(Init); \ No newline at end of file diff --git a/src/port/data/Saves.h b/src/port/data/Saves.h new file mode 100644 index 000000000..37ea2a5f1 --- /dev/null +++ b/src/port/data/Saves.h @@ -0,0 +1,22 @@ +#pragma once + +#include "game/save_file.h" + +#define CALL(func, ...) \ + func(__VA_ARGS__); \ + return; + +#ifdef __cplusplus +extern "C" { +#endif + +void RestoreMainMenuData(int32_t srcSlot); +void RestoreSaveFileData(int32_t fileIndex, int32_t srcSlot); +void SaveFileDoSave(int32_t fileIndex); +void SaveFileLoadAll(void); +void SaveMainMenuData(void); +bool ShouldLoadOldSaveFile(void); + +#ifdef __cplusplus +} +#endif \ No newline at end of file From b0b591aff739843448620634f0d7cebe86d12071 Mon Sep 17 00:00:00 2001 From: KiritoDv Date: Sat, 24 Jan 2026 15:35:43 -0600 Subject: [PATCH 2/5] Run clang --- src/port/data/Saves.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/port/data/Saves.cpp b/src/port/data/Saves.cpp index 290ee8814..7343249b5 100644 --- a/src/port/data/Saves.cpp +++ b/src/port/data/Saves.cpp @@ -27,7 +27,8 @@ void RestoreMainMenuData(int32_t srcSlot) { void RestoreSaveFileData(int32_t fileIndex, int32_t srcSlot) { int32_t destSlot = srcSlot ^ 1; - bcopy(&gSaveBuffer.files[fileIndex][srcSlot], &gSaveBuffer.files[fileIndex][destSlot], sizeof(gSaveBuffer.files[fileIndex][destSlot])); + bcopy(&gSaveBuffer.files[fileIndex][srcSlot], &gSaveBuffer.files[fileIndex][destSlot], + sizeof(gSaveBuffer.files[fileIndex][destSlot])); } void SaveFileDoSave(int32_t fileIndex) { @@ -98,7 +99,6 @@ void SaveMainMenuData(void) { file << j.dump(4); file.close(); } - } static RegisterShipInitFunc initFunc(Init); \ No newline at end of file From 90c69ac115cba585cb7878e394d960759153298f Mon Sep 17 00:00:00 2001 From: KiritoDv Date: Sat, 24 Jan 2026 15:41:25 -0600 Subject: [PATCH 3/5] Changed json dump to 1 instead of 4 --- src/port/data/Saves.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/port/data/Saves.cpp b/src/port/data/Saves.cpp index 7343249b5..e231f0114 100644 --- a/src/port/data/Saves.cpp +++ b/src/port/data/Saves.cpp @@ -38,7 +38,7 @@ void SaveFileDoSave(int32_t fileIndex) { } json j = gSaveBuffer.files[fileIndex][0]; - file << j.dump(4); + file << j.dump(1); file.close(); } @@ -96,7 +96,7 @@ void SaveMainMenuData(void) { } json j = gSaveBuffer.menuData[0]; - file << j.dump(4); + file << j.dump(1); file.close(); } } From f4cc8961a72cfa46f3bb40e60e02878d72a5d56a Mon Sep 17 00:00:00 2001 From: KiritoDv Date: Sat, 24 Jan 2026 15:53:22 -0600 Subject: [PATCH 4/5] Updated gitignore to account for old save-file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4feb8aa6f..a355234f3 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ build/* *.sav *.o2r *.map +default.sav.bak .assets-local.txt build/ From aa816c0a23312eaf7b1475f7fc0ec5b9f92267f5 Mon Sep 17 00:00:00 2001 From: KiritoDv Date: Sat, 24 Jan 2026 15:56:29 -0600 Subject: [PATCH 5/5] Fixed bcopy missing on windows --- src/port/data/Saves.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/port/data/Saves.cpp b/src/port/data/Saves.cpp index e231f0114..1b07ef943 100644 --- a/src/port/data/Saves.cpp +++ b/src/port/data/Saves.cpp @@ -1,5 +1,6 @@ #include "Saves.h" +#include "sm64.h" #include "port/ShipInit.hpp" #include "port/data/SaveConversion.h"