From e881a04f8645166183cfb6dbeed87cdfed4a658a Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Sun, 1 Mar 2026 19:57:43 -0600 Subject: [PATCH 01/14] YAAAA completely change up save system AUWEGBSLIUFJSAI --- autogen/convert_functions.py | 3 +- autogen/lua_definitions/constants.lua | 2 +- autogen/lua_definitions/functions.lua | 7 - docs/lua/functions-6.md | 24 ---- docs/lua/functions.md | 1 - include/PR/os_eeprom.h | 6 +- src/buffers/buffers.c | 2 +- src/game/save_file.c | 190 +++----------------------- src/game/save_file.h | 38 ++---- src/pc/configfile.c | 8 +- src/pc/configfile.h | 4 +- src/pc/djui/djui_panel_host_save.c | 9 +- src/pc/fs/fs.h | 4 +- src/pc/lua/smlua_constants_autogen.c | 2 +- src/pc/lua/smlua_functions_autogen.c | 20 --- src/pc/mods/mod_storage.cpp | 4 +- src/pc/mods/mod_storage.h | 2 +- src/pc/network/packets/packet_join.c | 10 +- src/pc/ultra_reimplementation.c | 29 ++-- 19 files changed, 79 insertions(+), 286 deletions(-) diff --git a/autogen/convert_functions.py b/autogen/convert_functions.py index 8846db56f..e34a52c2e 100644 --- a/autogen/convert_functions.py +++ b/autogen/convert_functions.py @@ -89,7 +89,7 @@ override_allowed_functions = { "src/pc/djui/djui_popup.h": [ "create" ], "src/pc/djui/djui_language.h": [ "djui_language_get" ], "src/pc/djui/djui_panel_menu.h": [ "djui_menu_get_rainbow_string_color" ], - "src/game/save_file.h": [ "get_level_", "save_file_get_", "save_file_set_flags", "save_file_clear_flags", "save_file_reload", "save_file_erase_current_backup_save", "save_file_set_star_flags", "save_file_is_cannon_unlocked", "save_file_set_cannon_unlocked", "touch_coin_score_age", "save_file_set_course_coin_score", "save_file_do_save", "save_file_remove_star_flags", "save_file_erase" ], + "src/game/save_file.h": [ "get_level_", "save_file_get_", "save_file_set_flags", "save_file_clear_flags", "save_file_reload", "save_file_erase_current_backup_save", "save_file_set_star_flags", "save_file_is_cannon_unlocked", "save_file_set_cannon_unlocked", "save_file_set_course_coin_score", "save_file_do_save", "save_file_remove_star_flags", "save_file_erase" ], "src/pc/lua/utils/smlua_model_utils.h": [ "smlua_model_util_get_id" ], "src/game/object_list_processor.h": [ "set_object_respawn_info_bits" ], "src/game/platform_displacement.h": [ "apply_platform_displacement" ], @@ -119,6 +119,7 @@ override_disallowed_functions = { "src/pc/djui/djui_console.h": [ " djui_console_create", "djui_console_message_create", "djui_console_message_dequeue" ], "src/pc/djui/djui_chat_message.h": [ "create_from" ], "src/game/interaction.h": [ "process_interaction", "_handle_" ], + "src/game/save_file.h": [ "save_file_get_dir" ], "src/game/sound_init.h": [ "_loop_", "thread4_", "set_sound_mode" ], "src/pc/network/network_utils.h": [ "network_get_player_text_color[^_]" ], "src/pc/network/network_player.h": [ "_init", "_connected[^_]", "_shutdown", "_disconnected", "_update", "construct_player_popup", "network_player_name_valid" ], diff --git a/autogen/lua_definitions/constants.lua b/autogen/lua_definitions/constants.lua index b1fd233f5..7ce77e0fa 100644 --- a/autogen/lua_definitions/constants.lua +++ b/autogen/lua_definitions/constants.lua @@ -6797,7 +6797,7 @@ METAL = CAP --- @type PlayerPart --- | `METAL` --- @type integer -NUM_SAVE_FILES = 4 +NUM_SAVE_FILES = 32 SAVE_FILE_A = 0 --- @type SaveFileIndex SAVE_FILE_B = 1 --- @type SaveFileIndex diff --git a/autogen/lua_definitions/functions.lua b/autogen/lua_definitions/functions.lua index f7b0aa63d..524e2aac0 100644 --- a/autogen/lua_definitions/functions.lua +++ b/autogen/lua_definitions/functions.lua @@ -9946,13 +9946,6 @@ function get_level_course_num(levelNum) -- ... end ---- @param fileIndex integer ---- @param courseIndex integer ---- Marks the coin score for a specific course as the newest among all save files. Adjusts the age of other scores to reflect the update. Useful for leaderboard tracking or displaying recent progress -function touch_coin_score_age(fileIndex, courseIndex) - -- ... -end - --- @param fileIndex integer --- @param forceSave integer --- Saves the current state of the game into a specified save file. Includes data verification and backup management. Useful for maintaining game progress during play or when saving manually diff --git a/docs/lua/functions-6.md b/docs/lua/functions-6.md index afd96447e..d3070b04a 100644 --- a/docs/lua/functions-6.md +++ b/docs/lua/functions-6.md @@ -4805,30 +4805,6 @@ Gets the level number's corresponding course number
-## [touch_coin_score_age](#touch_coin_score_age) - -### Description -Marks the coin score for a specific course as the newest among all save files. Adjusts the age of other scores to reflect the update. Useful for leaderboard tracking or displaying recent progress - -### Lua Example -`touch_coin_score_age(fileIndex, courseIndex)` - -### Parameters -| Field | Type | -| ----- | ---- | -| fileIndex | `integer` | -| courseIndex | `integer` | - -### Returns -- None - -### C Prototype -`void touch_coin_score_age(s32 fileIndex, s32 courseIndex);` - -[:arrow_up_small:](#) - -
- ## [save_file_do_save](#save_file_do_save) ### Description diff --git a/docs/lua/functions.md b/docs/lua/functions.md index e2f94b46b..9804e8424 100644 --- a/docs/lua/functions.md +++ b/docs/lua/functions.md @@ -1779,7 +1779,6 @@ - save_file.h - [get_level_num_from_course_num](functions-6.md#get_level_num_from_course_num) - [get_level_course_num](functions-6.md#get_level_course_num) - - [touch_coin_score_age](functions-6.md#touch_coin_score_age) - [save_file_do_save](functions-6.md#save_file_do_save) - [save_file_erase](functions-6.md#save_file_erase) - [save_file_erase_current_backup_save](functions-6.md#save_file_erase_current_backup_save) diff --git a/include/PR/os_eeprom.h b/include/PR/os_eeprom.h index b3bca8144..63a9e14c9 100644 --- a/include/PR/os_eeprom.h +++ b/include/PR/os_eeprom.h @@ -21,7 +21,7 @@ /*---------------------------------------------------------------------* Copyright (C) 1998 Nintendo. (Originated by SGI) - + $RCSfile: os_eeprom.h,v $ $Revision: 1.1 $ $Date: 1998/10/09 08:01:06 $ @@ -94,8 +94,8 @@ extern "C" { extern s32 osEepromProbe(OSMesgQueue *); extern s32 osEepromRead(OSMesgQueue *, u8, u8 *); extern s32 osEepromWrite(OSMesgQueue *, u8, u8 *); -extern s32 osEepromLongRead(OSMesgQueue *, u8, u8 *, int); -extern s32 osEepromLongWrite(OSMesgQueue *, u8, u8 *, int); +extern s32 osEepromLongRead(OSMesgQueue *, u8, u8, u8 *, int); +extern s32 osEepromLongWrite(OSMesgQueue *, u8, u8, u8 *, int); #endif /* defined(_LANGUAGE_C) || defined(_LANGUAGE_C_PLUS_PLUS) */ diff --git a/src/buffers/buffers.c b/src/buffers/buffers.c index 063aa7bdd..fb918d5a2 100644 --- a/src/buffers/buffers.c +++ b/src/buffers/buffers.c @@ -23,7 +23,7 @@ ALIGNED8 u8 gThread6Stack[0x2000]; ALIGNED8 u8 gGfxSPTaskStack[SP_DRAM_STACK_SIZE8] = { 0 }; // 0xc00 bytes for f3dex, 0x900 otherwise ALIGNED8 u8 gGfxSPTaskYieldBuffer[OS_YIELD_DATA_SIZE] = { 0 }; -// 0x200 bytes +// 0x80 bytes ALIGNED8 struct SaveBuffer gSaveBuffer = { 0 }; // 0x190a0 bytes struct GfxPool gGfxPools[GFX_NUM_POOLS] = { 0 }; diff --git a/src/game/save_file.c b/src/game/save_file.c index 91d9350fb..0d692a9e8 100644 --- a/src/game/save_file.c +++ b/src/game/save_file.c @@ -20,7 +20,6 @@ #define bcopy(b1,b2,len) (memmove((b2), (b1), (len)), (void) 0) #endif -#define MENU_DATA_MAGIC 0x4849 #define SAVE_FILE_MAGIC 0x4441 #define INVALID_FILE_INDEX(_fi) ((u32)_fi >= NUM_SAVE_FILES) @@ -29,13 +28,12 @@ #define INVALID_COURSE_STAR_INDEX(_ci) ((u32)_ci >= COURSE_COUNT) #define INVALID_COURSE_COIN_INDEX(_ci) ((u32)_ci >= COURSE_STAGES_COUNT) -STATIC_ASSERT(sizeof(struct SaveBuffer) == EEPROM_SIZE, "eeprom buffer size must match"); +STATIC_ASSERT(sizeof(struct SingleSaveFile) == EEPROM_SIZE, "eeprom buffer size must match"); extern struct SaveBuffer gSaveBuffer; struct WarpCheckpoint gWarpCheckpoint; -s8 gMainMenuDataModified; s8 gSaveFileModified; u8 gLastCompletedCourseNum = COURSE_NONE; @@ -100,10 +98,9 @@ s8 get_level_course_num(s16 levelNum) { return gLevelToCourseNumTable[levelNum]; } -// This was probably used to set progress to 100% for debugging, but -// it was removed from the release ROM. -static void stub_save_file_1(void) { - UNUSED s32 pad; +void save_file_get_dir(int fileIndex, char* outPath, size_t size) { + snprintf(outPath, size, "%s%s%d.bin", SAVE_DIRECTORY, SAVE_FILENAME, fileIndex); + printf("Returning filepath: %s\n", outPath); } /** @@ -114,19 +111,6 @@ static inline void bswap_signature(struct SaveBlockSignature *data) { data->chksum = BSWAP16(data->chksum); // valid as long as the checksum is a literal sum } -/** - * Byteswap all multibyte fields in a MainMenuSaveData. - */ -static inline void bswap_menudata(struct MainMenuSaveData *data) { - for (s32 i = 0; i < NUM_SAVE_FILES; ++i) - data->coinScoreAges[i] = BSWAP32(data->coinScoreAges[i]); - data->soundMode = BSWAP16(data->soundMode); -#ifdef VERSION_EU - data->language = BSWAP16(data->language); -#endif - bswap_signature(&data->signature); -} - /** * Byteswap all multibyte fields in a SaveFile. */ @@ -144,17 +128,17 @@ static inline void bswap_savefile(struct SaveFile *data) { * Try at most 4 times, and return 0 on success. On failure, return the status returned from * osEepromLongRead. It also returns 0 if EEPROM isn't loaded correctly in the system. */ -static s32 read_eeprom_data(void *buffer, s32 size) { +static s32 read_eeprom_data(u8 file, void *buffer, s32 size) { s32 status = 0; if (gEepromProbe != 0) { s32 triesLeft = 4; - u32 offset = (u32)((u8 *) buffer - (u8 *) &gSaveBuffer) / 8; + u32 offset = (u32)((u8 *) buffer - (u8 *) &gSaveBuffer.files[file]) / 8; do { block_until_rumble_pak_free(); triesLeft--; - status = osEepromLongRead(&gSIEventMesgQueue, offset, buffer, size); + status = osEepromLongRead(&gSIEventMesgQueue, file, offset, buffer, size); release_rumble_pak_control(); } while (triesLeft > 0 && status != 0); } @@ -168,7 +152,7 @@ static s32 read_eeprom_data(void *buffer, s32 size) { * Try at most 4 times, and return 0 on success. On failure, return the status returned from * osEepromLongWrite. Unlike read_eeprom_data, return 1 if EEPROM isn't loaded. */ -static s32 write_eeprom_data(void *buffer, s32 size, const uintptr_t baseofs) { +static s32 write_eeprom_data(u8 file, void *buffer, s32 size, const uintptr_t baseofs) { s32 status = 1; if (gEepromProbe != 0) { @@ -178,7 +162,7 @@ static s32 write_eeprom_data(void *buffer, s32 size, const uintptr_t baseofs) { do { block_until_rumble_pak_free(); triesLeft--; - status = osEepromLongWrite(&gSIEventMesgQueue, offset, buffer, size); + status = osEepromLongWrite(&gSIEventMesgQueue, file, offset, buffer, size); release_rumble_pak_control(); } while (triesLeft > 0 && status != 0); } @@ -194,32 +178,16 @@ static inline s32 write_eeprom_savefile(const u32 file, const u32 slot, const u3 if (INVALID_FILE_INDEX(file)) { return 0; } if (INVALID_SRC_SLOT(slot)) { return 0; } // calculate the EEPROM address using the file number and slot - const uintptr_t ofs = (u8*)&gSaveBuffer.files[file][slot] - (u8*)&gSaveBuffer; + const uintptr_t ofs = (u8*)&gSaveBuffer.files[file][slot] - (u8*)&gSaveBuffer.files[file]; #if IS_BIG_ENDIAN - return write_eeprom_data(&gSaveBuffer.files[file][slot], num * sizeof(struct SaveFile), ofs); + return write_eeprom_data(file, &gSaveBuffer.files[file][slot], num * sizeof(struct SaveFile), ofs); #else // byteswap the data and then write it struct SaveFile sf[num]; bcopy(&gSaveBuffer.files[file][slot], sf, num * sizeof(sf[0])); for (u32 i = 0; i < num; ++i) bswap_savefile(&sf[i]); - return write_eeprom_data(&sf, sizeof(sf), ofs); -#endif -} - -static inline s32 write_eeprom_menudata(const u32 slot, const u32 num) { - if (INVALID_SRC_SLOT(slot)) { return 0; } - // calculate the EEPROM address using the slot - const uintptr_t ofs = (u8*)&gSaveBuffer.menuData[slot] - (u8*)&gSaveBuffer; - -#if IS_BIG_ENDIAN - return write_eeprom_data(&gSaveBuffer.menuData[slot], num * sizeof(struct MainMenuSaveData), ofs); -#else - // byteswap the data and then write it - struct MainMenuSaveData md[num]; - bcopy(&gSaveBuffer.menuData[slot], md, num * sizeof(md[0])); - for (u32 i = 0; i < num; ++i) bswap_menudata(&md[i]); - return write_eeprom_data(&md, sizeof(md), ofs); + return write_eeprom_data(file, &sf, sizeof(sf), ofs); #endif } @@ -261,98 +229,6 @@ static void add_save_block_signature(void *buffer, s32 size, u16 magic) { sig->chksum = calc_checksum(buffer, size); } -/** - * Copy main menu data from one backup slot to the other slot. - */ -UNUSED static void restore_main_menu_data(s32 srcSlot) { - if (INVALID_SRC_SLOT(srcSlot)) { return; } - s32 destSlot = srcSlot ^ 1; - if (INVALID_SRC_SLOT(destSlot)) { return; } - - // Compute checksum on source data - add_save_block_signature(&gSaveBuffer.menuData[srcSlot], sizeof(gSaveBuffer.menuData[srcSlot]), MENU_DATA_MAGIC); - - // Copy source data to destination - bcopy(&gSaveBuffer.menuData[srcSlot], &gSaveBuffer.menuData[destSlot], sizeof(gSaveBuffer.menuData[destSlot])); - - // Write destination data to EEPROM - write_eeprom_menudata(destSlot, 1); -} - -static void save_main_menu_data(void) { - if (gMainMenuDataModified) { - // Compute checksum - add_save_block_signature(&gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0]), MENU_DATA_MAGIC); - - // Back up data - bcopy(&gSaveBuffer.menuData[0], &gSaveBuffer.menuData[1], sizeof(gSaveBuffer.menuData[1])); - - // Write to EEPROM - write_eeprom_menudata(0, 2); - - gMainMenuDataModified = FALSE; - } -} - -UNUSED static void wipe_main_menu_data(void) { - bzero(&gSaveBuffer.menuData[0], sizeof(gSaveBuffer.menuData[0])); - - // Set score ages for all courses to 3, 2, 1, and 0, respectively. - gSaveBuffer.menuData[0].coinScoreAges[0] = 0x3FFFFFFF; - gSaveBuffer.menuData[0].coinScoreAges[1] = 0x2AAAAAAA; - gSaveBuffer.menuData[0].coinScoreAges[2] = 0x15555555; - - gMainMenuDataModified = TRUE; - save_main_menu_data(); -} - -static s32 get_coin_score_age(s32 fileIndex, s32 courseIndex) { - if (INVALID_FILE_INDEX(fileIndex)) { return 0; } - return (gSaveBuffer.menuData[0].coinScoreAges[fileIndex] >> (2 * courseIndex)) & 0x3; -} - -static void set_coin_score_age(s32 fileIndex, s32 courseIndex, s32 age) { - if (INVALID_FILE_INDEX(fileIndex)) { return; } - s32 mask = 0x3 << (2 * courseIndex); - - gSaveBuffer.menuData[0].coinScoreAges[fileIndex] &= ~mask; - gSaveBuffer.menuData[0].coinScoreAges[fileIndex] |= age << (2 * courseIndex); -} - -/** - * Mark a coin score for a save file as the newest out of all save files. - */ -void touch_coin_score_age(s32 fileIndex, s32 courseIndex) { - if (INVALID_FILE_INDEX(fileIndex)) { return; } - s32 i; - u32 age; - u32 currentAge = get_coin_score_age(fileIndex, courseIndex); - - if (currentAge != 0) { - for (i = 0; i < NUM_SAVE_FILES; i++) { - age = get_coin_score_age(i, courseIndex); - if (age < currentAge) { - set_coin_score_age(i, courseIndex, age + 1); - } - } - - set_coin_score_age(fileIndex, courseIndex, 0); - gMainMenuDataModified = TRUE; - } -} - -/** - * Mark all coin scores for a save file as new. - */ -static void touch_high_score_ages(s32 fileIndex) { - if (INVALID_FILE_INDEX(fileIndex)) { return; } - s32 i; - - for (i = 0; i < 15; i++) { - touch_coin_score_age(fileIndex, i); - } -} - /** * Copy save file data from one backup slot to the other slot. */ @@ -380,8 +256,6 @@ UNUSED static void restore_save_file_data(s32 fileIndex, s32 srcSlot) { static u8 save_file_need_bswap(const struct SaveBuffer *buf) { // check all signatures just in case for (s32 i = 0; i < 2; ++i) { - if (buf->menuData[i].signature.magic == BSWAP16(MENU_DATA_MAGIC)) - return TRUE; for (s32 j = 0; j < NUM_SAVE_FILES; ++j) { if (buf->files[j][i].signature.magic == BSWAP16(SAVE_FILE_MAGIC)) return TRUE; @@ -394,8 +268,6 @@ static u8 save_file_need_bswap(const struct SaveBuffer *buf) { * Byteswap all multibyte fields in a SaveBuffer. */ static void save_file_bswap(struct SaveBuffer *buf) { - bswap_menudata(buf->menuData + 0); - bswap_menudata(buf->menuData + 1); for (s32 i = 0; i < NUM_SAVE_FILES; ++i) { bswap_savefile(buf->files[i] + 0); bswap_savefile(buf->files[i] + 1); @@ -426,13 +298,11 @@ void save_file_do_save(s32 fileIndex, s8 forceSave) { gSaveFileModified = FALSE; } - save_main_menu_data(); } void save_file_erase(s32 fileIndex) { if (INVALID_FILE_INDEX(fileIndex)) { return; } - touch_high_score_ages(fileIndex); bzero(&gSaveBuffer.files[fileIndex][0], sizeof(gSaveBuffer.files[fileIndex][0])); bzero(&gSaveBuffer.files[fileIndex][1], sizeof(gSaveBuffer.files[fileIndex][1])); @@ -465,7 +335,6 @@ BAD_RETURN(s32) save_file_copy(s32 srcFileIndex, s32 destFileIndex) { if (INVALID_FILE_INDEX(srcFileIndex)) { return; } if (INVALID_FILE_INDEX(destFileIndex)) { return; } - touch_high_score_ages(destFileIndex); bcopy(&gSaveBuffer.files[srcFileIndex][0], &gSaveBuffer.files[destFileIndex][0], sizeof(gSaveBuffer.files[destFileIndex][0])); bcopy(&gSaveBuffer.files[srcFileIndex][1], &gSaveBuffer.files[destFileIndex][1], @@ -478,12 +347,13 @@ BAD_RETURN(s32) save_file_copy(s32 srcFileIndex, s32 destFileIndex) { void save_file_load_all(UNUSED u8 reload) { //s32 file; - gMainMenuDataModified = FALSE; gSaveFileModified = FALSE; bzero(&gSaveBuffer, sizeof(gSaveBuffer)); - read_eeprom_data(&gSaveBuffer, sizeof(gSaveBuffer)); + for (int file = 0; file < NUM_SAVE_FILES; file++) { + read_eeprom_data(file, &gSaveBuffer.files[file], sizeof(gSaveBuffer.files[file])); + } if (save_file_need_bswap(&gSaveBuffer)) save_file_bswap(&gSaveBuffer); @@ -491,19 +361,6 @@ void save_file_load_all(UNUSED u8 reload) { // Verify the main menu data and create a backup copy if only one of the slots is valid. /* Disable this so the 'backup' slot can be used s32 validSlots; - 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. @@ -522,7 +379,6 @@ void save_file_load_all(UNUSED u8 reload) { } } */ - stub_save_file_1(); } /** @@ -557,7 +413,6 @@ void save_file_collect_star_or_key(s16 coinScore, s16 starIndex, u8 fromNetwork) if (coinScore > save_file_get_course_coin_score(fileIndex, courseIndex)) { gSaveBuffer.files[fileIndex][gSaveFileUsingBackupSlot].courseCoinScores[courseIndex] = coinScore; - touch_coin_score_age(fileIndex, courseIndex); gGotFileCoinHiScore = TRUE; gSaveFileModified = TRUE; @@ -595,24 +450,21 @@ s32 save_file_exists(s32 fileIndex) { } /** - * Get the maximum coin score across all files for a course. The lower 16 bits + * Get the maximum coin score across all save files for a course. The lower 16 bits * of the returned value are the score, and the upper 16 bits are the file number * of the save file with this score. */ u32 save_file_get_max_coin_score(s32 courseIndex) { s32 fileIndex; s32 maxCoinScore = -1; - s32 maxScoreAge = -1; s32 maxScoreFileNum = 0; for (fileIndex = 0; fileIndex < NUM_SAVE_FILES; fileIndex++) { if (save_file_get_star_flags(fileIndex, courseIndex) != 0) { s32 coinScore = save_file_get_course_coin_score(fileIndex, courseIndex); - s32 scoreAge = get_coin_score_age(fileIndex, courseIndex); - if (coinScore > maxCoinScore || (coinScore == maxCoinScore && scoreAge > maxScoreAge)) { + if (coinScore > maxCoinScore) { maxCoinScore = coinScore; - maxScoreAge = scoreAge; maxScoreFileNum = fileIndex + 1; } } @@ -805,16 +657,16 @@ s32 save_file_get_cap_pos(VEC_OUT Vec3s capPos) { return FALSE; } -void save_file_set_sound_mode(u16 mode) { - set_sound_mode(mode); +void save_file_set_sound_mode(UNUSED u16 mode) { + /*set_sound_mode(mode); gSaveBuffer.menuData[0].soundMode = mode; gMainMenuDataModified = TRUE; - save_main_menu_data(); + save_main_menu_data();*/ } u16 save_file_get_sound_mode(void) { - return gSaveBuffer.menuData[0].soundMode; + return 0; } void save_file_move_cap_to_default_location(void) { diff --git a/src/game/save_file.h b/src/game/save_file.h index a5ac6a59e..806ecd779 100644 --- a/src/game/save_file.h +++ b/src/game/save_file.h @@ -8,8 +8,9 @@ #include "course_table.h" -#define EEPROM_SIZE 0x200 -#define NUM_SAVE_FILES 4 +#define NUM_SAVE_FILES 32 +// size of savebuffer +#define EEPROM_SIZE 128 struct SaveBlockSignature { @@ -45,33 +46,18 @@ enum SaveFileIndex { SAVE_FILE_D }; -struct MainMenuSaveData +struct SingleSaveFile { - // Each save file has a 2 bit "age" for each course. The higher this value, - // the older the high score is. This is used for tie-breaking when displaying - // on the high score screen. - u32 coinScoreAges[NUM_SAVE_FILES]; - u16 soundMode; - -#ifdef VERSION_EU - u16 language; -#define SUBTRAHEND 8 -#else -#define SUBTRAHEND 6 -#endif - - // Pad to match the EEPROM size of 0x200 (10 bytes on JP/US, 8 bytes on EU) - u8 filler[EEPROM_SIZE / 2 - SUBTRAHEND - NUM_SAVE_FILES * (4 + sizeof(struct SaveFile))]; - - struct SaveBlockSignature signature; + // Each save file has two copies. If one is bad, the other is used as a backup. + struct SaveFile files[2]; + // Filler to make a single save file equal the eeprom size + u8 filler[EEPROM_SIZE - (sizeof(struct SaveFile) * 2)]; }; struct SaveBuffer { - // Each of the four save files has two copies. If one is bad, the other is used as a backup. + // For all save files, each save has two copies. If one is bad, the other is used as a backup. struct SaveFile files[NUM_SAVE_FILES][2]; - // The main menu data has two copies. If one is bad, the other is used as a backup. - struct MainMenuSaveData menuData[2]; }; extern u8 gLastCompletedCourseNum; @@ -134,11 +120,7 @@ s8 get_level_num_from_course_num(s16 courseNum); /* |description|Gets the level number's corresponding course number|descriptionEnd| */ s8 get_level_course_num(s16 levelNum); -/* |description| -Marks the coin score for a specific course as the newest among all save files. Adjusts the age of other scores to reflect the update. -Useful for leaderboard tracking or displaying recent progress -|descriptionEnd| */ -void touch_coin_score_age(s32 fileIndex, s32 courseIndex); +void save_file_get_dir(int fileIndex, char* outPath, size_t size); /* |description| Saves the current state of the game into a specified save file. Includes data verification and backup management. diff --git a/src/pc/configfile.c b/src/pc/configfile.c index f2267a7db..847f1d907 100644 --- a/src/pc/configfile.c +++ b/src/pc/configfile.c @@ -19,7 +19,6 @@ #include "network/moderator_list.h" #include "debuglog.h" #include "djui/djui_hud_utils.h" -#include "game/save_file.h" #include "pc/network/network_player.h" #include "pc/pc_main.h" @@ -58,12 +57,7 @@ struct FunctionConfigOption { /* *Config options and default values */ -char configSaveNames[4][MAX_SAVE_NAME_STRING] = { - "SM64", - "SM64", - "SM64", - "SM64" -}; +char configSaveNames[NUM_SAVE_FILES][MAX_SAVE_NAME_STRING] = { 0 }; // Video/audio stuff ConfigWindow configWindow = { diff --git a/src/pc/configfile.h b/src/pc/configfile.h index d537970e4..330e2c1f0 100644 --- a/src/pc/configfile.h +++ b/src/pc/configfile.h @@ -4,6 +4,8 @@ #include #include #include "game/player_palette.h" +#include "pc/lua/smlua_autogen.h" +#include "game/save_file.h" #define CONFIGFILE_DEFAULT "sm64config.txt" #define CONFIGFILE_BACKUP "sm64config-backup.txt" @@ -43,7 +45,7 @@ enum RefreshRateMode { RRM_MAX }; -extern char configSaveNames[4][MAX_SAVE_NAME_STRING]; +extern char configSaveNames[32][MAX_SAVE_NAME_STRING]; // display settings extern ConfigWindow configWindow; diff --git a/src/pc/djui/djui_panel_host_save.c b/src/pc/djui/djui_panel_host_save.c index aab34efad..4a68b0ba1 100644 --- a/src/pc/djui/djui_panel_host_save.c +++ b/src/pc/djui/djui_panel_host_save.c @@ -12,8 +12,6 @@ static struct DjuiBase* sSaveButtonCaller = NULL; static struct DjuiButton* sSaveButtons[NUM_SAVE_FILES] = { NULL }; static s32 sButtonTag = 0; -static char* sSaveLetters[NUM_SAVE_FILES] = { "A", "B", "C", "D" }; - static void djui_panel_host_save_update_button(struct DjuiButton* button, int slot); static void djui_panel_host_save_save_name_change(UNUSED struct DjuiBase* caller) { @@ -38,7 +36,9 @@ static void djui_panel_edit_create(struct DjuiBase* caller) { struct DjuiRect* rect1 = djui_rect_container_create(body, 32); { char buffer[64] = { 0 }; - djui_language_replace(DLANG(HOST_SAVE, EDIT_NAME), buffer, 64, '@', sSaveLetters[sButtonTag]); + char slotBuffer[4]; + snprintf(slotBuffer, sizeof(slotBuffer), "%d", sButtonTag); + djui_language_replace(DLANG(HOST_SAVE, EDIT_NAME), buffer, 64, '@', slotBuffer); struct DjuiText* text = djui_text_create(&rect1->base, buffer); djui_base_set_size_type(&text->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE); djui_base_set_color(&text->base, 220, 220, 220, 255); @@ -65,6 +65,9 @@ static void djui_panel_edit_create(struct DjuiBase* caller) { static void djui_panel_host_save_update_button(struct DjuiButton* button, int slot) { char starString[64] = { 0 }; + if (configSaveNames[slot][0] == '\0') { + snprintf(configSaveNames[slot], MAX_SAVE_NAME_STRING, "SM64"); + } snprintf(starString, 64, "%c x%d - %s", '~' + 1, save_file_get_total_star_count(slot, 0, 24), configSaveNames[slot]); djui_text_set_text(button->text, starString); } diff --git a/src/pc/fs/fs.h b/src/pc/fs/fs.h index a5b9d06ba..c2b784538 100644 --- a/src/pc/fs/fs.h +++ b/src/pc/fs/fs.h @@ -8,7 +8,9 @@ #include "../platform.h" -#define SAVE_FILENAME "sm64_save_file.bin" +#define SAVE_FILENAME_OLD "sm64_save_file.bin" +#define SAVE_DIRECTORY "saves/" +#define SAVE_FILENAME "sm64coopdx_save_file_" extern char fs_writepath[]; diff --git a/src/pc/lua/smlua_constants_autogen.c b/src/pc/lua/smlua_constants_autogen.c index c7fde7802..ba33bb751 100644 --- a/src/pc/lua/smlua_constants_autogen.c +++ b/src/pc/lua/smlua_constants_autogen.c @@ -2997,7 +2997,7 @@ char gSmluaConstants[] = "" "EMBLEM=7\n" "PLAYER_PART_MAX=8\n" "METAL=CAP\n" -"NUM_SAVE_FILES=4\n" +"NUM_SAVE_FILES=32\n" "SAVE_FILE_A=0\n" "SAVE_FILE_B=1\n" "SAVE_FILE_C=2\n" diff --git a/src/pc/lua/smlua_functions_autogen.c b/src/pc/lua/smlua_functions_autogen.c index c4b6b4f0e..e4ac40a77 100644 --- a/src/pc/lua/smlua_functions_autogen.c +++ b/src/pc/lua/smlua_functions_autogen.c @@ -29727,25 +29727,6 @@ int smlua_func_get_level_course_num(lua_State* L) { return 1; } -int smlua_func_touch_coin_score_age(lua_State* L) { - if (L == NULL) { return 0; } - - int top = lua_gettop(L); - if (top != 2) { - LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "touch_coin_score_age", 2, top); - return 0; - } - - s32 fileIndex = smlua_to_integer(L, 1); - if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 1, "touch_coin_score_age"); return 0; } - s32 courseIndex = smlua_to_integer(L, 2); - if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 2, "touch_coin_score_age"); return 0; } - - touch_coin_score_age(fileIndex, courseIndex); - - return 1; -} - int smlua_func_save_file_do_save(lua_State* L) { if (L == NULL) { return 0; } @@ -38334,7 +38315,6 @@ void smlua_bind_functions_autogen(void) { // save_file.h smlua_bind_function(L, "get_level_num_from_course_num", smlua_func_get_level_num_from_course_num); smlua_bind_function(L, "get_level_course_num", smlua_func_get_level_course_num); - smlua_bind_function(L, "touch_coin_score_age", smlua_func_touch_coin_score_age); smlua_bind_function(L, "save_file_do_save", smlua_func_save_file_do_save); smlua_bind_function(L, "save_file_erase", smlua_func_save_file_erase); smlua_bind_function(L, "save_file_erase_current_backup_save", smlua_func_save_file_erase_current_backup_save); diff --git a/src/pc/mods/mod_storage.cpp b/src/pc/mods/mod_storage.cpp index e64a9ff2a..4e4f42a24 100644 --- a/src/pc/mods/mod_storage.cpp +++ b/src/pc/mods/mod_storage.cpp @@ -61,7 +61,7 @@ static bool char_valid(const char* buffer, bool isKey) { } static void mod_storage_get_filename(char* dest) { - const char* path = fs_get_write_path(SAVE_DIRECTORY); // get user path + const char* path = fs_get_write_path(MS_SAVE_DIRECTORY); // get user path snprintf(dest, SYS_MAX_PATH - 1, "%s/%s", path, gLuaActiveMod->relativePath); // append sav folder strdelete(dest, ".lua"); // delete ".lua" from sav name strcat(dest, SAVE_EXTENSION); // append SAVE_EXTENSION @@ -88,7 +88,7 @@ static bool mod_storage_check_inputs(const char *key, const char *value, char *f if (!char_valid(value, false)) { return false; } // write: ensure savPath exists - const char* savPath = fs_get_write_path(SAVE_DIRECTORY); + const char* savPath = fs_get_write_path(MS_SAVE_DIRECTORY); if (!fs_sys_dir_exists(savPath)) { fs_sys_mkdir(savPath); } } diff --git a/src/pc/mods/mod_storage.h b/src/pc/mods/mod_storage.h index ee41af4a4..0c1d555d0 100644 --- a/src/pc/mods/mod_storage.h +++ b/src/pc/mods/mod_storage.h @@ -10,7 +10,7 @@ extern "C" { #define MAX_KEYS 4096 #define MAX_KEY_VALUE_LENGTH 1024 -#define SAVE_DIRECTORY "sav" +#define MS_SAVE_DIRECTORY "sav" #define SAVE_EXTENSION ".sav" /* |description|Saves a `key` corresponding to a string `value` to mod storage|descriptionEnd| */ diff --git a/src/pc/network/packets/packet_join.c b/src/pc/network/packets/packet_join.c index 0f1602352..29b665e1c 100644 --- a/src/pc/network/packets/packet_join.c +++ b/src/pc/network/packets/packet_join.c @@ -28,7 +28,7 @@ #include "pc/lua/utils/smlua_misc_utils.h" extern u8* gOverrideEeprom; -static u8 eeprom[512] = { 0 }; +static u8 eeprom[EEPROM_SIZE] = { 0 }; static u8 sJoinRequestPlayerModel; static struct PlayerPalette sJoinRequestPlayerPalette; @@ -100,9 +100,9 @@ void network_send_join(struct Packet* joinRequestPacket) { // do connection event network_player_connected(NPT_CLIENT, globalIndex, sJoinRequestPlayerModel, &sJoinRequestPlayerPalette, sJoinRequestPlayerName, sJoinRequestDiscordId); - fs_file_t* fp = fs_open(SAVE_FILENAME); + fs_file_t* fp = fs_open(SAVE_FILENAME_OLD); if (fp != NULL) { - fs_read(fp, eeprom, 512); + fs_read(fp, eeprom, EEPROM_SIZE); fs_close(fp); } @@ -126,7 +126,7 @@ void network_send_join(struct Packet* joinRequestPacket) { packet_write(&p, &gServerSettings.maxPlayers, sizeof(u8)); packet_write(&p, &gServerSettings.pauseAnywhere, sizeof(u8)); packet_write(&p, &gServerSettings.pvpType, sizeof(u8)); - packet_write(&p, eeprom, sizeof(u8) * 512); + packet_write(&p, eeprom, sizeof(u8) * EEPROM_SIZE); network_send_to(globalIndex, &p); LOG_INFO("sending join packet"); @@ -179,7 +179,7 @@ void network_receive_join(struct Packet* p) { packet_read(p, &gServerSettings.maxPlayers, sizeof(u8)); packet_read(p, &gServerSettings.pauseAnywhere, sizeof(u8)); packet_read(p, &gServerSettings.pvpType, sizeof(u8)); - packet_read(p, eeprom, sizeof(u8) * 512); + packet_read(p, eeprom, sizeof(u8) * EEPROM_SIZE); network_player_connected(NPT_SERVER, 0, 0, &DEFAULT_MARIO_PALETTE, "Player", "0"); network_player_connected(NPT_LOCAL, myGlobalIndex, configPlayerModel, &configPlayerPalette, configPlayerName, get_local_discord_id()); diff --git a/src/pc/ultra_reimplementation.c b/src/pc/ultra_reimplementation.c index 230f7e747..9f54e41b0 100644 --- a/src/pc/ultra_reimplementation.c +++ b/src/pc/ultra_reimplementation.c @@ -4,6 +4,7 @@ #include "macros.h" #include "platform.h" #include "fs/fs.h" +#include "game/save_file.h" u8* gOverrideEeprom = NULL; @@ -124,20 +125,22 @@ s32 osEepromProbe(UNUSED OSMesgQueue *mq) { return 1; } -s32 osEepromLongRead(UNUSED OSMesgQueue *mq, u8 address, u8 *buffer, int nbytes) { +s32 osEepromLongRead(UNUSED OSMesgQueue *mq, u8 fileIndex, u8 address, u8 *buffer, int nbytes) { if (gOverrideEeprom != NULL) { memcpy(buffer, gOverrideEeprom + address * 8, nbytes); return 0; } - u8 content[512]; + u8 content[EEPROM_SIZE]; s32 ret = -1; - fs_file_t *fp = fs_open(SAVE_FILENAME); + char filePath[256]; + save_file_get_dir(fileIndex, filePath, 256); + fs_file_t *fp = fs_open(filePath); if (fp == NULL) { return -1; } - if (fs_read(fp, content, 512) == 512) { + if (fs_read(fp, content, EEPROM_SIZE) == EEPROM_SIZE) { memcpy(buffer, content + address * 8, nbytes); ret = 0; } @@ -146,23 +149,29 @@ s32 osEepromLongRead(UNUSED OSMesgQueue *mq, u8 address, u8 *buffer, int nbytes) return ret; } -s32 osEepromLongWrite(UNUSED OSMesgQueue *mq, u8 address, u8 *buffer, int nbytes) { +s32 osEepromLongWrite(UNUSED OSMesgQueue *mq, u8 fileIndex, u8 address, u8 *buffer, int nbytes) { if (gOverrideEeprom != NULL) { memcpy(gOverrideEeprom + address * 8, buffer, nbytes); return 0; } - u8 content[512] = { 0 }; - if (address != 0 || nbytes != 512) { - osEepromLongRead(mq, 0, content, 512); + u8 content[EEPROM_SIZE] = { 0 }; + if (address != 0 || nbytes != EEPROM_SIZE) { + osEepromLongRead(mq, fileIndex, 0, content, EEPROM_SIZE); } memcpy(content + address * 8, buffer, nbytes); - FILE *fp = fopen(fs_get_write_path(SAVE_FILENAME), "wb"); + if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) { + fs_sys_mkdir(fs_get_write_path(SAVE_DIRECTORY)); + } + + char filePath[256]; + save_file_get_dir(fileIndex, filePath, 256); + FILE *fp = fopen(fs_get_write_path(filePath), "wb"); if (fp == NULL) { return -1; } - s32 ret = fwrite(content, 1, 512, fp) == 512 ? 0 : -1; + s32 ret = fwrite(content, 1, EEPROM_SIZE, fp) == EEPROM_SIZE ? 0 : -1; fclose(fp); return ret; From 0db3bd8812e200149ef60d8cc38fe8689b08f5af Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:11:01 -0600 Subject: [PATCH 02/14] strike --- autogen/convert_functions.py | 2 +- autogen/lua_definitions/constants.lua | 2 +- include/PR/os_eeprom.h | 1 + lang/English.ini | 4 + src/game/save_file.c | 37 +++++++-- src/game/save_file.h | 33 +++++++- src/pc/configfile.h | 2 +- src/pc/djui/djui_panel_host_save.c | 106 +++++++++++++++++++------- src/pc/lua/smlua_constants_autogen.c | 2 +- src/pc/ultra_reimplementation.c | 22 ++++++ 10 files changed, 171 insertions(+), 40 deletions(-) diff --git a/autogen/convert_functions.py b/autogen/convert_functions.py index e34a52c2e..0c2449f81 100644 --- a/autogen/convert_functions.py +++ b/autogen/convert_functions.py @@ -119,7 +119,7 @@ override_disallowed_functions = { "src/pc/djui/djui_console.h": [ " djui_console_create", "djui_console_message_create", "djui_console_message_dequeue" ], "src/pc/djui/djui_chat_message.h": [ "create_from" ], "src/game/interaction.h": [ "process_interaction", "_handle_" ], - "src/game/save_file.h": [ "save_file_get_dir" ], + "src/game/save_file.h": [ "save_file_get_dir", "save_file_get_first_available_index" ], "src/game/sound_init.h": [ "_loop_", "thread4_", "set_sound_mode" ], "src/pc/network/network_utils.h": [ "network_get_player_text_color[^_]" ], "src/pc/network/network_player.h": [ "_init", "_connected[^_]", "_shutdown", "_disconnected", "_update", "construct_player_popup", "network_player_name_valid" ], diff --git a/autogen/lua_definitions/constants.lua b/autogen/lua_definitions/constants.lua index 7ce77e0fa..4d0431b7c 100644 --- a/autogen/lua_definitions/constants.lua +++ b/autogen/lua_definitions/constants.lua @@ -6797,7 +6797,7 @@ METAL = CAP --- @type PlayerPart --- | `METAL` --- @type integer -NUM_SAVE_FILES = 32 +NUM_SAVE_FILES = 64 SAVE_FILE_A = 0 --- @type SaveFileIndex SAVE_FILE_B = 1 --- @type SaveFileIndex diff --git a/include/PR/os_eeprom.h b/include/PR/os_eeprom.h index 63a9e14c9..75cdfb0c2 100644 --- a/include/PR/os_eeprom.h +++ b/include/PR/os_eeprom.h @@ -94,6 +94,7 @@ extern "C" { extern s32 osEepromProbe(OSMesgQueue *); extern s32 osEepromRead(OSMesgQueue *, u8, u8 *); extern s32 osEepromWrite(OSMesgQueue *, u8, u8 *); +extern s32 osEepromLongReadLegacy(OSMesgQueue *, u8 , u8 *, int); extern s32 osEepromLongRead(OSMesgQueue *, u8, u8, u8 *, int); extern s32 osEepromLongWrite(OSMesgQueue *, u8, u8, u8 *, int); diff --git a/lang/English.ini b/lang/English.ini index 14ec1d462..175ff3822 100644 --- a/lang/English.ini +++ b/lang/English.ini @@ -219,6 +219,10 @@ SAVE_TITLE = "SAVE" ERASE_TITLE = "ERASE" CONFIRM = "Are you sure you want to erase this save slot?" ERASE = "Erase" +CREATE_SAVE = "Create Save" +CREATE = "Create" +CREATE_TITLE = "CREATE" +CREATE_NAME = "Set Save File @'s Name:" EDIT = "Edit" EDIT_TITLE = "EDIT" EDIT_NAME = "Edit Save File @'s Name:" diff --git a/src/game/save_file.c b/src/game/save_file.c index 0d692a9e8..78576cb36 100644 --- a/src/game/save_file.c +++ b/src/game/save_file.c @@ -98,11 +98,6 @@ s8 get_level_course_num(s16 levelNum) { return gLevelToCourseNumTable[levelNum]; } -void save_file_get_dir(int fileIndex, char* outPath, size_t size) { - snprintf(outPath, size, "%s%s%d.bin", SAVE_DIRECTORY, SAVE_FILENAME, fileIndex); - printf("Returning filepath: %s\n", outPath); -} - /** * Byteswap all multibyte fields in a SaveBlockSignature. */ @@ -274,6 +269,20 @@ static void save_file_bswap(struct SaveBuffer *buf) { } } +void save_file_get_dir(int fileIndex, char* outPath, size_t size) { + snprintf(outPath, size, "%s%s%d.bin", SAVE_DIRECTORY, SAVE_FILENAME, fileIndex); +} + +s32 save_file_get_first_available_index() { + if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return 0; + for (int i = 0; i < NUM_SAVE_FILES; i++) { + char filePath[256]; + save_file_get_dir(i, filePath, 256); + if (!fs_sys_file_exists(fs_get_write_path(filePath))) return i; + } + return NUM_SAVE_FILES; +} + void save_file_do_save(s32 fileIndex, s8 forceSave) { if (INVALID_FILE_INDEX(fileIndex)) { return; } if (gNetworkType != NT_SERVER) { @@ -306,8 +315,11 @@ void save_file_erase(s32 fileIndex) { bzero(&gSaveBuffer.files[fileIndex][0], sizeof(gSaveBuffer.files[fileIndex][0])); bzero(&gSaveBuffer.files[fileIndex][1], sizeof(gSaveBuffer.files[fileIndex][1])); - gSaveFileModified = TRUE; - save_file_do_save(fileIndex, TRUE); + if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return; + char filepath[256]; + save_file_get_dir(fileIndex, filepath, 256); + if (!fs_sys_file_exists(fs_get_write_path(filepath))) return; + remove(fs_get_write_path(filepath)); } void save_file_reload(u8 loadAll) { @@ -349,6 +361,17 @@ void save_file_load_all(UNUSED u8 reload) { gSaveFileModified = FALSE; + if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) { + // Attempt to get and convert old save data into new + struct LegacySaveBuffer saveBuffer = { 0 }; + s32 status = osEepromLongReadLegacy(&gSIEventMesgQueue, 0, (void*)&saveBuffer, sizeof(saveBuffer)); + // 0 is success + if (status == 0) { + for (int i = 0; i < 4; i++) + write_eeprom_data(i, saveBuffer.files[i], sizeof(saveBuffer.files[i]), 0); + } + } + bzero(&gSaveBuffer, sizeof(gSaveBuffer)); for (int file = 0; file < NUM_SAVE_FILES; file++) { diff --git a/src/game/save_file.h b/src/game/save_file.h index 806ecd779..ae9ced518 100644 --- a/src/game/save_file.h +++ b/src/game/save_file.h @@ -8,7 +8,7 @@ #include "course_table.h" -#define NUM_SAVE_FILES 32 +#define NUM_SAVE_FILES 64 // size of savebuffer #define EEPROM_SIZE 128 @@ -60,6 +60,35 @@ struct SaveBuffer struct SaveFile files[NUM_SAVE_FILES][2]; }; +// Legacy save info for loading old save files +struct LegacyMainMenuSaveData +{ + // Each save file has a 2 bit "age" for each course. The higher this value, + // the older the high score is. This is used for tie-breaking when displaying + // on the high score screen. + u32 coinScoreAges[4]; + u16 soundMode; + +#ifdef VERSION_EU + u16 language; +#define SUBTRAHEND 8 +#else +#define SUBTRAHEND 6 +#endif + + // Pad to match the EEPROM size of 0x200 (10 bytes on JP/US, 8 bytes on EU) + u8 filler[512 / 2 - SUBTRAHEND - 4 * (4 + sizeof(struct SaveFile))]; + + struct SaveBlockSignature signature; +}; + +struct LegacySaveBuffer { + // Each of the four save files has two copies. If one is bad, the other is used as a backup. + struct SaveFile files[4][2]; + // The main menu data has two copies. If one is bad, the other is used as a backup. + struct LegacyMainMenuSaveData menuData[2]; +}; + extern u8 gLastCompletedCourseNum; extern u8 gLastCompletedStarNum; extern s8 sUnusedGotGlobalCoinHiScore; @@ -122,6 +151,8 @@ s8 get_level_course_num(s16 levelNum); void save_file_get_dir(int fileIndex, char* outPath, size_t size); +s32 save_file_get_first_available_index(); + /* |description| Saves the current state of the game into a specified save file. Includes data verification and backup management. Useful for maintaining game progress during play or when saving manually diff --git a/src/pc/configfile.h b/src/pc/configfile.h index 330e2c1f0..c5b0cccf7 100644 --- a/src/pc/configfile.h +++ b/src/pc/configfile.h @@ -45,7 +45,7 @@ enum RefreshRateMode { RRM_MAX }; -extern char configSaveNames[32][MAX_SAVE_NAME_STRING]; +extern char configSaveNames[64][MAX_SAVE_NAME_STRING]; // display settings extern ConfigWindow configWindow; diff --git a/src/pc/djui/djui_panel_host_save.c b/src/pc/djui/djui_panel_host_save.c index 4a68b0ba1..6e5093a15 100644 --- a/src/pc/djui/djui_panel_host_save.c +++ b/src/pc/djui/djui_panel_host_save.c @@ -7,12 +7,22 @@ #include "game/save_file.h" #include "pc/configfile.h" +static struct DjuiFlowLayout* sSaveLayout = NULL; +static struct DjuiPaginated* sSavePaginated = NULL; static struct DjuiInputbox* sSaveNameInputBox = NULL; static struct DjuiBase* sSaveButtonCaller = NULL; static struct DjuiButton* sSaveButtons[NUM_SAVE_FILES] = { NULL }; static s32 sButtonTag = 0; +static bool sEditing = true; static void djui_panel_host_save_update_button(struct DjuiButton* button, int slot); +static void djui_panel_host_save_add_saves(struct DjuiBase* base); + +static void djui_panel_host_reload_saves() { + djui_base_destroy_children(&sSaveLayout->base); + djui_panel_host_save_add_saves(&sSaveLayout->base); + djui_paginated_calculate_height(sSavePaginated); +} static void djui_panel_host_save_save_name_change(UNUSED struct DjuiBase* caller) { snprintf(configSaveNames[sButtonTag], MAX_SAVE_NAME_STRING, "%s", sSaveNameInputBox->buffer); @@ -21,7 +31,23 @@ static void djui_panel_host_save_save_name_change(UNUSED struct DjuiBase* caller } } +static void djui_panel_create_create(struct DjuiBase* caller) { + if (sEditing) { return; } + if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) + fs_sys_mkdir(fs_get_write_path(SAVE_DIRECTORY)); + char filePath[256]; + save_file_get_dir(sButtonTag, filePath, 256); + if (fs_sys_file_exists(fs_get_write_path(filePath))) return; + u8 content[EEPROM_SIZE] = { 0 }; + FILE *fp = fopen(fs_get_write_path(filePath), "wb"); + if (fp == NULL) { return; } + fwrite(content, 1, EEPROM_SIZE, fp); + djui_panel_host_reload_saves(); + djui_panel_menu_back(caller); +} + static bool djui_panel_edit_back(UNUSED struct DjuiBase* caller) { + if (!sEditing) { return false; } if (configSaveNames[sButtonTag][0] == '\0') { snprintf(configSaveNames[sButtonTag], MAX_SAVE_NAME_STRING, "SM64"); } @@ -30,15 +56,15 @@ static bool djui_panel_edit_back(UNUSED struct DjuiBase* caller) { } static void djui_panel_edit_create(struct DjuiBase* caller) { - struct DjuiThreePanel* panel = djui_panel_menu_create(DLANG(HOST_SAVE, EDIT_TITLE), false); + struct DjuiThreePanel* panel = djui_panel_menu_create(sEditing ? DLANG(HOST_SAVE, EDIT_TITLE) : DLANG(HOST_SAVE, CREATE_TITLE), false); struct DjuiBase* body = djui_three_panel_get_body(panel); { struct DjuiRect* rect1 = djui_rect_container_create(body, 32); { char buffer[64] = { 0 }; char slotBuffer[4]; - snprintf(slotBuffer, sizeof(slotBuffer), "%d", sButtonTag); - djui_language_replace(DLANG(HOST_SAVE, EDIT_NAME), buffer, 64, '@', slotBuffer); + snprintf(slotBuffer, sizeof(slotBuffer), "%d", sButtonTag + 1); + djui_language_replace(sEditing ? DLANG(HOST_SAVE, EDIT_NAME) : DLANG(HOST_SAVE, CREATE_NAME), buffer, 64, '@', slotBuffer); struct DjuiText* text = djui_text_create(&rect1->base, buffer); djui_base_set_size_type(&text->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE); djui_base_set_color(&text->base, 220, 220, 220, 255); @@ -56,6 +82,7 @@ static void djui_panel_edit_create(struct DjuiBase* caller) { djui_interactable_hook_value_change(&sSaveNameInputBox->base, djui_panel_host_save_save_name_change); } + if (!sEditing) djui_button_create(body, DLANG(HOST_SAVE, CREATE), DJUI_BUTTON_STYLE_NORMAL, djui_panel_create_create); djui_button_create(body, DLANG(MENU, BACK), DJUI_BUTTON_STYLE_BACK, djui_panel_menu_back); } @@ -63,6 +90,19 @@ static void djui_panel_edit_create(struct DjuiBase* caller) { djui_panel_add(caller, panel, NULL); } +static void djui_panel_save_create(struct DjuiBase* caller) { + sEditing = false; + sButtonTag = save_file_get_first_available_index(); + if (sButtonTag >= NUM_SAVE_FILES) return; + djui_panel_edit_create(caller); +} + +static void djui_panel_host_save_edit(struct DjuiBase* caller) { + sEditing = true; + sButtonTag = caller->tag; + djui_panel_edit_create(caller); +} + static void djui_panel_host_save_update_button(struct DjuiButton* button, int slot) { char starString[64] = { 0 }; if (configSaveNames[slot][0] == '\0') { @@ -82,6 +122,7 @@ static void djui_panel_host_save_erase_yes(struct DjuiBase* caller) { save_file_erase(sButtonTag); play_character_sound(gMarioState, CHAR_SOUND_WAAAOOOW); djui_panel_host_save_update_button(sSaveButtons[sButtonTag], sButtonTag); + djui_panel_host_reload_saves(); djui_panel_menu_back(caller); } @@ -93,9 +134,33 @@ static void djui_panel_host_save_erase(struct DjuiBase* caller) { djui_panel_host_save_erase_yes); } -static void djui_panel_host_save_edit(struct DjuiBase* caller) { - sButtonTag = caller->tag; - djui_panel_edit_create(caller); +void djui_panel_host_save_add_saves(struct DjuiBase* base) { + if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return; + for (int i = 0; i < NUM_SAVE_FILES; i++) { + char filepath[256]; + save_file_get_dir(i, filepath, 256); + if (!fs_sys_file_exists(fs_get_write_path(filepath))) continue; + struct DjuiRect* rect1 = djui_rect_container_create(base, 32); + { + struct DjuiButton* button1 = djui_button_create(&rect1->base, "", DJUI_BUTTON_STYLE_NORMAL, djui_panel_host_save_button_click); + djui_panel_host_save_update_button(button1, i); + djui_base_set_size(&button1->base, 0.6f, 32); + button1->base.tag = i; + sSaveButtons[i] = button1; + + struct DjuiButton* button2 = djui_button_create(&rect1->base, DLANG(HOST_SAVE, ERASE), DJUI_BUTTON_STYLE_NORMAL, djui_panel_host_save_erase); + button2->base.tag = i; + djui_base_set_size(&button2->base, 0.19f, 32); + djui_base_set_alignment(&button2->base, DJUI_HALIGN_CENTER, DJUI_VALIGN_TOP); + djui_base_set_location(&button2->base, configDjuiThemeCenter ? 127 : button1->rect->base.width.value + 98, 0); + djui_base_set_enabled(&button2->base, gDjuiInMainMenu || gCurrSaveFileNum - 1 != i); + + struct DjuiButton* button3 = djui_button_create(&rect1->base, DLANG(HOST_SAVE, EDIT), DJUI_BUTTON_STYLE_NORMAL, djui_panel_host_save_edit); + button3->base.tag = i; + djui_base_set_size(&button3->base, 0.19f, 32); + djui_base_set_alignment(&button3->base, DJUI_HALIGN_RIGHT, DJUI_VALIGN_TOP); + } + } } void djui_panel_host_save_create(struct DjuiBase* caller) { @@ -104,29 +169,14 @@ void djui_panel_host_save_create(struct DjuiBase* caller) { struct DjuiThreePanel* panel = djui_panel_menu_create(DLANG(HOST_SAVE, SAVE_TITLE), false); struct DjuiBase* body = djui_three_panel_get_body(panel); { - for (int i = 0; i < NUM_SAVE_FILES; i++) { - struct DjuiRect* rect1 = djui_rect_container_create(body, 32); - { - struct DjuiButton* button1 = djui_button_create(&rect1->base, "", DJUI_BUTTON_STYLE_NORMAL, djui_panel_host_save_button_click); - djui_panel_host_save_update_button(button1, i); - djui_base_set_size(&button1->base, 0.6f, 32); - button1->base.tag = i; - sSaveButtons[i] = button1; - - struct DjuiButton* button2 = djui_button_create(&rect1->base, DLANG(HOST_SAVE, ERASE), DJUI_BUTTON_STYLE_NORMAL, djui_panel_host_save_erase); - button2->base.tag = i; - djui_base_set_size(&button2->base, 0.19f, 32); - djui_base_set_alignment(&button2->base, DJUI_HALIGN_CENTER, DJUI_VALIGN_TOP); - djui_base_set_location(&button2->base, configDjuiThemeCenter ? 127 : button1->rect->base.width.value + 98, 0); - djui_base_set_enabled(&button2->base, gDjuiInMainMenu || gCurrSaveFileNum - 1 != i); - - struct DjuiButton* button3 = djui_button_create(&rect1->base, DLANG(HOST_SAVE, EDIT), DJUI_BUTTON_STYLE_NORMAL, djui_panel_host_save_edit); - button3->base.tag = i; - djui_base_set_size(&button3->base, 0.19f, 32); - djui_base_set_alignment(&button3->base, DJUI_HALIGN_RIGHT, DJUI_VALIGN_TOP); - } - } + struct DjuiPaginated* paginated = djui_paginated_create(body, 8); + paginated->showMaxCount = true; + sSaveLayout = paginated->layout; + djui_panel_host_save_add_saves(&paginated->layout->base); + djui_paginated_calculate_height(paginated); + sSavePaginated = paginated; + djui_button_create(body, DLANG(HOST_SAVE, CREATE_SAVE), DJUI_BUTTON_STYLE_NORMAL, djui_panel_save_create); djui_button_create(body, DLANG(MENU, BACK), DJUI_BUTTON_STYLE_NORMAL, djui_panel_menu_back); } diff --git a/src/pc/lua/smlua_constants_autogen.c b/src/pc/lua/smlua_constants_autogen.c index ba33bb751..68d9e3c04 100644 --- a/src/pc/lua/smlua_constants_autogen.c +++ b/src/pc/lua/smlua_constants_autogen.c @@ -2997,7 +2997,7 @@ char gSmluaConstants[] = "" "EMBLEM=7\n" "PLAYER_PART_MAX=8\n" "METAL=CAP\n" -"NUM_SAVE_FILES=32\n" +"NUM_SAVE_FILES=64\n" "SAVE_FILE_A=0\n" "SAVE_FILE_B=1\n" "SAVE_FILE_C=2\n" diff --git a/src/pc/ultra_reimplementation.c b/src/pc/ultra_reimplementation.c index 9f54e41b0..269c099dd 100644 --- a/src/pc/ultra_reimplementation.c +++ b/src/pc/ultra_reimplementation.c @@ -125,6 +125,28 @@ s32 osEepromProbe(UNUSED OSMesgQueue *mq) { return 1; } +s32 osEepromLongReadLegacy(UNUSED OSMesgQueue *mq, u8 address, u8 *buffer, int nbytes) { + if (gOverrideEeprom != NULL) { + memcpy(buffer, gOverrideEeprom + address * 8, nbytes); + return 0; + } + + u8 content[512]; + s32 ret = -1; + + fs_file_t *fp = fs_open(SAVE_FILENAME_OLD); + if (fp == NULL) { + return -1; + } + if (fs_read(fp, content, 512) == 512) { + memcpy(buffer, content + address * 8, nbytes); + ret = 0; + } + fs_close(fp); + + return ret; +} + s32 osEepromLongRead(UNUSED OSMesgQueue *mq, u8 fileIndex, u8 address, u8 *buffer, int nbytes) { if (gOverrideEeprom != NULL) { memcpy(buffer, gOverrideEeprom + address * 8, nbytes); From 99beef174f0251e4a9dfde698846cab332fc86cd Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:26:22 -0600 Subject: [PATCH 03/14] syncing still not working :((( --- src/pc/network/packets/packet_join.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pc/network/packets/packet_join.c b/src/pc/network/packets/packet_join.c index 29b665e1c..78d0d99c4 100644 --- a/src/pc/network/packets/packet_join.c +++ b/src/pc/network/packets/packet_join.c @@ -100,7 +100,9 @@ void network_send_join(struct Packet* joinRequestPacket) { // do connection event network_player_connected(NPT_CLIENT, globalIndex, sJoinRequestPlayerModel, &sJoinRequestPlayerPalette, sJoinRequestPlayerName, sJoinRequestDiscordId); - fs_file_t* fp = fs_open(SAVE_FILENAME_OLD); + char filePath[256]; + save_file_get_dir(gCurrSaveFileNum - 1, filePath, 256); + fs_file_t* fp = fs_open(fs_get_write_path(filePath)); if (fp != NULL) { fs_read(fp, eeprom, EEPROM_SIZE); fs_close(fp); From 00e298db332d000280c7bb4d1a221006eeff6616 Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Sun, 1 Mar 2026 23:29:09 -0600 Subject: [PATCH 04/14] [build] Now it should work (i still need to test with another pc(weewoo)) --- src/pc/network/packets/packet_join.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pc/network/packets/packet_join.c b/src/pc/network/packets/packet_join.c index 78d0d99c4..300cdb40f 100644 --- a/src/pc/network/packets/packet_join.c +++ b/src/pc/network/packets/packet_join.c @@ -102,7 +102,7 @@ void network_send_join(struct Packet* joinRequestPacket) { char filePath[256]; save_file_get_dir(gCurrSaveFileNum - 1, filePath, 256); - fs_file_t* fp = fs_open(fs_get_write_path(filePath)); + fs_file_t* fp = fs_open(filePath); if (fp != NULL) { fs_read(fp, eeprom, EEPROM_SIZE); fs_close(fp); From fc5ad1ead8b12da4928b588febd5805e9655bbcf Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:58:05 -0600 Subject: [PATCH 05/14] make names stored inside save efile, some bug fixes, still need to do syncing --- autogen/convert_functions.py | 2 +- src/game/save_file.c | 92 ++++++++++++++++++++++++---- src/game/save_file.h | 7 ++- src/pc/configfile.c | 26 -------- src/pc/configfile.h | 5 +- src/pc/djui/djui_panel_host.c | 4 +- src/pc/djui/djui_panel_host_save.c | 92 ++++++++++++++++++++-------- src/pc/fs/fs.h | 4 +- src/pc/mods/mod_storage.cpp | 2 +- src/pc/mods/mod_storage.h | 2 +- src/pc/network/packets/packet_join.c | 2 +- src/pc/ultra_reimplementation.c | 6 +- 12 files changed, 167 insertions(+), 77 deletions(-) diff --git a/autogen/convert_functions.py b/autogen/convert_functions.py index 0c2449f81..1168aecd2 100644 --- a/autogen/convert_functions.py +++ b/autogen/convert_functions.py @@ -119,7 +119,7 @@ override_disallowed_functions = { "src/pc/djui/djui_console.h": [ " djui_console_create", "djui_console_message_create", "djui_console_message_dequeue" ], "src/pc/djui/djui_chat_message.h": [ "create_from" ], "src/game/interaction.h": [ "process_interaction", "_handle_" ], - "src/game/save_file.h": [ "save_file_get_dir", "save_file_get_first_available_index" ], + "src/game/save_file.h": [ "save_file_get_all_filenames", "save_file_get_dir", "save_file_get_first_available_index" ], "src/game/sound_init.h": [ "_loop_", "thread4_", "set_sound_mode" ], "src/pc/network/network_utils.h": [ "network_get_player_text_color[^_]" ], "src/pc/network/network_player.h": [ "_init", "_connected[^_]", "_shutdown", "_disconnected", "_update", "construct_player_popup", "network_player_name_valid" ], diff --git a/src/game/save_file.c b/src/game/save_file.c index 78576cb36..eafc0a458 100644 --- a/src/game/save_file.c +++ b/src/game/save_file.c @@ -14,6 +14,8 @@ #include "macros.h" #include "pc/network/network.h" #include "pc/lua/utils/smlua_level_utils.h" +#include "pc/mods/mod.h" +#include "pc/mods/mods_utils.h" #include "pc/utils/misc.h" #ifndef bcopy @@ -269,15 +271,88 @@ static void save_file_bswap(struct SaveBuffer *buf) { } } -void save_file_get_dir(int fileIndex, char* outPath, size_t size) { - snprintf(outPath, size, "%s%s%d.bin", SAVE_DIRECTORY, SAVE_FILENAME, fileIndex); +/** + * Converts old 512 byte save files into 4 new 128 byte save files + */ +static void save_file_convert_old_to_new() { + struct LegacySaveBuffer saveBuffer = { 0 }; + s32 status = osEepromLongReadLegacy(&gSIEventMesgQueue, 0, (void*)&saveBuffer, sizeof(saveBuffer)); + if (status == 0) { + for (int i = 0; i < 4; i++) + write_eeprom_data(i, saveBuffer.files[i], sizeof(saveBuffer.files[i]), 0); + } } +/** + * Gets all save file names +*/ +void save_file_get_all_filenames(char filenames[NUM_SAVE_FILES][MAX_SAVE_NAME_STRING]) { + char* directory = (char*)fs_get_write_path(SAVE_DIRECTORY); + if (!directory) return; + + if (!fs_sys_dir_exists(directory)) return; + struct dirent* dir = NULL; + + DIR* d = opendir(directory); + if (!d) { return; } + + // iterate + char path[SYS_MAX_PATH] = { 0 }; + while ((dir = readdir(d)) != NULL) { + // sanity check + if (!directory_sanity_check(dir, directory, path)) continue; + snprintf(path, SYS_MAX_PATH, "%s", dir->d_name); + if (strlen(path) == 0 || strlen(path) >= 256) continue; + + // verify filename follows format (index)_(name)(SAVE_EXTENSION) + int index = 0; + char name[MAX_SAVE_NAME_STRING]; + char extension[12]; + if (sscanf(path, "%d_%31[^.]%11s", &index, name, extension) == 3) { + if (index < 0 || index >= NUM_SAVE_FILES) continue; + if (strlen(name) == 0) continue; + if (strcmp(extension, SAVE_EXTENSION) != 0) continue; + snprintf(filenames[index], 256, "%s", name); + } + } + + closedir(d); +} + +/** + * Gets save file name for index. If it does not exist, use SM64 +*/ +void save_file_get_filename_at_index(int fileIndex, char outFilename[MAX_SAVE_NAME_STRING]) { + char filenames[NUM_SAVE_FILES][MAX_SAVE_NAME_STRING] = { 0 }; + save_file_get_all_filenames(filenames); + if (filenames[fileIndex] && filenames[fileIndex][0] != '\0') { + snprintf(outFilename, MAX_SAVE_NAME_STRING, "%s", filenames[fileIndex]); + } else { + snprintf(outFilename, MAX_SAVE_NAME_STRING, "%s", "SM64"); + } +} + +/** + * Get directory for a save file at index +*/ +void save_file_get_dir(int fileIndex, char* outPath, size_t size, char* overrideName) { + char name[MAX_SAVE_NAME_STRING] = { 0 }; + if (overrideName == NULL) { + save_file_get_filename_at_index(fileIndex, name); + } else { + snprintf(name, MAX_SAVE_NAME_STRING, "%s", overrideName); + } + snprintf(outPath, size, "%s%d_%s%s", SAVE_DIRECTORY, fileIndex, name, SAVE_EXTENSION); +} + +/** + * Gets the first available index that is not being used +*/ s32 save_file_get_first_available_index() { if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return 0; for (int i = 0; i < NUM_SAVE_FILES; i++) { char filePath[256]; - save_file_get_dir(i, filePath, 256); + save_file_get_dir(i, filePath, 256, NULL); if (!fs_sys_file_exists(fs_get_write_path(filePath))) return i; } return NUM_SAVE_FILES; @@ -317,7 +392,7 @@ void save_file_erase(s32 fileIndex) { if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return; char filepath[256]; - save_file_get_dir(fileIndex, filepath, 256); + save_file_get_dir(fileIndex, filepath, 256, NULL); if (!fs_sys_file_exists(fs_get_write_path(filepath))) return; remove(fs_get_write_path(filepath)); } @@ -362,14 +437,7 @@ void save_file_load_all(UNUSED u8 reload) { gSaveFileModified = FALSE; if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) { - // Attempt to get and convert old save data into new - struct LegacySaveBuffer saveBuffer = { 0 }; - s32 status = osEepromLongReadLegacy(&gSIEventMesgQueue, 0, (void*)&saveBuffer, sizeof(saveBuffer)); - // 0 is success - if (status == 0) { - for (int i = 0; i < 4; i++) - write_eeprom_data(i, saveBuffer.files[i], sizeof(saveBuffer.files[i]), 0); - } + save_file_convert_old_to_new(); } bzero(&gSaveBuffer, sizeof(gSaveBuffer)); diff --git a/src/game/save_file.h b/src/game/save_file.h index ae9ced518..ac9d9105a 100644 --- a/src/game/save_file.h +++ b/src/game/save_file.h @@ -9,6 +9,7 @@ #include "course_table.h" #define NUM_SAVE_FILES 64 +#define MAX_SAVE_NAME_STRING 32 // size of savebuffer #define EEPROM_SIZE 128 @@ -149,7 +150,11 @@ s8 get_level_num_from_course_num(s16 courseNum); /* |description|Gets the level number's corresponding course number|descriptionEnd| */ s8 get_level_course_num(s16 levelNum); -void save_file_get_dir(int fileIndex, char* outPath, size_t size); +void save_file_get_all_filenames(char filenames[NUM_SAVE_FILES][MAX_SAVE_NAME_STRING]); + +void save_file_get_filename_at_index(int fileIndex, char outFilename[MAX_SAVE_NAME_STRING]); + +void save_file_get_dir(int fileIndex, char* outPath, size_t size, char* overrideName); s32 save_file_get_first_available_index(); diff --git a/src/pc/configfile.c b/src/pc/configfile.c index 847f1d907..f6a8f2891 100644 --- a/src/pc/configfile.c +++ b/src/pc/configfile.c @@ -57,7 +57,6 @@ struct FunctionConfigOption { /* *Config options and default values */ -char configSaveNames[NUM_SAVE_FILES][MAX_SAVE_NAME_STRING] = { 0 }; // Video/audio stuff ConfigWindow configWindow = { @@ -522,36 +521,11 @@ static void dynos_pack_write(FILE* file) { } } -static void save_name_read(char** tokens, int numTokens) { - if (numTokens < 2) { return; } - char fullSaveName[MAX_SAVE_NAME_STRING] = { 0 }; - int index = 0; - for (int i = 1; i < numTokens; i++) { - if (i == 1) { - index = atoi(tokens[i]); - } else { - if (i > 2) { - strncat(fullSaveName, " ", MAX_SAVE_NAME_STRING - 1); - } - strncat(fullSaveName, tokens[i], MAX_SAVE_NAME_STRING - 1); - } - - } - snprintf(configSaveNames[index], MAX_SAVE_NAME_STRING, "%s", fullSaveName); -} - -static void save_name_write(FILE* file) { - for (int i = 0; i < NUM_SAVE_FILES; i++) { - fprintf(file, "%s %d %s\n", "save-name:", i, configSaveNames[i]); - } -} - static const struct FunctionConfigOption functionOptions[] = { { .name = "enable-mod:", .read = enable_mod_read, .write = enable_mod_write }, { .name = "ban:", .read = ban_read, .write = ban_write }, { .name = "moderator:", .read = moderator_read, .write = moderator_write }, { .name = "dynos-pack:", .read = dynos_pack_read, .write = dynos_pack_write }, - { .name = "save-name:", .read = save_name_read, .write = save_name_write } }; // Reads an entire line from a file (excluding the newline character) and returns an allocated string diff --git a/src/pc/configfile.h b/src/pc/configfile.h index c5b0cccf7..c513b0d32 100644 --- a/src/pc/configfile.h +++ b/src/pc/configfile.h @@ -5,7 +5,7 @@ #include #include "game/player_palette.h" #include "pc/lua/smlua_autogen.h" -#include "game/save_file.h" +#include "game/area.h" #define CONFIGFILE_DEFAULT "sm64config.txt" #define CONFIGFILE_BACKUP "sm64config-backup.txt" @@ -13,7 +13,6 @@ #define MAX_BINDS 3 #define MAX_VOLUME 127 #define MAX_CONFIG_STRING 64 -#define MAX_SAVE_NAME_STRING 32 #define DEFAULT_PORT 7777 #define DEFAULT_COOPNET_IP "net.coop64.us" @@ -45,8 +44,6 @@ enum RefreshRateMode { RRM_MAX }; -extern char configSaveNames[64][MAX_SAVE_NAME_STRING]; - // display settings extern ConfigWindow configWindow; extern ConfigStick configStick; diff --git a/src/pc/djui/djui_panel_host.c b/src/pc/djui/djui_panel_host.c index b1b069e4b..760b5ff9f 100644 --- a/src/pc/djui/djui_panel_host.c +++ b/src/pc/djui/djui_panel_host.c @@ -173,7 +173,9 @@ void djui_panel_host_create(struct DjuiBase* caller) { djui_text_set_drop_shadow(text1, 64, 64, 64, 100); char starString[64] = { 0 }; - snprintf(starString, 64, "%c x%d - %s", '~' + 1, save_file_get_total_star_count(configHostSaveSlot - 1, 0, 24), configSaveNames[configHostSaveSlot - 1]); + char saveName[MAX_SAVE_NAME_STRING] = { 0 }; + save_file_get_filename_at_index(configHostSaveSlot - 1, saveName); + snprintf(starString, 64, "%c x%d - %s", '~' + 1, save_file_get_total_star_count(configHostSaveSlot - 1, 0, 24), saveName); struct DjuiButton* button1 = djui_button_create(&rect2->base, starString, DJUI_BUTTON_STYLE_NORMAL, djui_panel_host_save_create); djui_base_set_size(&button1->base, 0.45f, 32); djui_base_set_alignment(&button1->base, DJUI_HALIGN_RIGHT, DJUI_VALIGN_TOP); diff --git a/src/pc/djui/djui_panel_host_save.c b/src/pc/djui/djui_panel_host_save.c index 6e5093a15..7fd0d60bc 100644 --- a/src/pc/djui/djui_panel_host_save.c +++ b/src/pc/djui/djui_panel_host_save.c @@ -12,6 +12,7 @@ static struct DjuiPaginated* sSavePaginated = NULL; static struct DjuiInputbox* sSaveNameInputBox = NULL; static struct DjuiBase* sSaveButtonCaller = NULL; static struct DjuiButton* sSaveButtons[NUM_SAVE_FILES] = { NULL }; +static char sSaveName[MAX_SAVE_NAME_STRING] = { 0 }; static s32 sButtonTag = 0; static bool sEditing = true; @@ -24,35 +25,74 @@ static void djui_panel_host_reload_saves() { djui_paginated_calculate_height(sSavePaginated); } +static void djui_panel_host_save_update_save_name() { + if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return; + if (strstr(sSaveName, ".")) return; + char filePath[256]; + save_file_get_dir(sButtonTag, filePath, 256, NULL); + char newFilePath[256]; + save_file_get_dir(sButtonTag, newFilePath, 256, sSaveName); + if (strcmp(filePath, newFilePath) == 0) return; + if (!fs_sys_file_exists(fs_get_write_path(filePath))) return; + // write the save data of the file to a variable + u8 content[EEPROM_SIZE] = { 0 }; + fs_file_t* oldFile = fs_open(filePath); + if (oldFile == NULL) return; + fs_read(oldFile, content, EEPROM_SIZE); + fs_close(oldFile); + // create a new file with the data + FILE* fp = fopen(fs_get_write_path(newFilePath), "wb"); + if (fp == NULL) return; + bool success = fwrite(content, 1, EEPROM_SIZE, fp) == EEPROM_SIZE; + fclose(fp); + if (success) { + // nuke old file + remove(fs_get_write_path(filePath)); + } else { + // uh oh! New file failed to be written to. Nuke new file + remove(fs_get_write_path(newFilePath)); + } + + djui_panel_host_reload_saves(); +} + static void djui_panel_host_save_save_name_change(UNUSED struct DjuiBase* caller) { - snprintf(configSaveNames[sButtonTag], MAX_SAVE_NAME_STRING, "%s", sSaveNameInputBox->buffer); + snprintf(sSaveName, MAX_SAVE_NAME_STRING, "%s", sSaveNameInputBox->buffer); if (strlen(sSaveNameInputBox->buffer) >= 64) { - djui_inputbox_set_text(sSaveNameInputBox, configSaveNames[sButtonTag]); + djui_inputbox_set_text(sSaveNameInputBox, sSaveName); + } + + if (strstr(sSaveName, ".")) { + djui_inputbox_set_text_color(sSaveNameInputBox, 255, 0, 0, 255); + } else { + djui_inputbox_set_text_color(sSaveNameInputBox, 0, 0, 0, 255); } } static void djui_panel_create_create(struct DjuiBase* caller) { - if (sEditing) { return; } if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) fs_sys_mkdir(fs_get_write_path(SAVE_DIRECTORY)); char filePath[256]; - save_file_get_dir(sButtonTag, filePath, 256); + save_file_get_dir(sButtonTag, filePath, 256, sSaveName); if (fs_sys_file_exists(fs_get_write_path(filePath))) return; u8 content[EEPROM_SIZE] = { 0 }; FILE *fp = fopen(fs_get_write_path(filePath), "wb"); - if (fp == NULL) { return; } + if (fp == NULL) return; fwrite(content, 1, EEPROM_SIZE, fp); + fclose(fp); djui_panel_host_reload_saves(); djui_panel_menu_back(caller); } -static bool djui_panel_edit_back(UNUSED struct DjuiBase* caller) { - if (!sEditing) { return false; } - if (configSaveNames[sButtonTag][0] == '\0') { - snprintf(configSaveNames[sButtonTag], MAX_SAVE_NAME_STRING, "SM64"); +static void djui_panel_edit_save(UNUSED struct DjuiBase* caller) { + if (sSaveName[0] == '\0' || strstr(sSaveName, ".")) { + snprintf(sSaveName, MAX_SAVE_NAME_STRING, "SM64"); + djui_panel_menu_back(caller); + return; } + djui_panel_host_save_update_save_name(); djui_panel_host_save_update_button(sSaveButtons[sButtonTag], sButtonTag); - return false; + djui_panel_menu_back(caller); } static void djui_panel_edit_create(struct DjuiBase* caller) { @@ -76,17 +116,18 @@ static void djui_panel_edit_create(struct DjuiBase* caller) { djui_base_set_size_type(&sSaveNameInputBox->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE); djui_base_set_size(&sSaveNameInputBox->base, 0.45f, 32); djui_base_set_alignment(&sSaveNameInputBox->base, DJUI_HALIGN_RIGHT, DJUI_VALIGN_TOP); - char saveName[MAX_SAVE_NAME_STRING] = { 0 }; - snprintf(saveName, MAX_SAVE_NAME_STRING, "%s", configSaveNames[sButtonTag]); - djui_inputbox_set_text(sSaveNameInputBox, saveName); + save_file_get_filename_at_index(sButtonTag, sSaveName); + djui_inputbox_set_text(sSaveNameInputBox, sSaveName); djui_interactable_hook_value_change(&sSaveNameInputBox->base, djui_panel_host_save_save_name_change); } - if (!sEditing) djui_button_create(body, DLANG(HOST_SAVE, CREATE), DJUI_BUTTON_STYLE_NORMAL, djui_panel_create_create); + if (!sEditing) + djui_button_create(body, DLANG(HOST_SAVE, CREATE), DJUI_BUTTON_STYLE_NORMAL, djui_panel_create_create); + else + djui_button_create(body, DLANG(HOST_SAVE, EDIT), DJUI_BUTTON_STYLE_NORMAL, djui_panel_edit_save); djui_button_create(body, DLANG(MENU, BACK), DJUI_BUTTON_STYLE_BACK, djui_panel_menu_back); } - panel->on_back = djui_panel_edit_back; djui_panel_add(caller, panel, NULL); } @@ -104,14 +145,19 @@ static void djui_panel_host_save_edit(struct DjuiBase* caller) { } static void djui_panel_host_save_update_button(struct DjuiButton* button, int slot) { + if (!button || !button->text) return; char starString[64] = { 0 }; - if (configSaveNames[slot][0] == '\0') { - snprintf(configSaveNames[slot], MAX_SAVE_NAME_STRING, "SM64"); - } - snprintf(starString, 64, "%c x%d - %s", '~' + 1, save_file_get_total_star_count(slot, 0, 24), configSaveNames[slot]); + char name[MAX_SAVE_NAME_STRING] = { 0 }; + save_file_get_filename_at_index(slot, name); + snprintf(starString, 64, "%c x%d - %s", '~' + 1, save_file_get_total_star_count(slot, 0, 24), name); djui_text_set_text(button->text, starString); } +static bool djui_panel_host_save_back(UNUSED struct DjuiBase* caller) { + djui_panel_host_save_update_button((struct DjuiButton*)sSaveButtonCaller, configHostSaveSlot - 1); + return false; +} + static void djui_panel_host_save_button_click(struct DjuiBase* caller) { configHostSaveSlot = caller->tag + 1; djui_panel_host_save_update_button((struct DjuiButton*)sSaveButtonCaller, configHostSaveSlot - 1); @@ -128,17 +174,14 @@ static void djui_panel_host_save_erase_yes(struct DjuiBase* caller) { static void djui_panel_host_save_erase(struct DjuiBase* caller) { sButtonTag = caller->tag; - djui_panel_confirm_create(caller, - DLANG(HOST_SAVE, ERASE_TITLE), - DLANG(HOST_SAVE, CONFIRM), - djui_panel_host_save_erase_yes); + djui_panel_confirm_create(caller, DLANG(HOST_SAVE, ERASE_TITLE), DLANG(HOST_SAVE, CONFIRM), djui_panel_host_save_erase_yes); } void djui_panel_host_save_add_saves(struct DjuiBase* base) { if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return; for (int i = 0; i < NUM_SAVE_FILES; i++) { char filepath[256]; - save_file_get_dir(i, filepath, 256); + save_file_get_dir(i, filepath, 256, NULL); if (!fs_sys_file_exists(fs_get_write_path(filepath))) continue; struct DjuiRect* rect1 = djui_rect_container_create(base, 32); { @@ -180,5 +223,6 @@ void djui_panel_host_save_create(struct DjuiBase* caller) { djui_button_create(body, DLANG(MENU, BACK), DJUI_BUTTON_STYLE_NORMAL, djui_panel_menu_back); } + panel->on_back = djui_panel_host_save_back; djui_panel_add(caller, panel, NULL); } diff --git a/src/pc/fs/fs.h b/src/pc/fs/fs.h index c2b784538..5b2bdc445 100644 --- a/src/pc/fs/fs.h +++ b/src/pc/fs/fs.h @@ -8,9 +8,9 @@ #include "../platform.h" -#define SAVE_FILENAME_OLD "sm64_save_file.bin" +#define SAVE_FILENAME "sm64_save_file.bin" #define SAVE_DIRECTORY "saves/" -#define SAVE_FILENAME "sm64coopdx_save_file_" +#define SAVE_EXTENSION ".bin" extern char fs_writepath[]; diff --git a/src/pc/mods/mod_storage.cpp b/src/pc/mods/mod_storage.cpp index 4e4f42a24..707c1a1e7 100644 --- a/src/pc/mods/mod_storage.cpp +++ b/src/pc/mods/mod_storage.cpp @@ -64,7 +64,7 @@ static void mod_storage_get_filename(char* dest) { const char* path = fs_get_write_path(MS_SAVE_DIRECTORY); // get user path snprintf(dest, SYS_MAX_PATH - 1, "%s/%s", path, gLuaActiveMod->relativePath); // append sav folder strdelete(dest, ".lua"); // delete ".lua" from sav name - strcat(dest, SAVE_EXTENSION); // append SAVE_EXTENSION + strcat(dest, MS_SAVE_EXTENSION); // append MS_SAVE_EXTENSION normalize_path(dest); // fix any out of place slashes } diff --git a/src/pc/mods/mod_storage.h b/src/pc/mods/mod_storage.h index 0c1d555d0..e00bd0e6e 100644 --- a/src/pc/mods/mod_storage.h +++ b/src/pc/mods/mod_storage.h @@ -11,7 +11,7 @@ extern "C" { #define MAX_KEYS 4096 #define MAX_KEY_VALUE_LENGTH 1024 #define MS_SAVE_DIRECTORY "sav" -#define SAVE_EXTENSION ".sav" +#define MS_SAVE_EXTENSION ".sav" /* |description|Saves a `key` corresponding to a string `value` to mod storage|descriptionEnd| */ bool mod_storage_save(const char* key, const char* value); diff --git a/src/pc/network/packets/packet_join.c b/src/pc/network/packets/packet_join.c index 300cdb40f..8895e9af3 100644 --- a/src/pc/network/packets/packet_join.c +++ b/src/pc/network/packets/packet_join.c @@ -101,7 +101,7 @@ void network_send_join(struct Packet* joinRequestPacket) { network_player_connected(NPT_CLIENT, globalIndex, sJoinRequestPlayerModel, &sJoinRequestPlayerPalette, sJoinRequestPlayerName, sJoinRequestDiscordId); char filePath[256]; - save_file_get_dir(gCurrSaveFileNum - 1, filePath, 256); + save_file_get_dir(gCurrSaveFileNum - 1, filePath, 256, NULL); fs_file_t* fp = fs_open(filePath); if (fp != NULL) { fs_read(fp, eeprom, EEPROM_SIZE); diff --git a/src/pc/ultra_reimplementation.c b/src/pc/ultra_reimplementation.c index 269c099dd..9db65fef0 100644 --- a/src/pc/ultra_reimplementation.c +++ b/src/pc/ultra_reimplementation.c @@ -134,7 +134,7 @@ s32 osEepromLongReadLegacy(UNUSED OSMesgQueue *mq, u8 address, u8 *buffer, int n u8 content[512]; s32 ret = -1; - fs_file_t *fp = fs_open(SAVE_FILENAME_OLD); + fs_file_t *fp = fs_open(SAVE_FILENAME); if (fp == NULL) { return -1; } @@ -157,7 +157,7 @@ s32 osEepromLongRead(UNUSED OSMesgQueue *mq, u8 fileIndex, u8 address, u8 *buffe s32 ret = -1; char filePath[256]; - save_file_get_dir(fileIndex, filePath, 256); + save_file_get_dir(fileIndex, filePath, 256, NULL); fs_file_t *fp = fs_open(filePath); if (fp == NULL) { return -1; @@ -188,7 +188,7 @@ s32 osEepromLongWrite(UNUSED OSMesgQueue *mq, u8 fileIndex, u8 address, u8 *buff } char filePath[256]; - save_file_get_dir(fileIndex, filePath, 256); + save_file_get_dir(fileIndex, filePath, 256, NULL); FILE *fp = fopen(fs_get_write_path(filePath), "wb"); if (fp == NULL) { return -1; From e7f06691a548a6b9aa272d045308783a1166306e Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Mon, 2 Mar 2026 21:57:24 -0600 Subject: [PATCH 06/14] syncing (should) work now --- src/pc/network/network.c | 2 + src/pc/network/packets/packet.c | 2 + src/pc/network/packets/packet.h | 12 ++- src/pc/network/packets/packet_download.c | 2 +- .../packets/packet_download_save_files.c | 97 +++++++++++++++++++ src/pc/network/packets/packet_reliable.c | 2 + 6 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/pc/network/packets/packet_download_save_files.c diff --git a/src/pc/network/network.c b/src/pc/network/network.c index 6bd1d4882..31f880991 100644 --- a/src/pc/network/network.c +++ b/src/pc/network/network.c @@ -239,6 +239,8 @@ bool network_allow_unknown_local_index(enum PacketType packetType) { || (packetType == PACKET_MOD_LIST_ENTRY) || (packetType == PACKET_MOD_LIST_FILE) || (packetType == PACKET_MOD_LIST_DONE) + || (packetType == PACKET_DOWNLOAD_SAVE_REQUEST) + || (packetType == PACKET_DOWNLOAD_SAVE_FILE) || (packetType == PACKET_DOWNLOAD_REQUEST) || (packetType == PACKET_DOWNLOAD) || (packetType == PACKET_KEEP_ALIVE) diff --git a/src/pc/network/packets/packet.c b/src/pc/network/packets/packet.c index 199a88e5a..67dd43fa6 100644 --- a/src/pc/network/packets/packet.c +++ b/src/pc/network/packets/packet.c @@ -95,6 +95,8 @@ void packet_process(struct Packet* p) { case PACKET_MODERATOR: network_receive_moderator(p); break; case PACKET_KEEP_ALIVE: network_receive_keep_alive(p); break; case PACKET_LEAVING: network_receive_leaving(p); break; + case PACKET_DOWNLOAD_SAVE_REQUEST: network_receive_download_saves_request(p); break; + case PACKET_DOWNLOAD_SAVE_FILE: network_receive_download_save(p); break; case PACKET_SAVE_FILE: network_receive_save_file(p); break; case PACKET_SAVE_SET_FLAG: network_receive_save_set_flag(p); break; case PACKET_SAVE_REMOVE_FLAG: network_receive_save_remove_flag(p); break; diff --git a/src/pc/network/packets/packet.h b/src/pc/network/packets/packet.h index 5ab859760..48290584f 100644 --- a/src/pc/network/packets/packet.h +++ b/src/pc/network/packets/packet.h @@ -31,6 +31,9 @@ enum PacketType { PACKET_KICK, PACKET_KEEP_ALIVE, PACKET_LEAVING, + PACKET_DOWNLOAD_SAVE_REQUEST, + PACKET_DOWNLOAD_SAVE_FILE, + PACKET_FINISHED_DOWNLOAD_SAVE_FILE, PACKET_SAVE_FILE, PACKET_SAVE_SET_FLAG, PACKET_SAVE_REMOVE_FLAG, @@ -74,7 +77,7 @@ enum PacketType { PACKET_LUA_CUSTOM, PACKET_LUA_CUSTOM_BYTESTRING, - + PACKET_COMMAND, PACKET_MODERATOR, @@ -256,6 +259,12 @@ void network_receive_keep_alive(struct Packet* p); void network_send_leaving(u8 globalIndex); void network_receive_leaving(struct Packet* p); +// packet_download_save_files.c +void network_send_download_save_files_request(void); +void network_receive_download_saves_request(UNUSED struct Packet* p); +void network_send_download_save(int chunk); +void network_receive_download_save(struct Packet* p); + // packet_save_file.c void network_send_save_file(s32 fileIndex); void network_receive_save_file(struct Packet* p); @@ -308,7 +317,6 @@ void network_send_area_request(struct NetworkPlayer* fromNp, struct NetworkPlaye void network_receive_area_request(struct Packet* p); // packet_area.c - void area_remove_sync_ids_add(u32 syncId); void area_remove_sync_ids_clear(void); void network_send_area(struct NetworkPlayer* toNp); diff --git a/src/pc/network/packets/packet_download.c b/src/pc/network/packets/packet_download.c index ab5aa4917..b10ab9606 100644 --- a/src/pc/network/packets/packet_download.c +++ b/src/pc/network/packets/packet_download.c @@ -199,7 +199,7 @@ static void network_update_offset_groups(void) { mod->enabled = true; } LOG_INFO("Download complete!"); - network_send_join_request(); + network_send_download_save_files_request(); return; } diff --git a/src/pc/network/packets/packet_download_save_files.c b/src/pc/network/packets/packet_download_save_files.c new file mode 100644 index 000000000..1dafb1383 --- /dev/null +++ b/src/pc/network/packets/packet_download_save_files.c @@ -0,0 +1,97 @@ +#include +#include "../network.h" +#include "PR/os_eeprom.h" +#include "game/save_file.h" +#include "pc/debuglog.h" + +extern u8* gOverrideEeprom; +static u8 eeprom[NUM_SAVE_FILES][EEPROM_SIZE] = { 0 }; +static int filledEepromData = 0; +static int chunks = (NUM_SAVE_FILES * EEPROM_SIZE + (PACKET_LENGTH - 8) - 1) / (PACKET_LENGTH - 8); + +void network_send_download_save_files_request(void) { + SOFT_ASSERT(gNetworkType == NT_CLIENT); + + filledEepromData = 0; + + struct Packet p = { 0 }; + packet_init(&p, PACKET_DOWNLOAD_SAVE_REQUEST, true, PLMT_NONE); + + network_send_to((gNetworkPlayerServer != NULL) ? gNetworkPlayerServer->localIndex : 0, &p); + LOG_INFO("sending download save files request"); +} + +void network_receive_download_saves_request(UNUSED struct Packet* p) { + SOFT_ASSERT(gNetworkType == NT_SERVER); + + for (int i = 0; i < chunks; i++) { + network_send_download_save(i); + } + + LOG_INFO("sending save info"); +} + +void network_send_download_save(int chunk) { + SOFT_ASSERT(gNetworkType == NT_SERVER); + + int startingSaveFile = chunk * (NUM_SAVE_FILES + chunks - 1) / chunks; + int endSaveFile = (chunk + 1) * (NUM_SAVE_FILES + chunks - 1) / chunks; + if (endSaveFile > NUM_SAVE_FILES) endSaveFile = NUM_SAVE_FILES; + + struct Packet p = { 0 }; + packet_init(&p, PACKET_DOWNLOAD_SAVE_FILE, true, PLMT_NONE); + packet_write(&p, &startingSaveFile, sizeof(startingSaveFile)); + packet_write(&p, &endSaveFile, sizeof(endSaveFile)); + + for (int i = startingSaveFile; i < endSaveFile; i++) { + u8 content[EEPROM_SIZE] = { 0 }; + char filePath[256]; + save_file_get_dir(i, filePath, 256, NULL); + fs_file_t* fp = fs_open(filePath); + if (fp != NULL) { + fs_read(fp, content, EEPROM_SIZE); + fs_close(fp); + } + packet_write(&p, content, sizeof(content)); + } + + network_send_to(0, &p); +} + +void network_receive_download_save(struct Packet* p) { + SOFT_ASSERT(gNetworkType == NT_CLIENT); + + if (p->localIndex != UNKNOWN_LOCAL_INDEX) { + if (gNetworkPlayerServer == NULL || gNetworkPlayerServer->localIndex != p->localIndex) { + LOG_ERROR("Received download from known local index '%d'", p->localIndex); + return; + } + } + + if (filledEepromData >= NUM_SAVE_FILES * EEPROM_SIZE) { + LOG_ERROR("Received eeprom data after eeprom was filled"); + djui_popup_create(DLANG(NOTIF, DISCONNECT_CLOSED), 1); + network_shutdown(false, false, false, false); + return; + } + + int startingSaveFile = 0; + int endSaveFile = 0; + packet_read(p, &startingSaveFile, sizeof(startingSaveFile)); + packet_read(p, &endSaveFile, sizeof(endSaveFile)); + + for (int i = startingSaveFile; i < endSaveFile; i++) { + packet_read(p, &eeprom[i], sizeof(eeprom[i])); + filledEepromData += EEPROM_SIZE; + } + + if (filledEepromData == NUM_SAVE_FILES * EEPROM_SIZE) { + filledEepromData = 0; + network_send_join_request(); + } else if (filledEepromData > NUM_SAVE_FILES * EEPROM_SIZE) { + LOG_ERROR("Filled eeprom data too much! Should be %d, but is %d", NUM_SAVE_FILES * EEPROM_SIZE, filledEepromData); + djui_popup_create(DLANG(NOTIF, DISCONNECT_CLOSED), 1); + network_shutdown(false, false, false, false); + return; + } +} diff --git a/src/pc/network/packets/packet_reliable.c b/src/pc/network/packets/packet_reliable.c index a1176a311..1486b1165 100644 --- a/src/pc/network/packets/packet_reliable.c +++ b/src/pc/network/packets/packet_reliable.c @@ -130,6 +130,8 @@ void network_remember_reliable(struct Packet* p) { static float adjust_max_elapsed(enum PacketType packetType, float maxElapsed) { switch (packetType) { + case PACKET_DOWNLOAD_SAVE_REQUEST: + case PACKET_DOWNLOAD_SAVE_FILE: case PACKET_DOWNLOAD_REQUEST: case PACKET_DOWNLOAD: case PACKET_MOD_LIST_REQUEST: From 1a44cf70534a8f2421664224de4788dcc4b82176 Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:17:58 -0600 Subject: [PATCH 07/14] modify `gOverrideEeprom` --- src/pc/network/network.c | 9 +++++---- .../packets/packet_download_save_files.c | 5 ++++- src/pc/network/packets/packet_join.c | 19 +------------------ src/pc/ultra_reimplementation.c | 15 +++++---------- 4 files changed, 15 insertions(+), 33 deletions(-) diff --git a/src/pc/network/network.c b/src/pc/network/network.c index 31f880991..c4b581d27 100644 --- a/src/pc/network/network.c +++ b/src/pc/network/network.c @@ -34,6 +34,7 @@ #include "game/first_person_cam.h" #include "game/envfx_snow.h" #include "game/mario.h" +#include "game/save_file.h" #include "engine/math_util.h" #include "engine/lighting_engine.h" #include "src/audio/load.h" @@ -169,8 +170,8 @@ bool network_init(enum NetworkType inNetworkType, bool reconnecting) { dynos_behavior_hook_all_custom_behaviors(); network_player_connected(NPT_LOCAL, 0, configPlayerModel, &configPlayerPalette, configPlayerName, get_local_discord_id()); - extern u8* gOverrideEeprom; - gOverrideEeprom = NULL; + extern u8* gOverrideEeprom[NUM_SAVE_FILES]; + memset(gOverrideEeprom, 0, sizeof(gOverrideEeprom)); if (gCurrLevelNum != (s16)gLevelValues.entryLevel) { extern s16 gChangeLevelTransition; @@ -705,8 +706,8 @@ void network_shutdown(bool sendLeaving, bool exiting, bool popup, bool reconnect dynos_model_clear_pool(MODEL_POOL_SESSION); // reset other stuff - extern u8* gOverrideEeprom; - gOverrideEeprom = NULL; + extern u8* gOverrideEeprom[NUM_SAVE_FILES]; + memset(gOverrideEeprom, 0, sizeof(gOverrideEeprom)); extern u8 gOverrideFreezeCamera; gOverrideFreezeCamera = false; gDjuiHudLockMouse = false; diff --git a/src/pc/network/packets/packet_download_save_files.c b/src/pc/network/packets/packet_download_save_files.c index 1dafb1383..9f9d738db 100644 --- a/src/pc/network/packets/packet_download_save_files.c +++ b/src/pc/network/packets/packet_download_save_files.c @@ -4,7 +4,7 @@ #include "game/save_file.h" #include "pc/debuglog.h" -extern u8* gOverrideEeprom; +extern u8* gOverrideEeprom[NUM_SAVE_FILES]; static u8 eeprom[NUM_SAVE_FILES][EEPROM_SIZE] = { 0 }; static int filledEepromData = 0; static int chunks = (NUM_SAVE_FILES * EEPROM_SIZE + (PACKET_LENGTH - 8) - 1) / (PACKET_LENGTH - 8); @@ -86,6 +86,9 @@ void network_receive_download_save(struct Packet* p) { } if (filledEepromData == NUM_SAVE_FILES * EEPROM_SIZE) { + for (int i = 0; i < NUM_SAVE_FILES; i++) { + gOverrideEeprom[i] = eeprom[i]; + } filledEepromData = 0; network_send_join_request(); } else if (filledEepromData > NUM_SAVE_FILES * EEPROM_SIZE) { diff --git a/src/pc/network/packets/packet_join.c b/src/pc/network/packets/packet_join.c index 8895e9af3..3cf5700c7 100644 --- a/src/pc/network/packets/packet_join.c +++ b/src/pc/network/packets/packet_join.c @@ -27,10 +27,7 @@ #include "pc/configfile.h" #include "pc/lua/utils/smlua_misc_utils.h" -extern u8* gOverrideEeprom; -static u8 eeprom[EEPROM_SIZE] = { 0 }; - -static u8 sJoinRequestPlayerModel; +static u8 sJoinRequestPlayerModel; static struct PlayerPalette sJoinRequestPlayerPalette; static char sJoinRequestPlayerName[MAX_CONFIG_STRING]; static char sJoinRequestDiscordId[64]; @@ -40,7 +37,6 @@ void network_send_join_request(void) { SOFT_ASSERT(gNetworkType == NT_CLIENT); gNetworkSentJoin = true; - gOverrideEeprom = eeprom; struct Packet p = { 0 }; packet_init(&p, PACKET_JOIN_REQUEST, true, PLMT_NONE); @@ -99,15 +95,6 @@ void network_send_join(struct Packet* joinRequestPacket) { // do connection event network_player_connected(NPT_CLIENT, globalIndex, sJoinRequestPlayerModel, &sJoinRequestPlayerPalette, sJoinRequestPlayerName, sJoinRequestDiscordId); - - char filePath[256]; - save_file_get_dir(gCurrSaveFileNum - 1, filePath, 256, NULL); - fs_file_t* fp = fs_open(filePath); - if (fp != NULL) { - fs_read(fp, eeprom, EEPROM_SIZE); - fs_close(fp); - } - char version[MAX_VERSION_LENGTH] = { 0 }; snprintf(version, MAX_VERSION_LENGTH, "%s", get_version()); LOG_INFO("sending version: %s", version); @@ -128,7 +115,6 @@ void network_send_join(struct Packet* joinRequestPacket) { packet_write(&p, &gServerSettings.maxPlayers, sizeof(u8)); packet_write(&p, &gServerSettings.pauseAnywhere, sizeof(u8)); packet_write(&p, &gServerSettings.pvpType, sizeof(u8)); - packet_write(&p, eeprom, sizeof(u8) * EEPROM_SIZE); network_send_to(globalIndex, &p); LOG_INFO("sending join packet"); @@ -142,8 +128,6 @@ void network_receive_join(struct Packet* p) { LOG_INFO("received join packet"); gCurrentlyJoining = true; - gOverrideEeprom = eeprom; - char version[MAX_VERSION_LENGTH] = { 0 }; snprintf(version, MAX_VERSION_LENGTH, "%s", get_version()); LOG_INFO("client has version: %s", version); @@ -181,7 +165,6 @@ void network_receive_join(struct Packet* p) { packet_read(p, &gServerSettings.maxPlayers, sizeof(u8)); packet_read(p, &gServerSettings.pauseAnywhere, sizeof(u8)); packet_read(p, &gServerSettings.pvpType, sizeof(u8)); - packet_read(p, eeprom, sizeof(u8) * EEPROM_SIZE); network_player_connected(NPT_SERVER, 0, 0, &DEFAULT_MARIO_PALETTE, "Player", "0"); network_player_connected(NPT_LOCAL, myGlobalIndex, configPlayerModel, &configPlayerPalette, configPlayerName, get_local_discord_id()); diff --git a/src/pc/ultra_reimplementation.c b/src/pc/ultra_reimplementation.c index 9db65fef0..dccc55cd6 100644 --- a/src/pc/ultra_reimplementation.c +++ b/src/pc/ultra_reimplementation.c @@ -6,7 +6,7 @@ #include "fs/fs.h" #include "game/save_file.h" -u8* gOverrideEeprom = NULL; +u8* gOverrideEeprom[NUM_SAVE_FILES] = { NULL }; extern OSMgrArgs piMgrArgs; @@ -126,11 +126,6 @@ s32 osEepromProbe(UNUSED OSMesgQueue *mq) { } s32 osEepromLongReadLegacy(UNUSED OSMesgQueue *mq, u8 address, u8 *buffer, int nbytes) { - if (gOverrideEeprom != NULL) { - memcpy(buffer, gOverrideEeprom + address * 8, nbytes); - return 0; - } - u8 content[512]; s32 ret = -1; @@ -148,8 +143,8 @@ s32 osEepromLongReadLegacy(UNUSED OSMesgQueue *mq, u8 address, u8 *buffer, int n } s32 osEepromLongRead(UNUSED OSMesgQueue *mq, u8 fileIndex, u8 address, u8 *buffer, int nbytes) { - if (gOverrideEeprom != NULL) { - memcpy(buffer, gOverrideEeprom + address * 8, nbytes); + if (gOverrideEeprom[fileIndex] != NULL) { + memcpy(buffer, gOverrideEeprom[fileIndex] + address * 8, nbytes); return 0; } @@ -172,8 +167,8 @@ s32 osEepromLongRead(UNUSED OSMesgQueue *mq, u8 fileIndex, u8 address, u8 *buffe } s32 osEepromLongWrite(UNUSED OSMesgQueue *mq, u8 fileIndex, u8 address, u8 *buffer, int nbytes) { - if (gOverrideEeprom != NULL) { - memcpy(gOverrideEeprom + address * 8, buffer, nbytes); + if (gOverrideEeprom[fileIndex] != NULL) { + memcpy(gOverrideEeprom[fileIndex] + address * 8, buffer, nbytes); return 0; } From 93e64a11a2063e2ac7df7f5ba0501dddc6dc6c95 Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:19:31 -0600 Subject: [PATCH 08/14] Add german and portuguese translations --- lang/English.ini | 6 +++--- lang/German.ini | 4 ++++ lang/Portuguese.ini | 6 +++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lang/English.ini b/lang/English.ini index 175ff3822..4a6a98e9b 100644 --- a/lang/English.ini +++ b/lang/English.ini @@ -219,13 +219,13 @@ SAVE_TITLE = "SAVE" ERASE_TITLE = "ERASE" CONFIRM = "Are you sure you want to erase this save slot?" ERASE = "Erase" +EDIT = "Edit" +EDIT_TITLE = "EDIT" +EDIT_NAME = "Edit Save File @'s Name:" CREATE_SAVE = "Create Save" CREATE = "Create" CREATE_TITLE = "CREATE" CREATE_NAME = "Set Save File @'s Name:" -EDIT = "Edit" -EDIT_TITLE = "EDIT" -EDIT_NAME = "Edit Save File @'s Name:" [HOST_SETTINGS] SETTINGS = "SETTINGS" diff --git a/lang/German.ini b/lang/German.ini index 46d0bf440..eb3ddd644 100644 --- a/lang/German.ini +++ b/lang/German.ini @@ -222,6 +222,10 @@ ERASE = "Löschen" EDIT = "Bearbeiten" EDIT_TITLE = "BEARBEITEN" EDIT_NAME = "Bearbeite den Namen des Spielstandes @:" +CREATE_SAVE = "Spielstand erstellen" +CREATE = "Erstellen" +CREATE_TITLE = "ERSTELLEN" +CREATE_NAME = "Name von Spielstand @ festlegen:" [HOST_SETTINGS] SETTINGS = "EINSTELLUNGEN" diff --git a/lang/Portuguese.ini b/lang/Portuguese.ini index e935499a6..4035e9467 100644 --- a/lang/Portuguese.ini +++ b/lang/Portuguese.ini @@ -222,6 +222,10 @@ EDIT_TITLE = "EDITAR ARQUIVO" EDIT_NAME = "Editar nome do arquivo salvo @:" CONFIRM = "Quer mesmo limpar este arquivo?" ERASE = "Limpar" +CREATE_SAVE = "Criar Arquivo" +CREATE = "Criar" +CREATE_TITLE = "CRIAR" +CREATE_NAME = "Nome do Arquivo @:" [HOST_SETTINGS] SETTINGS = "CONFIGURAÇÕES" @@ -412,7 +416,7 @@ MUTE_FOCUS_LOSS = "Silenciar quando a janela estiver desfocada" [LANGUAGE] LANGUAGE = "IDIOMA" -Czech = "Tcheco (Čeština)" +Czech = "Tcheco (Čeština)" Dutch = "Holandês (Nederlands)" French = "Francês (Français)" German = "Alemão (Deutsch)" From a63398ccbf6ac9308002f42de63a1fcb1f9e8513 Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:28:32 -0600 Subject: [PATCH 09/14] Allow save files to be imported via drag and drop --- autogen/convert_functions.py | 2 +- include/PR/os_eeprom.h | 4 +-- src/game/save_file.c | 39 +++++++++++++++++++++++----- src/game/save_file.h | 9 ++++++- src/pc/configfile.c | 10 +++++++ src/pc/mods/mod_import.c | 46 +++++++++++++++++++++++++++++++++ src/pc/ultra_reimplementation.c | 16 ++++++------ 7 files changed, 108 insertions(+), 18 deletions(-) diff --git a/autogen/convert_functions.py b/autogen/convert_functions.py index 1168aecd2..f9cfedb77 100644 --- a/autogen/convert_functions.py +++ b/autogen/convert_functions.py @@ -119,7 +119,7 @@ override_disallowed_functions = { "src/pc/djui/djui_console.h": [ " djui_console_create", "djui_console_message_create", "djui_console_message_dequeue" ], "src/pc/djui/djui_chat_message.h": [ "create_from" ], "src/game/interaction.h": [ "process_interaction", "_handle_" ], - "src/game/save_file.h": [ "save_file_get_all_filenames", "save_file_get_dir", "save_file_get_first_available_index" ], + "src/game/save_file.h": [ "save_file_get_all_filenames", "save_file_get_dir", "save_file_get_first_available_index", "save_file_get_amount_of_available_indexes", "save_file_get_first_active_index" ], "src/game/sound_init.h": [ "_loop_", "thread4_", "set_sound_mode" ], "src/pc/network/network_utils.h": [ "network_get_player_text_color[^_]" ], "src/pc/network/network_player.h": [ "_init", "_connected[^_]", "_shutdown", "_disconnected", "_update", "construct_player_popup", "network_player_name_valid" ], diff --git a/include/PR/os_eeprom.h b/include/PR/os_eeprom.h index 75cdfb0c2..95b1c17dc 100644 --- a/include/PR/os_eeprom.h +++ b/include/PR/os_eeprom.h @@ -94,8 +94,8 @@ extern "C" { extern s32 osEepromProbe(OSMesgQueue *); extern s32 osEepromRead(OSMesgQueue *, u8, u8 *); extern s32 osEepromWrite(OSMesgQueue *, u8, u8 *); -extern s32 osEepromLongReadLegacy(OSMesgQueue *, u8 , u8 *, int); -extern s32 osEepromLongRead(OSMesgQueue *, u8, u8, u8 *, int); +extern s32 osEepromLongRead(OSMesgQueue *, u8 , u8 *, int, char *, size_t); +extern s32 osEepromLongReadFile(OSMesgQueue *, u8, u8, u8 *, int); extern s32 osEepromLongWrite(OSMesgQueue *, u8, u8, u8 *, int); diff --git a/src/game/save_file.c b/src/game/save_file.c index eafc0a458..a705f42fe 100644 --- a/src/game/save_file.c +++ b/src/game/save_file.c @@ -123,9 +123,9 @@ static inline void bswap_savefile(struct SaveFile *data) { * Read from EEPROM to a given address. * The EEPROM address is computed using the offset of the destination address from gSaveBuffer. * Try at most 4 times, and return 0 on success. On failure, return the status returned from - * osEepromLongRead. It also returns 0 if EEPROM isn't loaded correctly in the system. + * osEepromLongReadFile. It also returns 0 if EEPROM isn't loaded correctly in the system. */ -static s32 read_eeprom_data(u8 file, void *buffer, s32 size) { +s32 read_eeprom_data(u8 file, void *buffer, s32 size) { s32 status = 0; if (gEepromProbe != 0) { @@ -135,7 +135,7 @@ static s32 read_eeprom_data(u8 file, void *buffer, s32 size) { do { block_until_rumble_pak_free(); triesLeft--; - status = osEepromLongRead(&gSIEventMesgQueue, file, offset, buffer, size); + status = osEepromLongReadFile(&gSIEventMesgQueue, file, offset, buffer, size); release_rumble_pak_control(); } while (triesLeft > 0 && status != 0); } @@ -149,7 +149,7 @@ static s32 read_eeprom_data(u8 file, void *buffer, s32 size) { * Try at most 4 times, and return 0 on success. On failure, return the status returned from * osEepromLongWrite. Unlike read_eeprom_data, return 1 if EEPROM isn't loaded. */ -static s32 write_eeprom_data(u8 file, void *buffer, s32 size, const uintptr_t baseofs) { +s32 write_eeprom_data(u8 file, void *buffer, s32 size, const uintptr_t baseofs) { s32 status = 1; if (gEepromProbe != 0) { @@ -276,7 +276,7 @@ static void save_file_bswap(struct SaveBuffer *buf) { */ static void save_file_convert_old_to_new() { struct LegacySaveBuffer saveBuffer = { 0 }; - s32 status = osEepromLongReadLegacy(&gSIEventMesgQueue, 0, (void*)&saveBuffer, sizeof(saveBuffer)); + s32 status = osEepromLongRead(&gSIEventMesgQueue, 0, (void*)&saveBuffer, sizeof(saveBuffer), (char*)fs_get_write_path(SAVE_FILENAME), 512); if (status == 0) { for (int i = 0; i < 4; i++) write_eeprom_data(i, saveBuffer.files[i], sizeof(saveBuffer.files[i]), 0); @@ -346,7 +346,7 @@ void save_file_get_dir(int fileIndex, char* outPath, size_t size, char* override } /** - * Gets the first available index that is not being used + * Gets the first available index that is not being used. Returns the number of save files on failure */ s32 save_file_get_first_available_index() { if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return 0; @@ -358,6 +358,33 @@ s32 save_file_get_first_available_index() { return NUM_SAVE_FILES; } +/** + * Gets the first available index that is active. Returns 0 on failure +*/ +s32 save_file_get_first_active_index() { + if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return 0; + for (int i = 0; i < NUM_SAVE_FILES; i++) { + char filePath[256]; + save_file_get_dir(i, filePath, 256, NULL); + if (fs_sys_file_exists(fs_get_write_path(filePath))) return i; + } + return 0; +} + +/** + * Gets the amount of available indexes +*/ +s32 save_file_get_amount_of_available_indexes() { + if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return 0; + int count = 0; + for (int i = 0; i < NUM_SAVE_FILES; i++) { + char filePath[256]; + save_file_get_dir(i, filePath, 256, NULL); + if (!fs_sys_file_exists(fs_get_write_path(filePath))) count++; + } + return count; +} + void save_file_do_save(s32 fileIndex, s8 forceSave) { if (INVALID_FILE_INDEX(fileIndex)) { return; } if (gNetworkType != NT_SERVER) { diff --git a/src/game/save_file.h b/src/game/save_file.h index ac9d9105a..0e89c5854 100644 --- a/src/game/save_file.h +++ b/src/game/save_file.h @@ -144,6 +144,9 @@ extern struct WarpCheckpoint gWarpCheckpoint; extern s8 gMainMenuDataModified; extern s8 gSaveFileModified; +s32 read_eeprom_data(u8 file, void *buffer, s32 size); +s32 write_eeprom_data(u8 file, void *buffer, s32 size, const uintptr_t baseofs); + /* |description|Gets the course number's corresponding level number|descriptionEnd| */ s8 get_level_num_from_course_num(s16 courseNum); @@ -158,6 +161,10 @@ void save_file_get_dir(int fileIndex, char* outPath, size_t size, char* override s32 save_file_get_first_available_index(); +s32 save_file_get_amount_of_available_indexes(); + +s32 save_file_get_first_active_index(); + /* |description| Saves the current state of the game into a specified save file. Includes data verification and backup management. Useful for maintaining game progress during play or when saving manually @@ -176,7 +183,7 @@ Erases the backup data for the current save file without affecting the primary s void save_file_erase_current_backup_save(void); BAD_RETURN(s32) save_file_copy(s32 srcFileIndex, s32 destFileIndex); -void save_file_load_all(u8 reload); +void save_file_load_all(UNUSED u8 reload); /* |description| Reloads the save file data into memory, optionally resetting all save files. Marks the save file as modified. diff --git a/src/pc/configfile.c b/src/pc/configfile.c index f6a8f2891..85c11010b 100644 --- a/src/pc/configfile.c +++ b/src/pc/configfile.c @@ -21,6 +21,7 @@ #include "djui/djui_hud_utils.h" #include "pc/network/network_player.h" #include "pc/pc_main.h" +#include "game/save_file.h" #define ARRAY_LEN(arr) (sizeof(arr) / sizeof(arr[0])) @@ -757,6 +758,15 @@ NEXT_OPTION: if (configDjuiTheme >= DJUI_THEME_MAX) { configDjuiTheme = 0; } if (configDjuiScale >= 5) { configDjuiScale = 0; } + if (configHostSaveSlot >= NUM_SAVE_FILES) { + configHostSaveSlot = save_file_get_first_active_index() + 1; + } else { + char filePath[256]; + save_file_get_dir(configHostSaveSlot - 1, filePath, 256, NULL); + if (!fs_sys_file_exists(fs_get_write_path(filePath))) + configHostSaveSlot = save_file_get_first_active_index() + 1; + } + if (gCLIOpts.fullscreen == 1) { configWindow.fullscreen = true; } else if (gCLIOpts.fullscreen == 2) { diff --git a/src/pc/mods/mod_import.c b/src/pc/mods/mod_import.c index ad9de35c7..fb9417f1e 100644 --- a/src/pc/mods/mod_import.c +++ b/src/pc/mods/mod_import.c @@ -8,6 +8,8 @@ #include "pc/djui/djui_popup.h" #include "mods.h" #include "mods_utils.h" +#include "game/save_file.h" +#include "buffers/buffers.h" static bool mod_import_lua(char* src) { char dst[SYS_MAX_PATH] = { 0 }; @@ -252,10 +254,48 @@ static bool mod_import_zip(char* path, bool* isLua, bool* isDynos) { return true; } +static bool mod_import_save(char* src) { + // check to see if it's the old format first + struct LegacySaveBuffer legacySaveBuffer = { 0 }; + s32 status = osEepromLongRead(NULL, 0, (void*)&legacySaveBuffer, sizeof(legacySaveBuffer), src, 512); + if (status == 0) { + // old data is a go, write eeprom data + if (save_file_get_amount_of_available_indexes() < 4) { + LOG_ERROR("Ran out of save files slots"); + return false; + } + for (int i = 0; i < 4; i++) { + int file = save_file_get_first_available_index(); + write_eeprom_data(file, legacySaveBuffer.files[i], sizeof(legacySaveBuffer.files[i]), 0); + } + LOG_INFO("Imported save: '%s' into 4 parts", src); + save_file_load_all(TRUE); + return true; + } + + // it's not legacy, try to load it using the new format + int firstIndex = save_file_get_first_available_index(); + if (firstIndex == NUM_SAVE_FILES) { + LOG_ERROR("Ran out of save files slots"); + return false; + } + status = osEepromLongRead(NULL, 0, (void*)&gSaveBuffer.files[firstIndex], sizeof(gSaveBuffer.files[firstIndex]), src, EEPROM_SIZE); + if (status == 0) { + // data is a go, write to eeprom data + write_eeprom_data(firstIndex, gSaveBuffer.files[firstIndex], sizeof(gSaveBuffer.files[firstIndex]), 0); + LOG_INFO("Imported save: '%s'", src); + save_file_load_all(TRUE); + return true; + } + + return false; +} + bool mod_import_file(char* path) { bool isLua = false; bool isDynos = false; bool isPalette = false; + bool isSave = false; bool ret = false; if (gNetworkType != NT_NONE && !path_ends_with(path, ".ini")) { @@ -271,6 +311,9 @@ bool mod_import_file(char* path) { ret = mod_import_palette(path); } else if (path_ends_with(path, ".zip")) { ret = mod_import_zip(path, &isLua, &isDynos); + } else if (path_ends_with(path, SAVE_EXTENSION)) { + isSave = true; + ret = mod_import_save(path); } char msg[SYS_MAX_PATH] = { 0 }; @@ -288,6 +331,9 @@ bool mod_import_file(char* path) { } else if (isPalette) { djui_language_replace(DLANG(NOTIF, IMPORT_PALETTE_SUCCESS), msg, SYS_MAX_PATH, '@', basename); djui_popup_create(msg, 2); + } else if (isSave) { + djui_language_replace(DLANG(NOTIF, IMPORT_SAVE_SUCCESS), msg, SYS_MAX_PATH, '@', basename); + djui_popup_create(msg, 2); } } else { djui_language_replace(DLANG(NOTIF, IMPORT_FAIL), msg, SYS_MAX_PATH, '@', basename); diff --git a/src/pc/ultra_reimplementation.c b/src/pc/ultra_reimplementation.c index dccc55cd6..ec2e1963e 100644 --- a/src/pc/ultra_reimplementation.c +++ b/src/pc/ultra_reimplementation.c @@ -125,24 +125,24 @@ s32 osEepromProbe(UNUSED OSMesgQueue *mq) { return 1; } -s32 osEepromLongReadLegacy(UNUSED OSMesgQueue *mq, u8 address, u8 *buffer, int nbytes) { - u8 content[512]; +s32 osEepromLongRead(UNUSED OSMesgQueue *mq, u8 address, u8 *buffer, int nbytes, char *path, size_t size) { + u8 content[size]; s32 ret = -1; - fs_file_t *fp = fs_open(SAVE_FILENAME); - if (fp == NULL) { + FILE *fp = fopen(path, "rb"); + if (!fp) { return -1; } - if (fs_read(fp, content, 512) == 512) { + if (fread(content, 1, size, fp) == size) { memcpy(buffer, content + address * 8, nbytes); ret = 0; } - fs_close(fp); + fclose(fp); return ret; } -s32 osEepromLongRead(UNUSED OSMesgQueue *mq, u8 fileIndex, u8 address, u8 *buffer, int nbytes) { +s32 osEepromLongReadFile(UNUSED OSMesgQueue *mq, u8 fileIndex, u8 address, u8 *buffer, int nbytes) { if (gOverrideEeprom[fileIndex] != NULL) { memcpy(buffer, gOverrideEeprom[fileIndex] + address * 8, nbytes); return 0; @@ -174,7 +174,7 @@ s32 osEepromLongWrite(UNUSED OSMesgQueue *mq, u8 fileIndex, u8 address, u8 *buff u8 content[EEPROM_SIZE] = { 0 }; if (address != 0 || nbytes != EEPROM_SIZE) { - osEepromLongRead(mq, fileIndex, 0, content, EEPROM_SIZE); + osEepromLongReadFile(mq, fileIndex, 0, content, EEPROM_SIZE); } memcpy(content + address * 8, buffer, nbytes); From b7b7f1288d59bcbe0be3c9c5957ea4e9af4b6f1c Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:30:16 -0600 Subject: [PATCH 10/14] Add english lang --- lang/English.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/lang/English.ini b/lang/English.ini index 4a6a98e9b..ff78d8ffd 100644 --- a/lang/English.ini +++ b/lang/English.ini @@ -18,6 +18,7 @@ DEBUG_FLY = "@ entered debug free-fly mode" IMPORT_MOD_SUCCESS = "\\#a0ffa0\\Imported mod\n\\#dcdcdc\\'@'" IMPORT_DYNOS_SUCCESS = "\\#a0ffa0\\Imported DynOS pack\n\\#dcdcdc\\'@'" IMPORT_PALETTE_SUCCESS = "\\#a0ffa0\\Imported palette preset\n\\#dcdcdc\\'@'" +IMPORT_SAVE_SUCCESS = "\\#a0ffa0\\Imported save\n\\#dcdcdc\\'@'" IMPORT_FAIL = "\\#ffa0a0\\Failed to import\n\\#dcdcdc\\'@'" IMPORT_FAIL_INGAME = "\\#ffa0a0\\Can not import while in-game" COOPNET_CONNECTION_FAILED = "\\#ffa0a0\\Could not connect to CoopNet!" From 74fef4cfc169bab70c520c10a24c2f8faf2b7ff3 Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:07:03 -0600 Subject: [PATCH 11/14] Finish german and portuguese translations --- lang/German.ini | 1 + lang/Portuguese.ini | 1 + 2 files changed, 2 insertions(+) diff --git a/lang/German.ini b/lang/German.ini index eb3ddd644..03a093e4d 100644 --- a/lang/German.ini +++ b/lang/German.ini @@ -18,6 +18,7 @@ DEBUG_FLY = "@ hat den Debug-Free-Fly-Modus aktiviert." IMPORT_MOD_SUCCESS = "\\#a0ffa0\\Mod erfolgreich importiert\n\\#dcdcdc\\'@'" IMPORT_DYNOS_SUCCESS = "\\#a0ffa0\\DynOS-Paket erfolgreich importiert\n\\#dcdcdc\\'@'" IMPORT_PALETTE_SUCCESS = "\\#a0ffa0\\Paletten-Voreinstellung erfolgreich importiert\n\\#dcdcdc\\'@'" +IMPORT_SAVE_SUCCESS = "\\#a0ffa0\\Spielstand importiert:\n\\#dcdcdc\\'@'" IMPORT_FAIL = "\\#ffa0a0\\Importierung fehlgeschlagen\n\\#dcdcdc\\'@'" IMPORT_FAIL_INGAME = "\\#ffa0a0\\Ein Fehler beim Importieren ist aufgetreten." COOPNET_CONNECTION_FAILED = "\\#ffa0a0\\Es konnte keine Verbindung zu CoopNet hergestellt werden!" diff --git a/lang/Portuguese.ini b/lang/Portuguese.ini index 4035e9467..3450c92f5 100644 --- a/lang/Portuguese.ini +++ b/lang/Portuguese.ini @@ -18,6 +18,7 @@ DEBUG_FLY = "@ entrou no modo de voo livre de debug" IMPORT_MOD_SUCCESS = "\\#dcdcdc\\'@'\n\\#a0ffa0\\Mod importado" IMPORT_DYNOS_SUCCESS = "\\#dcdcdc\\'@'\n\\#a0ffa0\\Pacote DynOS importado" IMPORT_PALETTE_SUCCESS = "\\#dcdcdc\\'@'\n\\#a0ffa0\\Paleta importada" +IMPORT_SAVE_SUCCESS = "\\#a0ffa0\\Arquivo importado\n\\#dcdcdc\\'@'" IMPORT_FAIL = "\\#dcdcdc\\'@'\n\\#ffa0a0\\Erro ao importar" IMPORT_FAIL_INGAME = "\\#ffa0a0\\Não é possível importar\ndurante o jogo" COOPNET_CONNECTION_FAILED = "\\#ffa0a0\\Não foi possível se conectar à CoopNet!" From 422c0bc1dc64188460b379e8d4ddbd5e1f9b5303 Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:40:07 -0500 Subject: [PATCH 12/14] Add russian translation --- lang/Russian.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lang/Russian.ini b/lang/Russian.ini index 52861a4cf..34e698d81 100644 --- a/lang/Russian.ini +++ b/lang/Russian.ini @@ -18,6 +18,7 @@ DEBUG_FLY = "@ вошел в режим полета при отладке" IMPORT_MOD_SUCCESS = "\\#a0ffa0\\Импортирован мод\n\\#dcdcdc\\'@'" IMPORT_DYNOS_SUCCESS = "\\#a0ffa0\\Импортированнан пакет DynOS\n\\#dcdcdc\\'@'" IMPORT_PALETTE_SUCCESS = "\\#a0ffa0\\Импортированный предустановленный набор палитр\n\\#dcdcdc\\'@'" +IMPORT_SAVE_SUCCESS = "\\#a0ffa0\\Импортировано сохранение\n\\#dcdcdc\\'@'" IMPORT_FAIL = "\\#ffa0a0\\Не удалось импортировать\n\\#dcdcdc\\'@'" IMPORT_FAIL_INGAME = "\\#ffa0a0\\Невозможно импортировать во время игры" COOPNET_CONNECTION_FAILED = "\\#ffa0a0\\Не удалось установить соединение с CoopNet!" @@ -221,6 +222,10 @@ ERASE = "Удалить" EDIT = "Редактировать" EDIT_TITLE = "РЕДАКТИРОВАТЬ" EDIT_NAME = "Редактировать имя файла сохранения @:" +CREATE_SAVE = "Создать сохранение" +CREATE = "Создать" +CREATE_TITLE = "CREATE" +CREATE_NAME = "Изменить название сохранения @:" [HOST_SETTINGS] SETTINGS = "SETTINGS" From 3842b2e2936702fe73ea731bb348c7f851cd5b4c Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:45:24 -0500 Subject: [PATCH 13/14] Some ui changes --- lang/English.ini | 1 + src/pc/djui/djui_panel_host_save.c | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lang/English.ini b/lang/English.ini index ff78d8ffd..5a27d5f5d 100644 --- a/lang/English.ini +++ b/lang/English.ini @@ -223,6 +223,7 @@ ERASE = "Erase" EDIT = "Edit" EDIT_TITLE = "EDIT" EDIT_NAME = "Edit Save File @'s Name:" +APPLY = "Apply" CREATE_SAVE = "Create Save" CREATE = "Create" CREATE_TITLE = "CREATE" diff --git a/src/pc/djui/djui_panel_host_save.c b/src/pc/djui/djui_panel_host_save.c index 7fd0d60bc..fdf0108db 100644 --- a/src/pc/djui/djui_panel_host_save.c +++ b/src/pc/djui/djui_panel_host_save.c @@ -121,11 +121,15 @@ static void djui_panel_edit_create(struct DjuiBase* caller) { djui_interactable_hook_value_change(&sSaveNameInputBox->base, djui_panel_host_save_save_name_change); } - if (!sEditing) - djui_button_create(body, DLANG(HOST_SAVE, CREATE), DJUI_BUTTON_STYLE_NORMAL, djui_panel_create_create); - else - djui_button_create(body, DLANG(HOST_SAVE, EDIT), DJUI_BUTTON_STYLE_NORMAL, djui_panel_edit_save); - djui_button_create(body, DLANG(MENU, BACK), DJUI_BUTTON_STYLE_BACK, djui_panel_menu_back); + struct DjuiRect* rect = djui_rect_container_create(body, 64); + { + djui_button_left_create(&rect->base, DLANG(MENU, BACK), DJUI_BUTTON_STYLE_BACK, djui_panel_menu_back); + if (!sEditing) { + djui_button_right_create(&rect->base, DLANG(HOST_SAVE, CREATE), DJUI_BUTTON_STYLE_NORMAL, djui_panel_create_create); + } else { + djui_button_right_create(&rect->base, DLANG(HOST_SAVE, APPLY), DJUI_BUTTON_STYLE_NORMAL, djui_panel_edit_save); + } + } } djui_panel_add(caller, panel, NULL); From eae34aabe69c2c6bb9f6713f9c008f0619fa48a3 Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:06:46 -0500 Subject: [PATCH 14/14] Transfer old save names to new format --- autogen/convert_functions.py | 2 +- src/game/save_file.c | 58 ++++++++++++++++++++++++++++-- src/game/save_file.h | 4 +++ src/pc/configfile.c | 35 +++++++++++++++++- src/pc/configfile.h | 3 ++ src/pc/djui/djui_panel_host_save.c | 28 +-------------- src/pc/ultra_reimplementation.c | 1 + 7 files changed, 99 insertions(+), 32 deletions(-) diff --git a/autogen/convert_functions.py b/autogen/convert_functions.py index f9cfedb77..98a3c376e 100644 --- a/autogen/convert_functions.py +++ b/autogen/convert_functions.py @@ -119,7 +119,7 @@ override_disallowed_functions = { "src/pc/djui/djui_console.h": [ " djui_console_create", "djui_console_message_create", "djui_console_message_dequeue" ], "src/pc/djui/djui_chat_message.h": [ "create_from" ], "src/game/interaction.h": [ "process_interaction", "_handle_" ], - "src/game/save_file.h": [ "save_file_get_all_filenames", "save_file_get_dir", "save_file_get_first_available_index", "save_file_get_amount_of_available_indexes", "save_file_get_first_active_index" ], + "src/game/save_file.h": [ "save_file_get_all_filenames", "save_file_get_dir", "save_file_get_first_available_index", "save_file_get_amount_of_available_indexes", "save_file_get_first_active_index", "save_file_rename_file" ], "src/game/sound_init.h": [ "_loop_", "thread4_", "set_sound_mode" ], "src/pc/network/network_utils.h": [ "network_get_player_text_color[^_]" ], "src/pc/network/network_player.h": [ "_init", "_connected[^_]", "_shutdown", "_disconnected", "_update", "construct_player_popup", "network_player_name_valid" ], diff --git a/src/game/save_file.c b/src/game/save_file.c index a705f42fe..22a21efe7 100644 --- a/src/game/save_file.c +++ b/src/game/save_file.c @@ -277,9 +277,10 @@ static void save_file_bswap(struct SaveBuffer *buf) { static void save_file_convert_old_to_new() { struct LegacySaveBuffer saveBuffer = { 0 }; s32 status = osEepromLongRead(&gSIEventMesgQueue, 0, (void*)&saveBuffer, sizeof(saveBuffer), (char*)fs_get_write_path(SAVE_FILENAME), 512); - if (status == 0) { - for (int i = 0; i < 4; i++) - write_eeprom_data(i, saveBuffer.files[i], sizeof(saveBuffer.files[i]), 0); + if (status != 0) return; + for (int i = 0; i < 4; i++) { + write_eeprom_data(i, saveBuffer.files[i], sizeof(saveBuffer.files[i]), 0); + save_file_rename_file(i, configSaveNames[i]); } } @@ -385,6 +386,45 @@ s32 save_file_get_amount_of_available_indexes() { return count; } +/** + * Renames a specific save file. Returns false on failure +*/ +bool save_file_rename_file(s32 fileIndex, char* name) { + if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return false; + if (strstr(name, ".")) return false; + + char filePath[SYS_MAX_PATH]; + save_file_get_dir(fileIndex, filePath, 256, NULL); + char newFilePath[SYS_MAX_PATH]; + save_file_get_dir(fileIndex, newFilePath, 256, name); + + if (strcmp(filePath, newFilePath) == 0) return false; + if (!fs_sys_file_exists(fs_get_write_path(filePath))) return false; + + // write the save data of the file to a variable + u8 content[EEPROM_SIZE] = { 0 }; + fs_file_t* oldFile = fs_open(filePath); + if (oldFile == NULL) return false; + fs_read(oldFile, content, EEPROM_SIZE); + fs_close(oldFile); + + // create a new file with the data + FILE* fp = fopen(fs_get_write_path(newFilePath), "wb"); + if (fp == NULL) return false; + bool success = fwrite(content, 1, EEPROM_SIZE, fp) == EEPROM_SIZE; + fclose(fp); + if (success) { + // nuke old file! + remove(fs_get_write_path(filePath)); + } else { + // uh oh! new file failed to be written to :( nuke new file!! + remove(fs_get_write_path(newFilePath)); + } +} + +/** + * Saves file data to the disk +*/ void save_file_do_save(s32 fileIndex, s8 forceSave) { if (INVALID_FILE_INDEX(fileIndex)) { return; } if (gNetworkType != NT_SERVER) { @@ -411,6 +451,9 @@ void save_file_do_save(s32 fileIndex, s8 forceSave) { } } +/** + * Erases save file ingame and from the disk +*/ void save_file_erase(s32 fileIndex) { if (INVALID_FILE_INDEX(fileIndex)) { return; } @@ -424,6 +467,9 @@ void save_file_erase(s32 fileIndex) { remove(fs_get_write_path(filepath)); } +/** + * Reloads save file from the disk and updates mario's stars +*/ void save_file_reload(u8 loadAll) { gSaveFileModified = TRUE; update_all_mario_stars(); @@ -435,6 +481,9 @@ void save_file_reload(u8 loadAll) { } } +/** + * Erases the current backup save +*/ void save_file_erase_current_backup_save(void) { if (INVALID_FILE_INDEX(gCurrSaveFileNum-1)) { return; } if (gNetworkType != NT_SERVER) { return; } @@ -458,6 +507,9 @@ BAD_RETURN(s32) save_file_copy(s32 srcFileIndex, s32 destFileIndex) { save_file_do_save(destFileIndex, TRUE); } +/** + * Loads save file data from disk +*/ void save_file_load_all(UNUSED u8 reload) { //s32 file; diff --git a/src/game/save_file.h b/src/game/save_file.h index 0e89c5854..8e89f4371 100644 --- a/src/game/save_file.h +++ b/src/game/save_file.h @@ -139,6 +139,8 @@ struct WarpCheckpoint { /*0x04*/ u8 warpNode; }; +struct WarpNode; + extern struct WarpCheckpoint gWarpCheckpoint; extern s8 gMainMenuDataModified; @@ -165,6 +167,8 @@ s32 save_file_get_amount_of_available_indexes(); s32 save_file_get_first_active_index(); +bool save_file_rename_file(s32 fileIndex, char* name); + /* |description| Saves the current state of the game into a specified save file. Includes data verification and backup management. Useful for maintaining game progress during play or when saving manually diff --git a/src/pc/configfile.c b/src/pc/configfile.c index 85c11010b..dada22dae 100644 --- a/src/pc/configfile.c +++ b/src/pc/configfile.c @@ -21,7 +21,6 @@ #include "djui/djui_hud_utils.h" #include "pc/network/network_player.h" #include "pc/pc_main.h" -#include "game/save_file.h" #define ARRAY_LEN(arr) (sizeof(arr) / sizeof(arr[0])) @@ -59,6 +58,14 @@ struct FunctionConfigOption { *Config options and default values */ +// still exists for compatibility reasons +char configSaveNames[4][MAX_SAVE_NAME_STRING] = { + "SM64", + "SM64", + "SM64", + "SM64" +}; + // Video/audio stuff ConfigWindow configWindow = { .x = WAPI_WIN_CENTERPOS, @@ -522,11 +529,37 @@ static void dynos_pack_write(FILE* file) { } } +static void save_name_read(char** tokens, int numTokens) { + if (numTokens < 2) { return; } + char fullSaveName[MAX_SAVE_NAME_STRING] = { 0 }; + int index = 0; + for (int i = 1; i < numTokens; i++) { + if (i == 1) { + index = atoi(tokens[i]); + } else { + if (i > 2) { + strncat(fullSaveName, " ", MAX_SAVE_NAME_STRING - 1); + } + strncat(fullSaveName, tokens[i], MAX_SAVE_NAME_STRING - 1); + } + + } + snprintf(configSaveNames[index], MAX_SAVE_NAME_STRING, "%s", fullSaveName); +} + +static void save_name_write(FILE* file) { + // don't write save name data anymore, this only exists for compat + /*for (int i = 0; i < 4; i++) { + fprintf(file, "%s %d %s\n", "save-name:", i, configSaveNames[i]); + }*/ +} + static const struct FunctionConfigOption functionOptions[] = { { .name = "enable-mod:", .read = enable_mod_read, .write = enable_mod_write }, { .name = "ban:", .read = ban_read, .write = ban_write }, { .name = "moderator:", .read = moderator_read, .write = moderator_write }, { .name = "dynos-pack:", .read = dynos_pack_read, .write = dynos_pack_write }, + { .name = "save-name:", .read = save_name_read, .write = save_name_write } }; // Reads an entire line from a file (excluding the newline character) and returns an allocated string diff --git a/src/pc/configfile.h b/src/pc/configfile.h index c513b0d32..77c26fa13 100644 --- a/src/pc/configfile.h +++ b/src/pc/configfile.h @@ -6,6 +6,7 @@ #include "game/player_palette.h" #include "pc/lua/smlua_autogen.h" #include "game/area.h" +#include "game/save_file.h" #define CONFIGFILE_DEFAULT "sm64config.txt" #define CONFIGFILE_BACKUP "sm64config-backup.txt" @@ -44,6 +45,8 @@ enum RefreshRateMode { RRM_MAX }; +extern char configSaveNames[4][MAX_SAVE_NAME_STRING]; + // display settings extern ConfigWindow configWindow; extern ConfigStick configStick; diff --git a/src/pc/djui/djui_panel_host_save.c b/src/pc/djui/djui_panel_host_save.c index fdf0108db..3de76980e 100644 --- a/src/pc/djui/djui_panel_host_save.c +++ b/src/pc/djui/djui_panel_host_save.c @@ -26,33 +26,7 @@ static void djui_panel_host_reload_saves() { } static void djui_panel_host_save_update_save_name() { - if (!fs_sys_dir_exists(fs_get_write_path(SAVE_DIRECTORY))) return; - if (strstr(sSaveName, ".")) return; - char filePath[256]; - save_file_get_dir(sButtonTag, filePath, 256, NULL); - char newFilePath[256]; - save_file_get_dir(sButtonTag, newFilePath, 256, sSaveName); - if (strcmp(filePath, newFilePath) == 0) return; - if (!fs_sys_file_exists(fs_get_write_path(filePath))) return; - // write the save data of the file to a variable - u8 content[EEPROM_SIZE] = { 0 }; - fs_file_t* oldFile = fs_open(filePath); - if (oldFile == NULL) return; - fs_read(oldFile, content, EEPROM_SIZE); - fs_close(oldFile); - // create a new file with the data - FILE* fp = fopen(fs_get_write_path(newFilePath), "wb"); - if (fp == NULL) return; - bool success = fwrite(content, 1, EEPROM_SIZE, fp) == EEPROM_SIZE; - fclose(fp); - if (success) { - // nuke old file - remove(fs_get_write_path(filePath)); - } else { - // uh oh! New file failed to be written to. Nuke new file - remove(fs_get_write_path(newFilePath)); - } - + save_file_rename_file(sButtonTag, sSaveName); djui_panel_host_reload_saves(); } diff --git a/src/pc/ultra_reimplementation.c b/src/pc/ultra_reimplementation.c index ec2e1963e..c74ae6d2d 100644 --- a/src/pc/ultra_reimplementation.c +++ b/src/pc/ultra_reimplementation.c @@ -4,6 +4,7 @@ #include "macros.h" #include "platform.h" #include "fs/fs.h" +#include "configfile.h" #include "game/save_file.h" u8* gOverrideEeprom[NUM_SAVE_FILES] = { NULL };