#include #include #include #include #include #include #include struct { std::vector save_buffer; std::thread saving_thread; std::filesystem::path save_file_path; moodycamel::LightweightSemaphore write_sempahore; // Used to tell the saving thread that a file swap is pending. moodycamel::LightweightSemaphore swap_file_pending_sempahore; // Used to tell the consumer thread that the saving thread is ready for a file swap. moodycamel::LightweightSemaphore swap_file_ready_sempahore; std::mutex save_buffer_mutex; } save_context; // The current game's save directory within the config path. const std::u8string save_folder = u8"saves"; // The current game's config directory path. extern std::filesystem::path config_path; // The current game's save type. ultramodern::SaveType save_type = ultramodern::SaveType::None; void ultramodern::set_save_type(ultramodern::SaveType type) { save_type = type; } ultramodern::SaveType ultramodern::get_save_type() { return save_type; } bool ultramodern::eeprom_allowed() { return save_type == SaveType::Eep4k || save_type == SaveType::Eep16k || save_type == SaveType::AllowAll; } bool ultramodern::sram_allowed() { return save_type == SaveType::Sram || save_type == SaveType::AllowAll; } bool ultramodern::flashram_allowed() { return save_type == SaveType::Flashram || save_type == SaveType::AllowAll; } std::filesystem::path ultramodern::get_save_file_path() { return save_context.save_file_path; } void ultramodern::set_save_file_path(const std::u8string& subfolder, const std::u8string& name) { std::filesystem::path save_folder_path = config_path / save_folder; if (!subfolder.empty()) { save_folder_path = save_folder_path / subfolder; } save_context.save_file_path = save_folder_path / (name + u8".bin"); } void update_save_file() { bool saving_failed = false; { std::ofstream save_file = ultramodern::open_output_file_with_backup(ultramodern::get_save_file_path(), std::ios_base::binary); if (save_file.good()) { std::lock_guard lock{ save_context.save_buffer_mutex }; save_file.write(save_context.save_buffer.data(), save_context.save_buffer.size()); } else { saving_failed = true; } } if (!saving_failed) { saving_failed = !ultramodern::finalize_output_file_with_backup(ultramodern::get_save_file_path()); } if (saving_failed) { ultramodern::error_handling::message_box("Failed to write to the save file. Check your file permissions and whether the save folder has been moved to Dropbox or similar, as this can cause issues."); } } extern std::atomic_bool exited; void saving_thread_func(RDRAM_ARG1) { while (!exited) { bool save_buffer_updated = false; // Repeatedly wait for a new action to be sent. constexpr int64_t wait_time_microseconds = 10000; constexpr int max_actions = 128; int num_actions = 0; // Wait up to the given timeout for a write to come in. Allow multiple writes to coalesce together into a single save. // Cap the number of coalesced writes to guarantee that the save buffer eventually gets written out to the file even if the game // is constantly sending writes. while (save_context.write_sempahore.wait(wait_time_microseconds) && num_actions < max_actions) { save_buffer_updated = true; num_actions++; } // If an action came through that affected the save file, save the updated contents. if (save_buffer_updated) { update_save_file(); } if (save_context.swap_file_pending_sempahore.tryWait()) { save_context.swap_file_ready_sempahore.signal(); } } } void ultramodern::save_write_ptr(const void* in, uint32_t offset, uint32_t count) { assert(offset + count <= save_context.save_buffer.size()); { std::lock_guard lock { save_context.save_buffer_mutex }; memcpy(&save_context.save_buffer[offset], in, count); } save_context.write_sempahore.signal(); } void ultramodern::save_read_ptr(void *out, uint32_t offset, uint32_t count) { assert(offset + count <= save_context.save_buffer.size()); std::lock_guard lock { save_context.save_buffer_mutex }; std::memcpy(out, &save_context.save_buffer[offset], count); } void ultramodern::save_clear(uint32_t start, uint32_t size, char value) { assert(start + size < save_context.save_buffer.size()); { std::lock_guard lock { save_context.save_buffer_mutex }; std::fill_n(save_context.save_buffer.begin() + start, size, value); } save_context.write_sempahore.signal(); } size_t ultramodern::get_save_size(ultramodern::SaveType save_type) { switch (save_type) { case ultramodern::SaveType::AllowAll: case ultramodern::SaveType::Flashram: return 0x20000; case ultramodern::SaveType::Sram: return 0x8000; case ultramodern::SaveType::Eep16k: return 0x800; case ultramodern::SaveType::Eep4k: return 0x200; case ultramodern::SaveType::None: return 0; } return 0; } void read_save_file() { std::filesystem::path save_file_path = ultramodern::get_save_file_path(); // Ensure the save file directory exists. std::filesystem::create_directories(save_file_path.parent_path()); // Read the save file if it exists. std::ifstream save_file = ultramodern::open_input_file_with_backup(save_file_path, std::ios_base::binary); if (save_file.good()) { save_file.read(save_context.save_buffer.data(), save_context.save_buffer.size()); } else { // Otherwise clear the save file to all zeroes. std::fill(save_context.save_buffer.begin(), save_context.save_buffer.end(), 0); } } void ultramodern::init_saving(RDRAM_ARG const std::u8string& name) { set_save_file_path(u8"", name); save_context.save_buffer.resize(get_save_size(ultramodern::get_save_type())); read_save_file(); save_context.saving_thread = std::thread{saving_thread_func, PASS_RDRAM}; } void ultramodern::change_save_file(const std::u8string& subfolder, const std::u8string& name) { // Tell the saving thread that a file swap is pending. save_context.swap_file_pending_sempahore.signal(); // Wait until the saving thread indicates it's ready to swap files. save_context.swap_file_ready_sempahore.wait(); // Perform the save file swap. set_save_file_path(subfolder, name); read_save_file(); } void ultramodern::join_saving_thread() { if (save_context.saving_thread.joinable()) { save_context.saving_thread.join(); } }