diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index fbaf051..80c1b6d 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -158,7 +158,7 @@ jobs: # enable ccache set $env:PATH="$env:USERPROFILE/.cargo/bin;$env:PATH" - cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER=clang-cl -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build -DCMAKE_CXX_FLAGS="-Xclang -fcxx-exceptions" + cmake -DCMAKE_BUILD_TYPE=${{ matrix.type }} -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER=clang-cl -DCMAKE_C_COMPILER=clang-cl -DCMAKE_MAKE_PROGRAM=ninja -G Ninja -S . -B cmake-build -DCMAKE_CXX_FLAGS="-Xclang -fexceptions -Xclang -fcxx-exceptions" cmake --build cmake-build --config ${{ matrix.type }} --target Zelda64Recompiled -j 8 - name: Prepare Archive run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 6262d24..f2be1d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -130,6 +130,7 @@ set (SOURCES ${CMAKE_SOURCE_DIR}/src/recomp/dp.cpp ${CMAKE_SOURCE_DIR}/src/recomp/eep.cpp ${CMAKE_SOURCE_DIR}/src/recomp/euc-jp.cpp + ${CMAKE_SOURCE_DIR}/src/recomp/files.cpp ${CMAKE_SOURCE_DIR}/src/recomp/flash.cpp ${CMAKE_SOURCE_DIR}/src/recomp/math_routines.cpp ${CMAKE_SOURCE_DIR}/src/recomp/overlays.cpp diff --git a/include/recomp_files.h b/include/recomp_files.h new file mode 100644 index 0000000..63e3e9d --- /dev/null +++ b/include/recomp_files.h @@ -0,0 +1,14 @@ +#ifndef __RECOMP_FILES_H__ +#define __RECOMP_FILES_H__ + +#include +#include + +namespace recomp { + std::ifstream open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::in); + std::ifstream open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::in); + std::ofstream open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode = std::ios_base::out); + bool finalize_output_file_with_backup(const std::filesystem::path& filepath); +}; + +#endif diff --git a/src/game/config.cpp b/src/game/config.cpp index 6c1922c..e90e20b 100644 --- a/src/game/config.cpp +++ b/src/game/config.cpp @@ -1,6 +1,7 @@ #include "recomp_config.h" #include "recomp_input.h" #include "recomp_sound.h" +#include "recomp_files.h" #include "../../ultramodern/config.hpp" #include #include @@ -153,9 +154,48 @@ std::filesystem::path recomp::get_app_folder_path() { return recomp_dir; } -void save_general_config(const std::filesystem::path& path) { - std::ofstream config_file{path}; - +bool read_json(std::ifstream input_file, nlohmann::json& json_out) { + if (!input_file.good()) { + return false; + } + + try { + input_file >> json_out; + } + catch (nlohmann::json::parse_error&) { + return false; + } + return true; +} + +bool read_json_with_backups(const std::filesystem::path& path, nlohmann::json& json_out) { + // Try reading and parsing the base file. + if (read_json(std::ifstream{path}, json_out)) { + return true; + } + + // Try reading and parsing the backup file. + if (read_json(recomp::open_input_backup_file(path), json_out)) { + return true; + } + + // Both reads failed. + return false; +} + +bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::json& json_data) { + { + std::ofstream output_file = recomp::open_output_file_with_backup(path); + if (!output_file.good()) { + return false; + } + + output_file << std::setw(4) << json_data; + } + return recomp::finalize_output_file_with_backup(path); +} + +bool save_general_config(const std::filesystem::path& path) { nlohmann::json config_json{}; recomp::to_json(config_json["targeting_mode"], recomp::get_targeting_mode()); @@ -169,7 +209,8 @@ void save_general_config(const std::filesystem::path& path) { config_json["analog_cam_mode"] = recomp::get_analog_cam_mode(); config_json["analog_camera_invert_mode"] = recomp::get_analog_camera_invert_mode(); config_json["debug_mode"] = recomp::get_debug_mode_enabled(); - config_file << std::setw(4) << config_json; + + return save_json_with_backups(path, config_json); } void set_general_settings_from_json(const nlohmann::json& config_json) { @@ -186,13 +227,14 @@ void set_general_settings_from_json(const nlohmann::json& config_json) { recomp::set_debug_mode_enabled(from_or_default(config_json, "debug_mode", false)); } -void load_general_config(const std::filesystem::path& path) { - std::ifstream config_file{path}; +bool load_general_config(const std::filesystem::path& path) { nlohmann::json config_json{}; - - config_file >> config_json; + if (!read_json_with_backups(path, config_json)) { + return false; + } set_general_settings_from_json(config_json); + return true; } void assign_mapping(recomp::InputDevice device, recomp::GameInput input, const std::vector& value) { @@ -263,23 +305,22 @@ void reset_graphics_options() { ultramodern::set_graphics_config(new_config); } -void save_graphics_config(const std::filesystem::path& path) { - std::ofstream config_file{path}; - +bool save_graphics_config(const std::filesystem::path& path) { nlohmann::json config_json{}; ultramodern::to_json(config_json, ultramodern::get_graphics_config()); - config_file << std::setw(4) << config_json; + return save_json_with_backups(path, config_json); } -void load_graphics_config(const std::filesystem::path& path) { - std::ifstream config_file{path}; +bool load_graphics_config(const std::filesystem::path& path) { nlohmann::json config_json{}; - - config_file >> config_json; + if (!read_json_with_backups(path, config_json)) { + return false; + } ultramodern::GraphicsConfig new_config{}; ultramodern::from_json(config_json, new_config); ultramodern::set_graphics_config(new_config); + return true; } void add_input_bindings(nlohmann::json& out, recomp::GameInput input, recomp::InputDevice device) { @@ -291,7 +332,7 @@ void add_input_bindings(nlohmann::json& out, recomp::GameInput input, recomp::In } }; -void save_controls_config(const std::filesystem::path& path) { +bool save_controls_config(const std::filesystem::path& path) { nlohmann::json config_json{}; config_json["keyboard"] = {}; @@ -304,8 +345,7 @@ void save_controls_config(const std::filesystem::path& path) { add_input_bindings(config_json["controller"], cur_input, recomp::InputDevice::Controller); } - std::ofstream config_file{path}; - config_file << std::setw(4) << config_json; + return save_json_with_backups(path, config_json); } bool load_input_device_from_json(const nlohmann::json& config_json, recomp::InputDevice device, const std::string& key) { @@ -349,11 +389,11 @@ bool load_input_device_from_json(const nlohmann::json& config_json, recomp::Inpu return true; } -void load_controls_config(const std::filesystem::path& path) { - std::ifstream config_file{path}; +bool load_controls_config(const std::filesystem::path& path) { nlohmann::json config_json{}; - - config_file >> config_json; + if (!read_json_with_backups(path, config_json)) { + return false; + } if (!load_input_device_from_json(config_json, recomp::InputDevice::Keyboard, "keyboard")) { assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings); @@ -362,29 +402,30 @@ void load_controls_config(const std::filesystem::path& path) { if (!load_input_device_from_json(config_json, recomp::InputDevice::Controller, "controller")) { assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings); } + return true; } -void save_sound_config(const std::filesystem::path& path) { +bool save_sound_config(const std::filesystem::path& path) { nlohmann::json config_json{}; config_json["main_volume"] = recomp::get_main_volume(); config_json["bgm_volume"] = recomp::get_bgm_volume(); config_json["low_health_beeps"] = recomp::get_low_health_beeps_enabled(); - - std::ofstream config_file{path}; - config_file << std::setw(4) << config_json; + + return save_json_with_backups(path, config_json); } -void load_sound_config(const std::filesystem::path& path) { - std::ifstream config_file{path}; +bool load_sound_config(const std::filesystem::path& path) { nlohmann::json config_json{}; - - config_file >> config_json; + if (!read_json_with_backups(path, config_json)) { + return false; + } recomp::reset_sound_settings(); call_if_key_exists(recomp::set_main_volume, config_json, "main_volume"); call_if_key_exists(recomp::set_bgm_volume, config_json, "bgm_volume"); call_if_key_exists(recomp::set_low_health_beeps_enabled, config_json, "low_health_beeps"); + return true; } void recomp::load_config() { @@ -400,35 +441,25 @@ void recomp::load_config() { std::filesystem::create_directories(recomp_dir); } - if (std::filesystem::exists(general_path)) { - load_general_config(general_path); - } - else { + // TODO error handling for failing to save config files after resetting them. + + if (!load_general_config(general_path)) { // Set the general settings from an empty json to use defaults. set_general_settings_from_json({}); save_general_config(general_path); } - if (std::filesystem::exists(graphics_path)) { - load_graphics_config(graphics_path); - } - else { + if (!load_graphics_config(graphics_path)) { reset_graphics_options(); save_graphics_config(graphics_path); } - if (std::filesystem::exists(controls_path)) { - load_controls_config(controls_path); - } - else { + if (!load_controls_config(controls_path)) { recomp::reset_input_bindings(); save_controls_config(controls_path); } - if (std::filesystem::exists(sound_path)) { - load_sound_config(sound_path); - } - else { + if (!load_sound_config(sound_path)) { recomp::reset_sound_settings(); save_sound_config(sound_path); } @@ -442,6 +473,8 @@ void recomp::save_config() { } std::filesystem::create_directories(recomp_dir); + + // TODO error handling for failing to save config files. save_general_config(recomp_dir / general_filename); save_graphics_config(recomp_dir / graphics_filename); diff --git a/src/recomp/files.cpp b/src/recomp/files.cpp new file mode 100644 index 0000000..fa4ed70 --- /dev/null +++ b/src/recomp/files.cpp @@ -0,0 +1,51 @@ +#include "recomp_files.h" + +constexpr std::u8string_view backup_suffix = u8".bak"; +constexpr std::u8string_view temp_suffix = u8".temp"; + +std::ifstream recomp::open_input_backup_file(const std::filesystem::path& filepath, std::ios_base::openmode mode) { + std::filesystem::path backup_path{filepath}; + backup_path += backup_suffix; + return std::ifstream{backup_path, mode}; +} + +std::ifstream recomp::open_input_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { + std::ifstream ret{filepath, mode}; + + // Check if the file failed to open and open the corresponding backup file instead if so. + if (!ret.good()) { + return open_input_backup_file(filepath, mode); + } + + return ret; +} + +std::ofstream recomp::open_output_file_with_backup(const std::filesystem::path& filepath, std::ios_base::openmode mode) { + std::filesystem::path temp_path{filepath}; + temp_path += temp_suffix; + std::ofstream temp_file_out{ temp_path, mode }; + + return temp_file_out; +} + +bool recomp::finalize_output_file_with_backup(const std::filesystem::path& filepath) { + std::filesystem::path backup_path{filepath}; + backup_path += backup_suffix; + + std::filesystem::path temp_path{filepath}; + temp_path += temp_suffix; + + std::error_code ec; + if (std::filesystem::exists(filepath, ec)) { + std::filesystem::copy_file(filepath, backup_path, std::filesystem::copy_options::overwrite_existing, ec); + if (ec) { + return false; + } + } + std::filesystem::copy_file(temp_path, filepath, std::filesystem::copy_options::overwrite_existing, ec); + if (ec) { + return false; + } + std::filesystem::remove(temp_path, ec); + return true; +} diff --git a/src/recomp/pi.cpp b/src/recomp/pi.cpp index 592a234..2bad386 100644 --- a/src/recomp/pi.cpp +++ b/src/recomp/pi.cpp @@ -7,6 +7,7 @@ #include "recomp.h" #include "recomp_game.h" #include "recomp_config.h" +#include "recomp_files.h" #include "../ultramodern/ultra64.h" #include "../ultramodern/ultramodern.hpp" @@ -96,42 +97,28 @@ struct { const std::u8string save_folder = u8"saves"; const std::u8string save_filename = std::u8string{recomp::mm_game_id} + u8".bin"; -const std::u8string save_filename_temp = std::u8string{recomp::mm_game_id} + u8".bin.temp"; -const std::u8string save_filename_backup = std::u8string{recomp::mm_game_id} + u8".bin.bak"; std::filesystem::path get_save_file_path() { return recomp::get_app_folder_path() / save_folder / save_filename; } -std::filesystem::path get_save_file_path_temp() { - return recomp::get_app_folder_path() / save_folder / save_filename_temp; -} - -std::filesystem::path get_save_file_path_backup() { - return recomp::get_app_folder_path() / save_folder / save_filename_backup; -} - void update_save_file() { + bool saving_failed = false; { - std::ofstream save_file{ get_save_file_path_temp(), std::ios_base::binary }; + std::ofstream save_file = recomp::open_output_file_with_backup(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 { - recomp::message_box("Failed to write to the save file. Check your file permissions. If you have moved your appdata folder to Dropbox or similar, this can cause issues."); + saving_failed = false; } } - std::error_code ec; - if (std::filesystem::exists(get_save_file_path(), ec)) { - std::filesystem::copy_file(get_save_file_path(), get_save_file_path_backup(), std::filesystem::copy_options::overwrite_existing, ec); - if (ec) { - printf("[ERROR] Failed to copy save file backup\n"); - } + if (!saving_failed) { + saving_failed = recomp::finalize_output_file_with_backup(get_save_file_path()); } - std::filesystem::copy_file(get_save_file_path_temp(), get_save_file_path(), std::filesystem::copy_options::overwrite_existing, ec); - if (ec) { + if (saving_failed) { recomp::message_box("Failed to write to the save file. Check your file permissions. If you have moved your appdata folder to Dropbox or similar, this can cause issues."); } } @@ -199,24 +186,18 @@ void save_clear(uint32_t start, uint32_t size, char value) { void ultramodern::init_saving(RDRAM_ARG1) { std::filesystem::path save_file_path = get_save_file_path(); - std::filesystem::path save_file_path_backup = get_save_file_path_backup(); // 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{ save_file_path, std::ios_base::binary }; + std::ifstream save_file = recomp::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 { - // Reading the save file faield, so try to read the backup save file. - std::ifstream save_file_backup{ save_file_path_backup, std::ios_base::binary }; - if (save_file_backup.good()) { - save_file_backup.read(save_context.save_buffer.data(), save_context.save_buffer.size()); - } else { - // Otherwise clear the save file to all zeroes. - save_context.save_buffer.fill(0); - } + } + else { + // Otherwise clear the save file to all zeroes. + save_context.save_buffer.fill(0); } save_context.saving_thread = std::thread{saving_thread_func, PASS_RDRAM};