diff --git a/include/recomp_config.h b/include/recomp_config.h index a3caaee..c15604b 100644 --- a/include/recomp_config.h +++ b/include/recomp_config.h @@ -5,7 +5,7 @@ #include "../ultramodern/config.hpp" namespace recomp { - constexpr std::u8string_view program_id = u8"ZeldaRecomp"; + constexpr std::u8string_view program_id = u8"Zelda64Recompiled"; void load_config(); void save_config(); diff --git a/src/game/config.cpp b/src/game/config.cpp index 739d051..1c0ff23 100644 --- a/src/game/config.cpp +++ b/src/game/config.cpp @@ -1,36 +1,109 @@ #include "recomp_config.h" #include "recomp_input.h" #include "../../ultramodern/config.hpp" +#include +#include +#include + +#if defined(_WIN32) +#include +#elif defined(__linux__) +#include +#include +#endif + +constexpr std::u8string_view graphics_filename = u8"graphics.json"; +constexpr std::u8string_view controls_filename = u8"controls.json"; + +namespace ultramodern { + void to_json(json& j, const GraphicsConfig& config) { + j = json{ + {"res_option", config.res_option}, + {"wm_option", config.wm_option}, + {"ar_option", config.ar_option}, + {"msaa_option", config.msaa_option}, + {"rr_option", config.rr_option}, + {"rr_manual_value", config.rr_manual_value}, + }; + } + + void from_json(const json& j, GraphicsConfig& config) { + j.at("res_option") .get_to(config.res_option); + j.at("wm_option") .get_to(config.wm_option); + j.at("ar_option") .get_to(config.ar_option); + j.at("msaa_option") .get_to(config.msaa_option); + j.at("rr_option") .get_to(config.rr_option); + j.at("rr_manual_value").get_to(config.rr_manual_value); + } +} + +namespace recomp { + void to_json(json& j, const InputField& field) { + j = json{ {"input_type", field.input_type}, {"input_id", field.input_id} }; + } + + void from_json(const json& j, InputField& field) { + j.at("input_type").get_to(field.input_type); + j.at("input_id").get_to(field.input_id); + } +} + +std::filesystem::path get_config_folder_path() { + std::filesystem::path recomp_dir{}; + +#if defined(_WIN32) + // Deduce local app data path. + PWSTR known_path = NULL; + HRESULT result = SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, NULL, &known_path); + if (result == S_OK) { + recomp_dir = std::filesystem::path{known_path} / recomp::program_id; + } + + CoTaskMemFree(known_path); +#elif defined(__linux__) + const char *homedir; + + if ((homedir = getenv("HOME")) == nullptr) { + homedir = getpwuid(getuid())->pw_dir; + } + + if (homedir != nullptr) { + recomp_dir = std::filesystem::path{homedir} / (std::string{"."} + recomp::program_id); + } +#endif + + return recomp_dir; +} + +void assign_mapping(recomp::InputDevice device, recomp::GameInput input, const std::vector& value) { + for (size_t binding_index = 0; binding_index < std::min(value.size(), recomp::bindings_per_input); binding_index++) { + recomp::set_input_binding(input, binding_index, device, value[binding_index]); + } +}; + +void assign_all_mappings(recomp::InputDevice device, const recomp::DefaultN64Mappings& values) { + assign_mapping(device, recomp::GameInput::A, values.a); + assign_mapping(device, recomp::GameInput::B, values.b); + assign_mapping(device, recomp::GameInput::Z, values.z); + assign_mapping(device, recomp::GameInput::START, values.start); + assign_mapping(device, recomp::GameInput::DPAD_UP, values.dpad_up); + assign_mapping(device, recomp::GameInput::DPAD_DOWN, values.dpad_down); + assign_mapping(device, recomp::GameInput::DPAD_LEFT, values.dpad_left); + assign_mapping(device, recomp::GameInput::DPAD_RIGHT, values.dpad_right); + assign_mapping(device, recomp::GameInput::L, values.l); + assign_mapping(device, recomp::GameInput::R, values.r); + assign_mapping(device, recomp::GameInput::C_UP, values.c_up); + assign_mapping(device, recomp::GameInput::C_DOWN, values.c_down); + assign_mapping(device, recomp::GameInput::C_LEFT, values.c_left); + assign_mapping(device, recomp::GameInput::C_RIGHT, values.c_right); + + assign_mapping(device, recomp::GameInput::X_AXIS_NEG, values.analog_left); + assign_mapping(device, recomp::GameInput::X_AXIS_POS, values.analog_right); + assign_mapping(device, recomp::GameInput::Y_AXIS_NEG, values.analog_down); + assign_mapping(device, recomp::GameInput::Y_AXIS_POS, values.analog_up); +}; void recomp::reset_input_bindings() { - auto assign_mapping = [](recomp::InputDevice device, recomp::GameInput input, const std::vector& value) { - for (size_t binding_index = 0; binding_index < std::min(value.size(), recomp::bindings_per_input); binding_index++) { - recomp::set_input_binding(input, binding_index, device, value[binding_index]); - } - }; - - auto assign_all_mappings = [&](recomp::InputDevice device, const recomp::DefaultN64Mappings& values) { - assign_mapping(device, recomp::GameInput::A, values.a); - assign_mapping(device, recomp::GameInput::B, values.b); - assign_mapping(device, recomp::GameInput::Z, values.z); - assign_mapping(device, recomp::GameInput::START, values.start); - assign_mapping(device, recomp::GameInput::DPAD_UP, values.dpad_up); - assign_mapping(device, recomp::GameInput::DPAD_DOWN, values.dpad_down); - assign_mapping(device, recomp::GameInput::DPAD_LEFT, values.dpad_left); - assign_mapping(device, recomp::GameInput::DPAD_RIGHT, values.dpad_right); - assign_mapping(device, recomp::GameInput::L, values.l); - assign_mapping(device, recomp::GameInput::R, values.r); - assign_mapping(device, recomp::GameInput::C_UP, values.c_up); - assign_mapping(device, recomp::GameInput::C_DOWN, values.c_down); - assign_mapping(device, recomp::GameInput::C_LEFT, values.c_left); - assign_mapping(device, recomp::GameInput::C_RIGHT, values.c_right); - - assign_mapping(device, recomp::GameInput::X_AXIS_NEG, values.analog_left); - assign_mapping(device, recomp::GameInput::X_AXIS_POS, values.analog_right); - assign_mapping(device, recomp::GameInput::Y_AXIS_NEG, values.analog_down); - assign_mapping(device, recomp::GameInput::Y_AXIS_POS, values.analog_up); - }; - assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings); assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings); } @@ -38,7 +111,7 @@ void recomp::reset_input_bindings() { void recomp::reset_graphics_options() { ultramodern::GraphicsConfig new_config{}; new_config.res_option = ultramodern::Resolution::Auto; - new_config.wm_option = ultramodern::WindowMode::Fullscreen; + new_config.wm_option = ultramodern::WindowMode::Windowed; new_config.ar_option = RT64::UserConfiguration::AspectRatio::Expand; new_config.msaa_option = RT64::UserConfiguration::Antialiasing::MSAA4X; new_config.rr_option = RT64::UserConfiguration::RefreshRate::Original; @@ -46,14 +119,127 @@ void recomp::reset_graphics_options() { ultramodern::set_graphics_config(new_config); } -void recomp::load_config() { - // TODO load from a file if one exists. - recomp::reset_input_bindings(); +void save_graphics_config(const std::filesystem::path& path) { + std::ofstream config_file{path}; + + nlohmann::json config_json{}; + ultramodern::to_json(config_json, ultramodern::get_graphics_config()); + config_file << std::setw(4) << config_json; +} - // TODO load from a file if one exists. - recomp::reset_graphics_options(); +void load_graphics_config(const std::filesystem::path& path) { + std::ifstream config_file{path}; + nlohmann::json config_json{}; + + config_file >> config_json; + + ultramodern::GraphicsConfig new_config{}; + ultramodern::from_json(config_json, new_config); + ultramodern::set_graphics_config(new_config); +} + +void add_input_bindings(nlohmann::json& out, recomp::GameInput input, recomp::InputDevice device) { + const std::string& input_name = recomp::get_input_enum_name(input); + nlohmann::json& out_array = out[input_name]; + out_array = nlohmann::json::array(); + for (size_t binding_index = 0; binding_index < recomp::bindings_per_input; binding_index++) { + out_array[binding_index] = recomp::get_input_binding(input, binding_index, device); + } +}; + +void save_controls_config(const std::filesystem::path& path) { + nlohmann::json config_json{}; + config_json["keyboard"] = {}; + config_json["controller"] = {}; + + for (size_t i = 0; i < recomp::get_num_inputs(); i++) { + recomp::GameInput cur_input = static_cast(i); + + add_input_bindings(config_json["keyboard"], cur_input, recomp::InputDevice::Keyboard); + add_input_bindings(config_json["controller"], cur_input, recomp::InputDevice::Controller); + } + + std::ofstream config_file{path}; + config_file << std::setw(4) << config_json; +} + +bool load_input_device_from_json(const nlohmann::json& config_json, recomp::InputDevice device, const std::string& key) { + // Check if the json object for the given key exists. + auto find_it = config_json.find(key); + if (find_it == config_json.end()) { + return false; + } + + const nlohmann::json& mappings_json = *find_it; + + for (size_t i = 0; i < recomp::get_num_inputs(); i++) { + recomp::GameInput cur_input = static_cast(i); + const std::string& input_name = recomp::get_input_enum_name(cur_input); + + // Check if the json object for the given input exists and that it's an array. + auto find_input_it = mappings_json.find(input_name); + if (find_input_it == mappings_json.end() || !find_input_it->is_array()) { + continue; + } + const nlohmann::json& input_json = *find_input_it; + + // Deserialize all the bindings from the json array (up to the max number of bindings per input). + for (size_t binding_index = 0; binding_index < std::min(recomp::bindings_per_input, input_json.size()); binding_index++) { + recomp::InputField cur_field{}; + recomp::from_json(input_json[binding_index], cur_field); + recomp::set_input_binding(cur_input, binding_index, device, cur_field); + } + } + + return true; +} + +void load_controls_config(const std::filesystem::path& path) { + std::ifstream config_file{path}; + nlohmann::json config_json{}; + + config_file >> config_json; + + if (!load_input_device_from_json(config_json, recomp::InputDevice::Keyboard, "keyboard")) { + assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings); + } + + if (!load_input_device_from_json(config_json, recomp::InputDevice::Controller, "controller")) { + assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_mappings); + } +} + +void recomp::load_config() { + std::filesystem::path recomp_dir = get_config_folder_path(); + std::filesystem::path graphics_path = recomp_dir / graphics_filename; + std::filesystem::path controls_path = recomp_dir / controls_filename; + + if (std::filesystem::exists(graphics_path)) { + load_graphics_config(graphics_path); + } + else { + recomp::reset_graphics_options(); + save_graphics_config(graphics_path); + } + + if (std::filesystem::exists(controls_path)) { + load_controls_config(controls_path); + } + else { + recomp::reset_input_bindings(); + save_controls_config(controls_path); + } } void recomp::save_config() { - + std::filesystem::path recomp_dir = get_config_folder_path(); + + if (recomp_dir.empty()) { + return; + } + + std::filesystem::create_directories(recomp_dir); + + save_graphics_config(recomp_dir / graphics_filename); + save_controls_config(recomp_dir / controls_filename); } diff --git a/src/game/input.cpp b/src/game/input.cpp index 742d3be..d99548e 100644 --- a/src/game/input.cpp +++ b/src/game/input.cpp @@ -422,8 +422,9 @@ std::string controller_button_to_string(SDL_GameControllerButton button) { // return ""; case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_TOUCHPAD: return "\u21E7"; + default: + return "Button " + std::to_string(button); } - return "Button " + std::to_string(button); } std::string controller_axis_to_string(int axis) { @@ -442,8 +443,9 @@ std::string controller_axis_to_string(int axis) { return positive ? "\u219A" : "\u21DC"; case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_TRIGGERRIGHT: return positive ? "\u219B" : "\u21DD"; + default: + return "Axis " + std::to_string(actual_axis) + (positive ? '+' : '-'); } - return "Axis " + std::to_string(actual_axis) + (positive ? '+' : '-'); } std::string recomp::InputField::to_string() const { @@ -454,6 +456,7 @@ std::string recomp::InputField::to_string() const { return controller_button_to_string((SDL_GameControllerButton)input_id); case InputType::ControllerAnalog: return controller_axis_to_string(input_id); + default: + return std::to_string(input_type) + "," + std::to_string(input_id); } - return std::to_string(input_type) + "," + std::to_string(input_id); } diff --git a/src/ui/ui_config.cpp b/src/ui/ui_config.cpp index 892193f..1bbdf44 100644 --- a/src/ui/ui_config.cpp +++ b/src/ui/ui_config.cpp @@ -11,17 +11,6 @@ Rml::DataModelHandle controls_model_handle; // True if controller config menu is open, false if keyboard config menu is open, undefined otherwise bool configuring_controller = false; -NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::Resolution, { - {ultramodern::Resolution::Original, "Original"}, - {ultramodern::Resolution::Original2x, "Original2x"}, - {ultramodern::Resolution::Auto, "Auto"}, -}); - -NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::WindowMode, { - {ultramodern::WindowMode::Windowed, "Windowed"}, - {ultramodern::WindowMode::Fullscreen, "Fullscreen"} -}); - template void get_option(const T& input, Rml::Variant& output) { std::string value = ""; @@ -69,14 +58,6 @@ void recomp::finish_scanning_input(recomp::InputField scanned_field) { controls_model_handle.DirtyVariable("active_binding_slot"); } -// Counts down every frame while positive until it reaches 0, then saves the graphics config file. -// This prevents a graphics config that would cause the game to crash from -static std::atomic_int save_graphics_config_frame_timer; - -void queue_saving_graphics_config() { - save_graphics_config_frame_timer = 5; -} - void close_config_menu() { recomp::save_config(); diff --git a/ultramodern/config.hpp b/ultramodern/config.hpp index ba6d135..1086d75 100644 --- a/ultramodern/config.hpp +++ b/ultramodern/config.hpp @@ -28,7 +28,18 @@ namespace ultramodern { }; void set_graphics_config(const GraphicsConfig& config); - const GraphicsConfig& get_graphics_config(); + GraphicsConfig get_graphics_config(); + + NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::Resolution, { + {ultramodern::Resolution::Original, "Original"}, + {ultramodern::Resolution::Original2x, "Original2x"}, + {ultramodern::Resolution::Auto, "Auto"}, + }); + + NLOHMANN_JSON_SERIALIZE_ENUM(ultramodern::WindowMode, { + {ultramodern::WindowMode::Windowed, "Windowed"}, + {ultramodern::WindowMode::Fullscreen, "Fullscreen"} + }); }; #endif