// ===================================================================================== // Copyright (c) 2021-2025 Dave Bernazzani (wavemotion-dave) // // Copying and distribution of this emulator, its source code and associated // readme files, with or without modification, are permitted in any medium without // royalty provided the this copyright notice is used and wavemotion-dave (NINTV-DS) // and Kyle Davis (BLISS) are thanked profusely. // // The NINTV-DS emulator is offered as-is, without any warranty. // ===================================================================================== #include #include #include #include #include #include "nintv-ds.h" #include "savestate.h" #include "bgMenu-Green.h" #include "bgMenu-White.h" #include "config.h" #include "printf.h" extern Emulator *currentEmu; extern Rip *currentRip; extern UINT16 global_frames; #define CURRENT_SAVE_FILE_VER 0x000C // ------------------------------------------------------ // We allow up to 3 saves per game. More than enough. // ------------------------------------------------------ struct { UINT16 saveFileVersion; UINT8 bSlotUsed[3]; char slotTimestamp[3][20]; struct _stateStruct slot[3]; } saveState; // Only for the games that require this... it's larger than all of the other saveState stuff above... JLPState jlpState[3]; SlowRAM16State slowRAM16State[3]; SlowRAM8State slowRAM8State[3]; extern UINT16 gLastBankers[]; char savefilename[128]; // ---------------------------------------------------------------------------------------------------- // Write a new save file to disc. We stamp out the slot that is being written using the NDS timestamp. // ---------------------------------------------------------------------------------------------------- BOOL do_save(const CHAR* filename, UINT8 slot) { BOOL didSave = FALSE; time_t unixTime = time(NULL); struct tm* timeStruct = gmtime((const time_t *)&unixTime); saveState.saveFileVersion = CURRENT_SAVE_FILE_VER; saveState.bSlotUsed[slot] = TRUE; sprintf(saveState.slotTimestamp[slot], "%02d-%02d-%04d %02d:%02d:%02d", timeStruct->tm_mday, timeStruct->tm_mon+1, timeStruct->tm_year+1900, timeStruct->tm_hour, timeStruct->tm_min, timeStruct->tm_sec); // Ask the emulator to save it's state... currentEmu->SaveState(&saveState.slot[slot]); saveState.slot[slot].global_frames = global_frames; saveState.slot[slot].emu_frames = emu_frames; // Save the 16 possible ROM Bankers so we can put the system back to the right state for (UINT8 i=0; i<16; i++) { saveState.slot[slot].lastBankers[i] = gLastBankers[i]; } // Save off the ECS RAM. Most games don't use it, but it's only 2K and we have dedicated space for it... for (int i=0; iJLP16Bit) currentRip->JLP16Bit->getState(&jlpState[slot]); // And even fewer games utilize extra RAM... this significantly increases the size of the save file if (slow_ram16_idx != 0) memcpy(slowRAM16State[slot].image, slow_ram16, 0x4000*sizeof(UINT16)); if (slow_ram8_idx != 0) memcpy(slowRAM8State[slot].image, slow_ram8, 0x1800*sizeof(UINT16)); // Technically we could spill 2K into the "ECS 8-bit" area but that's already preserved above. // Write the entire save states as a single file... overwrite if it exists. FILE* file = fopen(filename, "wb+"); if (file != NULL) { fwrite(&saveState, 1, sizeof(saveState), file); if (currentRip->JLP16Bit) fwrite(&jlpState, 1, sizeof(jlpState), file); // A few gaems utilize the JLP RAM if (slow_ram16_idx != 0) fwrite(slowRAM16State, 1, sizeof(slowRAM16State), file); // A tiny fraction of games need even MORE ram... we have a large "slow" buffer for those... if (slow_ram8_idx != 0) fwrite(slowRAM8State, 1, sizeof(slowRAM8State), file); // A tiny fraction of games need even MORE ram... we have a large "slow" buffer for those... didSave = TRUE; fclose(file); } return didSave; } // ---------------------------------------------------------------------------------- // Load a save state from a slot... caller should check that the slot is populated. // ---------------------------------------------------------------------------------- BOOL do_load(const CHAR* filename, UINT8 slot) { BOOL didLoadState = FALSE; memset(&saveState, 0x00, sizeof(saveState)); FILE* file = fopen(filename, "rb"); if (file != NULL) { int size = fread(&saveState, 1, sizeof(saveState), file); if ((size != sizeof(saveState)) || (saveState.saveFileVersion != CURRENT_SAVE_FILE_VER)) { memset(&saveState, 0x00, sizeof(saveState)); memset(&jlpState, 0x00, sizeof(jlpState)); } else { if (currentRip->JLP16Bit) fread(&jlpState, 1, sizeof(jlpState), file); // A few games utilize the JLP RAM if (slow_ram16_idx != 0) fread(slowRAM16State, 1, sizeof(slowRAM16State), file); // A tiny fraction of games need even MORE ram... we have a large "slow" buffer for those... if (slow_ram8_idx != 0) fread(slowRAM8State, 1, sizeof(slowRAM8State), file); // A tiny fraction of games need even MORE ram... we have a large "slow" buffer for those... // Ask the emulator to restore it's state... currentEmu->LoadState(&saveState.slot[slot]); //Restore ECS RAM. Most games don't use it, but it's only 2K and we have dedicated space for it... for (int i=0; iJLP16Bit) currentRip->JLP16Bit->setState(&jlpState[slot]); if (slow_ram16_idx != 0) memcpy(slow_ram16, slowRAM16State[slot].image, 0x4000*sizeof(UINT16)); if (slow_ram8_idx != 0) memcpy(slow_ram8, slowRAM8State[slot].image, 0x1800*sizeof(UINT16)); // Technically we could spill 2K into the "ECS 8-bit" area but that's already restored above. global_frames = saveState.slot[slot].global_frames; emu_frames = saveState.slot[slot].emu_frames; // We need to run through all the last known banking writes and poke those back into the system for (UINT8 i=0; i<16; i++) { gLastBankers[i] = saveState.slot[slot].lastBankers[i]; if (gLastBankers[i] != 0x0000) { // ---------------------------------------------------------------------------------------------------- // Sometimes poking values back in at xFFF will trigger a write to the last WORD in the GRAM memory // due to aliases. We want to make sure we don't disturb that when we restore state as the original // game programmer may have done this in a very careful way and here we're being very brute-force. // ---------------------------------------------------------------------------------------------------- UINT16 save_gram = gram_image[GRAM_SIZE-1]; currentEmu->memoryBus.poke((i<<12)|0xFFF, gLastBankers[i]); gram_image[GRAM_SIZE-1] = save_gram; } } // Refresh the fast memory now that we've got it all loaded in... currentEmu->RefreshFastMemory(); if (myGlobalConfig.erase_saves) { remove(savefilename); } didLoadState = TRUE; } fclose(file); } return didLoadState; } // ------------------------------------------------------------------------- // Store the save state file - use global config to determine where to // place the .sav file (or, by default, it goes in the same directory // as the game .rom or .bin file that was loaded). // ------------------------------------------------------------------------- void get_save_filename(void) { if (myGlobalConfig.save_dir == 1) { DIR* dir = opendir("/roms/sav"); // See if directory exists if (dir) closedir(dir); // Directory exists. All good. else mkdir("/roms/sav", 0777); // Doesn't exist - make it... strcpy(savefilename, "/roms/sav/"); } else if (myGlobalConfig.save_dir == 2) { DIR* dir = opendir("/roms/intv/sav"); // See if directory exists if (dir) closedir(dir); // Directory exists. All good. else mkdir("/roms/intv/sav", 0777); // Doesn't exist - make it... strcpy(savefilename, "/roms/intv/sav/"); } else if (myGlobalConfig.save_dir == 3) { DIR* dir = opendir("/data/sav"); // See if directory exists if (dir) closedir(dir); // Directory exists. All good. else mkdir("/data/sav", 0777); // Doesn't exist - make it... strcpy(savefilename, "/data/sav/"); } else strcpy(savefilename, ""); strcat(savefilename, currentRip->GetFileName()); savefilename[strlen(savefilename)-4] = 0; strcat(savefilename, ".sav"); } // ----------------------------------------------------------------------------- // Read the save file into memory... we purosely keep this fairly small (less // than 32k) for 2 reasons - it takes up valuable RAM and we would prefer that // this take up less than 1 sector on the SD card (default sector size is 32k). // ----------------------------------------------------------------------------- void just_read_save_file(void) { if (currentRip != NULL) { get_save_filename(); memset(&saveState, 0x00, sizeof(saveState)); FILE* file = fopen(savefilename, "rb"); if (file != NULL) { int size = fread(&saveState, 1, sizeof(saveState), file); fclose(file); if ((size != sizeof(saveState)) || (saveState.saveFileVersion != CURRENT_SAVE_FILE_VER)) { memset(&saveState, 0x00, sizeof(saveState)); } } } } // ----------------------------------------------------------------------------- // Call into all the objects in the system to gather the save state information // and then write that set of data out to the disc... // ----------------------------------------------------------------------------- void state_save(UINT8 slot) { if (currentRip != NULL) { get_save_filename(); dsPrintValue(10,23,0, (char*)"STATE SAVED"); do_save(savefilename, slot); WAITVBL;WAITVBL;WAITVBL; dsPrintValue(10,23,0, (char*)" "); } } // ------------------------------------------------------------------------------ // Pull back state information and call into all of the objects to restore // the saved state and get us back to the save point... // ------------------------------------------------------------------------------ BOOL state_restore(UINT8 slot) { if (currentRip != NULL) { if (saveState.bSlotUsed[slot] == TRUE) { get_save_filename(); dsPrintValue(10,23,0, (char*)"STATE RESTORED"); do_load(savefilename, slot); WAITVBL;WAITVBL;WAITVBL;WAITVBL; dsPrintValue(10,23,0, (char*)" "); return TRUE; } } return FALSE; } // ----------------------------------- // Wipe the save file from disc... // ----------------------------------- void clear_save_file(void) { if (currentRip != NULL) { get_save_filename(); remove(savefilename); memset(&saveState, 0x00, sizeof(saveState)); WAITVBL;WAITVBL;WAITVBL;WAITVBL; dsPrintValue(10,23,0, (char*)" "); } } // ------------------------------------------------------------------ // A mini state machine / menu so the user can pick which of the // three possible save slots to use for save / restore. They can // also erase the save file if they choose... // ------------------------------------------------------------------ #define SAVE_MENU_ITEMS 8 const char *savestate_menu[SAVE_MENU_ITEMS] = { "SAVE TO SLOT 1", "SAVE TO SLOT 2", "SAVE TO SLOT 3", "RESTORE FROM SLOT 1", "RESTORE FROM SLOT 2", "RESTORE FROM SLOT 3", "ERASE SAVE FILE", "EXIT THIS MENU", }; // ------------------------------------------------------------------------ // Show to the user if this slot has any data -- if it has data, we also // show the date and time at which the game was saved... we use the NDS // time/date for the saved timestamp. // ------------------------------------------------------------------------ void show_slot_info(UINT8 slot) { if (slot == 255) { dsPrintValue(8,15, 0, (char*)" "); dsPrintValue(8,16, 0, (char*)" "); } else { if (saveState.bSlotUsed[slot] == FALSE) { dsPrintValue(8,15, 0, (char*)"THIS SLOT IS EMPTY "); dsPrintValue(8,16, 0, (char*)" "); } else { dsPrintValue(8,15, 0, (char*)"THIS SLOT LAST SAVED "); dsPrintValue(8,16, 0, (char*)saveState.slotTimestamp[slot]); } } } // Quick load from slot 1 void quick_load(void) { just_read_save_file(); state_restore(0); } // ------------------------------------------------------------------------ // Show the save/restore menu and let the user pick an option (or exit). // ------------------------------------------------------------------------ void savestate_entry(void) { UINT8 current_entry = 0; char bDone = 0; just_read_save_file(); dsShowBannerScreen(); swiWaitForVBlank(); dsPrintValue(8,3,0, (char*)"SAVE/RESTORE STATE"); dsPrintValue(4,20,0, (char*)"PRESS UP/DOWN AND A=SELECT"); for (int i=0; i 0) current_entry--; else current_entry=(SAVE_MENU_ITEMS-1); dsPrintValue(8,5+current_entry, 1, (char*)savestate_menu[current_entry]); if (current_entry == 0 || current_entry == 3) show_slot_info(0); else if (current_entry == 1 || current_entry == 4) show_slot_info(1); else if (current_entry == 2 || current_entry == 5) show_slot_info(2); else show_slot_info(255); } if (keys_pressed & KEY_A) { switch (current_entry) { case 0: state_save(0); show_slot_info(0); break; case 1: state_save(1); show_slot_info(1); break; case 2: state_save(2); show_slot_info(2); break; case 3: if (state_restore(0)) bDone=1; break; case 4: if (state_restore(1)) bDone=1; break; case 5: if (state_restore(2)) bDone=1; break; case 6: clear_save_file(); show_slot_info(255); break; case 7: bDone=1; break; } } if (keys_pressed & KEY_B) { bDone = 1; } swiWaitForVBlank(); } } } // End of file