From af14101bdb56ba6aeb382b967b4e0cef25e89801 Mon Sep 17 00:00:00 2001 From: thecozies <79979276+thecozies@users.noreply.github.com> Date: Thu, 20 Jun 2024 11:59:54 -0500 Subject: [PATCH 01/25] init recomp config_store --- librecomp/CMakeLists.txt | 1 + librecomp/include/librecomp/config_store.hpp | 72 ++++++++++++++++++++ librecomp/src/config_store.cpp | 22 ++++++ 3 files changed, 95 insertions(+) create mode 100644 librecomp/include/librecomp/config_store.hpp create mode 100644 librecomp/src/config_store.cpp diff --git a/librecomp/CMakeLists.txt b/librecomp/CMakeLists.txt index 9f78d4f9..b7c5c2b5 100644 --- a/librecomp/CMakeLists.txt +++ b/librecomp/CMakeLists.txt @@ -8,6 +8,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) # Define the library add_library(librecomp STATIC "${CMAKE_CURRENT_SOURCE_DIR}/src/ai.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/config_store.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/cont.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/dp.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/eep.cpp" diff --git a/librecomp/include/librecomp/config_store.hpp b/librecomp/include/librecomp/config_store.hpp new file mode 100644 index 00000000..8d5400f4 --- /dev/null +++ b/librecomp/include/librecomp/config_store.hpp @@ -0,0 +1,72 @@ +#ifndef __RECOMP_CONFIG_STORE_H__ +#define __RECOMP_CONFIG_STORE_H__ + +#include +#include +#include +#include +#include + +struct string_hash { + using is_transparent = void; + [[nodiscard]] size_t operator()(const char *txt) const { + return std::hash{}(txt); + } + [[nodiscard]] size_t operator()(std::string_view txt) const { + return std::hash{}(txt); + } + [[nodiscard]] size_t operator()(const std::string &txt) const { + return std::hash{}(txt); + } +}; + +namespace recomp { + typedef std::variant config_store_value; + typedef std::unordered_map> config_store_map; + + struct ConfigStore { + recomp::config_store_map map; + recomp::config_store_map default_map; + std::mutex store_mutex; + std::mutex default_store_mutex; + }; + + extern ConfigStore config_store; + + void set_config_store_value(std::string key, config_store_value value); + void set_config_store_default_value(std::string key, config_store_value value); + void set_config_store_value_and_default(std::string key, config_store_value value, config_store_value default_value); + + template + T get_config_store_default_value(std::string key) { + std::lock_guard lock{ config_store.default_store_mutex }; + auto it = config_store.default_map.find(key); + if (it != config_store.default_map.end()) { + if (std::holds_alternative(it->second)) { + return std::get(it->second); + } else { + throw std::runtime_error("Stored value is not of requested type"); + } + } else { + throw std::runtime_error("Key not found"); + } + }; + + // Get a value from the config store, if it doesn't exist then return the default value + template + T get_config_store_value(std::string key) { + std::lock_guard lock{ config_store.store_mutex }; + auto it = config_store.map.find(key); + if (it != config_store.map.end()) { + if (std::holds_alternative(it->second)) { + return std::get(it->second); + } else { + throw std::runtime_error("Stored value is not of requested type"); + } + } else { + return get_config_store_default_value(key); + } + }; +}; + +#endif diff --git a/librecomp/src/config_store.cpp b/librecomp/src/config_store.cpp new file mode 100644 index 00000000..1dad2010 --- /dev/null +++ b/librecomp/src/config_store.cpp @@ -0,0 +1,22 @@ +#include "config_store.hpp" + +namespace recomp { + +ConfigStore config_store = {{}, {}}; + +void set_config_store_value(std::string key, config_store_value value) { + std::lock_guard lock{ config_store.store_mutex }; + config_store.map[key] = value; +} + +void set_config_store_default_value(std::string key, config_store_value value) { + std::lock_guard lock{ config_store.default_store_mutex }; + config_store.default_map[key] = value; +} + +void set_config_store_value_and_default(std::string key, config_store_value value, config_store_value default_value) { + set_config_store_value(key, value); + set_config_store_default_value(key, default_value); +} + +} From d82601222b5bbad75195d39f1a04aef550af5a6d Mon Sep 17 00:00:00 2001 From: thecozies <79979276+thecozies@users.noreply.github.com> Date: Thu, 20 Jun 2024 12:22:26 -0500 Subject: [PATCH 02/25] Use a custom hash class to enable hetereogenous lookup --- librecomp/include/librecomp/config_store.hpp | 11 ++++++----- librecomp/src/config_store.cpp | 10 +++++----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/librecomp/include/librecomp/config_store.hpp b/librecomp/include/librecomp/config_store.hpp index 8d5400f4..91ce7f7c 100644 --- a/librecomp/include/librecomp/config_store.hpp +++ b/librecomp/include/librecomp/config_store.hpp @@ -7,6 +7,7 @@ #include #include +// Use a custom hash class to enable hetereogenous lookup struct string_hash { using is_transparent = void; [[nodiscard]] size_t operator()(const char *txt) const { @@ -33,12 +34,12 @@ namespace recomp { extern ConfigStore config_store; - void set_config_store_value(std::string key, config_store_value value); - void set_config_store_default_value(std::string key, config_store_value value); - void set_config_store_value_and_default(std::string key, config_store_value value, config_store_value default_value); + void set_config_store_value(std::string_view key, config_store_value value); + void set_config_store_default_value(std::string_view key, config_store_value value); + void set_config_store_value_and_default(std::string_view key, config_store_value value, config_store_value default_value); template - T get_config_store_default_value(std::string key) { + T get_config_store_default_value(std::string_view key) { std::lock_guard lock{ config_store.default_store_mutex }; auto it = config_store.default_map.find(key); if (it != config_store.default_map.end()) { @@ -54,7 +55,7 @@ namespace recomp { // Get a value from the config store, if it doesn't exist then return the default value template - T get_config_store_value(std::string key) { + T get_config_store_value(std::string_view key) { std::lock_guard lock{ config_store.store_mutex }; auto it = config_store.map.find(key); if (it != config_store.map.end()) { diff --git a/librecomp/src/config_store.cpp b/librecomp/src/config_store.cpp index 1dad2010..3e965400 100644 --- a/librecomp/src/config_store.cpp +++ b/librecomp/src/config_store.cpp @@ -4,17 +4,17 @@ namespace recomp { ConfigStore config_store = {{}, {}}; -void set_config_store_value(std::string key, config_store_value value) { +void set_config_store_value(std::string_view key, config_store_value value) { std::lock_guard lock{ config_store.store_mutex }; - config_store.map[key] = value; + config_store.map[std::string{key}] = value; } -void set_config_store_default_value(std::string key, config_store_value value) { +void set_config_store_default_value(std::string_view key, config_store_value value) { std::lock_guard lock{ config_store.default_store_mutex }; - config_store.default_map[key] = value; + config_store.default_map[std::string{key}] = value; } -void set_config_store_value_and_default(std::string key, config_store_value value, config_store_value default_value) { +void set_config_store_value_and_default(std::string_view key, config_store_value value, config_store_value default_value) { set_config_store_value(key, value); set_config_store_default_value(key, default_value); } From 9406d7f197d24cea554b764a4a4155f26ff66f54 Mon Sep 17 00:00:00 2001 From: thecozies <79979276+thecozies@users.noreply.github.com> Date: Thu, 11 Jul 2024 12:28:11 -0500 Subject: [PATCH 03/25] Added config registry/option files --- librecomp/CMakeLists.txt | 5 +- librecomp/include/librecomp/config.hpp | 17 ++ librecomp/src/config/ConfigOption.cpp | 31 +++ librecomp/src/config/ConfigOption.hpp | 67 +++++ librecomp/src/config/ConfigRegistry.cpp | 257 ++++++++++++++++++ librecomp/src/config/ConfigRegistry.hpp | 66 +++++ .../ConfigStore.cpp} | 4 +- .../config/ConfigStore.hpp} | 6 +- 8 files changed, 447 insertions(+), 6 deletions(-) create mode 100644 librecomp/include/librecomp/config.hpp create mode 100644 librecomp/src/config/ConfigOption.cpp create mode 100644 librecomp/src/config/ConfigOption.hpp create mode 100644 librecomp/src/config/ConfigRegistry.cpp create mode 100644 librecomp/src/config/ConfigRegistry.hpp rename librecomp/src/{config_store.cpp => config/ConfigStore.cpp} (93%) rename librecomp/{include/librecomp/config_store.hpp => src/config/ConfigStore.hpp} (95%) diff --git a/librecomp/CMakeLists.txt b/librecomp/CMakeLists.txt index b7c5c2b5..f698b5f9 100644 --- a/librecomp/CMakeLists.txt +++ b/librecomp/CMakeLists.txt @@ -8,7 +8,9 @@ set(CMAKE_CXX_EXTENSIONS OFF) # Define the library add_library(librecomp STATIC "${CMAKE_CURRENT_SOURCE_DIR}/src/ai.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/config_store.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/config/ConfigOption.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/config/ConfigRegistry.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/config/ConfigStore.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/cont.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/dp.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/eep.cpp" @@ -36,6 +38,7 @@ add_library(librecomp STATIC target_include_directories(librecomp PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" + "${CMAKE_CURRENT_SOURCE_DIR}/src" "${PROJECT_SOURCE_DIR}/../ultramodern/include" "${PROJECT_SOURCE_DIR}/../thirdparty" "${PROJECT_SOURCE_DIR}/../thirdparty/concurrentqueue" diff --git a/librecomp/include/librecomp/config.hpp b/librecomp/include/librecomp/config.hpp new file mode 100644 index 00000000..4ddbfc30 --- /dev/null +++ b/librecomp/include/librecomp/config.hpp @@ -0,0 +1,17 @@ +#ifndef __RECOMP_CONFIG_H__ +#define __RECOMP_CONFIG_H__ + +#include +#include +#include +#include +#include +#include "json/json.hpp" + +// namespace recomp::config + +#include "config/ConfigStore.hpp" +#include "config/ConfigRegistry.hpp" +#include "config/ConfigOption.hpp" + +#endif diff --git a/librecomp/src/config/ConfigOption.cpp b/librecomp/src/config/ConfigOption.cpp new file mode 100644 index 00000000..c6ca3f25 --- /dev/null +++ b/librecomp/src/config/ConfigOption.cpp @@ -0,0 +1,31 @@ + +#include "config.hpp" + +#include +#include + +using json = nlohmann::json; +namespace recomp::config { + +bool ConfigOption::validate(json& opt_json) { + auto type_struct = get_type_structure(); + for (const auto& [key, expected_type] : type_struct) { + if (!opt_json.contains(key) || opt_json[key].type() != expected_type) { + return false; + } + } + return true; +} + +ConfigOption::ConfigOption(json& opt_json) +{ + type = opt_json[ConfigOption::schema::type]; + key = opt_json[ConfigOption::schema::key]; +} + +ConfigOption::~ConfigOption() +{ + +} + +} // namespace Rml diff --git a/librecomp/src/config/ConfigOption.hpp b/librecomp/src/config/ConfigOption.hpp new file mode 100644 index 00000000..71a067b9 --- /dev/null +++ b/librecomp/src/config/ConfigOption.hpp @@ -0,0 +1,67 @@ +#ifndef __RECOMP_CONFIG_OPTION_H__ +#define __RECOMP_CONFIG_OPTION_H__ + +#include +#include "json/json.hpp" + +namespace recomp::config { + +enum class ConfigOptionType { + Label, + // Base types + Checkbox, + RadioTabs, + Dropdown, + Range, + Color, + Trigger, + // Group types + CheckboxGroup, + Group, + OptionCount +}; + +NLOHMANN_JSON_SERIALIZE_ENUM(ConfigOptionType, { + {ConfigOptionType::Label, "Label"}, + {ConfigOptionType::Checkbox, "Checkbox"}, + {ConfigOptionType::RadioTabs, "RadioTabs"}, + {ConfigOptionType::Dropdown, "Dropdown"}, + {ConfigOptionType::Range, "Range"}, + {ConfigOptionType::Color, "Color"}, + {ConfigOptionType::Trigger, "Trigger"}, + {ConfigOptionType::CheckboxGroup, "CheckboxGroup"}, + {ConfigOptionType::Group, "Group"} +}); + +typedef std::unordered_map config_type_structure; + +inline bool config_option_is_group(ConfigOptionType option_type) { + return option_type == ConfigOptionType::Group || option_type == ConfigOptionType::CheckboxGroup; +} + +/** + Base Config Option class. Defines what type of option or group it is, and + sets the key. + */ + +class ConfigOption { +public: + ConfigOption(nlohmann::json& opt_json); + virtual ~ConfigOption(); + + bool validate(nlohmann::json& opt_json); + + ConfigOptionType type; + std::string key; + + struct schema { + static constexpr const char* type = "type"; + static constexpr const char* key = "key"; + }; + +protected: + virtual const config_type_structure& get_type_structure() const = 0; +}; + +} +#endif diff --git a/librecomp/src/config/ConfigRegistry.cpp b/librecomp/src/config/ConfigRegistry.cpp new file mode 100644 index 00000000..75e34e13 --- /dev/null +++ b/librecomp/src/config/ConfigRegistry.cpp @@ -0,0 +1,257 @@ +#include "config.hpp" + +using json = nlohmann::json; + +namespace recomp::config { + +ConfigRegistry config_registry = {{}, {}}; + +#define TODO_PARSE_ERROR(m, t) printf("Value at key: \"%s\" was invalid. Expected: \"%s\"\n", m, t) + +std::string get_string_in_json(const json& j, const std::string& key) { + std::string ret; + + auto find_it = j.find(key); + if (find_it != j.end()) { + auto at_val = j.at(key); + if (!at_val.is_string()) { + TODO_PARSE_ERROR(key, "string"); + } + + find_it->get_to(ret); + } + + return ret; +} + +std::string get_string_in_json_with_default(const json& j, const std::string& key, const std::string& default_val) { + std::string ret; + + auto find_it = j.find(key); + if (find_it != j.end()) { + auto at_val = j.at(key); + if (!at_val.is_string()) { + return default_val; + } + + find_it->get_to(ret); + } else { + return default_val; + } + + return ret; +} + +static bool validate_json_value_is_array(const json &j, const std::string& json_path, const std::string& arr_key) { + const auto &options = j.find(arr_key); + if (options == j.end()) { + TODO_PARSE_ERROR(json_path + "/" + arr_key, "array"); + return false; + } + const auto &opt_array = j[arr_key]; + + if (!opt_array.is_array()) { + TODO_PARSE_ERROR(json_path + "/" + arr_key, "array"); + return false; + } + + return true; +} + +// Option +void register_config_option( + const json& j, + const std::string& previous_key, // previous path before current key + const std::string& config_group, + const std::string& json_path // path to this json object from the root +) { + const std::string key = get_string_in_json(j, ConfigOption::schema::key); + const std::string this_key = previous_key + "/" + key; + + ConfigOptionType type = get_value_in_json(j, ConfigOption::schema::type); + + config_registry.key_ref_map[this_key] = { config_group, json_path }; + + switch (type) { + case ConfigOptionType::Checkbox: { + bool default_val = false; + if (j.find("default") != j.end()) { + default_val = get_value_in_json(j, "default"); + } + set_config_store_value_and_default(this_key, default_val, default_val); + break; + } + case ConfigOptionType::Color: { + const std::string rgb_strings[] = {"r", "g", "b"}; + + // read and ensure default is an array of numbers + if (j.find("default") != j.end()) { + if (!validate_json_value_is_array(j, json_path, "default")) { + return; + } + + const auto &col_array = j["default"]; + + for (int i = 0; i < col_array.size(); i++) { + const auto &j_opt = col_array[i]; + if (!j_opt.is_number()) { + TODO_PARSE_ERROR(this_key + "/default/" + std::to_string(i) , "number"); + return; + } + const int opt_val = j_opt.get(); + set_config_store_value_and_default(this_key + ":" + rgb_strings[i], opt_val, opt_val); + } + } else { + // "default" doesn't exist, set to black + set_config_store_value_and_default(this_key + ":" + rgb_strings[0], 0, 0); + set_config_store_value_and_default(this_key + ":" + rgb_strings[1], 0, 0); + set_config_store_value_and_default(this_key + ":" + rgb_strings[2], 0, 0); + } + + break; + } + case ConfigOptionType::RadioTabs: { + if (!validate_json_value_is_array(j, json_path, "values")) { + return; + } + + int default_val = 0; + if (j.find("default") != j.end()) { + const auto &opt_array = j["values"]; + const std::string default_val_string = get_string_in_json(j, "default"); + // Based on default value's string, find which option index corresponds + for (int i = 0; i < opt_array.size(); i++) { + const auto &j_opt = opt_array[i]; + if (!j_opt.is_string()) { + TODO_PARSE_ERROR(key + "/values/" + std::to_string(i) , "string"); + return; + } + const std::string opt_val = j_opt.get(); + if (opt_val == default_val_string) { + default_val = i; + break; + } + } + } + set_config_store_value_and_default(this_key, default_val, default_val); + break; + } + case ConfigOptionType::Range: { + int default_val = 0; + int max = 0; + int min = 0; + int step = 1; + + if (j.find("default") != j.end()) { + default_val = get_value_in_json(j, "default"); + } + + // Max is required + if (j.find("max") != j.end()) { + max = get_value_in_json(j, "max"); + if (default_val > max) default_val = max; + } else { + TODO_PARSE_ERROR(key + "/max", "int"); + return; + } + + if (j.find("min") != j.end()) { + min = get_value_in_json(j, "min"); + if (default_val < min) default_val = min; + } + + if (j.find("step") != j.end()) { + step = get_value_in_json(j, "step"); + } + + assert(max > min); + assert(step < max - min); + set_config_store_value_and_default(this_key, default_val, default_val); + break; + } + } + if (j.find("label") != j.end()) { + const std::string label = get_string_in_json(j, "label"); + set_config_store_value(this_key, label); + } + + if ((type == ConfigOptionType::Group || type == ConfigOptionType::CheckboxGroup) && j.find("options") != j.end()) { + if (!validate_json_value_is_array(j, json_path, "options")) { + return; + } + + const auto &opt_array = j["options"]; + + for (int i = 0; i < opt_array.size(); i++) { + const auto &el = opt_array[i]; + register_config_option( + el, + this_key, + config_group, + json_path + "/options/" + std::to_string(i) + ); + } + } +} + +void register_config(const std::string &json_str, const std::string &config_group) { + config_registry.group_json_map[config_group] = json::parse(json_str); + const auto &j = config_registry.group_json_map[config_group]; + + if (!j.is_array()) { + TODO_PARSE_ERROR("/", "array"); + return; + } + + for (int i = 0; i < j.size(); i++) { + const auto &el = j[i]; + register_config_option( + el, + config_group, + config_group, + "/" + std::to_string(i) // json_path at top level + ); + } +} + +void register_translation(const std::string &json_str, const std::string &config_group) { + const auto &j = json::parse(json_str); + + if (!j.is_object()) { + TODO_PARSE_ERROR("/", "object"); + return; + } + + for (auto& el : j.items()) + { + std::string translation_key = "translations/" + config_group + "/" + el.key(); + const std::string value = el.value(); + set_config_store_value(translation_key, value); + } +} + +json get_json_from_key(const std::string &config_key) { + if (config_registry.key_ref_map.find(config_key) == config_registry.key_ref_map.end()) { + // TODO: handle not finding config_key + printf("FAILURE: AddOptionTypeElement failed to find config_key '%s' in config_registry.key_ref_map\n", config_key); + } + const JSONRef& json_ref = config_registry.key_ref_map[config_key]; + const json& group_json = config_registry.group_json_map[json_ref.config_group]; + json::json_pointer pointer(json_ref.json_path); + return group_json[pointer]; +} + +bool config_key_is_base_group(const std::string &config_group_key) { + // determine if the key references a base group by checking the group_json_map + if (config_registry.group_json_map.find(config_group_key) == config_registry.group_json_map.end()) { + return false; + } + return true; +} + +json& get_group_json(const std::string &config_group_key) { + return config_registry.group_json_map[config_group_key]; +} + + +} // namespace recompui diff --git a/librecomp/src/config/ConfigRegistry.hpp b/librecomp/src/config/ConfigRegistry.hpp new file mode 100644 index 00000000..44ffdfbd --- /dev/null +++ b/librecomp/src/config/ConfigRegistry.hpp @@ -0,0 +1,66 @@ +#ifndef __RECOMP_CONFIG_REGISTRY_H__ +#define __RECOMP_CONFIG_REGISTRY_H__ + +#include +#include +#include +#include +#include +#include +#include "json/json.hpp" + +namespace recomp::config { + struct JSONRef { + std::string config_group; + std::string json_path; // used as a json pointer + }; + // config key -> JSONRef + typedef std::unordered_map config_registry_key_reference_map; + // config group -> json + typedef std::unordered_map config_registry_group_json_map; + + struct ConfigRegistry { + config_registry_key_reference_map key_ref_map; + config_registry_group_json_map group_json_map; + }; + + extern ConfigRegistry config_registry; + + void register_config(const std::string& json_str, const std::string& config_group); + void register_translation(const std::string &json_str, const std::string &config_group); + + nlohmann::json get_json_from_key(const std::string &config_key); + std::string get_string_in_json(const nlohmann::json& j, const std::string& key); + std::string get_string_in_json_with_default(const nlohmann::json& j, const std::string& key, const std::string& default_val); + bool config_key_is_base_group(const std::string &config_group_key); + nlohmann::json& get_group_json(const std::string &config_group_key); + + #define TODO_PARSE_ERROR(m, t) printf("Value at key: \"%s\" was invalid. Expected: \"%s\"\n", m, t) + + template + T get_value_in_json(const nlohmann::json& j, const std::string& key) { + T ret; + + auto find_it = j.find(key); + if (find_it != j.end()) { + auto at_val = j.at(key); + find_it->get_to(ret); + } + + return ret; + } + + template + T get_value_in_json_with_default(const nlohmann::json& j, const std::string& key, T default_val) { + T ret = default_val; + + auto find_it = j.find(key); + if (find_it != j.end()) { + auto at_val = j.at(key); + find_it->get_to(ret); + } + + return ret; + } +} +#endif diff --git a/librecomp/src/config_store.cpp b/librecomp/src/config/ConfigStore.cpp similarity index 93% rename from librecomp/src/config_store.cpp rename to librecomp/src/config/ConfigStore.cpp index 3e965400..ad019667 100644 --- a/librecomp/src/config_store.cpp +++ b/librecomp/src/config/ConfigStore.cpp @@ -1,6 +1,6 @@ -#include "config_store.hpp" +#include "config.hpp" -namespace recomp { +namespace recomp::config { ConfigStore config_store = {{}, {}}; diff --git a/librecomp/include/librecomp/config_store.hpp b/librecomp/src/config/ConfigStore.hpp similarity index 95% rename from librecomp/include/librecomp/config_store.hpp rename to librecomp/src/config/ConfigStore.hpp index 91ce7f7c..74c29523 100644 --- a/librecomp/include/librecomp/config_store.hpp +++ b/librecomp/src/config/ConfigStore.hpp @@ -21,13 +21,13 @@ struct string_hash { } }; -namespace recomp { +namespace recomp::config { typedef std::variant config_store_value; typedef std::unordered_map> config_store_map; struct ConfigStore { - recomp::config_store_map map; - recomp::config_store_map default_map; + config_store_map map; + config_store_map default_map; std::mutex store_mutex; std::mutex default_store_mutex; }; From e3016df67b2d6ec0d69e062d25153f625c7c13e5 Mon Sep 17 00:00:00 2001 From: thecozies <79979276+thecozies@users.noreply.github.com> Date: Sat, 20 Jul 2024 10:00:53 -0500 Subject: [PATCH 04/25] switch to using usings --- librecomp/src/config/ConfigRegistry.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/librecomp/src/config/ConfigRegistry.hpp b/librecomp/src/config/ConfigRegistry.hpp index 44ffdfbd..e9dd19d1 100644 --- a/librecomp/src/config/ConfigRegistry.hpp +++ b/librecomp/src/config/ConfigRegistry.hpp @@ -15,9 +15,9 @@ namespace recomp::config { std::string json_path; // used as a json pointer }; // config key -> JSONRef - typedef std::unordered_map config_registry_key_reference_map; + using config_registry_key_reference_map = std::unordered_map; // config group -> json - typedef std::unordered_map config_registry_group_json_map; + using config_registry_group_json_map = std::unordered_map; struct ConfigRegistry { config_registry_key_reference_map key_ref_map; From 15e0b39fc3fd60fe0cd355e6bc624dfc8a3c1060 Mon Sep 17 00:00:00 2001 From: thecozies <79979276+thecozies@users.noreply.github.com> Date: Mon, 22 Jul 2024 09:40:28 -0500 Subject: [PATCH 05/25] dropdown config type --- librecomp/src/config/ConfigRegistry.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/librecomp/src/config/ConfigRegistry.cpp b/librecomp/src/config/ConfigRegistry.cpp index 75e34e13..eaddf89d 100644 --- a/librecomp/src/config/ConfigRegistry.cpp +++ b/librecomp/src/config/ConfigRegistry.cpp @@ -110,6 +110,7 @@ void register_config_option( break; } + case ConfigOptionType::Dropdown: case ConfigOptionType::RadioTabs: { if (!validate_json_value_is_array(j, json_path, "values")) { return; From df02da1f6a8cbb9505a69b48691015217fdabe26 Mon Sep 17 00:00:00 2001 From: thecozies <79979276+thecozies@users.noreply.github.com> Date: Wed, 7 Aug 2024 09:18:06 -0500 Subject: [PATCH 06/25] Added TextField option type --- librecomp/src/config/ConfigOption.hpp | 8 ++++--- librecomp/src/config/ConfigRegistry.cpp | 14 ++++++++++++ librecomp/src/config/ConfigStore.hpp | 29 ++++++++++++++++++++++--- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/librecomp/src/config/ConfigOption.hpp b/librecomp/src/config/ConfigOption.hpp index 71a067b9..fef446f9 100644 --- a/librecomp/src/config/ConfigOption.hpp +++ b/librecomp/src/config/ConfigOption.hpp @@ -7,14 +7,15 @@ namespace recomp::config { enum class ConfigOptionType { - Label, + Label, // todo // Base types Checkbox, RadioTabs, Dropdown, Range, Color, - Trigger, + Button, // todo + TextField, // Group types CheckboxGroup, Group, @@ -28,7 +29,8 @@ NLOHMANN_JSON_SERIALIZE_ENUM(ConfigOptionType, { {ConfigOptionType::Dropdown, "Dropdown"}, {ConfigOptionType::Range, "Range"}, {ConfigOptionType::Color, "Color"}, - {ConfigOptionType::Trigger, "Trigger"}, + {ConfigOptionType::Button, "Button"}, + {ConfigOptionType::TextField, "TextField"}, {ConfigOptionType::CheckboxGroup, "CheckboxGroup"}, {ConfigOptionType::Group, "Group"} }); diff --git a/librecomp/src/config/ConfigRegistry.cpp b/librecomp/src/config/ConfigRegistry.cpp index eaddf89d..0b1375aa 100644 --- a/librecomp/src/config/ConfigRegistry.cpp +++ b/librecomp/src/config/ConfigRegistry.cpp @@ -73,6 +73,20 @@ void register_config_option( config_registry.key_ref_map[this_key] = { config_group, json_path }; switch (type) { + case ConfigOptionType::TextField: { + std::string default_val = ""; + if (j.find("default") != j.end()) { + default_val = get_string_in_json(j, "default"); + } + if (j.find("maxlength") != j.end()) { + if (!j["maxlength"].is_number()) { + TODO_PARSE_ERROR(this_key + "/maxlength" , "number"); + return; + } + } + set_config_store_value_and_default(this_key, default_val, default_val); + break; + } case ConfigOptionType::Checkbox: { bool default_val = false; if (j.find("default") != j.end()) { diff --git a/librecomp/src/config/ConfigStore.hpp b/librecomp/src/config/ConfigStore.hpp index 74c29523..20a2f754 100644 --- a/librecomp/src/config/ConfigStore.hpp +++ b/librecomp/src/config/ConfigStore.hpp @@ -21,9 +21,9 @@ struct string_hash { } }; -namespace recomp::config { - typedef std::variant config_store_value; - typedef std::unordered_map> config_store_map; +namespace recomp::config { + using config_store_value = std::variant; + using config_store_map = std::unordered_map>; struct ConfigStore { config_store_map map; @@ -34,6 +34,13 @@ namespace recomp::config { extern ConfigStore config_store; + // Index of variant type config_store_value + enum class ConfigStoreValueType { + Null, + String, + Int + }; + void set_config_store_value(std::string_view key, config_store_value value); void set_config_store_default_value(std::string_view key, config_store_value value); void set_config_store_value_and_default(std::string_view key, config_store_value value, config_store_value default_value); @@ -68,6 +75,22 @@ namespace recomp::config { return get_config_store_default_value(key); } }; + + // Get a value from the config store, if it doesn't exist then return the supplied value + template + T get_config_store_value_with_default(std::string_view key, T default_value) { + std::lock_guard lock{ config_store.store_mutex }; + auto it = config_store.map.find(key); + if (it != config_store.map.end()) { + if (std::holds_alternative(it->second)) { + return std::get(it->second); + } else { + throw std::runtime_error("Stored value is not of requested type"); + } + } else { + return default_value; + } + }; }; #endif From 499fa67b6264399a9214bac13c2f474a289733be Mon Sep 17 00:00:00 2001 From: thecozies <79979276+thecozies@users.noreply.github.com> Date: Mon, 2 Sep 2024 10:47:09 -0500 Subject: [PATCH 07/25] parse/validate button config type --- librecomp/src/config/ConfigRegistry.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/librecomp/src/config/ConfigRegistry.cpp b/librecomp/src/config/ConfigRegistry.cpp index 0b1375aa..199dad84 100644 --- a/librecomp/src/config/ConfigRegistry.cpp +++ b/librecomp/src/config/ConfigRegistry.cpp @@ -73,6 +73,22 @@ void register_config_option( config_registry.key_ref_map[this_key] = { config_group, json_path }; switch (type) { + case ConfigOptionType::Button: { + if (j.find("callback") != j.end()) { + if (!j["callback"].is_string()) { + TODO_PARSE_ERROR(this_key + "/callback" , "string"); + return; + } + } + if (j.find("variant") != j.end()) { + if (!j["variant"].is_string()) { + TODO_PARSE_ERROR(this_key + "/variant" , "string"); + return; + } + } + + break; + } case ConfigOptionType::TextField: { std::string default_val = ""; if (j.find("default") != j.end()) { From b11f4a91da414807e39f37f05bfd4bd63185e297 Mon Sep 17 00:00:00 2001 From: thecozies <79979276+thecozies@users.noreply.github.com> Date: Mon, 2 Sep 2024 10:47:59 -0500 Subject: [PATCH 08/25] wip callback registry --- librecomp/src/config/CallbackRegistry.cpp | 45 +++++++++++++++++++++++ librecomp/src/config/CallbackRegistry.hpp | 33 +++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 librecomp/src/config/CallbackRegistry.cpp create mode 100644 librecomp/src/config/CallbackRegistry.hpp diff --git a/librecomp/src/config/CallbackRegistry.cpp b/librecomp/src/config/CallbackRegistry.cpp new file mode 100644 index 00000000..d7b75856 --- /dev/null +++ b/librecomp/src/config/CallbackRegistry.cpp @@ -0,0 +1,45 @@ +#include "CallbackRegistry.hpp" + +namespace recomp::config { + +callback_registry_map callback_registry = {}; + +static recomp_callback_internal nullcb; + +void register_callback(const std::string& config_group, const std::string& key, recomp_callback& callback) { + callback_registry[config_group + "/" + key] = callback; +}; + +const void *get_pointer_from_conf_value(config_store_value& val) { + if (std::holds_alternative(val)) { + return &std::get(val); + } + if (std::holds_alternative(val)) { + return &std::get(val); + } + return nullptr; +} + +void invoke_callback(const std::string& key) { + auto find_it = callback_registry.find(key); + if (find_it == callback_registry.end()) { + printf("ERROR: Could not locate callback at '%s'.\n", key); + return; + } + + auto& cb = callback_registry.at(key); + + config_store_value& val = get_config_store_value(key); + + if (const auto& func = *std::get_if(&cb)) { + func(key, val); + } else if (const auto& func = *std::get_if(&cb)) { + func( + key.c_str(), + get_pointer_from_conf_value(val), + (ConfigStoreValueType)val.index() + ); + } +}; + +} diff --git a/librecomp/src/config/CallbackRegistry.hpp b/librecomp/src/config/CallbackRegistry.hpp new file mode 100644 index 00000000..1f1e782c --- /dev/null +++ b/librecomp/src/config/CallbackRegistry.hpp @@ -0,0 +1,33 @@ +#ifndef __RECOMP_CALLBACK_REGISTRY_H__ +#define __RECOMP_CALLBACK_REGISTRY_H__ + +#include +#include +#include +#include +#include +#include +#include "json/json.hpp" +#include "ConfigStore.hpp" + +namespace recomp::config { + using recomp_callback_internal = std::function; + + using recomp_callback_external = std::function; + + using recomp_callback = std::variant; + + using callback_registry_map = std::unordered_map; + + extern callback_registry_map callback_registry; + + void register_callback(const std::string& config_group, const std::string& key, recomp_callback& callback); +} +#endif From 83045aec3b6e3c6ca91bd5537adefdc8f5cdd405 Mon Sep 17 00:00:00 2001 From: Dario Date: Thu, 16 Jan 2025 22:35:05 -0300 Subject: [PATCH 09/25] Add auto enabled. --- librecomp/include/librecomp/mods.hpp | 1 + librecomp/src/recomp.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index b5c421d8..6bf004ae 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -461,6 +461,7 @@ namespace recomp { void scan_mods(); void enable_mod(const std::string& mod_id, bool enabled); bool is_mod_enabled(const std::string& mod_id); + bool is_mod_auto_enabled(const std::string& mod_id); ModContentTypeId register_mod_content_type(const ModContentType& type); bool register_mod_container_type(const std::string& extension, const std::vector& content_types, bool requires_manifest); } diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index da26074e..a64da686 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -510,6 +510,11 @@ bool recomp::mods::is_mod_enabled(const std::string& mod_id) { return mod_context->is_mod_enabled(mod_id); } +bool recomp::mods::is_mod_auto_enabled(const std::string& mod_id) { + std::lock_guard lock{ mod_context_mutex }; + return false; // TODO +} + std::vector recomp::mods::get_mod_details(const std::string& mod_game_id) { std::lock_guard lock { mod_context_mutex }; return mod_context->get_mod_details(mod_game_id); From 985e600f7af7396ef53f0f7a69f8903d21ce9076 Mon Sep 17 00:00:00 2001 From: Dario Date: Sun, 19 Jan 2025 17:14:32 -0300 Subject: [PATCH 10/25] Cleanup. --- librecomp/CMakeLists.txt | 3 - librecomp/include/librecomp/config.hpp | 6 - librecomp/src/config/CallbackRegistry.cpp | 45 ---- librecomp/src/config/CallbackRegistry.hpp | 33 --- librecomp/src/config/ConfigOption.cpp | 31 --- librecomp/src/config/ConfigOption.hpp | 69 ------ librecomp/src/config/ConfigRegistry.cpp | 288 ---------------------- librecomp/src/config/ConfigRegistry.hpp | 66 ----- librecomp/src/config/ConfigStore.cpp | 22 -- librecomp/src/config/ConfigStore.hpp | 96 -------- 10 files changed, 659 deletions(-) delete mode 100644 librecomp/src/config/CallbackRegistry.cpp delete mode 100644 librecomp/src/config/CallbackRegistry.hpp delete mode 100644 librecomp/src/config/ConfigOption.cpp delete mode 100644 librecomp/src/config/ConfigOption.hpp delete mode 100644 librecomp/src/config/ConfigRegistry.cpp delete mode 100644 librecomp/src/config/ConfigRegistry.hpp delete mode 100644 librecomp/src/config/ConfigStore.cpp delete mode 100644 librecomp/src/config/ConfigStore.hpp diff --git a/librecomp/CMakeLists.txt b/librecomp/CMakeLists.txt index f698b5f9..61db595e 100644 --- a/librecomp/CMakeLists.txt +++ b/librecomp/CMakeLists.txt @@ -8,9 +8,6 @@ set(CMAKE_CXX_EXTENSIONS OFF) # Define the library add_library(librecomp STATIC "${CMAKE_CURRENT_SOURCE_DIR}/src/ai.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/config/ConfigOption.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/config/ConfigRegistry.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/config/ConfigStore.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/cont.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/dp.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/eep.cpp" diff --git a/librecomp/include/librecomp/config.hpp b/librecomp/include/librecomp/config.hpp index 4ddbfc30..9df4e77b 100644 --- a/librecomp/include/librecomp/config.hpp +++ b/librecomp/include/librecomp/config.hpp @@ -8,10 +8,4 @@ #include #include "json/json.hpp" -// namespace recomp::config - -#include "config/ConfigStore.hpp" -#include "config/ConfigRegistry.hpp" -#include "config/ConfigOption.hpp" - #endif diff --git a/librecomp/src/config/CallbackRegistry.cpp b/librecomp/src/config/CallbackRegistry.cpp deleted file mode 100644 index d7b75856..00000000 --- a/librecomp/src/config/CallbackRegistry.cpp +++ /dev/null @@ -1,45 +0,0 @@ -#include "CallbackRegistry.hpp" - -namespace recomp::config { - -callback_registry_map callback_registry = {}; - -static recomp_callback_internal nullcb; - -void register_callback(const std::string& config_group, const std::string& key, recomp_callback& callback) { - callback_registry[config_group + "/" + key] = callback; -}; - -const void *get_pointer_from_conf_value(config_store_value& val) { - if (std::holds_alternative(val)) { - return &std::get(val); - } - if (std::holds_alternative(val)) { - return &std::get(val); - } - return nullptr; -} - -void invoke_callback(const std::string& key) { - auto find_it = callback_registry.find(key); - if (find_it == callback_registry.end()) { - printf("ERROR: Could not locate callback at '%s'.\n", key); - return; - } - - auto& cb = callback_registry.at(key); - - config_store_value& val = get_config_store_value(key); - - if (const auto& func = *std::get_if(&cb)) { - func(key, val); - } else if (const auto& func = *std::get_if(&cb)) { - func( - key.c_str(), - get_pointer_from_conf_value(val), - (ConfigStoreValueType)val.index() - ); - } -}; - -} diff --git a/librecomp/src/config/CallbackRegistry.hpp b/librecomp/src/config/CallbackRegistry.hpp deleted file mode 100644 index 1f1e782c..00000000 --- a/librecomp/src/config/CallbackRegistry.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef __RECOMP_CALLBACK_REGISTRY_H__ -#define __RECOMP_CALLBACK_REGISTRY_H__ - -#include -#include -#include -#include -#include -#include -#include "json/json.hpp" -#include "ConfigStore.hpp" - -namespace recomp::config { - using recomp_callback_internal = std::function; - - using recomp_callback_external = std::function; - - using recomp_callback = std::variant; - - using callback_registry_map = std::unordered_map; - - extern callback_registry_map callback_registry; - - void register_callback(const std::string& config_group, const std::string& key, recomp_callback& callback); -} -#endif diff --git a/librecomp/src/config/ConfigOption.cpp b/librecomp/src/config/ConfigOption.cpp deleted file mode 100644 index c6ca3f25..00000000 --- a/librecomp/src/config/ConfigOption.cpp +++ /dev/null @@ -1,31 +0,0 @@ - -#include "config.hpp" - -#include -#include - -using json = nlohmann::json; -namespace recomp::config { - -bool ConfigOption::validate(json& opt_json) { - auto type_struct = get_type_structure(); - for (const auto& [key, expected_type] : type_struct) { - if (!opt_json.contains(key) || opt_json[key].type() != expected_type) { - return false; - } - } - return true; -} - -ConfigOption::ConfigOption(json& opt_json) -{ - type = opt_json[ConfigOption::schema::type]; - key = opt_json[ConfigOption::schema::key]; -} - -ConfigOption::~ConfigOption() -{ - -} - -} // namespace Rml diff --git a/librecomp/src/config/ConfigOption.hpp b/librecomp/src/config/ConfigOption.hpp deleted file mode 100644 index fef446f9..00000000 --- a/librecomp/src/config/ConfigOption.hpp +++ /dev/null @@ -1,69 +0,0 @@ -#ifndef __RECOMP_CONFIG_OPTION_H__ -#define __RECOMP_CONFIG_OPTION_H__ - -#include -#include "json/json.hpp" - -namespace recomp::config { - -enum class ConfigOptionType { - Label, // todo - // Base types - Checkbox, - RadioTabs, - Dropdown, - Range, - Color, - Button, // todo - TextField, - // Group types - CheckboxGroup, - Group, - OptionCount -}; - -NLOHMANN_JSON_SERIALIZE_ENUM(ConfigOptionType, { - {ConfigOptionType::Label, "Label"}, - {ConfigOptionType::Checkbox, "Checkbox"}, - {ConfigOptionType::RadioTabs, "RadioTabs"}, - {ConfigOptionType::Dropdown, "Dropdown"}, - {ConfigOptionType::Range, "Range"}, - {ConfigOptionType::Color, "Color"}, - {ConfigOptionType::Button, "Button"}, - {ConfigOptionType::TextField, "TextField"}, - {ConfigOptionType::CheckboxGroup, "CheckboxGroup"}, - {ConfigOptionType::Group, "Group"} -}); - -typedef std::unordered_map config_type_structure; - -inline bool config_option_is_group(ConfigOptionType option_type) { - return option_type == ConfigOptionType::Group || option_type == ConfigOptionType::CheckboxGroup; -} - -/** - Base Config Option class. Defines what type of option or group it is, and - sets the key. - */ - -class ConfigOption { -public: - ConfigOption(nlohmann::json& opt_json); - virtual ~ConfigOption(); - - bool validate(nlohmann::json& opt_json); - - ConfigOptionType type; - std::string key; - - struct schema { - static constexpr const char* type = "type"; - static constexpr const char* key = "key"; - }; - -protected: - virtual const config_type_structure& get_type_structure() const = 0; -}; - -} -#endif diff --git a/librecomp/src/config/ConfigRegistry.cpp b/librecomp/src/config/ConfigRegistry.cpp deleted file mode 100644 index 199dad84..00000000 --- a/librecomp/src/config/ConfigRegistry.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#include "config.hpp" - -using json = nlohmann::json; - -namespace recomp::config { - -ConfigRegistry config_registry = {{}, {}}; - -#define TODO_PARSE_ERROR(m, t) printf("Value at key: \"%s\" was invalid. Expected: \"%s\"\n", m, t) - -std::string get_string_in_json(const json& j, const std::string& key) { - std::string ret; - - auto find_it = j.find(key); - if (find_it != j.end()) { - auto at_val = j.at(key); - if (!at_val.is_string()) { - TODO_PARSE_ERROR(key, "string"); - } - - find_it->get_to(ret); - } - - return ret; -} - -std::string get_string_in_json_with_default(const json& j, const std::string& key, const std::string& default_val) { - std::string ret; - - auto find_it = j.find(key); - if (find_it != j.end()) { - auto at_val = j.at(key); - if (!at_val.is_string()) { - return default_val; - } - - find_it->get_to(ret); - } else { - return default_val; - } - - return ret; -} - -static bool validate_json_value_is_array(const json &j, const std::string& json_path, const std::string& arr_key) { - const auto &options = j.find(arr_key); - if (options == j.end()) { - TODO_PARSE_ERROR(json_path + "/" + arr_key, "array"); - return false; - } - const auto &opt_array = j[arr_key]; - - if (!opt_array.is_array()) { - TODO_PARSE_ERROR(json_path + "/" + arr_key, "array"); - return false; - } - - return true; -} - -// Option -void register_config_option( - const json& j, - const std::string& previous_key, // previous path before current key - const std::string& config_group, - const std::string& json_path // path to this json object from the root -) { - const std::string key = get_string_in_json(j, ConfigOption::schema::key); - const std::string this_key = previous_key + "/" + key; - - ConfigOptionType type = get_value_in_json(j, ConfigOption::schema::type); - - config_registry.key_ref_map[this_key] = { config_group, json_path }; - - switch (type) { - case ConfigOptionType::Button: { - if (j.find("callback") != j.end()) { - if (!j["callback"].is_string()) { - TODO_PARSE_ERROR(this_key + "/callback" , "string"); - return; - } - } - if (j.find("variant") != j.end()) { - if (!j["variant"].is_string()) { - TODO_PARSE_ERROR(this_key + "/variant" , "string"); - return; - } - } - - break; - } - case ConfigOptionType::TextField: { - std::string default_val = ""; - if (j.find("default") != j.end()) { - default_val = get_string_in_json(j, "default"); - } - if (j.find("maxlength") != j.end()) { - if (!j["maxlength"].is_number()) { - TODO_PARSE_ERROR(this_key + "/maxlength" , "number"); - return; - } - } - set_config_store_value_and_default(this_key, default_val, default_val); - break; - } - case ConfigOptionType::Checkbox: { - bool default_val = false; - if (j.find("default") != j.end()) { - default_val = get_value_in_json(j, "default"); - } - set_config_store_value_and_default(this_key, default_val, default_val); - break; - } - case ConfigOptionType::Color: { - const std::string rgb_strings[] = {"r", "g", "b"}; - - // read and ensure default is an array of numbers - if (j.find("default") != j.end()) { - if (!validate_json_value_is_array(j, json_path, "default")) { - return; - } - - const auto &col_array = j["default"]; - - for (int i = 0; i < col_array.size(); i++) { - const auto &j_opt = col_array[i]; - if (!j_opt.is_number()) { - TODO_PARSE_ERROR(this_key + "/default/" + std::to_string(i) , "number"); - return; - } - const int opt_val = j_opt.get(); - set_config_store_value_and_default(this_key + ":" + rgb_strings[i], opt_val, opt_val); - } - } else { - // "default" doesn't exist, set to black - set_config_store_value_and_default(this_key + ":" + rgb_strings[0], 0, 0); - set_config_store_value_and_default(this_key + ":" + rgb_strings[1], 0, 0); - set_config_store_value_and_default(this_key + ":" + rgb_strings[2], 0, 0); - } - - break; - } - case ConfigOptionType::Dropdown: - case ConfigOptionType::RadioTabs: { - if (!validate_json_value_is_array(j, json_path, "values")) { - return; - } - - int default_val = 0; - if (j.find("default") != j.end()) { - const auto &opt_array = j["values"]; - const std::string default_val_string = get_string_in_json(j, "default"); - // Based on default value's string, find which option index corresponds - for (int i = 0; i < opt_array.size(); i++) { - const auto &j_opt = opt_array[i]; - if (!j_opt.is_string()) { - TODO_PARSE_ERROR(key + "/values/" + std::to_string(i) , "string"); - return; - } - const std::string opt_val = j_opt.get(); - if (opt_val == default_val_string) { - default_val = i; - break; - } - } - } - set_config_store_value_and_default(this_key, default_val, default_val); - break; - } - case ConfigOptionType::Range: { - int default_val = 0; - int max = 0; - int min = 0; - int step = 1; - - if (j.find("default") != j.end()) { - default_val = get_value_in_json(j, "default"); - } - - // Max is required - if (j.find("max") != j.end()) { - max = get_value_in_json(j, "max"); - if (default_val > max) default_val = max; - } else { - TODO_PARSE_ERROR(key + "/max", "int"); - return; - } - - if (j.find("min") != j.end()) { - min = get_value_in_json(j, "min"); - if (default_val < min) default_val = min; - } - - if (j.find("step") != j.end()) { - step = get_value_in_json(j, "step"); - } - - assert(max > min); - assert(step < max - min); - set_config_store_value_and_default(this_key, default_val, default_val); - break; - } - } - if (j.find("label") != j.end()) { - const std::string label = get_string_in_json(j, "label"); - set_config_store_value(this_key, label); - } - - if ((type == ConfigOptionType::Group || type == ConfigOptionType::CheckboxGroup) && j.find("options") != j.end()) { - if (!validate_json_value_is_array(j, json_path, "options")) { - return; - } - - const auto &opt_array = j["options"]; - - for (int i = 0; i < opt_array.size(); i++) { - const auto &el = opt_array[i]; - register_config_option( - el, - this_key, - config_group, - json_path + "/options/" + std::to_string(i) - ); - } - } -} - -void register_config(const std::string &json_str, const std::string &config_group) { - config_registry.group_json_map[config_group] = json::parse(json_str); - const auto &j = config_registry.group_json_map[config_group]; - - if (!j.is_array()) { - TODO_PARSE_ERROR("/", "array"); - return; - } - - for (int i = 0; i < j.size(); i++) { - const auto &el = j[i]; - register_config_option( - el, - config_group, - config_group, - "/" + std::to_string(i) // json_path at top level - ); - } -} - -void register_translation(const std::string &json_str, const std::string &config_group) { - const auto &j = json::parse(json_str); - - if (!j.is_object()) { - TODO_PARSE_ERROR("/", "object"); - return; - } - - for (auto& el : j.items()) - { - std::string translation_key = "translations/" + config_group + "/" + el.key(); - const std::string value = el.value(); - set_config_store_value(translation_key, value); - } -} - -json get_json_from_key(const std::string &config_key) { - if (config_registry.key_ref_map.find(config_key) == config_registry.key_ref_map.end()) { - // TODO: handle not finding config_key - printf("FAILURE: AddOptionTypeElement failed to find config_key '%s' in config_registry.key_ref_map\n", config_key); - } - const JSONRef& json_ref = config_registry.key_ref_map[config_key]; - const json& group_json = config_registry.group_json_map[json_ref.config_group]; - json::json_pointer pointer(json_ref.json_path); - return group_json[pointer]; -} - -bool config_key_is_base_group(const std::string &config_group_key) { - // determine if the key references a base group by checking the group_json_map - if (config_registry.group_json_map.find(config_group_key) == config_registry.group_json_map.end()) { - return false; - } - return true; -} - -json& get_group_json(const std::string &config_group_key) { - return config_registry.group_json_map[config_group_key]; -} - - -} // namespace recompui diff --git a/librecomp/src/config/ConfigRegistry.hpp b/librecomp/src/config/ConfigRegistry.hpp deleted file mode 100644 index e9dd19d1..00000000 --- a/librecomp/src/config/ConfigRegistry.hpp +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef __RECOMP_CONFIG_REGISTRY_H__ -#define __RECOMP_CONFIG_REGISTRY_H__ - -#include -#include -#include -#include -#include -#include -#include "json/json.hpp" - -namespace recomp::config { - struct JSONRef { - std::string config_group; - std::string json_path; // used as a json pointer - }; - // config key -> JSONRef - using config_registry_key_reference_map = std::unordered_map; - // config group -> json - using config_registry_group_json_map = std::unordered_map; - - struct ConfigRegistry { - config_registry_key_reference_map key_ref_map; - config_registry_group_json_map group_json_map; - }; - - extern ConfigRegistry config_registry; - - void register_config(const std::string& json_str, const std::string& config_group); - void register_translation(const std::string &json_str, const std::string &config_group); - - nlohmann::json get_json_from_key(const std::string &config_key); - std::string get_string_in_json(const nlohmann::json& j, const std::string& key); - std::string get_string_in_json_with_default(const nlohmann::json& j, const std::string& key, const std::string& default_val); - bool config_key_is_base_group(const std::string &config_group_key); - nlohmann::json& get_group_json(const std::string &config_group_key); - - #define TODO_PARSE_ERROR(m, t) printf("Value at key: \"%s\" was invalid. Expected: \"%s\"\n", m, t) - - template - T get_value_in_json(const nlohmann::json& j, const std::string& key) { - T ret; - - auto find_it = j.find(key); - if (find_it != j.end()) { - auto at_val = j.at(key); - find_it->get_to(ret); - } - - return ret; - } - - template - T get_value_in_json_with_default(const nlohmann::json& j, const std::string& key, T default_val) { - T ret = default_val; - - auto find_it = j.find(key); - if (find_it != j.end()) { - auto at_val = j.at(key); - find_it->get_to(ret); - } - - return ret; - } -} -#endif diff --git a/librecomp/src/config/ConfigStore.cpp b/librecomp/src/config/ConfigStore.cpp deleted file mode 100644 index ad019667..00000000 --- a/librecomp/src/config/ConfigStore.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include "config.hpp" - -namespace recomp::config { - -ConfigStore config_store = {{}, {}}; - -void set_config_store_value(std::string_view key, config_store_value value) { - std::lock_guard lock{ config_store.store_mutex }; - config_store.map[std::string{key}] = value; -} - -void set_config_store_default_value(std::string_view key, config_store_value value) { - std::lock_guard lock{ config_store.default_store_mutex }; - config_store.default_map[std::string{key}] = value; -} - -void set_config_store_value_and_default(std::string_view key, config_store_value value, config_store_value default_value) { - set_config_store_value(key, value); - set_config_store_default_value(key, default_value); -} - -} diff --git a/librecomp/src/config/ConfigStore.hpp b/librecomp/src/config/ConfigStore.hpp deleted file mode 100644 index 20a2f754..00000000 --- a/librecomp/src/config/ConfigStore.hpp +++ /dev/null @@ -1,96 +0,0 @@ -#ifndef __RECOMP_CONFIG_STORE_H__ -#define __RECOMP_CONFIG_STORE_H__ - -#include -#include -#include -#include -#include - -// Use a custom hash class to enable hetereogenous lookup -struct string_hash { - using is_transparent = void; - [[nodiscard]] size_t operator()(const char *txt) const { - return std::hash{}(txt); - } - [[nodiscard]] size_t operator()(std::string_view txt) const { - return std::hash{}(txt); - } - [[nodiscard]] size_t operator()(const std::string &txt) const { - return std::hash{}(txt); - } -}; - -namespace recomp::config { - using config_store_value = std::variant; - using config_store_map = std::unordered_map>; - - struct ConfigStore { - config_store_map map; - config_store_map default_map; - std::mutex store_mutex; - std::mutex default_store_mutex; - }; - - extern ConfigStore config_store; - - // Index of variant type config_store_value - enum class ConfigStoreValueType { - Null, - String, - Int - }; - - void set_config_store_value(std::string_view key, config_store_value value); - void set_config_store_default_value(std::string_view key, config_store_value value); - void set_config_store_value_and_default(std::string_view key, config_store_value value, config_store_value default_value); - - template - T get_config_store_default_value(std::string_view key) { - std::lock_guard lock{ config_store.default_store_mutex }; - auto it = config_store.default_map.find(key); - if (it != config_store.default_map.end()) { - if (std::holds_alternative(it->second)) { - return std::get(it->second); - } else { - throw std::runtime_error("Stored value is not of requested type"); - } - } else { - throw std::runtime_error("Key not found"); - } - }; - - // Get a value from the config store, if it doesn't exist then return the default value - template - T get_config_store_value(std::string_view key) { - std::lock_guard lock{ config_store.store_mutex }; - auto it = config_store.map.find(key); - if (it != config_store.map.end()) { - if (std::holds_alternative(it->second)) { - return std::get(it->second); - } else { - throw std::runtime_error("Stored value is not of requested type"); - } - } else { - return get_config_store_default_value(key); - } - }; - - // Get a value from the config store, if it doesn't exist then return the supplied value - template - T get_config_store_value_with_default(std::string_view key, T default_value) { - std::lock_guard lock{ config_store.store_mutex }; - auto it = config_store.map.find(key); - if (it != config_store.map.end()) { - if (std::holds_alternative(it->second)) { - return std::get(it->second); - } else { - throw std::runtime_error("Stored value is not of requested type"); - } - } else { - return default_value; - } - }; -}; - -#endif From 0b18a3529284f0ac830d0368f8616f191882de78 Mon Sep 17 00:00:00 2001 From: Dario Date: Sun, 19 Jan 2025 23:33:31 -0300 Subject: [PATCH 11/25] Add support for config schema. --- librecomp/include/librecomp/config.hpp | 11 -- librecomp/include/librecomp/mods.hpp | 46 +++++ librecomp/src/mod_manifest.cpp | 250 +++++++++++++++++++++++-- librecomp/src/mods.cpp | 11 ++ librecomp/src/recomp.cpp | 5 + 5 files changed, 292 insertions(+), 31 deletions(-) delete mode 100644 librecomp/include/librecomp/config.hpp diff --git a/librecomp/include/librecomp/config.hpp b/librecomp/include/librecomp/config.hpp deleted file mode 100644 index 9df4e77b..00000000 --- a/librecomp/include/librecomp/config.hpp +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef __RECOMP_CONFIG_H__ -#define __RECOMP_CONFIG_H__ - -#include -#include -#include -#include -#include -#include "json/json.hpp" - -#endif diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index 6bf004ae..4dcbea44 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -65,6 +65,9 @@ namespace recomp { FailedToParseManifest, InvalidManifestSchema, IncorrectManifestFieldType, + MissingConfigSchemaField, + IncorrectConfigSchemaType, + InvalidConfigSchemaDefault, InvalidVersionString, InvalidMinimumRecompVersionString, InvalidDependencyString, @@ -115,6 +118,13 @@ namespace recomp { std::string error_to_string(CodeModLoadError); + enum class ConfigOptionType { + None, + Enum, + Number, + String + }; + struct ModFileHandle { virtual ~ModFileHandle() = default; virtual std::vector read_file(const std::string& filepath, bool& exists) const = 0; @@ -154,6 +164,38 @@ namespace recomp { Version version; }; + struct ConfigOptionEnum { + std::vector options; + uint32_t default_value = 0; + }; + + struct ConfigOptionNumber { + double min = 0.0; + double max = 0.0; + double step = 0.0; + int precision = 0; + bool percent = false; + double default_value = 0.0; + }; + + struct ConfigOptionString { + std::string default_value; + }; + + typedef std::variant ConfigOptionVariant; + + struct ConfigOption { + std::string id; + std::string name; + std::string description; + ConfigOptionType type; + ConfigOptionVariant variant; + }; + + struct ConfigSchema { + std::vector options; + }; + struct ModDetails { std::string mod_id; std::string display_name; @@ -176,6 +218,7 @@ namespace recomp { std::vector authors; std::vector dependencies; std::unordered_map dependencies_by_id; + ConfigSchema config_schema; Version minimum_recomp_version; Version version; bool runtime_toggleable; @@ -258,6 +301,7 @@ namespace recomp { std::vector load_mods(const GameEntry& game_entry, uint8_t* rdram, int32_t load_address, uint32_t& ram_used); void unload_mods(); std::vector get_mod_details(const std::string& mod_game_id); + const ConfigSchema &get_mod_config_schema(const std::string &mod_id) const; ModContentTypeId register_content_type(const ModContentType& type); bool register_container_type(const std::string& extension, const std::vector& content_types, bool requires_manifest); ModContentTypeId get_code_content_type() const { return code_content_type_id; } @@ -299,6 +343,7 @@ namespace recomp { // Tracks which hook slots have already been processed. Used to regenerate vanilla functions as needed // to add hooks to any functions that weren't already replaced by a mod. std::vector processed_hook_slots; + ConfigSchema empty_schema; size_t num_events = 0; ModContentTypeId code_content_type_id; size_t active_game = (size_t)-1; @@ -462,6 +507,7 @@ namespace recomp { void enable_mod(const std::string& mod_id, bool enabled); bool is_mod_enabled(const std::string& mod_id); bool is_mod_auto_enabled(const std::string& mod_id); + const ConfigSchema &get_mod_config_schema(const std::string &mod_id); ModContentTypeId register_mod_content_type(const ModContentType& type); bool register_mod_container_type(const std::string& extension, const std::vector& content_types, bool requires_manifest); } diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp index 0d71fe94..515c3658 100644 --- a/librecomp/src/mod_manifest.cpp +++ b/librecomp/src/mod_manifest.cpp @@ -131,16 +131,6 @@ bool recomp::mods::LooseModFileHandle::file_exists(const std::string& filepath) return true; } -enum class ManifestField { - GameModId, - Id, - Version, - Authors, - MinimumRecompVersion, - Dependencies, - NativeLibraries, -}; - const std::string game_mod_id_key = "game_id"; const std::string mod_id_key = "id"; const std::string display_name_key = "display_name"; @@ -151,16 +141,7 @@ const std::string authors_key = "authors"; const std::string minimum_recomp_version_key = "minimum_recomp_version"; const std::string dependencies_key = "dependencies"; const std::string native_libraries_key = "native_libraries"; - -std::unordered_map field_map { - { game_mod_id_key, ManifestField::GameModId }, - { mod_id_key, ManifestField::Id }, - { version_key, ManifestField::Version }, - { authors_key, ManifestField::Authors }, - { minimum_recomp_version_key, ManifestField::MinimumRecompVersion }, - { dependencies_key, ManifestField::Dependencies }, - { native_libraries_key, ManifestField::NativeLibraries }, -}; +const std::string config_schema_key = "config_schema"; template bool get_to(const nlohmann::json& val, T2& out) { @@ -298,6 +279,200 @@ recomp::mods::ModOpenError try_get_vec(std::vector& out, const nlohmann::jso return recomp::mods::ModOpenError::Good; } +constexpr std::string_view config_schema_id_key = "id"; +constexpr std::string_view config_schema_name_key = "name"; +constexpr std::string_view config_schema_description_key = "description"; +constexpr std::string_view config_schema_type_key = "type"; +constexpr std::string_view config_schema_min_key = "min"; +constexpr std::string_view config_schema_max_key = "max"; +constexpr std::string_view config_schema_step_key = "step"; +constexpr std::string_view config_schema_precision_key = "precision"; +constexpr std::string_view config_schema_percent_key = "percent"; +constexpr std::string_view config_schema_options_key = "options"; +constexpr std::string_view config_schema_default_key = "min"; + +std::unordered_map config_option_map{ + { "Enum", recomp::mods::ConfigOptionType::Enum}, + { "Number", recomp::mods::ConfigOptionType::Number}, + { "String", recomp::mods::ConfigOptionType::String}, +}; + +recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::json &config_schema_json, recomp::mods::ModManifest &ret, std::string &error_param) { + using json = nlohmann::json; + recomp::mods::ConfigOption option; + auto id = config_schema_json.find(config_schema_id_key); + if (id != config_schema_json.end()) { + if (!get_to(*id, option.id)) { + error_param = config_schema_id_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + else { + error_param = config_schema_id_key; + return recomp::mods::ModOpenError::MissingConfigSchemaField; + } + + auto name = config_schema_json.find(config_schema_name_key); + if (name != config_schema_json.end()) { + if (!get_to(*name, option.name)) { + error_param = config_schema_name_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + else { + error_param = config_schema_name_key; + return recomp::mods::ModOpenError::MissingConfigSchemaField; + } + + auto description = config_schema_json.find(config_schema_description_key); + if (description != config_schema_json.end()) { + if (!get_to(*description, option.description)) { + error_param = config_schema_description_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + + auto type = config_schema_json.find(config_schema_type_key); + if (type != config_schema_json.end()) { + std::string type_string; + if (!get_to(*type, type_string)) { + error_param = config_schema_type_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + else { + auto it = config_option_map.find(type_string); + if (it != config_option_map.end()) { + option.type = it->second; + } + else { + error_param = config_schema_type_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + } + else { + error_param = config_schema_type_key; + return recomp::mods::ModOpenError::MissingConfigSchemaField; + } + + switch (option.type) { + case recomp::mods::ConfigOptionType::Enum: + { + recomp::mods::ConfigOptionEnum option_enum; + + auto options = config_schema_json.find(config_schema_options_key); + if (options != config_schema_json.end()) { + if (!get_to_vec(*options, option_enum.options)) { + error_param = config_schema_options_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + + auto default_value = config_schema_json.find(config_schema_default_key); + if (default_value != config_schema_json.end()) { + std::string default_value_string; + if (get_to(*default_value, default_value_string)) { + auto it = std::find(option_enum.options.begin(), option_enum.options.end(), default_value_string); + if (it != option_enum.options.end()) { + option_enum.default_value = uint32_t(it - option_enum.options.begin()); + } + else { + error_param = config_schema_default_key; + return recomp::mods::ModOpenError::InvalidConfigSchemaDefault; + } + } + else { + error_param = config_schema_default_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + + option.variant = option_enum; + + } + break; + case recomp::mods::ConfigOptionType::Number: + { + recomp::mods::ConfigOptionNumber option_number; + + auto min = config_schema_json.find(config_schema_min_key); + if (min != config_schema_json.end()) { + if (!get_to(*min, option_number.min)) { + error_param = config_schema_min_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + + auto max = config_schema_json.find(config_schema_max_key); + if (max != config_schema_json.end()) { + if (!get_to(*max, option_number.max)) { + error_param = config_schema_max_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + + auto step = config_schema_json.find(config_schema_step_key); + if (step != config_schema_json.end()) { + if (!get_to(*step, option_number.step)) { + error_param = config_schema_step_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + + auto precision = config_schema_json.find(config_schema_precision_key); + if (precision != config_schema_json.end()) { + int64_t precision_int64; + if (get_to(*precision, precision_int64)) { + option_number.precision = precision_int64; + } + else { + error_param = config_schema_precision_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + + auto percent = config_schema_json.find(config_schema_percent_key); + if (percent != config_schema_json.end()) { + if (!get_to(*percent, option_number.percent)) { + error_param = config_schema_percent_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + + auto default_value = config_schema_json.find(config_schema_default_key); + if (default_value != config_schema_json.end()) { + if (!get_to(*default_value, option_number.default_value)) { + error_param = config_schema_default_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + + option.variant = option_number; + } + break; + case recomp::mods::ConfigOptionType::String: + { + recomp::mods::ConfigOptionString option_string; + + auto default_value = config_schema_json.find(config_schema_default_key); + if (default_value != config_schema_json.end()) { + if (!get_to(*default_value, option_string.default_value)) { + error_param = config_schema_default_key; + return recomp::mods::ModOpenError::IncorrectConfigSchemaType; + } + } + + option.variant = option_string; + } + break; + default: + break; + } + + ret.config_schema.options.push_back(option); + return recomp::mods::ModOpenError::Good; +} + recomp::mods::ModOpenError parse_manifest(recomp::mods::ModManifest& ret, const std::vector& manifest_data, std::string& error_param) { using json = nlohmann::json; json manifest_json = json::parse(manifest_data.begin(), manifest_data.end(), nullptr, false); @@ -399,6 +574,35 @@ recomp::mods::ModOpenError parse_manifest(recomp::mods::ModManifest& ret, const } } + // Config schema (optional) + auto find_config_schema_it = manifest_json.find(config_schema_key); + if (find_config_schema_it != manifest_json.end()) { + auto& val = *find_config_schema_it; + if (!val.is_object()) { + error_param = config_schema_key; + return recomp::mods::ModOpenError::IncorrectManifestFieldType; + } + + auto options = val.find(config_schema_options_key); + if (options != val.end()) { + if (!options->is_array()) { + error_param = config_schema_options_key; + return recomp::mods::ModOpenError::IncorrectManifestFieldType; + } + + for (const json &option : *options) { + recomp::mods::ModOpenError open_error = parse_manifest_config_schema_option(option, ret, error_param); + if (open_error != recomp::mods::ModOpenError::Good) { + return open_error; + } + } + } + else { + error_param = config_schema_options_key; + return recomp::mods::ModOpenError::MissingConfigSchemaField; + } + } + return recomp::mods::ModOpenError::Good; } @@ -547,6 +751,12 @@ std::string recomp::mods::error_to_string(ModOpenError error) { return "Mod's mod.json has an invalid schema"; case ModOpenError::IncorrectManifestFieldType: return "Incorrect type for field in mod.json"; + case ModOpenError::MissingConfigSchemaField: + return "Missing required field in config schema in mod.json"; + case ModOpenError::IncorrectConfigSchemaType: + return "Incorrect type for field in config schema in mod.json"; + case ModOpenError::InvalidConfigSchemaDefault: + return "Invalid default for option in config schema in mod.json"; case ModOpenError::InvalidVersionString: return "Invalid version string in mod.json"; case ModOpenError::InvalidMinimumRecompVersionString: diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index ac45252a..0d4fe3e9 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -881,6 +881,17 @@ N64Recomp::Context context_from_regenerated_list(const RegeneratedList& regenlis return ret; } +const recomp::mods::ConfigSchema &recomp::mods::ModContext::get_mod_config_schema(const std::string &mod_id) const { + // Check that the mod exists. + auto find_it = opened_mods_by_id.find(mod_id); + if (find_it == opened_mods_by_id.end()) { + return empty_schema; + } + + const ModHandle &mod = opened_mods[find_it->second]; + return mod.manifest.config_schema; +} + std::vector recomp::mods::ModContext::load_mods(const GameEntry& game_entry, uint8_t* rdram, int32_t load_address, uint32_t& ram_used) { std::vector ret{}; ram_used = 0; diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index a64da686..08d324d0 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -515,6 +515,11 @@ bool recomp::mods::is_mod_auto_enabled(const std::string& mod_id) { return false; // TODO } +const recomp::mods::ConfigSchema &recomp::mods::get_mod_config_schema(const std::string &mod_id) { + std::lock_guard lock{ mod_context_mutex }; + return mod_context->get_mod_config_schema(mod_id); +} + std::vector recomp::mods::get_mod_details(const std::string& mod_game_id) { std::lock_guard lock { mod_context_mutex }; return mod_context->get_mod_details(mod_game_id); From ce872d7e3c4a48a062b1bc1ddc87b81b91c537d2 Mon Sep 17 00:00:00 2001 From: Mr-Wiseguy Date: Mon, 20 Jan 2025 18:23:42 -0500 Subject: [PATCH 12/25] Add float arg1 helpers --- librecomp/include/librecomp/helpers.hpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/librecomp/include/librecomp/helpers.hpp b/librecomp/include/librecomp/helpers.hpp index 9f023675..08d45444 100644 --- a/librecomp/include/librecomp/helpers.hpp +++ b/librecomp/include/librecomp/helpers.hpp @@ -36,6 +36,21 @@ T _arg(uint8_t* rdram, recomp_context* ctx) { } } +inline float _arg_float_a1(uint8_t* rdram, recomp_context* ctx) { + (void)rdram; + union { + u32 as_u32; + float as_float; + } ret{}; + ret.as_u32 = _arg<1, u32>(rdram, ctx); + return ret.as_float; +} + +inline float _arg_float_f14(uint8_t* rdram, recomp_context* ctx) { + (void)rdram; + return ctx->f14.fl; +} + template void _return(recomp_context* ctx, T val) { static_assert(sizeof(T) <= 4 && "Only 32-bit value returns supported currently"); From 843c1a1dcc5822d8c48270043942015236d54362 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 21 Jan 2025 22:52:39 -0300 Subject: [PATCH 13/25] Config storage for mods. --- librecomp/include/librecomp/mods.hpp | 31 ++++- librecomp/src/mod_manifest.cpp | 118 +++++++++++++++- librecomp/src/mods.cpp | 200 +++++++++++++++++++++++++-- librecomp/src/recomp.cpp | 33 +++-- 4 files changed, 356 insertions(+), 26 deletions(-) diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index 4dcbea44..af16c70a 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -13,6 +13,9 @@ #include #include #include +#include + +#include "blockingconcurrentqueue.h" #define MINIZ_NO_DEFLATE_APIS #define MINIZ_NO_ARCHIVE_WRITING_APIS @@ -55,6 +58,9 @@ struct std::hash namespace recomp { namespace mods { + static constexpr std::string_view mods_directory = "mods"; + static constexpr std::string_view mod_config_directory = "mod_config"; + enum class ModOpenError { Good, DoesNotExist, @@ -194,6 +200,13 @@ namespace recomp { struct ConfigSchema { std::vector options; + std::unordered_map options_by_id; + }; + + typedef std::variant ConfigValueVariant; + + struct ConfigStorage { + std::unordered_map value_map; }; struct ModDetails { @@ -302,6 +315,9 @@ namespace recomp { void unload_mods(); std::vector get_mod_details(const std::string& mod_game_id); const ConfigSchema &get_mod_config_schema(const std::string &mod_id) const; + void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value); + ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id); + void set_mod_config_path(const std::filesystem::path &path); ModContentTypeId register_content_type(const ModContentType& type); bool register_container_type(const std::string& extension, const std::vector& content_types, bool requires_manifest); ModContentTypeId get_code_content_type() const { return code_content_type_id; } @@ -313,13 +329,14 @@ namespace recomp { CodeModLoadError init_mod_code(uint8_t* rdram, const std::unordered_map& section_vrom_map, ModHandle& mod, int32_t load_address, bool hooks_available, uint32_t& ram_used, std::string& error_param); CodeModLoadError load_mod_code(uint8_t* rdram, ModHandle& mod, uint32_t base_event_index, std::string& error_param); CodeModLoadError resolve_code_dependencies(ModHandle& mod, const std::unordered_map& base_patched_funcs, std::string& error_param); - void add_opened_mod(ModManifest&& manifest, std::vector&& game_indices, std::vector&& detected_content_types); + void add_opened_mod(ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& detected_content_types); void close_mods(); std::vector regenerate_with_hooks( const std::vector>& sorted_unprocessed_hooks, const std::unordered_map& section_vrom_map, const std::unordered_map& base_patched_funcs, std::span decompressed_rom); + void dirty_mod_configuration_thread_process(); static void on_code_mod_enabled(ModContext& context, const ModHandle& mod); @@ -329,10 +346,15 @@ namespace recomp { std::unordered_map mod_game_ids; std::vector opened_mods; std::unordered_map opened_mods_by_id; + std::mutex opened_mods_mutex; std::unordered_set mod_ids; std::unordered_set enabled_mods; std::unordered_map patched_funcs; std::unordered_map loaded_mods_by_id; + std::unique_ptr dirty_mod_configuration_thread; + moodycamel::BlockingConcurrentQueue dirty_mod_configuration_thread_queue; + std::filesystem::path mod_config_path; + std::mutex mod_config_storage_mutex; std::vector loaded_code_mods; // Code handle for vanilla code that was regenerated to add hooks. std::unique_ptr regenerated_code_handle; @@ -366,13 +388,14 @@ namespace recomp { public: // TODO make these private and expose methods for the functionality they're currently used in. ModManifest manifest; + ConfigStorage config_storage; std::unique_ptr code_handle; std::unique_ptr recompiler_context; std::vector section_load_addresses; // Content types present in this mod. std::vector content_types; - ModHandle(const ModContext& context, ModManifest&& manifest, std::vector&& game_indices, std::vector&& content_types); + ModHandle(const ModContext& context, ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& content_types); ModHandle(const ModHandle& rhs) = delete; ModHandle& operator=(const ModHandle& rhs) = delete; ModHandle(ModHandle&& rhs); @@ -502,12 +525,14 @@ namespace recomp { CodeModLoadError validate_api_version(uint32_t api_version, std::string& error_param); - void initialize_mod_recompiler(); + void initialize_mods(); void scan_mods(); void enable_mod(const std::string& mod_id, bool enabled); bool is_mod_enabled(const std::string& mod_id); bool is_mod_auto_enabled(const std::string& mod_id); const ConfigSchema &get_mod_config_schema(const std::string &mod_id); + void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value); + ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id); ModContentTypeId register_mod_content_type(const ModContentType& type); bool register_mod_container_type(const std::string& extension, const std::vector& content_types, bool requires_manifest); } diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp index 515c3658..6d85acd4 100644 --- a/librecomp/src/mod_manifest.cpp +++ b/librecomp/src/mod_manifest.cpp @@ -3,8 +3,38 @@ #include "json/json.hpp" #include "recompiler/context.h" +#include "librecomp/files.hpp" #include "librecomp/mods.hpp" +static 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; +} + +static 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; +} + recomp::mods::ZipModFileHandle::~ZipModFileHandle() { if (file_handle) { fclose(file_handle); @@ -469,7 +499,9 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j break; } - ret.config_schema.options.push_back(option); + ret.config_schema.options_by_id.emplace(option.id, ret.config_schema.options.size()); + ret.config_schema.options.emplace_back(option); + return recomp::mods::ModOpenError::Good; } @@ -606,6 +638,83 @@ recomp::mods::ModOpenError parse_manifest(recomp::mods::ModManifest& ret, const return recomp::mods::ModOpenError::Good; } +bool parse_mod_config_storage(const std::filesystem::path &path, const std::string &expected_mod_id, recomp::mods::ConfigStorage &config_storage, const recomp::mods::ConfigSchema &config_schema) { + using json = nlohmann::json; + json config_json; + if (!read_json_with_backups(path, config_json)) { + return false; + } + + auto mod_id = config_json.find("mod_id"); + if (mod_id != config_json.end()) { + std::string mod_id_str; + if (get_to(*mod_id, mod_id_str)) { + if (*mod_id != expected_mod_id) { + // The mod's ID doesn't match. + return false; + } + } + else { + // The mod ID is not a string. + return false; + } + } + else { + // The configuration file doesn't have a mod ID. + return false; + } + + auto storage_json = config_json.find("storage"); + if (storage_json == config_json.end()) { + // The configuration file doesn't have a storage object. + return false; + } + + if (!storage_json->is_object()) { + // The storage key does not correspond to an object. + return false; + } + + // Only parse the object for known option types based on the schema. + int64_t value_int64; + double value_double; + std::string value_str; + for (const recomp::mods::ConfigOption &option : config_schema.options) { + auto option_json = storage_json->find(option.id); + if (option_json == storage_json->end()) { + // Option doesn't exist in storage. + continue; + } + + switch (option.type) { + case recomp::mods::ConfigOptionType::Enum: + if (get_to(*option_json, value_int64)) { + config_storage.value_map[option.id] = uint32_t(value_int64); + } + + break; + case recomp::mods::ConfigOptionType::Number: + if (get_to(*option_json, value_double)) { + config_storage.value_map[option.id] = value_double; + } + + break; + case recomp::mods::ConfigOptionType::String: { + if (get_to(*option_json, value_str)) { + config_storage.value_map[option.id] = value_str; + } + + break; + } + default: + assert(false && "Unknown option type."); + break; + } + } + + return true; +} + recomp::mods::ModOpenError recomp::mods::ModContext::open_mod(const std::filesystem::path& mod_path, std::string& error_param, const std::vector& supported_content_types, bool requires_manifest) { ModManifest manifest{}; std::error_code ec; @@ -724,9 +833,14 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod(const std::filesys } } + // Read the mod config if it exists. + ConfigStorage config_storage; + std::filesystem::path config_path = mod_config_path / (manifest.mod_id + ".json"); + parse_mod_config_storage(config_path, manifest.mod_id, config_storage, manifest.config_schema); + // Store the loaded mod manifest in a new mod handle. manifest.mod_root_path = mod_path; - add_opened_mod(std::move(manifest), std::move(game_indices), std::move(detected_content_types)); + add_opened_mod(std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types)); return ModOpenError::Good; } diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index 0d4fe3e9..c3da7f48 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -3,6 +3,7 @@ #include #include +#include "librecomp/files.hpp" #include "librecomp/mods.hpp" #include "librecomp/overlays.hpp" #include "librecomp/game.hpp" @@ -213,8 +214,9 @@ recomp::mods::CodeModLoadError recomp::mods::validate_api_version(uint32_t api_v } } -recomp::mods::ModHandle::ModHandle(const ModContext& context, ModManifest&& manifest, std::vector&& game_indices, std::vector&& content_types) : +recomp::mods::ModHandle::ModHandle(const ModContext& context, ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& content_types) : manifest(std::move(manifest)), + config_storage(std::move(config_storage)), code_handle(), recompiler_context{std::make_unique()}, content_types{std::move(content_types)}, @@ -539,10 +541,11 @@ void unpatch_func(void* target_func, const recomp::mods::PatchData& data) { protect(target_func, old_flags); } -void recomp::mods::ModContext::add_opened_mod(ModManifest&& manifest, std::vector&& game_indices, std::vector&& detected_content_types) { +void recomp::mods::ModContext::add_opened_mod(ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& detected_content_types) { + std::unique_lock lock(opened_mods_mutex); size_t mod_index = opened_mods.size(); opened_mods_by_id.emplace(manifest.mod_id, mod_index); - opened_mods.emplace_back(*this, std::move(manifest), std::move(game_indices), std::move(detected_content_types)); + opened_mods.emplace_back(*this, std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types)); } recomp::mods::ModLoadError recomp::mods::ModContext::load_mod(recomp::mods::ModHandle& mod, std::string& error_param) { @@ -567,12 +570,107 @@ void recomp::mods::ModContext::register_game(const std::string& mod_game_id) { } void recomp::mods::ModContext::close_mods() { + std::unique_lock lock(opened_mods_mutex); opened_mods_by_id.clear(); opened_mods.clear(); mod_ids.clear(); enabled_mods.clear(); } +bool save_mod_config_storage(const std::filesystem::path &path, const std::string &mod_id, const recomp::Version &mod_version, const recomp::mods::ConfigStorage &config_storage, const recomp::mods::ConfigSchema &config_schema) { + nlohmann::json config_json; + config_json["mod_id"] = mod_id; + config_json["mod_version"] = mod_version.to_string(); + config_json["recomp_version"] = recomp::get_project_version().to_string(); + + nlohmann::json &storage_json = config_json["storage"]; + for (auto it : config_storage.value_map) { + auto id_it = config_schema.options_by_id.find(it.first); + if (id_it == config_schema.options_by_id.end()) { + continue; + } + + const recomp::mods::ConfigOption &config_option = config_schema.options[id_it->second]; + switch (config_option.type) { + case recomp::mods::ConfigOptionType::Enum: + storage_json[it.first] = std::get(config_option.variant).options[std::get(it.second)]; + break; + case recomp::mods::ConfigOptionType::Number: + storage_json[it.first] = std::get(it.second); + break; + case recomp::mods::ConfigOptionType::String: + storage_json[it.first] = std::get(it.second); + break; + default: + assert(false && "Unknown config type."); + break; + } + } + + std::ofstream output_file = recomp::open_output_file_with_backup(path); + if (!output_file.good()) { + return false; + } + + output_file << std::setw(4) << config_json; + output_file.close(); + + return recomp::finalize_output_file_with_backup(path); +} + +void recomp::mods::ModContext::dirty_mod_configuration_thread_process() { + using namespace std::chrono_literals; + std::string mod_id; + std::unordered_set pending_mods; + std::unordered_map pending_mod_storage; + std::unordered_map pending_mod_schema; + std::unordered_map pending_mod_version; + std::filesystem::path config_path; + bool active = true; + while (active) { + // Wait for at least one mod to require writing. + dirty_mod_configuration_thread_queue.wait_dequeue(mod_id); + + if (!mod_id.empty()) { + pending_mods.emplace(mod_id); + } + else { + active = false; + } + + // Clear out the entire queue to coalesce all writes with a timeout. + while (active && dirty_mod_configuration_thread_queue.wait_dequeue_timed(mod_id, 1s)) { + if (!mod_id.empty()) { + pending_mods.emplace(mod_id); + } + else { + active = false; + } + } + + if (active && !pending_mods.empty()) { + { + std::unique_lock opened_mods_lock(opened_mods_mutex); + for (const std::string &id : pending_mods) { + auto it = opened_mods_by_id.find(id); + if (it != opened_mods_by_id.end()) { + const ModHandle &mod = opened_mods[it->second]; + std::unique_lock config_storage_lock(mod_config_storage_mutex); + pending_mod_storage[id] = mod.config_storage; + pending_mod_schema[id] = mod.manifest.config_schema; + pending_mod_version[id] = mod.manifest.version; + } + } + } + + for (const std::string &id : pending_mods) { + config_path = mod_config_path / std::string(id + ".json"); + save_mod_config_storage(config_path, id, pending_mod_version[id], pending_mod_storage[id], pending_mod_schema[id]); + } + } + } +} + std::vector recomp::mods::ModContext::scan_mod_folder(const std::filesystem::path& mod_folder) { std::vector ret{}; std::error_code ec; @@ -623,6 +721,8 @@ recomp::mods::ModContext::ModContext() { // Register the default mod container type (.nrm) and allow it to have any content type by passing an empty vector. register_container_type(std::string{ modpaths::default_mod_extension }, {}, true); + + dirty_mod_configuration_thread = std::make_unique(&ModContext::dirty_mod_configuration_thread_process, this); } void recomp::mods::ModContext::on_code_mod_enabled(ModContext& context, const ModHandle& mod) { @@ -635,8 +735,11 @@ void recomp::mods::ModContext::on_code_mod_enabled(ModContext& context, const Mo } } -// Nothing needed for this, it just need to be explicitly declared outside the header to allow forward declaration of ModHandle. -recomp::mods::ModContext::~ModContext() = default; +recomp::mods::ModContext::~ModContext() { + dirty_mod_configuration_thread_queue.enqueue(std::string()); + dirty_mod_configuration_thread->join(); + dirty_mod_configuration_thread.reset(); +} recomp::mods::ModContentTypeId recomp::mods::ModContext::register_content_type(const ModContentType& type) { size_t ret = content_types.size(); @@ -892,6 +995,89 @@ const recomp::mods::ConfigSchema &recomp::mods::ModContext::get_mod_config_schem return mod.manifest.config_schema; } +void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value) { + // Check that the mod exists. + auto find_it = opened_mods_by_id.find(mod_id); + if (find_it == opened_mods_by_id.end()) { + return; + } + + ModHandle &mod = opened_mods[find_it->second]; + std::unique_lock lock(mod_config_storage_mutex); + auto option_by_id_it = mod.manifest.config_schema.options_by_id.find(option_id); + if (option_by_id_it != mod.manifest.config_schema.options_by_id.end()) { + // Only accept setting values if the value exists and the variant is the right type. + const ConfigOption &option = mod.manifest.config_schema.options[option_by_id_it->second]; + switch (option.type) { + case ConfigOptionType::Enum: + if (std::holds_alternative(value)) { + if (std::get(value) < std::get(option.variant).options.size()) { + mod.config_storage.value_map[option_id] = value; + } + } + + break; + case ConfigOptionType::Number: + if (std::holds_alternative(value)) { + mod.config_storage.value_map[option_id] = value; + } + + break; + case ConfigOptionType::String: + if (std::holds_alternative(value)) { + mod.config_storage.value_map[option_id] = value; + } + + break; + default: + assert(false && "Unknown config option type."); + return; + } + } + + // Notify the asynchronous thread it should save the configuration for this mod. + dirty_mod_configuration_thread_queue.enqueue(mod_id); +} + +recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(const std::string &mod_id, const std::string &option_id) { + // Check that the mod exists. + auto find_it = opened_mods_by_id.find(mod_id); + if (find_it == opened_mods_by_id.end()) { + return std::monostate(); + } + + const ModHandle &mod = opened_mods[find_it->second]; + std::unique_lock lock(mod_config_storage_mutex); + auto it = mod.config_storage.value_map.find(option_id); + if (it != mod.config_storage.value_map.end()) { + return it->second; + } + else { + // Attempt to see if we can find a default value from the schema. + auto option_by_id_it = mod.manifest.config_schema.options_by_id.find(option_id); + if (option_by_id_it == mod.manifest.config_schema.options_by_id.end()) { + return std::monostate(); + } + + const ConfigOption &option = mod.manifest.config_schema.options[option_by_id_it->second]; + switch (option.type) { + case ConfigOptionType::Enum: + return std::get(option.variant).default_value; + case ConfigOptionType::Number: + return std::get(option.variant).default_value; + case ConfigOptionType::String: + return std::get(option.variant).default_value; + default: + assert(false && "Unknown config option type."); + return std::monostate(); + } + } +} + +void recomp::mods::ModContext::set_mod_config_path(const std::filesystem::path &path) { + mod_config_path = path; +} + std::vector recomp::mods::ModContext::load_mods(const GameEntry& game_entry, uint8_t* rdram, int32_t load_address, uint32_t& ram_used) { std::vector ret{}; ram_used = 0; @@ -1872,7 +2058,3 @@ void recomp::mods::ModContext::unload_mods() { num_events = recomp::overlays::num_base_events(); active_game = (size_t)-1; } - -void recomp::mods::initialize_mod_recompiler() { - N64Recomp::live_recompiler_init(); -} diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index 08d324d0..0ebb3730 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -23,6 +23,7 @@ #include "ultramodern/error_handling.hpp" #include "librecomp/addresses.hpp" #include "librecomp/mods.hpp" +#include "recompiler/live_recompiler.h" #ifdef _WIN32 # define WIN32_LEAN_AND_MEAN @@ -37,16 +38,6 @@ #define PATHFMT "%s" #endif -#ifdef _MSC_VER -inline uint32_t byteswap(uint32_t val) { - return _byteswap_ulong(val); -} -#else -constexpr uint32_t byteswap(uint32_t val) { - return __builtin_bswap32(val); -} -#endif - enum GameStatus { None, Running, @@ -91,11 +82,18 @@ bool recomp::register_game(const recomp::GameEntry& entry) { return true; } +void recomp::mods::initialize_mods() { + N64Recomp::live_recompiler_init(); + std::filesystem::create_directories(config_path / mods_directory); + std::filesystem::create_directories(config_path / mod_config_directory); + mod_context->set_mod_config_path(config_path / mod_config_directory); +} + void recomp::mods::scan_mods() { std::vector mod_open_errors; { std::lock_guard mod_lock{ mod_context_mutex }; - mod_open_errors = mod_context->scan_mod_folder(config_path / "mods"); + mod_open_errors = mod_context->scan_mod_folder(config_path / mods_directory); } for (const auto& cur_error : mod_open_errors) { printf("Error opening mod " PATHFMT ": %s (%s)\n", cur_error.mod_path.c_str(), recomp::mods::error_to_string(cur_error.error).c_str(), cur_error.error_param.c_str()); @@ -520,6 +518,16 @@ const recomp::mods::ConfigSchema &recomp::mods::get_mod_config_schema(const std: return mod_context->get_mod_config_schema(mod_id); } +void recomp::mods::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value) { + std::lock_guard lock{ mod_context_mutex }; + return mod_context->set_mod_config_value(mod_id, option_id, value); +} + +recomp::mods::ConfigValueVariant recomp::mods::get_mod_config_value(const std::string &mod_id, const std::string &option_id) { + std::lock_guard lock{ mod_context_mutex }; + return mod_context->get_mod_config_value(mod_id, option_id); +} + std::vector recomp::mods::get_mod_details(const std::string& mod_game_id) { std::lock_guard lock { mod_context_mutex }; return mod_context->get_mod_details(mod_game_id); @@ -653,7 +661,8 @@ void recomp::start( } } - recomp::mods::initialize_mod_recompiler(); + recomp::mods::initialize_mods(); + recomp::mods::scan_mods(); // Allocate rdram without comitting it. Use a platform-specific virtual allocation function // that initializes to zero. Protect the region above the memory size to catch accesses to invalid addresses. From 240c3f566d6de2ebe16b92af4a9334661602f365 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 21 Jan 2025 23:08:04 -0300 Subject: [PATCH 14/25] Proper enum parsing. --- librecomp/src/mod_manifest.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp index 6d85acd4..99552d7d 100644 --- a/librecomp/src/mod_manifest.cpp +++ b/librecomp/src/mod_manifest.cpp @@ -688,8 +688,12 @@ bool parse_mod_config_storage(const std::filesystem::path &path, const std::stri switch (option.type) { case recomp::mods::ConfigOptionType::Enum: - if (get_to(*option_json, value_int64)) { - config_storage.value_map[option.id] = uint32_t(value_int64); + if (get_to(*option_json, value_str)) { + const recomp::mods::ConfigOptionEnum &option_enum = std::get(option.variant); + auto option_it = std::find(option_enum.options.begin(), option_enum.options.end(), value_str); + if (option_it != option_enum.options.end()) { + config_storage.value_map[option.id] = uint32_t(option_it - option_enum.options.begin()); + } } break; From 7ca672b1966f17873ce641a9b29cbd922ba32dd8 Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 22 Jan 2025 23:27:36 -0300 Subject: [PATCH 15/25] Persist mod order and enable. --- librecomp/include/librecomp/mods.hpp | 30 +++- librecomp/src/mod_manifest.cpp | 2 +- librecomp/src/mods.cpp | 258 ++++++++++++++++++++++++--- librecomp/src/recomp.cpp | 12 +- 4 files changed, 266 insertions(+), 36 deletions(-) diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index af16c70a..c73a9772 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -259,6 +259,7 @@ namespace recomp { }; std::vector get_mod_details(const std::string& mod_game_id); + void set_mod_index(const std::string &mod_game_id, const std::string &mod_id, size_t index); // Internal functions, TODO move to an internal header. struct PatchData { @@ -300,6 +301,20 @@ namespace recomp { bool requires_manifest; }; + struct ModConfigQueueSaveMod { + std::string mod_id; + }; + + struct ModConfigQueueSave { + uint32_t pad; + }; + + struct ModConfigQueueEnd { + uint32_t pad; + }; + + typedef std::variant ModConfigQueueVariant; + class LiveRecompilerCodeHandle; class ModContext { public: @@ -308,16 +323,19 @@ namespace recomp { void register_game(const std::string& mod_game_id); std::vector scan_mod_folder(const std::filesystem::path& mod_folder); - void enable_mod(const std::string& mod_id, bool enabled); + void load_mods_config(); + void enable_mod(const std::string& mod_id, bool enabled, bool trigger_save); bool is_mod_enabled(const std::string& mod_id); size_t num_opened_mods(); std::vector load_mods(const GameEntry& game_entry, uint8_t* rdram, int32_t load_address, uint32_t& ram_used); void unload_mods(); std::vector get_mod_details(const std::string& mod_game_id); + void set_mod_index(const std::string &mod_game_id, const std::string &mod_id, size_t index); const ConfigSchema &get_mod_config_schema(const std::string &mod_id) const; void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value); ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id); - void set_mod_config_path(const std::filesystem::path &path); + void set_mods_config_path(const std::filesystem::path &path); + void set_mod_config_directory(const std::filesystem::path &path); ModContentTypeId register_content_type(const ModContentType& type); bool register_container_type(const std::string& extension, const std::vector& content_types, bool requires_manifest); ModContentTypeId get_code_content_type() const { return code_content_type_id; } @@ -346,14 +364,16 @@ namespace recomp { std::unordered_map mod_game_ids; std::vector opened_mods; std::unordered_map opened_mods_by_id; + std::vector opened_mods_order; std::mutex opened_mods_mutex; std::unordered_set mod_ids; std::unordered_set enabled_mods; std::unordered_map patched_funcs; std::unordered_map loaded_mods_by_id; - std::unique_ptr dirty_mod_configuration_thread; - moodycamel::BlockingConcurrentQueue dirty_mod_configuration_thread_queue; - std::filesystem::path mod_config_path; + std::unique_ptr mod_configuration_thread; + moodycamel::BlockingConcurrentQueue mod_configuration_thread_queue; + std::filesystem::path mods_config_path; + std::filesystem::path mod_config_directory; std::mutex mod_config_storage_mutex; std::vector loaded_code_mods; // Code handle for vanilla code that was regenerated to add hooks. diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp index 99552d7d..c7a8d96f 100644 --- a/librecomp/src/mod_manifest.cpp +++ b/librecomp/src/mod_manifest.cpp @@ -839,7 +839,7 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod(const std::filesys // Read the mod config if it exists. ConfigStorage config_storage; - std::filesystem::path config_path = mod_config_path / (manifest.mod_id + ".json"); + std::filesystem::path config_path = mod_config_directory / (manifest.mod_id + ".json"); parse_mod_config_storage(config_path, manifest.mod_id, config_storage, manifest.config_schema); // Store the loaded mod manifest in a new mod handle. diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index c3da7f48..070b0c7f 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -10,6 +10,58 @@ #include "recompiler/context.h" #include "recompiler/live_recompiler.h" +static 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; +} + +static 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; +} + + +template +bool get_to_vec(const nlohmann::json& val, std::vector& out) { + const nlohmann::json::array_t* ptr = val.get_ptr(); + if (ptr == nullptr) { + return false; + } + + out.clear(); + + for (const nlohmann::json& cur_val : *ptr) { + const T1* temp_ptr = cur_val.get_ptr(); + if (temp_ptr == nullptr) { + out.clear(); + return false; + } + + out.emplace_back(*temp_ptr); + } + + return true; +} + // Architecture detection. // MSVC x86_64 @@ -546,6 +598,7 @@ void recomp::mods::ModContext::add_opened_mod(ModManifest&& manifest, ConfigStor size_t mod_index = opened_mods.size(); opened_mods_by_id.emplace(manifest.mod_id, mod_index); opened_mods.emplace_back(*this, std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types)); + opened_mods_order.emplace_back(mod_index); } recomp::mods::ModLoadError recomp::mods::ModContext::load_mod(recomp::mods::ModHandle& mod, std::string& error_param) { @@ -573,17 +626,19 @@ void recomp::mods::ModContext::close_mods() { std::unique_lock lock(opened_mods_mutex); opened_mods_by_id.clear(); opened_mods.clear(); + opened_mods_order.clear(); mod_ids.clear(); enabled_mods.clear(); } bool save_mod_config_storage(const std::filesystem::path &path, const std::string &mod_id, const recomp::Version &mod_version, const recomp::mods::ConfigStorage &config_storage, const recomp::mods::ConfigSchema &config_schema) { - nlohmann::json config_json; + using json = nlohmann::json; + json config_json; config_json["mod_id"] = mod_id; config_json["mod_version"] = mod_version.to_string(); config_json["recomp_version"] = recomp::get_project_version().to_string(); - nlohmann::json &storage_json = config_json["storage"]; + json &storage_json = config_json["storage"]; for (auto it : config_storage.value_map) { auto id_it = config_schema.options_by_id.find(it.first); if (id_it == config_schema.options_by_id.end()) { @@ -618,34 +673,78 @@ bool save_mod_config_storage(const std::filesystem::path &path, const std::strin return recomp::finalize_output_file_with_backup(path); } +bool parse_mods_config(const std::filesystem::path &path, std::unordered_set &enabled_mods, std::vector &mod_order) { + using json = nlohmann::json; + json config_json; + if (!read_json_with_backups(path, config_json)) { + return false; + } + + auto enabled_mods_json = config_json.find("enabled_mods"); + if (enabled_mods_json != config_json.end()) { + std::vector enabled_mods_vector; + if (get_to_vec(*enabled_mods_json, enabled_mods_vector)) { + for (const std::string &mod_id : enabled_mods_vector) { + enabled_mods.emplace(mod_id); + } + } + } + + auto mod_order_json = config_json.find("mod_order"); + if (mod_order_json != config_json.end()) { + get_to_vec(*mod_order_json, mod_order); + } + + return true; +} + +bool save_mods_config(const std::filesystem::path &path, const std::unordered_set &enabled_mods, const std::vector &mod_order) { + nlohmann::json config_json; + config_json["enabled_mods"] = enabled_mods; + config_json["mod_order"] = mod_order; + + std::ofstream output_file = recomp::open_output_file_with_backup(path); + if (!output_file.good()) { + return false; + } + + output_file << std::setw(4) << config_json; + output_file.close(); + + return recomp::finalize_output_file_with_backup(path); +} + void recomp::mods::ModContext::dirty_mod_configuration_thread_process() { using namespace std::chrono_literals; - std::string mod_id; + ModConfigQueueVariant variant; + ModConfigQueueSaveMod save_mod; std::unordered_set pending_mods; std::unordered_map pending_mod_storage; std::unordered_map pending_mod_schema; std::unordered_map pending_mod_version; + std::unordered_set config_enabled_mods; + std::vector config_mod_order; + bool pending_config_save = false; std::filesystem::path config_path; bool active = true; + auto handle_variant = [&](const ModConfigQueueVariant &variant) { + if (std::get_if(&variant) != nullptr) { + active = false; + } + else if (std::get_if(&variant) != nullptr) { + pending_config_save = true; + } + }; + while (active) { // Wait for at least one mod to require writing. - dirty_mod_configuration_thread_queue.wait_dequeue(mod_id); + mod_configuration_thread_queue.wait_dequeue(variant); + handle_variant(variant); - if (!mod_id.empty()) { - pending_mods.emplace(mod_id); - } - else { - active = false; - } // Clear out the entire queue to coalesce all writes with a timeout. - while (active && dirty_mod_configuration_thread_queue.wait_dequeue_timed(mod_id, 1s)) { - if (!mod_id.empty()) { - pending_mods.emplace(mod_id); - } - else { - active = false; - } + while (active && mod_configuration_thread_queue.wait_dequeue_timed(variant, 1s)) { + handle_variant(variant); } if (active && !pending_mods.empty()) { @@ -664,9 +763,26 @@ void recomp::mods::ModContext::dirty_mod_configuration_thread_process() { } for (const std::string &id : pending_mods) { - config_path = mod_config_path / std::string(id + ".json"); + config_path = mod_config_directory / std::string(id + ".json"); save_mod_config_storage(config_path, id, pending_mod_version[id], pending_mod_storage[id], pending_mod_schema[id]); } + + pending_mods.clear(); + } + + if (active && pending_config_save) { + { + // Store the enabled mods and the order. + std::unique_lock lock(opened_mods_mutex); + config_enabled_mods = enabled_mods; + config_mod_order.clear(); + for (size_t mod_index : opened_mods_order) { + config_mod_order.emplace_back(opened_mods[mod_index].manifest.mod_id); + } + } + + save_mods_config(mods_config_path, config_enabled_mods, config_mod_order); + pending_config_save = false; } } } @@ -709,6 +825,38 @@ std::vector recomp::mods::ModContext::scan_mo return ret; } +void recomp::mods::ModContext::load_mods_config() { + std::unordered_set config_enabled_mods; + std::vector config_mod_order; + bool parsed = parse_mods_config(mods_config_path, config_enabled_mods, config_mod_order); + if (parsed) { + for (const std::string &mod_id : config_enabled_mods) { + enable_mod(mod_id, true, false); + } + + { + std::unique_lock lock(opened_mods_mutex); + + // Fill a vector with the relative order of the mods. Existing mods will get ordered below new mods. + std::vector sort_order; + sort_order.resize(opened_mods.size()); + std::iota(sort_order.begin(), sort_order.end(), 0); + for (size_t i = 0; i < config_mod_order.size(); i++) { + auto it = opened_mods_by_id.find(config_mod_order[i]); + if (it != opened_mods_by_id.end()) { + sort_order[it->second] = opened_mods.size() + i; + } + } + + // Run the sort using the relative order computed before. + std::iota(opened_mods_order.begin(), opened_mods_order.end(), 0); + std::sort(opened_mods_order.begin(), opened_mods_order.end(), [&](size_t i, size_t j) { + return sort_order[i] < sort_order[j]; + }); + } + } +} + recomp::mods::ModContext::ModContext() { // Register the code content type. ModContentType code_content_type { @@ -722,7 +870,7 @@ recomp::mods::ModContext::ModContext() { // Register the default mod container type (.nrm) and allow it to have any content type by passing an empty vector. register_container_type(std::string{ modpaths::default_mod_extension }, {}, true); - dirty_mod_configuration_thread = std::make_unique(&ModContext::dirty_mod_configuration_thread_process, this); + mod_configuration_thread = std::make_unique(&ModContext::dirty_mod_configuration_thread_process, this); } void recomp::mods::ModContext::on_code_mod_enabled(ModContext& context, const ModHandle& mod) { @@ -736,9 +884,9 @@ void recomp::mods::ModContext::on_code_mod_enabled(ModContext& context, const Mo } recomp::mods::ModContext::~ModContext() { - dirty_mod_configuration_thread_queue.enqueue(std::string()); - dirty_mod_configuration_thread->join(); - dirty_mod_configuration_thread.reset(); + mod_configuration_thread_queue.enqueue(ModConfigQueueEnd()); + mod_configuration_thread->join(); + mod_configuration_thread.reset(); } recomp::mods::ModContentTypeId recomp::mods::ModContext::register_content_type(const ModContentType& type) { @@ -785,8 +933,9 @@ bool recomp::mods::ModContext::is_content_runtime_toggleable(ModContentTypeId co return content_types[content_type.value].allow_runtime_toggle; } -void recomp::mods::ModContext::enable_mod(const std::string& mod_id, bool enabled) { +void recomp::mods::ModContext::enable_mod(const std::string& mod_id, bool enabled, bool trigger_save) { // Check that the mod exists. + std::unique_lock lock(opened_mods_mutex); auto find_it = opened_mods_by_id.find(mod_id); if (find_it == opened_mods_by_id.end()) { return; @@ -823,6 +972,10 @@ void recomp::mods::ModContext::enable_mod(const std::string& mod_id, bool enable } } } + + if (trigger_save) { + mod_configuration_thread_queue.enqueue(ModConfigQueueSave()); + } } bool recomp::mods::ModContext::is_mod_enabled(const std::string& mod_id) { @@ -833,7 +986,7 @@ size_t recomp::mods::ModContext::num_opened_mods() { return opened_mods.size(); } -std::vector recomp::mods::ModContext::get_mod_details(const std::string& mod_game_id) { +std::vector recomp::mods::ModContext::get_mod_details(const std::string &mod_game_id) { std::vector ret{}; bool all_games = mod_game_id.empty(); size_t game_index = (size_t)-1; @@ -843,7 +996,8 @@ std::vector recomp::mods::ModContext::get_mod_details( game_index = find_game_it->second; } - for (const ModHandle& mod : opened_mods) { + for (size_t mod_index : opened_mods_order) { + const ModHandle &mod = opened_mods[mod_index]; if (all_games || mod.is_for_game(game_index)) { std::vector cur_dependencies{}; @@ -984,6 +1138,50 @@ N64Recomp::Context context_from_regenerated_list(const RegeneratedList& regenlis return ret; } +void recomp::mods::ModContext::set_mod_index(const std::string &mod_game_id, const std::string &mod_id, size_t index) { + std::unique_lock lock(opened_mods_mutex); + bool all_games = mod_game_id.empty(); + size_t game_index = (size_t)-1; + auto find_game_it = mod_game_ids.find(mod_game_id); + if (find_game_it != mod_game_ids.end()) { + game_index = find_game_it->second; + } + + auto id_it = opened_mods_by_id.find(mod_id); + if (id_it == opened_mods_by_id.end()) { + return; + } + + size_t mod_index = id_it->second; + size_t search_index = 0; + bool inserted = false; + bool erased = false; + for (size_t i = 0; i < opened_mods_order.size() && (!inserted || !erased); i++) { + size_t current_index = opened_mods_order[i]; + const ModHandle &mod = opened_mods[current_index]; + if (all_games || mod.is_for_game(game_index)) { + if (index == search_index) { + // This index corresponds to the one from the view. Insert the mod here. + opened_mods_order.insert(opened_mods_order.begin() + i, mod_index); + inserted = true; + } + else if (mod_index == current_index) { + // This index corresponds to the previous position the mod had. Erase it. + opened_mods_order.erase(opened_mods_order.begin() + i); + erased = true; + } + + search_index++; + } + } + + if (!inserted) { + opened_mods_order.push_back(mod_index); + } + + mod_configuration_thread_queue.enqueue(ModConfigQueueSave()); +} + const recomp::mods::ConfigSchema &recomp::mods::ModContext::get_mod_config_schema(const std::string &mod_id) const { // Check that the mod exists. auto find_it = opened_mods_by_id.find(mod_id); @@ -1036,7 +1234,7 @@ void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, c } // Notify the asynchronous thread it should save the configuration for this mod. - dirty_mod_configuration_thread_queue.enqueue(mod_id); + mod_configuration_thread_queue.enqueue(ModConfigQueueSaveMod(mod_id)); } recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(const std::string &mod_id, const std::string &option_id) { @@ -1074,8 +1272,12 @@ recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value( } } -void recomp::mods::ModContext::set_mod_config_path(const std::filesystem::path &path) { - mod_config_path = path; +void recomp::mods::ModContext::set_mods_config_path(const std::filesystem::path &path) { + mods_config_path = path; +} + +void recomp::mods::ModContext::set_mod_config_directory(const std::filesystem::path &path) { + mod_config_directory = path; } std::vector recomp::mods::ModContext::load_mods(const GameEntry& game_entry, uint8_t* rdram, int32_t load_address, uint32_t& ram_used) { diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index 0ebb3730..ad57f363 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -86,7 +86,8 @@ void recomp::mods::initialize_mods() { N64Recomp::live_recompiler_init(); std::filesystem::create_directories(config_path / mods_directory); std::filesystem::create_directories(config_path / mod_config_directory); - mod_context->set_mod_config_path(config_path / mod_config_directory); + mod_context->set_mods_config_path(config_path / "mods.json"); + mod_context->set_mod_config_directory(config_path / mod_config_directory); } void recomp::mods::scan_mods() { @@ -98,6 +99,8 @@ void recomp::mods::scan_mods() { for (const auto& cur_error : mod_open_errors) { printf("Error opening mod " PATHFMT ": %s (%s)\n", cur_error.mod_path.c_str(), recomp::mods::error_to_string(cur_error.error).c_str(), cur_error.error_param.c_str()); } + + mod_context->load_mods_config(); } recomp::mods::ModContentTypeId recomp::mods::register_mod_content_type(const ModContentType& type) { @@ -500,7 +503,7 @@ void ultramodern::quit() { void recomp::mods::enable_mod(const std::string& mod_id, bool enabled) { std::lock_guard lock { mod_context_mutex }; - return mod_context->enable_mod(mod_id, enabled); + return mod_context->enable_mod(mod_id, enabled, true); } bool recomp::mods::is_mod_enabled(const std::string& mod_id) { @@ -533,6 +536,11 @@ std::vector recomp::mods::get_mod_details(const std::s return mod_context->get_mod_details(mod_game_id); } +void recomp::mods::set_mod_index(const std::string &mod_game_id, const std::string &mod_id, size_t index) { + std::lock_guard lock{ mod_context_mutex }; + return mod_context->set_mod_index(mod_game_id, mod_id, index); +} + bool wait_for_game_started(uint8_t* rdram, recomp_context* context) { game_status.wait(GameStatus::None); From 40df967a5c9af1c23303628f023cbbd90cf4fbd9 Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 24 Jan 2025 23:11:04 -0300 Subject: [PATCH 16/25] Enable new mods by default. --- librecomp/src/mods.cpp | 50 ++++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index 070b0c7f..b0febf84 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -828,32 +828,34 @@ std::vector recomp::mods::ModContext::scan_mo void recomp::mods::ModContext::load_mods_config() { std::unordered_set config_enabled_mods; std::vector config_mod_order; - bool parsed = parse_mods_config(mods_config_path, config_enabled_mods, config_mod_order); - if (parsed) { - for (const std::string &mod_id : config_enabled_mods) { + std::vector opened_mod_is_known; + parse_mods_config(mods_config_path, config_enabled_mods, config_mod_order); + + // Fill a vector with the relative order of the mods. Existing mods will get ordered below new mods. + std::vector sort_order; + sort_order.resize(opened_mods.size()); + opened_mod_is_known.resize(opened_mods.size(), false); + std::iota(sort_order.begin(), sort_order.end(), 0); + for (size_t i = 0; i < config_mod_order.size(); i++) { + auto it = opened_mods_by_id.find(config_mod_order[i]); + if (it != opened_mods_by_id.end()) { + sort_order[it->second] = opened_mods.size() + i; + opened_mod_is_known[it->second] = true; + } + } + + // Run the sort using the relative order computed before. + std::iota(opened_mods_order.begin(), opened_mods_order.end(), 0); + std::sort(opened_mods_order.begin(), opened_mods_order.end(), [&](size_t i, size_t j) { + return sort_order[i] < sort_order[j]; + }); + + // Enable mods that are specified in the configuration or mods that are considered new. + for (size_t i = 0; i < opened_mods.size(); i++) { + const std::string &mod_id = opened_mods[i].manifest.mod_id; + if (!opened_mod_is_known[i] || (config_enabled_mods.find(mod_id) != config_enabled_mods.end())) { enable_mod(mod_id, true, false); } - - { - std::unique_lock lock(opened_mods_mutex); - - // Fill a vector with the relative order of the mods. Existing mods will get ordered below new mods. - std::vector sort_order; - sort_order.resize(opened_mods.size()); - std::iota(sort_order.begin(), sort_order.end(), 0); - for (size_t i = 0; i < config_mod_order.size(); i++) { - auto it = opened_mods_by_id.find(config_mod_order[i]); - if (it != opened_mods_by_id.end()) { - sort_order[it->second] = opened_mods.size() + i; - } - } - - // Run the sort using the relative order computed before. - std::iota(opened_mods_order.begin(), opened_mods_order.end(), 0); - std::sort(opened_mods_order.begin(), opened_mods_order.end(), [&](size_t i, size_t j) { - return sort_order[i] < sort_order[j]; - }); - } } } From 653a82816eacd8225da2909fdb4c7904506fba36 Mon Sep 17 00:00:00 2001 From: Dario Date: Mon, 27 Jan 2025 20:45:21 -0300 Subject: [PATCH 17/25] Mods directory. --- librecomp/include/librecomp/mods.hpp | 1 + librecomp/src/recomp.cpp | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index c73a9772..ca53a44a 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -547,6 +547,7 @@ namespace recomp { void initialize_mods(); void scan_mods(); + std::filesystem::path get_mods_directory(); void enable_mod(const std::string& mod_id, bool enabled); bool is_mod_enabled(const std::string& mod_id); bool is_mod_auto_enabled(const std::string& mod_id); diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index ad57f363..80a2842c 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -103,6 +103,10 @@ void recomp::mods::scan_mods() { mod_context->load_mods_config(); } +std::filesystem::path recomp::mods::get_mods_directory() { + return config_path / mods_directory; +} + recomp::mods::ModContentTypeId recomp::mods::register_mod_content_type(const ModContentType& type) { std::lock_guard mod_lock{ mod_context_mutex }; return mod_context->register_content_type(type); From 5bde72d8ac136ce76168dc3e313f11c628a08ad5 Mon Sep 17 00:00:00 2001 From: Dario Date: Tue, 28 Jan 2025 22:33:00 -0300 Subject: [PATCH 18/25] Parse thumbnail when opening mods. --- librecomp/include/librecomp/mods.hpp | 8 ++++++-- librecomp/src/mod_manifest.cpp | 11 ++++++++++- librecomp/src/mods.cpp | 20 ++++++++++++++++---- librecomp/src/recomp.cpp | 5 +++++ 4 files changed, 37 insertions(+), 7 deletions(-) diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index ca53a44a..aad13856 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -332,6 +332,7 @@ namespace recomp { std::vector get_mod_details(const std::string& mod_game_id); void set_mod_index(const std::string &mod_game_id, const std::string &mod_id, size_t index); const ConfigSchema &get_mod_config_schema(const std::string &mod_id) const; + const std::vector &get_mod_thumbnail(const std::string &mod_id) const; void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value); ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id); void set_mods_config_path(const std::filesystem::path &path); @@ -347,7 +348,7 @@ namespace recomp { CodeModLoadError init_mod_code(uint8_t* rdram, const std::unordered_map& section_vrom_map, ModHandle& mod, int32_t load_address, bool hooks_available, uint32_t& ram_used, std::string& error_param); CodeModLoadError load_mod_code(uint8_t* rdram, ModHandle& mod, uint32_t base_event_index, std::string& error_param); CodeModLoadError resolve_code_dependencies(ModHandle& mod, const std::unordered_map& base_patched_funcs, std::string& error_param); - void add_opened_mod(ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& detected_content_types); + void add_opened_mod(ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& detected_content_types, std::vector&& thumbnail); void close_mods(); std::vector regenerate_with_hooks( const std::vector>& sorted_unprocessed_hooks, @@ -386,6 +387,7 @@ namespace recomp { // to add hooks to any functions that weren't already replaced by a mod. std::vector processed_hook_slots; ConfigSchema empty_schema; + std::vector empty_bytes; size_t num_events = 0; ModContentTypeId code_content_type_id; size_t active_game = (size_t)-1; @@ -414,8 +416,9 @@ namespace recomp { std::vector section_load_addresses; // Content types present in this mod. std::vector content_types; + std::vector thumbnail; - ModHandle(const ModContext& context, ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& content_types); + ModHandle(const ModContext& context, ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& content_types, std::vector&& thumbnail); ModHandle(const ModHandle& rhs) = delete; ModHandle& operator=(const ModHandle& rhs) = delete; ModHandle(ModHandle&& rhs); @@ -552,6 +555,7 @@ namespace recomp { bool is_mod_enabled(const std::string& mod_id); bool is_mod_auto_enabled(const std::string& mod_id); const ConfigSchema &get_mod_config_schema(const std::string &mod_id); + const std::vector &get_mod_thumbnail(const std::string &mod_id); void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value); ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id); ModContentTypeId register_mod_content_type(const ModContentType& type); diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp index c7a8d96f..e3791a4a 100644 --- a/librecomp/src/mod_manifest.cpp +++ b/librecomp/src/mod_manifest.cpp @@ -842,9 +842,18 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod(const std::filesys std::filesystem::path config_path = mod_config_directory / (manifest.mod_id + ".json"); parse_mod_config_storage(config_path, manifest.mod_id, config_storage, manifest.config_schema); + // Read the mod thumbnail if it exists. + static const std::string thumbnail_dds_name = "thumb.dds"; + static const std::string thumbnail_png_name = "thumb.png"; + bool exists = false; + std::vector thumbnail_data = manifest.file_handle->read_file(thumbnail_dds_name, exists); + if (!exists) { + thumbnail_data = manifest.file_handle->read_file(thumbnail_png_name, exists); + } + // Store the loaded mod manifest in a new mod handle. manifest.mod_root_path = mod_path; - add_opened_mod(std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types)); + add_opened_mod(std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types), std::move(thumbnail_data)); return ModOpenError::Good; } diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index b0febf84..07282415 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -266,13 +266,14 @@ recomp::mods::CodeModLoadError recomp::mods::validate_api_version(uint32_t api_v } } -recomp::mods::ModHandle::ModHandle(const ModContext& context, ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& content_types) : +recomp::mods::ModHandle::ModHandle(const ModContext& context, ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& content_types, std::vector&& thumbnail) : manifest(std::move(manifest)), config_storage(std::move(config_storage)), code_handle(), recompiler_context{std::make_unique()}, content_types{std::move(content_types)}, - game_indices{std::move(game_indices)} + game_indices{std::move(game_indices)}, + thumbnail{std::move(thumbnail)} { runtime_toggleable = true; for (ModContentTypeId type : this->content_types) { @@ -593,11 +594,11 @@ void unpatch_func(void* target_func, const recomp::mods::PatchData& data) { protect(target_func, old_flags); } -void recomp::mods::ModContext::add_opened_mod(ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& detected_content_types) { +void recomp::mods::ModContext::add_opened_mod(ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& detected_content_types, std::vector&& thumbnail) { std::unique_lock lock(opened_mods_mutex); size_t mod_index = opened_mods.size(); opened_mods_by_id.emplace(manifest.mod_id, mod_index); - opened_mods.emplace_back(*this, std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types)); + opened_mods.emplace_back(*this, std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types), std::move(thumbnail)); opened_mods_order.emplace_back(mod_index); } @@ -1195,6 +1196,17 @@ const recomp::mods::ConfigSchema &recomp::mods::ModContext::get_mod_config_schem return mod.manifest.config_schema; } +const std::vector &recomp::mods::ModContext::get_mod_thumbnail(const std::string &mod_id) const { + // Check that the mod exists. + auto find_it = opened_mods_by_id.find(mod_id); + if (find_it == opened_mods_by_id.end()) { + return empty_bytes; + } + + const ModHandle &mod = opened_mods[find_it->second]; + return mod.thumbnail; +} + void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value) { // Check that the mod exists. auto find_it = opened_mods_by_id.find(mod_id); diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index 80a2842c..e53ac1e5 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -525,6 +525,11 @@ const recomp::mods::ConfigSchema &recomp::mods::get_mod_config_schema(const std: return mod_context->get_mod_config_schema(mod_id); } +const std::vector &recomp::mods::get_mod_thumbnail(const std::string &mod_id) { + std::lock_guard lock{ mod_context_mutex }; + return mod_context->get_mod_thumbnail(mod_id); +} + void recomp::mods::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value) { std::lock_guard lock{ mod_context_mutex }; return mod_context->set_mod_config_value(mod_id, option_id, value); From e814ad6f833e1e73269625b8beef67f50dd54682 Mon Sep 17 00:00:00 2001 From: Dario Date: Wed, 29 Jan 2025 23:53:22 -0300 Subject: [PATCH 19/25] Auto-enabled mods. --- librecomp/include/librecomp/mods.hpp | 2 + librecomp/src/mods.cpp | 80 ++++++++++++++++++++++++++-- librecomp/src/recomp.cpp | 2 +- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index aad13856..ee983ccd 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -326,6 +326,7 @@ namespace recomp { void load_mods_config(); void enable_mod(const std::string& mod_id, bool enabled, bool trigger_save); bool is_mod_enabled(const std::string& mod_id); + bool is_mod_auto_enabled(const std::string& mod_id); size_t num_opened_mods(); std::vector load_mods(const GameEntry& game_entry, uint8_t* rdram, int32_t load_address, uint32_t& ram_used); void unload_mods(); @@ -369,6 +370,7 @@ namespace recomp { std::mutex opened_mods_mutex; std::unordered_set mod_ids; std::unordered_set enabled_mods; + std::unordered_set auto_enabled_mods; std::unordered_map patched_funcs; std::unordered_map loaded_mods_by_id; std::unique_ptr mod_configuration_thread; diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index 07282415..9b1f80ad 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -272,8 +272,8 @@ recomp::mods::ModHandle::ModHandle(const ModContext& context, ModManifest&& mani code_handle(), recompiler_context{std::make_unique()}, content_types{std::move(content_types)}, - game_indices{std::move(game_indices)}, - thumbnail{std::move(thumbnail)} + thumbnail{ std::move(thumbnail) }, + game_indices{std::move(game_indices)} { runtime_toggleable = true; for (ModContentTypeId type : this->content_types) { @@ -630,6 +630,7 @@ void recomp::mods::ModContext::close_mods() { opened_mods_order.clear(); mod_ids.clear(); enabled_mods.clear(); + auto_enabled_mods.clear(); } bool save_mod_config_storage(const std::filesystem::path &path, const std::string &mod_id, const recomp::Version &mod_version, const recomp::mods::ConfigStorage &config_storage, const recomp::mods::ConfigSchema &config_schema) { @@ -959,21 +960,90 @@ void recomp::mods::ModContext::enable_mod(const std::string& mod_id, bool enable if (enabled) { bool was_enabled = enabled_mods.emplace(mod_id).second; + // If mods have been loaded and a mod was successfully enabled by this call, call the on_enabled handlers for its content types. if (was_enabled && mods_loaded) { for (ModContentTypeId type_id : mod.content_types) { content_types[type_id.value].on_enabled(*this, mod); } } + + if (was_enabled) { + std::vector mod_stack; + mod_stack.emplace_back(mod_id); + while (!mod_stack.empty()) { + std::string mod_from_stack = std::move(mod_stack.back()); + mod_stack.pop_back(); + + auto mod_from_stack_it = opened_mods_by_id.find(mod_from_stack); + if (mod_from_stack_it != opened_mods_by_id.end()) { + const ModHandle &mod_from_stack_handle = opened_mods[mod_from_stack_it->second]; + for (const Dependency &dependency : mod_from_stack_handle.manifest.dependencies) { + if (!auto_enabled_mods.contains(dependency.mod_id)) { + auto_enabled_mods.emplace(dependency.mod_id); + mod_stack.emplace_back(dependency.mod_id); + + if (mods_loaded) { + for (ModContentTypeId type_id : mod_from_stack_handle.content_types) { + content_types[type_id.value].on_enabled(*this, mod_from_stack_handle); + } + } + } + } + } + } + } } else { bool was_disabled = enabled_mods.erase(mod_id) != 0; + // If mods have been loaded and a mod was successfully disabled by this call, call the on_disabled handlers for its content types. if (was_disabled && mods_loaded) { for (ModContentTypeId type_id : mod.content_types) { content_types[type_id.value].on_disabled(*this, mod); } } + + if (was_disabled) { + // The algorithm needs to be run again with a new set of auto-enabled mods from scratch for all enabled mods. + std::unordered_set new_auto_enabled_mods; + for (const std::string &enabled_mod_id : enabled_mods) { + std::vector mod_stack; + mod_stack.emplace_back(enabled_mod_id); + while (!mod_stack.empty()) { + std::string mod_from_stack = std::move(mod_stack.back()); + mod_stack.pop_back(); + + auto mod_from_stack_it = opened_mods_by_id.find(mod_from_stack); + if (mod_from_stack_it != opened_mods_by_id.end()) { + const ModHandle &mod_from_stack_handle = opened_mods[mod_from_stack_it->second]; + for (const Dependency &dependency : mod_from_stack_handle.manifest.dependencies) { + if (!new_auto_enabled_mods.contains(dependency.mod_id)) { + new_auto_enabled_mods.emplace(dependency.mod_id); + mod_stack.emplace_back(dependency.mod_id); + } + } + } + } + } + + if (mods_loaded) { + // Before replacing the old set with the new one, whatever does not exist in the new set anymore should trigger it's on_disabled callback. + for (const std::string &enabled_mod_id : auto_enabled_mods) { + if (!new_auto_enabled_mods.contains(enabled_mod_id)) { + auto enabled_mod_it = opened_mods_by_id.find(enabled_mod_id); + if (enabled_mod_it != opened_mods_by_id.end()) { + const ModHandle &enabled_mod_handle = opened_mods[enabled_mod_it->second]; + for (ModContentTypeId type_id : enabled_mod_handle.content_types) { + content_types[type_id.value].on_disabled(*this, enabled_mod_handle); + } + } + } + } + } + + auto_enabled_mods = new_auto_enabled_mods; + } } if (trigger_save) { @@ -985,6 +1055,10 @@ bool recomp::mods::ModContext::is_mod_enabled(const std::string& mod_id) { return enabled_mods.contains(mod_id); } +bool recomp::mods::ModContext::is_mod_auto_enabled(const std::string& mod_id) { + return auto_enabled_mods.contains(mod_id); +} + size_t recomp::mods::ModContext::num_opened_mods() { return opened_mods.size(); } @@ -1338,7 +1412,7 @@ std::vector recomp::mods::ModContext::load_mo // Find and load active mods. for (size_t mod_index = 0; mod_index < opened_mods.size(); mod_index++) { auto& mod = opened_mods[mod_index]; - if (mod.is_for_game(mod_game_index) && enabled_mods.contains(mod.manifest.mod_id)) { + if (mod.is_for_game(mod_game_index) && (enabled_mods.contains(mod.manifest.mod_id) || auto_enabled_mods.contains(mod.manifest.mod_id))) { active_mods.push_back(mod_index); loaded_mods_by_id.emplace(mod.manifest.mod_id, mod_index); diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index e53ac1e5..9316c6b3 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -517,7 +517,7 @@ bool recomp::mods::is_mod_enabled(const std::string& mod_id) { bool recomp::mods::is_mod_auto_enabled(const std::string& mod_id) { std::lock_guard lock{ mod_context_mutex }; - return false; // TODO + return mod_context->is_mod_auto_enabled(mod_id); } const recomp::mods::ConfigSchema &recomp::mods::get_mod_config_schema(const std::string &mod_id) { From e6d1ed5dd6f1a73d033ad321a0d52d1d31679b76 Mon Sep 17 00:00:00 2001 From: Mr-Wiseguy Date: Fri, 31 Jan 2025 00:09:04 -0500 Subject: [PATCH 20/25] Implement extended function exports that pass the caller mod's index as an extra argument --- N64Recomp | 2 +- librecomp/include/librecomp/mods.hpp | 5 ++++- librecomp/include/librecomp/overlays.hpp | 2 ++ librecomp/src/mods.cpp | 12 ++++++++++-- librecomp/src/overlays.cpp | 13 +++++++++++++ 5 files changed, 30 insertions(+), 4 deletions(-) diff --git a/N64Recomp b/N64Recomp index 8781eb44..2af6f2d1 160000 --- a/N64Recomp +++ b/N64Recomp @@ -1 +1 @@ -Subproject commit 8781eb44acbf55cb6a109d2aa5529aadb95a419d +Subproject commit 2af6f2d161f755ca68f8baeb4dc5b69ef58a862c diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index ee983ccd..824a3d23 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -30,6 +30,7 @@ namespace N64Recomp { class Context; struct LiveGeneratorOutput; + class ShimFunction; }; namespace recomp { @@ -348,7 +349,7 @@ namespace recomp { void check_dependencies(ModHandle& mod, std::vector>& errors); CodeModLoadError init_mod_code(uint8_t* rdram, const std::unordered_map& section_vrom_map, ModHandle& mod, int32_t load_address, bool hooks_available, uint32_t& ram_used, std::string& error_param); CodeModLoadError load_mod_code(uint8_t* rdram, ModHandle& mod, uint32_t base_event_index, std::string& error_param); - CodeModLoadError resolve_code_dependencies(ModHandle& mod, const std::unordered_map& base_patched_funcs, std::string& error_param); + CodeModLoadError resolve_code_dependencies(ModHandle& mod, size_t mod_index, const std::unordered_map& base_patched_funcs, std::string& error_param); void add_opened_mod(ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& detected_content_types, std::vector&& thumbnail); void close_mods(); std::vector regenerate_with_hooks( @@ -388,6 +389,8 @@ namespace recomp { // Tracks which hook slots have already been processed. Used to regenerate vanilla functions as needed // to add hooks to any functions that weren't already replaced by a mod. std::vector processed_hook_slots; + // Generated shim functions to use for implementing shim exports. + std::vector> shim_functions; ConfigSchema empty_schema; std::vector empty_bytes; size_t num_events = 0; diff --git a/librecomp/include/librecomp/overlays.hpp b/librecomp/include/librecomp/overlays.hpp index 40cbb969..35703bd8 100644 --- a/librecomp/include/librecomp/overlays.hpp +++ b/librecomp/include/librecomp/overlays.hpp @@ -25,6 +25,7 @@ namespace recomp { void register_patches(const char* patch_data, size_t patch_size, SectionTableEntry* code_sections, size_t num_sections); void register_base_export(const std::string& name, recomp_func_t* func); + void register_ext_base_export(const std::string& name, recomp_func_ext_t* func); void register_base_exports(const FunctionExport* exports); void register_base_events(char const* const* event_names); void register_manual_patch_symbols(const ManualPatchSymbol* manual_patch_symbols); @@ -38,6 +39,7 @@ namespace recomp { bool get_func_entry_by_section_index_function_offset(uint16_t code_section_index, uint32_t function_offset, FuncEntry& func_out); recomp_func_t* get_func_by_section_index_function_offset(uint16_t code_section_index, uint32_t function_offset); recomp_func_t* get_base_export(const std::string& export_name); + recomp_func_ext_t* get_ext_base_export(const std::string& export_name); size_t get_base_event_index(const std::string& event_name); size_t num_base_events(); diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index 9b1f80ad..24012127 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -1520,7 +1520,7 @@ std::vector recomp::mods::ModContext::load_mo for (size_t mod_index : loaded_code_mods) { auto& mod = opened_mods[mod_index]; std::string cur_error_param; - CodeModLoadError cur_error = resolve_code_dependencies(mod, base_patched_funcs, cur_error_param); + CodeModLoadError cur_error = resolve_code_dependencies(mod, mod_index, base_patched_funcs, cur_error_param); if (cur_error != CodeModLoadError::Good) { if (cur_error_param.empty()) { ret.emplace_back(mod.manifest.mod_id, ModLoadError::FailedToLoadCode, error_to_string(cur_error)); @@ -2172,7 +2172,7 @@ recomp::mods::CodeModLoadError recomp::mods::ModContext::load_mod_code(uint8_t* return CodeModLoadError::Good; } -recomp::mods::CodeModLoadError recomp::mods::ModContext::resolve_code_dependencies(recomp::mods::ModHandle& mod, const std::unordered_map& base_patched_funcs, std::string& error_param) { +recomp::mods::CodeModLoadError recomp::mods::ModContext::resolve_code_dependencies(recomp::mods::ModHandle& mod, size_t mod_index, const std::unordered_map& base_patched_funcs, std::string& error_param) { // Reference symbols. std::string reference_syms_error_param{}; CodeModLoadError reference_syms_error = mod.code_handle->populate_reference_symbols(*mod.recompiler_context, reference_syms_error_param); @@ -2201,6 +2201,13 @@ recomp::mods::CodeModLoadError recomp::mods::ModContext::resolve_code_dependenci if (dependency_id == N64Recomp::DependencyBaseRecomp) { recomp_func_t* func_ptr = recomp::overlays::get_base_export(imported_func.base.name); did_find_func = func_ptr != nullptr; + if (!did_find_func) { + recomp_func_ext_t* func_ext_ptr = recomp::overlays::get_ext_base_export(imported_func.base.name); + did_find_func = func_ext_ptr != nullptr; + if (did_find_func) { + func_ptr = shim_functions.emplace_back(std::make_unique(func_ext_ptr, mod_index)).get()->get_func(); + } + } func_handle = func_ptr; } else if (dependency_id == N64Recomp::DependencySelf) { @@ -2343,6 +2350,7 @@ void recomp::mods::ModContext::unload_mods() { loaded_mods_by_id.clear(); hook_slots.clear(); processed_hook_slots.clear(); + shim_functions.clear(); recomp::mods::reset_events(); recomp::mods::reset_hooks(); num_events = recomp::overlays::num_base_events(); diff --git a/librecomp/src/overlays.cpp b/librecomp/src/overlays.cpp index e807cfa5..8c8e5d1d 100644 --- a/librecomp/src/overlays.cpp +++ b/librecomp/src/overlays.cpp @@ -38,6 +38,7 @@ static std::unordered_map patch_code_sections_by_rom{}; static std::vector loaded_sections{}; static std::unordered_map func_map{}; static std::unordered_map base_exports{}; +static std::unordered_map ext_base_exports{}; static std::unordered_map base_events; static std::unordered_map manual_patch_symbols_by_vram; @@ -67,6 +68,10 @@ void recomp::overlays::register_base_export(const std::string& name, recomp_func base_exports.emplace(name, func); } +void recomp::overlays::register_ext_base_export(const std::string& name, recomp_func_ext_t* func) { + ext_base_exports.emplace(name, func); +} + void recomp::overlays::register_base_exports(const FunctionExport* export_list) { std::unordered_map patch_func_vram_map{}; @@ -98,6 +103,14 @@ recomp_func_t* recomp::overlays::get_base_export(const std::string& export_name) return it->second; } +recomp_func_ext_t* recomp::overlays::get_ext_base_export(const std::string& export_name) { + auto it = ext_base_exports.find(export_name); + if (it == ext_base_exports.end()) { + return nullptr; + } + return it->second; +} + void recomp::overlays::register_base_events(char const* const* event_names) { for (size_t event_index = 0; event_names[event_index] != nullptr; event_index++) { base_events.emplace(event_names[event_index], event_index); From fb57858a68fe40e029abb83d27961896a3080718 Mon Sep 17 00:00:00 2001 From: Mr-Wiseguy Date: Fri, 31 Jan 2025 01:51:58 -0500 Subject: [PATCH 21/25] Fix mod configs not saving and default value not getting parsed --- librecomp/src/mod_manifest.cpp | 2 +- librecomp/src/mods.cpp | 35 +++++++++++++++++++++++++++------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp index e3791a4a..38410d3d 100644 --- a/librecomp/src/mod_manifest.cpp +++ b/librecomp/src/mod_manifest.cpp @@ -319,7 +319,7 @@ constexpr std::string_view config_schema_step_key = "step"; constexpr std::string_view config_schema_precision_key = "precision"; constexpr std::string_view config_schema_percent_key = "percent"; constexpr std::string_view config_schema_options_key = "options"; -constexpr std::string_view config_schema_default_key = "min"; +constexpr std::string_view config_schema_default_key = "default"; std::unordered_map config_option_map{ { "Enum", recomp::mods::ConfigOptionType::Enum}, diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index 24012127..e15beb10 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -736,6 +736,9 @@ void recomp::mods::ModContext::dirty_mod_configuration_thread_process() { else if (std::get_if(&variant) != nullptr) { pending_config_save = true; } + else if (const ModConfigQueueSaveMod* queue_save_mod = std::get_if(&variant)) { + pending_mods.emplace(queue_save_mod->mod_id); + } }; while (active) { @@ -1281,14 +1284,13 @@ const std::vector &recomp::mods::ModContext::get_mod_thumbnail(const std:: return mod.thumbnail; } -void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value) { +void recomp::mods::ModContext::set_mod_config_value(size_t mod_index, const std::string &option_id, const ConfigValueVariant &value) { // Check that the mod exists. - auto find_it = opened_mods_by_id.find(mod_id); - if (find_it == opened_mods_by_id.end()) { + if (mod_index >= opened_mods.size()) { return; } - ModHandle &mod = opened_mods[find_it->second]; + ModHandle &mod = opened_mods[mod_index]; std::unique_lock lock(mod_config_storage_mutex); auto option_by_id_it = mod.manifest.config_schema.options_by_id.find(option_id); if (option_by_id_it != mod.manifest.config_schema.options_by_id.end()) { @@ -1322,17 +1324,26 @@ void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, c } // Notify the asynchronous thread it should save the configuration for this mod. - mod_configuration_thread_queue.enqueue(ModConfigQueueSaveMod(mod_id)); + mod_configuration_thread_queue.enqueue(ModConfigQueueSaveMod(mod.manifest.mod_id)); } -recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(const std::string &mod_id, const std::string &option_id) { +void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value) { // Check that the mod exists. auto find_it = opened_mods_by_id.find(mod_id); if (find_it == opened_mods_by_id.end()) { + return; + } + + set_mod_config_value(find_it->second, option_id, value); +} + +recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(size_t mod_index, const std::string &option_id) { + // Check that the mod exists. + if (mod_index >= opened_mods.size()) { return std::monostate(); } - const ModHandle &mod = opened_mods[find_it->second]; + const ModHandle &mod = opened_mods[mod_index]; std::unique_lock lock(mod_config_storage_mutex); auto it = mod.config_storage.value_map.find(option_id); if (it != mod.config_storage.value_map.end()) { @@ -1360,6 +1371,16 @@ recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value( } } +recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(const std::string &mod_id, const std::string &option_id) { + // Check that the mod exists. + auto find_it = opened_mods_by_id.find(mod_id); + if (find_it == opened_mods_by_id.end()) { + return std::monostate(); + } + + return get_mod_config_value(find_it->second, option_id); +} + void recomp::mods::ModContext::set_mods_config_path(const std::filesystem::path &path) { mods_config_path = path; } From 2d76c8434b1337a230086f756ec3df01cbc74219 Mon Sep 17 00:00:00 2001 From: Mr-Wiseguy Date: Fri, 31 Jan 2025 01:59:13 -0500 Subject: [PATCH 22/25] Implement API to allow mods to read their config values --- librecomp/CMakeLists.txt | 1 + librecomp/include/librecomp/helpers.hpp | 22 +++++++++ librecomp/include/librecomp/mods.hpp | 7 +++ librecomp/src/mod_config_api.cpp | 60 +++++++++++++++++++++++++ librecomp/src/recomp.cpp | 11 +++++ 5 files changed, 101 insertions(+) create mode 100644 librecomp/src/mod_config_api.cpp diff --git a/librecomp/CMakeLists.txt b/librecomp/CMakeLists.txt index 61db595e..9c4bf53b 100644 --- a/librecomp/CMakeLists.txt +++ b/librecomp/CMakeLists.txt @@ -20,6 +20,7 @@ add_library(librecomp STATIC "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_events.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_hooks.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_manifest.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/mod_config_api.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/overlays.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/pak.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/pi.cpp" diff --git a/librecomp/include/librecomp/helpers.hpp b/librecomp/include/librecomp/helpers.hpp index 08d45444..d8f5afd0 100644 --- a/librecomp/include/librecomp/helpers.hpp +++ b/librecomp/include/librecomp/helpers.hpp @@ -1,6 +1,8 @@ #ifndef __RECOMP_HELPERS__ #define __RECOMP_HELPERS__ +#include + #include "recomp.h" #include @@ -51,6 +53,26 @@ inline float _arg_float_f14(uint8_t* rdram, recomp_context* ctx) { return ctx->f14.fl; } +template +std::string _arg_string(uint8_t* rdram, recomp_context* ctx) { + PTR(char) str = _arg(rdram, ctx); + + // Get the length of the byteswapped string. + size_t len = 0; + while (MEM_B(str, len) != 0x00) { + len++; + } + + std::string ret{}; + ret.reserve(len + 1); + + for (size_t i = 0; i < len; i++) { + ret += (char)MEM_B(str, i); + } + + return ret; +} + template void _return(recomp_context* ctx, T val) { static_assert(sizeof(T) <= 4 && "Only 32-bit value returns supported currently"); diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index 824a3d23..20b7f808 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -335,7 +335,9 @@ namespace recomp { void set_mod_index(const std::string &mod_game_id, const std::string &mod_id, size_t index); const ConfigSchema &get_mod_config_schema(const std::string &mod_id) const; const std::vector &get_mod_thumbnail(const std::string &mod_id) const; + void set_mod_config_value(size_t mod_index, const std::string &option_id, const ConfigValueVariant &value); void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value); + ConfigValueVariant get_mod_config_value(size_t mod_index, const std::string &option_id); ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id); void set_mods_config_path(const std::filesystem::path &path); void set_mod_config_directory(const std::filesystem::path &path); @@ -561,10 +563,15 @@ namespace recomp { bool is_mod_auto_enabled(const std::string& mod_id); const ConfigSchema &get_mod_config_schema(const std::string &mod_id); const std::vector &get_mod_thumbnail(const std::string &mod_id); + void set_mod_config_value(size_t mod_index, const std::string &option_id, const ConfigValueVariant &value); void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value); + ConfigValueVariant get_mod_config_value(size_t mod_index, const std::string &option_id); ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id); ModContentTypeId register_mod_content_type(const ModContentType& type); bool register_mod_container_type(const std::string& extension, const std::vector& content_types, bool requires_manifest); + + + void register_config_exports(); } }; diff --git a/librecomp/src/mod_config_api.cpp b/librecomp/src/mod_config_api.cpp new file mode 100644 index 00000000..f619b2f9 --- /dev/null +++ b/librecomp/src/mod_config_api.cpp @@ -0,0 +1,60 @@ +#include "librecomp/mods.hpp" +#include "librecomp/helpers.hpp" +#include "librecomp/addresses.hpp" + +void recomp_get_config_u32(uint8_t* rdram, recomp_context* ctx, size_t mod_index) { + recomp::mods::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx)); + if (uint32_t* as_u32 = std::get_if(&val)) { + _return(ctx, *as_u32); + } + else { + _return(ctx, uint32_t{0}); + } +} + +void recomp_get_config_double(uint8_t* rdram, recomp_context* ctx, size_t mod_index) { + recomp::mods::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx)); + if (double* as_double = std::get_if(&val)) { + ctx->f0.d = *as_double; + } + else { + ctx->f0.d = 0.0; + } +} + +void recomp_get_config_string(uint8_t* rdram, recomp_context* ctx, size_t mod_index) { + recomp::mods::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx)); + if (std::string* as_string = std::get_if(&val)) { + const std::string& str = *as_string; + // Allocate space in the recomp heap to hold the string, including the null terminator. + size_t alloc_size = (str.size() + 1 + 15) & ~15; + gpr offset = reinterpret_cast(recomp::alloc(rdram, alloc_size)) - rdram; + gpr addr = offset + 0xFFFFFFFF80000000ULL; + + // Copy the string's data into the allocated memory and null terminate it. + for (size_t i = 0; i < str.size(); i++) { + MEM_B(i, addr) = str[i]; + } + MEM_B(str.size(), addr) = 0; + + // Return the allocated memory. + ctx->r2 = addr; + } + else { + _return(ctx, NULLPTR); + } +} + +void recomp_free_config_string(uint8_t* rdram, recomp_context* ctx) { + gpr str_rdram = (gpr)_arg<0, PTR(char)>(rdram, ctx); + gpr offset = str_rdram - 0xFFFFFFFF80000000ULL; + + recomp::free(rdram, rdram + offset); +} + +void recomp::mods::register_config_exports() { + recomp::overlays::register_ext_base_export("recomp_get_config_u32", recomp_get_config_u32); + recomp::overlays::register_ext_base_export("recomp_get_config_double", recomp_get_config_double); + recomp::overlays::register_ext_base_export("recomp_get_config_string", recomp_get_config_string); + recomp::overlays::register_base_export("recomp_free_config_string", recomp_free_config_string); +} diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index 9316c6b3..2ac8d46b 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -530,11 +530,21 @@ const std::vector &recomp::mods::get_mod_thumbnail(const std::string &mod_ return mod_context->get_mod_thumbnail(mod_id); } +void recomp::mods::set_mod_config_value(size_t mod_index, const std::string &option_id, const ConfigValueVariant &value) { + std::lock_guard lock{ mod_context_mutex }; + return mod_context->set_mod_config_value(mod_index, option_id, value); +} + void recomp::mods::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value) { std::lock_guard lock{ mod_context_mutex }; return mod_context->set_mod_config_value(mod_id, option_id, value); } +recomp::mods::ConfigValueVariant recomp::mods::get_mod_config_value(size_t mod_index, const std::string &option_id) { + std::lock_guard lock{ mod_context_mutex }; + return mod_context->get_mod_config_value(mod_index, option_id); +} + recomp::mods::ConfigValueVariant recomp::mods::get_mod_config_value(const std::string &mod_id, const std::string &option_id) { std::lock_guard lock{ mod_context_mutex }; return mod_context->get_mod_config_value(mod_id, option_id); @@ -714,6 +724,7 @@ void recomp::start( } recomp::register_heap_exports(); + recomp::mods::register_config_exports(); std::thread game_thread{[](ultramodern::renderer::WindowHandle window_handle, uint8_t* rdram) { debug_printf("[Recomp] Starting\n"); From 21bea3b0d1e038a2c33a17aa62d56390a26e3731 Mon Sep 17 00:00:00 2001 From: Mr-Wiseguy Date: Fri, 31 Jan 2025 02:29:42 -0500 Subject: [PATCH 23/25] Fix config value parsing to allow integral values for double fields --- librecomp/src/mod_config_api.cpp | 8 +++++++- librecomp/src/mod_manifest.cpp | 18 ++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/librecomp/src/mod_config_api.cpp b/librecomp/src/mod_config_api.cpp index f619b2f9..b1c430ae 100644 --- a/librecomp/src/mod_config_api.cpp +++ b/librecomp/src/mod_config_api.cpp @@ -7,6 +7,9 @@ void recomp_get_config_u32(uint8_t* rdram, recomp_context* ctx, size_t mod_index if (uint32_t* as_u32 = std::get_if(&val)) { _return(ctx, *as_u32); } + else if (double* as_double = std::get_if(&val)) { + _return(ctx, uint32_t(int32_t(*as_double))); + } else { _return(ctx, uint32_t{0}); } @@ -14,7 +17,10 @@ void recomp_get_config_u32(uint8_t* rdram, recomp_context* ctx, size_t mod_index void recomp_get_config_double(uint8_t* rdram, recomp_context* ctx, size_t mod_index) { recomp::mods::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx)); - if (double* as_double = std::get_if(&val)) { + if (uint32_t* as_u32 = std::get_if(&val)) { + ctx->f0.d = double(*as_u32); + } + else if (double* as_double = std::get_if(&val)) { ctx->f0.d = *as_double; } else { diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp index 38410d3d..4ce7a595 100644 --- a/librecomp/src/mod_manifest.cpp +++ b/librecomp/src/mod_manifest.cpp @@ -427,26 +427,29 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j auto min = config_schema_json.find(config_schema_min_key); if (min != config_schema_json.end()) { - if (!get_to(*min, option_number.min)) { + if (!min->is_number()) { error_param = config_schema_min_key; return recomp::mods::ModOpenError::IncorrectConfigSchemaType; } + option_number.min = min->template get(); } auto max = config_schema_json.find(config_schema_max_key); if (max != config_schema_json.end()) { - if (!get_to(*max, option_number.max)) { + if (!max->is_number()) { error_param = config_schema_max_key; return recomp::mods::ModOpenError::IncorrectConfigSchemaType; } + option_number.max = max->template get(); } auto step = config_schema_json.find(config_schema_step_key); if (step != config_schema_json.end()) { - if (!get_to(*step, option_number.step)) { + if (!step->is_number()) { error_param = config_schema_step_key; return recomp::mods::ModOpenError::IncorrectConfigSchemaType; } + option_number.step = step->template get(); } auto precision = config_schema_json.find(config_schema_precision_key); @@ -471,10 +474,11 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j auto default_value = config_schema_json.find(config_schema_default_key); if (default_value != config_schema_json.end()) { - if (!get_to(*default_value, option_number.default_value)) { + if (!default_value->is_number()) { error_param = config_schema_default_key; return recomp::mods::ModOpenError::IncorrectConfigSchemaType; } + option_number.default_value = default_value->template get(); } option.variant = option_number; @@ -676,8 +680,6 @@ bool parse_mod_config_storage(const std::filesystem::path &path, const std::stri } // Only parse the object for known option types based on the schema. - int64_t value_int64; - double value_double; std::string value_str; for (const recomp::mods::ConfigOption &option : config_schema.options) { auto option_json = storage_json->find(option.id); @@ -698,8 +700,8 @@ bool parse_mod_config_storage(const std::filesystem::path &path, const std::stri break; case recomp::mods::ConfigOptionType::Number: - if (get_to(*option_json, value_double)) { - config_storage.value_map[option.id] = value_double; + if (option_json->is_number()) { + config_storage.value_map[option.id] = option_json->template get(); } break; From 8147964aa5972bae4b1f264a0f76f90f90ae911f Mon Sep 17 00:00:00 2001 From: Dario Date: Fri, 31 Jan 2025 22:21:21 -0300 Subject: [PATCH 24/25] Change construction of ModConfigQueueSaveMod. --- librecomp/src/mods.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index e15beb10..4b2ea0d7 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -1324,7 +1324,7 @@ void recomp::mods::ModContext::set_mod_config_value(size_t mod_index, const std: } // Notify the asynchronous thread it should save the configuration for this mod. - mod_configuration_thread_queue.enqueue(ModConfigQueueSaveMod(mod.manifest.mod_id)); + mod_configuration_thread_queue.enqueue(ModConfigQueueSaveMod{ mod.manifest.mod_id }); } void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const ConfigValueVariant &value) { From dfbea470537eec4e209674258ab54cab93afe984 Mon Sep 17 00:00:00 2001 From: Mr-Wiseguy Date: Tue, 11 Feb 2025 23:01:10 -0500 Subject: [PATCH 25/25] Fix N64Recomp commit after rebase --- N64Recomp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/N64Recomp b/N64Recomp index 2af6f2d1..8781eb44 160000 --- a/N64Recomp +++ b/N64Recomp @@ -1 +1 @@ -Subproject commit 2af6f2d161f755ca68f8baeb4dc5b69ef58a862c +Subproject commit 8781eb44acbf55cb6a109d2aa5529aadb95a419d