Save controls and graphics configs to files after editing and load them on startup
This commit is contained in:
parent
8e4e4b1cae
commit
992bdb67e9
|
@ -5,7 +5,7 @@
|
||||||
#include "../ultramodern/config.hpp"
|
#include "../ultramodern/config.hpp"
|
||||||
|
|
||||||
namespace recomp {
|
namespace recomp {
|
||||||
constexpr std::u8string_view program_id = u8"ZeldaRecomp";
|
constexpr std::u8string_view program_id = u8"Zelda64Recompiled";
|
||||||
|
|
||||||
void load_config();
|
void load_config();
|
||||||
void save_config();
|
void save_config();
|
||||||
|
|
|
@ -1,15 +1,87 @@
|
||||||
#include "recomp_config.h"
|
#include "recomp_config.h"
|
||||||
#include "recomp_input.h"
|
#include "recomp_input.h"
|
||||||
#include "../../ultramodern/config.hpp"
|
#include "../../ultramodern/config.hpp"
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iomanip>
|
||||||
|
|
||||||
void recomp::reset_input_bindings() {
|
#if defined(_WIN32)
|
||||||
auto assign_mapping = [](recomp::InputDevice device, recomp::GameInput input, const std::vector<recomp::InputField>& value) {
|
#include <Shlobj.h>
|
||||||
|
#elif defined(__linux__)
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <pwd.h>
|
||||||
|
#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<recomp::InputField>& value) {
|
||||||
for (size_t binding_index = 0; binding_index < std::min(value.size(), recomp::bindings_per_input); binding_index++) {
|
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]);
|
recomp::set_input_binding(input, binding_index, device, value[binding_index]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
auto assign_all_mappings = [&](recomp::InputDevice device, const recomp::DefaultN64Mappings& values) {
|
void assign_all_mappings(recomp::InputDevice device, const recomp::DefaultN64Mappings& values) {
|
||||||
assign_mapping(device, recomp::GameInput::A, values.a);
|
assign_mapping(device, recomp::GameInput::A, values.a);
|
||||||
assign_mapping(device, recomp::GameInput::B, values.b);
|
assign_mapping(device, recomp::GameInput::B, values.b);
|
||||||
assign_mapping(device, recomp::GameInput::Z, values.z);
|
assign_mapping(device, recomp::GameInput::Z, values.z);
|
||||||
|
@ -31,6 +103,7 @@ void recomp::reset_input_bindings() {
|
||||||
assign_mapping(device, recomp::GameInput::Y_AXIS_POS, values.analog_up);
|
assign_mapping(device, recomp::GameInput::Y_AXIS_POS, values.analog_up);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
void recomp::reset_input_bindings() {
|
||||||
assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings);
|
assign_all_mappings(recomp::InputDevice::Keyboard, recomp::default_n64_keyboard_mappings);
|
||||||
assign_all_mappings(recomp::InputDevice::Controller, recomp::default_n64_controller_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() {
|
void recomp::reset_graphics_options() {
|
||||||
ultramodern::GraphicsConfig new_config{};
|
ultramodern::GraphicsConfig new_config{};
|
||||||
new_config.res_option = ultramodern::Resolution::Auto;
|
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.ar_option = RT64::UserConfiguration::AspectRatio::Expand;
|
||||||
new_config.msaa_option = RT64::UserConfiguration::Antialiasing::MSAA4X;
|
new_config.msaa_option = RT64::UserConfiguration::Antialiasing::MSAA4X;
|
||||||
new_config.rr_option = RT64::UserConfiguration::RefreshRate::Original;
|
new_config.rr_option = RT64::UserConfiguration::RefreshRate::Original;
|
||||||
|
@ -46,14 +119,127 @@ void recomp::reset_graphics_options() {
|
||||||
ultramodern::set_graphics_config(new_config);
|
ultramodern::set_graphics_config(new_config);
|
||||||
}
|
}
|
||||||
|
|
||||||
void recomp::load_config() {
|
void save_graphics_config(const std::filesystem::path& path) {
|
||||||
// TODO load from a file if one exists.
|
std::ofstream config_file{path};
|
||||||
recomp::reset_input_bindings();
|
|
||||||
|
|
||||||
// TODO load from a file if one exists.
|
nlohmann::json config_json{};
|
||||||
|
ultramodern::to_json(config_json, ultramodern::get_graphics_config());
|
||||||
|
config_file << std::setw(4) << config_json;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<recomp::GameInput>(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<recomp::GameInput>(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();
|
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() {
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -422,9 +422,10 @@ std::string controller_button_to_string(SDL_GameControllerButton button) {
|
||||||
// return "";
|
// return "";
|
||||||
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_TOUCHPAD:
|
case SDL_GameControllerButton::SDL_CONTROLLER_BUTTON_TOUCHPAD:
|
||||||
return "\u21E7";
|
return "\u21E7";
|
||||||
}
|
default:
|
||||||
return "Button " + std::to_string(button);
|
return "Button " + std::to_string(button);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
std::string controller_axis_to_string(int axis) {
|
std::string controller_axis_to_string(int axis) {
|
||||||
bool positive = axis > 0;
|
bool positive = axis > 0;
|
||||||
|
@ -442,9 +443,10 @@ std::string controller_axis_to_string(int axis) {
|
||||||
return positive ? "\u219A" : "\u21DC";
|
return positive ? "\u219A" : "\u21DC";
|
||||||
case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
|
case SDL_GameControllerAxis::SDL_CONTROLLER_AXIS_TRIGGERRIGHT:
|
||||||
return positive ? "\u219B" : "\u21DD";
|
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 {
|
std::string recomp::InputField::to_string() const {
|
||||||
switch ((InputType)input_type) {
|
switch ((InputType)input_type) {
|
||||||
|
@ -454,6 +456,7 @@ std::string recomp::InputField::to_string() const {
|
||||||
return controller_button_to_string((SDL_GameControllerButton)input_id);
|
return controller_button_to_string((SDL_GameControllerButton)input_id);
|
||||||
case InputType::ControllerAnalog:
|
case InputType::ControllerAnalog:
|
||||||
return controller_axis_to_string(input_id);
|
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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
// True if controller config menu is open, false if keyboard config menu is open, undefined otherwise
|
||||||
bool configuring_controller = false;
|
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 <typename T>
|
template <typename T>
|
||||||
void get_option(const T& input, Rml::Variant& output) {
|
void get_option(const T& input, Rml::Variant& output) {
|
||||||
std::string value = "";
|
std::string value = "";
|
||||||
|
@ -69,14 +58,6 @@ void recomp::finish_scanning_input(recomp::InputField scanned_field) {
|
||||||
controls_model_handle.DirtyVariable("active_binding_slot");
|
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() {
|
void close_config_menu() {
|
||||||
recomp::save_config();
|
recomp::save_config();
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,18 @@ namespace ultramodern {
|
||||||
};
|
};
|
||||||
|
|
||||||
void set_graphics_config(const GraphicsConfig& config);
|
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
|
#endif
|
||||||
|
|
Loading…
Reference in New Issue