From 58b023750ec1aa36f5028c47d3ca08f03262c0ad Mon Sep 17 00:00:00 2001 From: thecozies <79979276+thecozies@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:30:48 -0500 Subject: [PATCH] Separate configuration into librecomp/config --- librecomp/CMakeLists.txt | 2 + librecomp/include/librecomp/config.hpp | 326 +++++++++++++ librecomp/include/librecomp/game.hpp | 1 + librecomp/include/librecomp/mods.hpp | 88 +--- librecomp/src/config.cpp | 638 +++++++++++++++++++++++++ librecomp/src/config_option.cpp | 73 +++ librecomp/src/mod_config_api.cpp | 6 +- librecomp/src/mod_manifest.cpp | 302 ++++++++---- librecomp/src/mods.cpp | 140 +----- librecomp/src/recomp.cpp | 14 +- 10 files changed, 1293 insertions(+), 297 deletions(-) create mode 100644 librecomp/include/librecomp/config.hpp create mode 100644 librecomp/src/config.cpp create mode 100644 librecomp/src/config_option.cpp diff --git a/librecomp/CMakeLists.txt b/librecomp/CMakeLists.txt index 9c4bf53..56167de 100644 --- a/librecomp/CMakeLists.txt +++ b/librecomp/CMakeLists.txt @@ -9,6 +9,8 @@ set(CMAKE_CXX_EXTENSIONS OFF) add_library(librecomp STATIC "${CMAKE_CURRENT_SOURCE_DIR}/src/ai.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/cont.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/config_option.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/config.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/dp.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/eep.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/euc-jp.cpp" diff --git a/librecomp/include/librecomp/config.hpp b/librecomp/include/librecomp/config.hpp new file mode 100644 index 0000000..d989b67 --- /dev/null +++ b/librecomp/include/librecomp/config.hpp @@ -0,0 +1,326 @@ +#ifndef __RECOMP_CONFIG_HPP__ +#define __RECOMP_CONFIG_HPP__ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "recomp.h" + +namespace recomp { + namespace config { + enum class ConfigOptionType { + None, + Enum, + Number, + String, + Bool + }; + + struct ConfigOptionEnumOption { + uint32_t value; + std::string key; + std::string name; + + template + ConfigOptionEnumOption(ENUM_TYPE value, std::string key, std::string name) + : value(static_cast(value)), key(key), name(name) {} + + template + ConfigOptionEnumOption(ENUM_TYPE value, std::string key) + : value(static_cast(value)), key(key), name(key) {} + }; + + struct ConfigOptionEnum { + std::vector options; + uint32_t default_value = 0; + + // Case insensitive search for an option based on a key string. (Matches against options[n].key) + std::vector::const_iterator find_option_from_string(const std::string& option_key) const; + // Search for an option that has a specific value. (Matches against options[n].value) + std::vector::const_iterator find_option_from_value(uint32_t value) const; + // Verify an option has a unique key and a unique value + bool can_add_option(const std::string& option_key, uint32_t option_value) const; + }; + + 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; + }; + + struct ConfigOptionBool { + bool default_value; + }; + + typedef std::variant ConfigOptionVariant; + + struct ConfigOption { + std::string id; + std::string name; + std::string description; + bool hidden = false; + ConfigOptionType type; + ConfigOptionVariant variant; + }; + + typedef std::variant ConfigValueVariant; + + // Manages value dependencies between config options (e.g. option is hidden or disabled from other option being a certain value) . + class ConfigOptionDependency { + private: + // Maps options to the options that are affected by their values + std::unordered_map> option_to_dependencies = {}; + // Maps dependent options to the values that the source option can be + std::unordered_map> dependency_to_values = {}; + public: + ConfigOptionDependency() = default; + + // Add dependency. When is one of the , is affected. + void add_option_dependency(size_t dependent_option_index, size_t source_option_index, std::vector &values); + + // Check which dependent options are affected by the value of the source option. + // Returns a map of dependent options and if they are a match + std::unordered_map check_option_dependencies(size_t source_option_index, ConfigValueVariant value); + }; + + struct ConfigSchema { + std::vector options; + std::unordered_map options_by_id; + ConfigOptionDependency disable_dependencies; + ConfigOptionDependency hidden_dependencies; + }; + + struct ConfigStorage { + std::unordered_map value_map; + }; + + enum class ConfigOptionUpdateType { + Disabled, + Hidden, + EnumDetails, + EnumDisabled, + Value, + Description + }; + + struct ConfigOptionUpdateContext { + size_t option_index; + std::vector updates = {}; + }; + + enum class OptionChangeContext { + Load, + Temporary, + Permanent + }; + + using on_option_change_callback = std::function; + + using parse_option_func = std::function; + using serialize_option_func = std::function; + + class Config { + public: + std::string name; + // id is used for the file name (e.g. general.json) and storing keys + std::string id; + // If true, any configuration changes are temporarily stored until Apply is pressed. + // Changing the tab will prompt the user to either apply or cancel changes. + bool requires_confirmation = false; + + std::unordered_set modified_options = {}; + + // For base game configs + Config(std::string name, std::string id, bool requires_confirmation = false); + // For mod configs + Config(); + + void set_id(const std::string &id); + void set_mod_version(const std::string &mod_version); + + void add_option(const ConfigOption& option); + + void add_enum_option( + const std::string &id, + const std::string &name, + const std::string &description, + const std::vector &options, + uint32_t default_value, + bool hidden = false + ); + + template + void add_enum_option( + const std::string &id, + const std::string &name, + const std::string &description, + const std::vector &options, + ENUM_TYPE default_value, + bool hidden = false + ) { + add_enum_option(id, name, description, options, static_cast(default_value), hidden); + }; + + void add_number_option( + const std::string &id, + const std::string &name, + const std::string &description, + double min = 0, + double max = 0, + double step = 1, + int precision = 0, + bool percent = false, + double default_value = 0, + bool hidden = false + ); + + void add_string_option( + const std::string &id, + const std::string &name, + const std::string &description, + const std::string &default_value, + bool hidden = false + ); + + void add_bool_option( + const std::string &id, + const std::string &name, + const std::string &description, + bool default_value = false, + bool hidden = false + ); + + const ConfigValueVariant get_option_value(const std::string& option_id) const; + const ConfigValueVariant get_temp_option_value(const std::string& option_id) const; + // This should only be used internally to recompui. Other changes to values should be done through update_option_value + // so rendering can be updated with your new set value. + void set_option_value(const std::string& option_id, ConfigValueVariant value); + bool get_enum_option_disabled(size_t option_index, uint32_t enum_index); + void add_option_change_callback(const std::string& option_id, on_option_change_callback callback); + void set_apply_callback(std::function callback) { + apply_callback = callback; + } + void set_save_callback(std::function callback) { + save_callback = callback; + } + + void report_config_option_update(size_t option_index, ConfigOptionUpdateType update_type); + void update_option_disabled(size_t option_index, bool disabled); + void update_option_disabled(const std::string& option_id, bool disabled); + void update_option_hidden(size_t option_index, bool hidden); + void update_option_hidden(const std::string& option_id, bool hidden); + void update_option_enum_details(const std::string& option_id, const std::string& enum_details); + void update_option_value(const std::string& option_id, ConfigValueVariant value); + void update_option_description(const std::string& option_id, const std::string& new_description); + void update_enum_option_disabled(const std::string& option_id, uint32_t enum_index, bool disabled); + + // Makes the dependent option disabled when the source option is set to any of the specified values. + void add_option_disable_dependency(const std::string& dependent_option_id, const std::string& source_option_id, std::vector &values); + template + void add_option_disable_dependency(const std::string& dependent_option_id, const std::string& source_option_id, ENUM_TYPE... enum_values) { + std::vector values; + for (const auto& value : {enum_values...}) { + values.push_back(static_cast(value)); + } + add_option_disable_dependency(dependent_option_id, source_option_id, values); + }; + // Makes the dependent option hidden when the source option is set to any of the specified values. + // Does not override the option's inherent hidden property if set. + void add_option_hidden_dependency(const std::string& dependent_option_id, const std::string& source_option_id, std::vector &values); + template + void add_option_hidden_dependency(const std::string& dependent_option_id, const std::string& source_option_id, ENUM_TYPE... enum_values) { + std::vector values; + for (const auto& value : {enum_values...}) { + values.push_back(static_cast(value)); + } + add_option_hidden_dependency(dependent_option_id, source_option_id, values); + }; + void add_option_hidden_dependency(const std::string& dependent_option_id, const std::string& source_option_id, bool bool_val) { + std::vector values = { bool_val }; + add_option_hidden_dependency(dependent_option_id, source_option_id, values); + }; + + bool load_config(std::function validate_callback = nullptr); + bool save_config(); + bool save_config_json(nlohmann::json config_json) const; + nlohmann::json get_json_config() const; + + + void revert_temp_config(); + bool is_dirty(); + + std::vector get_config_option_updates() { return config_option_updates; } + bool is_config_option_disabled(size_t option_index) { return disabled_options.contains(option_index); } + bool is_config_option_hidden(size_t option_index); + void clear_config_option_updates() { + config_option_updates.clear(); + } + std::string get_enum_option_details(size_t option_index); + void on_json_parse_option(const std::string& option_id, parse_option_func callback) { + json_parse_option_map[option_id] = callback; + } + void on_json_serialize_option(const std::string& option_id, serialize_option_func callback) { + json_serialize_option_map[option_id] = callback; + } + + const ConfigStorage& get_config_storage() const; + const ConfigSchema& get_config_schema() const; + + private: + bool loaded_config = false; + bool is_mod_config = false; + + std::string config_file_name; + std::string mod_version; // only used if mod + + ConfigSchema schema; + ConfigStorage storage; + ConfigStorage temp_storage; + + std::unordered_map option_change_callbacks = {}; + std::function apply_callback = nullptr; + std::function save_callback = nullptr; + std::vector config_option_updates = {}; + std::unordered_set disabled_options = {}; + std::unordered_set hidden_options = {}; + std::unordered_map enum_option_details = {}; + std::unordered_map> enum_options_disabled = {}; + + std::unordered_map json_parse_option_map = {}; + std::unordered_map json_serialize_option_map = {}; + + const ConfigValueVariant get_option_value_from_storage(const std::string& option_id, const ConfigStorage& src) const; + + void derive_all_config_option_dependencies(); + void derive_option_dependencies(size_t option_index); + void try_call_option_change_callback(const std::string& option_id, ConfigValueVariant value, ConfigValueVariant prev_value, OptionChangeContext change_context); + const ConfigValueVariant get_option_default_value(const std::string& option_id) const; + void determine_changed_option(const std::string& option_id); + ConfigValueVariant parse_config_option_json_value(const nlohmann::json& json_value, const ConfigOption &option); + + // Return pointer to the root of where the config values should be stored in the json. + nlohmann::json *get_config_storage_root(nlohmann::json* json); + nlohmann::json get_storage_json() const; + }; + } +} + +#endif // __RECOMP_CONFIG_HPP__ diff --git a/librecomp/include/librecomp/game.hpp b/librecomp/include/librecomp/game.hpp index 56480d6..16f731e 100644 --- a/librecomp/include/librecomp/game.hpp +++ b/librecomp/include/librecomp/game.hpp @@ -70,6 +70,7 @@ namespace recomp { OtherError }; void register_config_path(std::filesystem::path path); + std::filesystem::path get_config_path(); bool register_game(const recomp::GameEntry& entry); void check_all_stored_roms(); bool load_stored_rom(std::u8string& game_id); diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index 6a637be..ac83c6f 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -27,6 +27,7 @@ #include "librecomp/game.hpp" #include "librecomp/sections.h" #include "librecomp/overlays.hpp" +#include "librecomp/config.hpp" namespace N64Recomp { class Context; @@ -81,7 +82,10 @@ namespace recomp { InvalidDependencyString, MissingManifestField, DuplicateMod, - WrongGame + WrongGame, + InvalidDisableOptionDependency, + InvalidHiddenOptionDependency, + DuplicateEnumStrings, }; std::string error_to_string(ModOpenError); @@ -127,14 +131,6 @@ namespace recomp { std::string error_to_string(CodeModLoadError); - enum class ConfigOptionType { - None, - Enum, - Number, - String, - Bool - }; - enum class DependencyStatus { // Do not change these values as they're exposed in the mod API! @@ -189,49 +185,6 @@ namespace recomp { bool optional; }; - 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; - }; - - struct ConfigOptionBool { - bool 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; - std::unordered_map options_by_id; - }; - - typedef std::variant ConfigValueVariant; - - struct ConfigStorage { - std::unordered_map value_map; - }; - struct ModDetails { std::string mod_id; std::string display_name; @@ -255,7 +208,6 @@ 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; @@ -371,12 +323,12 @@ namespace recomp { recomp::Version get_mod_version(size_t mod_index); std::string get_mod_id(size_t mod_index); 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 config::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) const; - ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id) const; + void set_mod_config_value(size_t mod_index, const std::string &option_id, const config::ConfigValueVariant &value); + void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const config::ConfigValueVariant &value); + config::ConfigValueVariant get_mod_config_value(size_t mod_index, const std::string &option_id) const; + config::ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id) const; 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); @@ -396,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, 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 add_opened_mod(ModManifest&& manifest, config::Config&& config, std::vector&& game_indices, std::vector&& detected_content_types, std::vector&& thumbnail); std::vector regenerate_with_hooks( const std::vector>& sorted_unprocessed_hooks, const std::unordered_map& section_vrom_map, @@ -440,7 +392,7 @@ namespace recomp { std::vector processed_hook_slots; // Generated shim functions to use for implementing shim exports. std::vector> shim_functions; - ConfigSchema empty_schema; + config::ConfigSchema empty_schema; std::vector empty_bytes; size_t num_events = 0; ModContentTypeId code_content_type_id; @@ -464,7 +416,7 @@ namespace recomp { public: // TODO make these private and expose methods for the functionality they're currently used in. ModManifest manifest; - ConfigStorage config_storage; + config::Config config; std::unique_ptr code_handle; std::unique_ptr recompiler_context; std::vector section_load_addresses; @@ -472,7 +424,7 @@ namespace recomp { std::vector content_types; std::vector thumbnail; - ModHandle(const ModContext& context, ModManifest&& manifest, ConfigStorage&& config_storage, std::vector&& game_indices, std::vector&& content_types, std::vector&& thumbnail); + ModHandle(const ModContext& context, ModManifest&& manifest, config::Config&& config, 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); @@ -618,7 +570,7 @@ namespace recomp { void register_hook_exports(); void run_hook(uint8_t* rdram, recomp_context* ctx, size_t hook_slot_index); - ModOpenError parse_manifest(ModManifest &ret, const std::vector &manifest_data, std::string &error_param); + ModOpenError parse_manifest(ModManifest &ret, const std::vector &manifest_data, std::string &error_param, recomp::config::Config *config); CodeModLoadError validate_api_version(uint32_t api_version, std::string& error_param); void initialize_mods(); @@ -633,12 +585,12 @@ 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); + const config::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); + void set_mod_config_value(size_t mod_index, const std::string &option_id, const config::ConfigValueVariant &value); + void set_mod_config_value(const std::string &mod_id, const std::string &option_id, const config::ConfigValueVariant &value); + config::ConfigValueVariant get_mod_config_value(size_t mod_index, const std::string &option_id); + config::ConfigValueVariant get_mod_config_value(const std::string &mod_id, const std::string &option_id); std::string get_mod_id_from_filename(const std::filesystem::path& mod_filename); std::filesystem::path get_mod_filename(const std::string& mod_id); size_t get_mod_order_index(const std::string& mod_id); diff --git a/librecomp/src/config.cpp b/librecomp/src/config.cpp new file mode 100644 index 0000000..3ad39d9 --- /dev/null +++ b/librecomp/src/config.cpp @@ -0,0 +1,638 @@ +#include +#include "librecomp/files.hpp" +#include "librecomp/config.hpp" +#include "librecomp/game.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; +} + +static bool save_json_with_backups(const std::filesystem::path& path, const nlohmann::json& json_data) { + { + std::ofstream output_file = recomp::open_output_file_with_backup(path); + if (!output_file.good()) { + return false; + } + + output_file << std::setw(4) << json_data; + } + return recomp::finalize_output_file_with_backup(path); +} + +static std::filesystem::path get_path_to_config(bool is_mod_config) { + if (is_mod_config) { + return recomp::get_config_path() / recomp::mods::mod_config_directory; + } + return recomp::get_config_path(); +} + +namespace recomp::config { +Config::Config(std::string name, std::string id, bool requires_confirmation) { + this->name = name; + this->id = id; + this->requires_confirmation = requires_confirmation; + schema.options.clear(); + schema.options_by_id.clear(); + storage.value_map.clear(); + temp_storage.value_map.clear(); + + config_file_name = this->id + ".json"; +} + +Config::Config() { + is_mod_config = true; + requires_confirmation = false; + name = "Mod Config"; + + schema.options.clear(); + schema.options_by_id.clear(); + storage.value_map.clear(); + temp_storage.value_map.clear(); +} + +void Config::set_id(const std::string &id) { + this->id = id; + config_file_name = this->id + ".json"; +} +void Config::set_mod_version(const std::string &mod_version) { + this->mod_version = mod_version; +} + +const ConfigStorage& Config::get_config_storage() const { + return storage; +} + +const ConfigSchema& Config::get_config_schema() const { + return schema; +} + +nlohmann::json *Config::get_config_storage_root(nlohmann::json* json) { + if (is_mod_config) { + return &(*json)["storage"]; + } + return json; +} + +void Config::add_option(const ConfigOption& option) { + if (loaded_config) { + assert(false && "Cannot add options after config has been loaded."); + } + schema.options.push_back(option); + schema.options_by_id[option.id] = schema.options.size() - 1; + + ConfigValueVariant default_value = std::monostate(); + switch (option.type) { + case ConfigOptionType::None: + assert(false && "Cannot add option with type None."); + break; + case ConfigOptionType::Enum: + default_value = std::get(option.variant).default_value; + break; + case ConfigOptionType::Number: + default_value = std::get(option.variant).default_value; + break; + case ConfigOptionType::String: + default_value = std::get(option.variant).default_value; + break; + case ConfigOptionType::Bool: + default_value = std::get(option.variant).default_value; + break; + } + + storage.value_map[option.id] = default_value; + if (requires_confirmation) { + temp_storage.value_map[option.id] = default_value; + } +} + +void Config::add_enum_option( + const std::string &id, + const std::string &name, + const std::string &description, + const std::vector &options, + uint32_t default_value, + bool hidden +) { + ConfigOption option; + option.id = id; + option.name = name; + option.description = description; + option.type = ConfigOptionType::Enum; + option.hidden = hidden; + + ConfigOptionEnum option_enum = {{}, default_value}; + + // Note: this is a bit too predictive since this calls add_option + size_t option_index = schema.options.size(); + + for (const auto &option : options) { + assert(option_enum.can_add_option(option.key, option.value) && "Duplicate enum option key or value."); + option_enum.options.push_back(option); + } + + if (option_enum.find_option_from_value(default_value) == option_enum.options.end()) { + assert(false && "Default value must match to an option."); + } + + option.variant = option_enum; + + add_option(option); +} + +void Config::add_number_option( + const std::string &id, + const std::string &name, + const std::string &description, + double min, + double max, + double step, + int precision, + bool percent, + double default_value, + bool hidden +) { + ConfigOption option; + option.id = id; + option.name = name; + option.description = description; + option.type = ConfigOptionType::Number; + option.variant = ConfigOptionNumber{min, max, step, precision, percent, default_value}; + option.hidden = hidden; + + add_option(option); +} + +void Config::add_string_option( + const std::string &id, + const std::string &name, + const std::string &description, + const std::string &default_value, + bool hidden +) { + ConfigOption option; + option.id = id; + option.name = name; + option.description = description; + option.type = ConfigOptionType::String; + option.variant = ConfigOptionString{default_value}; + option.hidden = hidden; + + add_option(option); +} + +void Config::add_bool_option( + const std::string &id, + const std::string &name, + const std::string &description, + bool default_value, + bool hidden +) { + ConfigOption option; + option.id = id; + option.name = name; + option.description = description; + option.type = ConfigOptionType::Bool; + option.variant = ConfigOptionBool{default_value}; + option.hidden = hidden; + + add_option(option); +} + +const ConfigValueVariant Config::get_option_default_value(const std::string& option_id) const { + auto option_by_id_it = schema.options_by_id.find(option_id); + if (option_by_id_it == schema.options_by_id.end()) { + assert(false && "Option not found."); + return std::monostate(); + } + + const ConfigOption &option = 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; + case ConfigOptionType::Bool: + return std::get(option.variant).default_value; + default: + assert(false && "Unknown config option type."); + return std::monostate(); + } +} + +const ConfigValueVariant Config::get_option_value_from_storage(const std::string& option_id, const ConfigStorage& src) const { + auto it = src.value_map.find(option_id); + if (it != src.value_map.end()) { + return it->second; + } + return get_option_default_value(option_id); +} + +const ConfigValueVariant Config::get_option_value(const std::string& option_id) const { + return get_option_value_from_storage(option_id, storage); +} + +const ConfigValueVariant Config::get_temp_option_value(const std::string& option_id) const { + return get_option_value_from_storage(option_id, temp_storage); +} + +void Config::determine_changed_option(const std::string& option_id) { + if (get_option_value(option_id) != get_temp_option_value(option_id)) { + modified_options.insert(schema.options_by_id[option_id]); + } else { + modified_options.erase(schema.options_by_id[option_id]); + } +} + +void Config::try_call_option_change_callback(const std::string& option_id, ConfigValueVariant value, ConfigValueVariant prev_value, OptionChangeContext change_context) { + size_t option_index = schema.options_by_id[option_id]; + auto callback_it = option_change_callbacks.find(option_index); + bool is_load = (change_context == OptionChangeContext::Load); + bool value_changed = (value != prev_value); + if (callback_it != option_change_callbacks.end() && (is_load || value_changed)) { + callback_it->second(value, prev_value, change_context); + } +} + +void Config::set_option_value(const std::string& option_id, ConfigValueVariant value) { + ConfigStorage &storage = requires_confirmation ? temp_storage : storage; + + auto it = storage.value_map.find(option_id); + if (it != storage.value_map.end()) { + ConfigValueVariant prev_value = it->second; + it->second = value; + + if (requires_confirmation) { + determine_changed_option(option_id); + try_call_option_change_callback(option_id, value, prev_value, OptionChangeContext::Temporary); + } else { + try_call_option_change_callback(option_id, value, prev_value, OptionChangeContext::Permanent); + } + + derive_option_dependencies(schema.options_by_id[option_id]); + } +} + +bool Config::get_enum_option_disabled(size_t option_index, uint32_t enum_index) { + auto enum_it = enum_options_disabled.find(option_index); + if (enum_it != enum_options_disabled.end()) { + return enum_it->second.contains(enum_index); + } + return false; +} + +nlohmann::json Config::get_json_config() const { + if (is_mod_config) { + nlohmann::json config_json; + if (id.empty()) { + assert(false && "Mod ID does not exist for this config."); + } + if (mod_version.empty()) { + assert(false && "Mod version does not exist for this config."); + } + config_json["mod_id"] = id; + config_json["mod_version"] = mod_version; + config_json["recomp_version"] = recomp::get_project_version().to_string(); + config_json["storage"] = get_storage_json(); + return config_json; + } + return get_storage_json(); +} + +nlohmann::json Config::get_storage_json() const { + nlohmann::json json; + for (const auto& option : schema.options) { + const ConfigValueVariant value = get_option_value(option.id); + + if (json_serialize_option_map.contains(option.id)) { + auto cb = json_serialize_option_map.at(option.id); + json[option.id] = cb(value); + continue; + } + + switch (option.type) { + case ConfigOptionType::Enum: { + auto &option_enum = std::get(option.variant); + auto found_opt = option_enum.find_option_from_value(std::get(value)); + if (found_opt != option_enum.options.end()) { + json[option.id] = found_opt->key; + } + break; + } + case ConfigOptionType::Number: { + auto &option_number = std::get(option.variant); + if (option_number.precision == 0) { + json[option.id] = static_cast(std::get(value)); + } else { + json[option.id] = std::get(value); + } + break; + } + case ConfigOptionType::String: + json[option.id] = std::get(value); + break; + case ConfigOptionType::Bool: + json[option.id] = std::get(value); + break; + } + } + return json; +} + +bool Config::save_config_json(nlohmann::json config_json) const { + std::filesystem::path file_path = get_path_to_config(is_mod_config) / config_file_name; + + bool result = save_json_with_backups(file_path, config_json); + if (save_callback) { + save_callback(); + } + + return result; +} + +bool Config::save_config() { + if (requires_confirmation) { + for (const auto& option : schema.options) { + ConfigValueVariant prev_value = get_option_value(option.id); + ConfigValueVariant cur_value = get_temp_option_value(option.id); + storage.value_map[option.id] = cur_value; + try_call_option_change_callback(option.id, cur_value, prev_value, OptionChangeContext::Permanent); + } + + if (apply_callback && is_dirty()) { + apply_callback(); + } + + modified_options.clear(); + } + + return save_config_json(get_json_config()); +} + +void Config::derive_option_dependencies(size_t option_index) { + auto &option_id = schema.options[option_index].id; + auto value = requires_confirmation ? get_temp_option_value(option_id) : get_option_value(option_id); + + auto disable_result = schema.disable_dependencies.check_option_dependencies(option_index, value); + for (auto &option_res : disable_result) { + update_option_disabled(option_res.first, option_res.second); + } + + auto hidden_result = schema.hidden_dependencies.check_option_dependencies(option_index, value); + for (auto &option_res : hidden_result) { + update_option_hidden(option_res.first, option_res.second); + } +} + +void Config::derive_all_config_option_dependencies() { + for (size_t option_index = 0; option_index < schema.options.size(); option_index++) { + derive_option_dependencies(option_index); + } +} + +ConfigValueVariant Config::parse_config_option_json_value(const nlohmann::json& json_value, const ConfigOption &option) { + if (json_parse_option_map.contains(option.id)) { + return json_parse_option_map[option.id](json_value); + } + + bool is_null = json_value.is_null(); + + switch (option.type) { + case ConfigOptionType::None: + default: { + return {}; + } + case ConfigOptionType::Enum: { + if (is_null) { + return std::get(option.variant).default_value; + } + std::string enum_string_value = json_value.get(); + auto option_variant = std::get(option.variant); + auto found_opt = option_variant.find_option_from_string(enum_string_value); + if (found_opt != option_variant.options.end()) { + return found_opt->value; + } else { + return std::get(option.variant).default_value; + } + } + case ConfigOptionType::Number: + if (is_null) { + return std::get(option.variant).default_value; + } + return json_value.get(); + case ConfigOptionType::String: + if (is_null) { + return std::get(option.variant).default_value; + } + return json_value.get(); + case ConfigOptionType::Bool: + if (is_null) { + return std::get(option.variant).default_value; + break; + } + return json_value.get(); + } +} + +bool Config::load_config(std::function validate_callback) { + std::filesystem::path file_path = get_path_to_config(is_mod_config) / config_file_name; + nlohmann::json config_json{}; + + if (!read_json_with_backups(file_path, config_json)) { + if (requires_confirmation) { + revert_temp_config(); + } + save_config(); + derive_all_config_option_dependencies(); + clear_config_option_updates(); + loaded_config = true; + return true; + } + + if (validate_callback != nullptr && !validate_callback(config_json)) { + return false; + } + + nlohmann::json *json_config_root = get_config_storage_root(&config_json); + + for (const auto& option : schema.options) { + auto json_value = (*json_config_root)[option.id]; + + auto value = parse_config_option_json_value(json_value, option); + storage.value_map[option.id] = value; + + if (requires_confirmation) { + temp_storage.value_map[option.id] = value; + } + try_call_option_change_callback(option.id, value, value, OptionChangeContext::Load); + } + + derive_all_config_option_dependencies(); + clear_config_option_updates(); + + loaded_config = true; + return true; +} + +void Config::revert_temp_config() { + if (!requires_confirmation) { + return; + } + + modified_options.clear(); + + for (const auto& option : schema.options) { + temp_storage.value_map[option.id] = get_option_value(option.id); + } + derive_all_config_option_dependencies(); +} + +bool Config::is_dirty() { + return !modified_options.empty(); +} + +void Config::add_option_change_callback(const std::string& option_id, on_option_change_callback callback) { + size_t option_index = schema.options_by_id[option_id]; + option_change_callbacks[option_index] = callback; +} + +void Config::report_config_option_update(size_t option_index, ConfigOptionUpdateType update_type) { + ConfigOptionUpdateContext *update_context = nullptr; + for (auto &context : config_option_updates) { + if (context.option_index == option_index) { + update_context = &context; + break; + } + } + + if (update_context == nullptr) { + config_option_updates.push_back({option_index, {}}); + update_context = &config_option_updates.back(); + } + + update_context->updates.push_back(update_type); +} + +void Config::update_option_disabled(size_t option_index, bool disabled) { + bool was_disabled = is_config_option_disabled(option_index); + if (was_disabled == disabled) return; + + if (disabled) { + disabled_options.insert(option_index); + } else { + disabled_options.erase(option_index); + } + report_config_option_update(option_index, ConfigOptionUpdateType::Disabled); +}; + +void Config::update_option_disabled(const std::string& option_id, bool disabled) { + size_t option_index = schema.options_by_id[option_id]; + update_option_disabled(option_index, disabled); +}; + +void Config::update_option_hidden(size_t option_index, bool hidden) { + if (schema.options[option_index].hidden) { + // unchangeable - always hidden + return; + } + bool was_hidden = is_config_option_hidden(option_index); + if (was_hidden == hidden) { + return; + } + if (hidden) { + hidden_options.insert(option_index); + } else { + hidden_options.erase(option_index); + } + report_config_option_update(option_index, ConfigOptionUpdateType::Hidden); +}; + +void Config::update_option_hidden(const std::string& option_id, bool hidden) { + size_t option_index = schema.options_by_id[option_id]; + update_option_hidden(option_index, hidden); +}; + +void Config::update_option_enum_details(const std::string& option_id, const std::string& enum_details) { + size_t option_index = schema.options_by_id[option_id]; + enum_option_details[option_index] = enum_details; + report_config_option_update(option_index, ConfigOptionUpdateType::EnumDetails); +}; + +void Config::update_option_value(const std::string& option_id, ConfigValueVariant value) { + size_t option_index = schema.options_by_id[option_id]; + // This could potentially cause an update loop due to set_option_value calling change callbacks, which could call this function. + // It seems more important to call change callbacks AND respect requires_confirmation + set_option_value(option_id, value); + report_config_option_update(option_index, ConfigOptionUpdateType::Value); +}; + +void Config::update_option_description(const std::string& option_id, const std::string& new_description) { + size_t option_index = schema.options_by_id[option_id]; + schema.options[option_index].description = new_description; + report_config_option_update(option_index, ConfigOptionUpdateType::Description); +} + +void Config::update_enum_option_disabled(const std::string& option_id, uint32_t enum_index, bool disabled) { + size_t option_index = schema.options_by_id[option_id]; + if (!enum_options_disabled.contains(option_index)) { + enum_options_disabled[option_index] = {}; + } + if (disabled) { + enum_options_disabled[option_index].insert(enum_index); + } else { + enum_options_disabled[option_index].erase(enum_index); + } + report_config_option_update(option_index, ConfigOptionUpdateType::EnumDisabled); +} + +void Config::add_option_disable_dependency(const std::string& dependent_option_id, const std::string& source_option_id, std::vector &values) { + size_t dependent_index = schema.options_by_id[dependent_option_id]; + size_t source_index = schema.options_by_id[source_option_id]; + schema.disable_dependencies.add_option_dependency(dependent_index, source_index, values); +} + +void Config::add_option_hidden_dependency(const std::string& dependent_option_id, const std::string& source_option_id, std::vector &values) { + size_t dependent_index = schema.options_by_id[dependent_option_id]; + size_t source_index = schema.options_by_id[source_option_id]; + schema.hidden_dependencies.add_option_dependency(dependent_index, source_index, values); +} + +std::string Config::get_enum_option_details(size_t option_index) { + if (!enum_option_details.contains(option_index)) { + return std::string(); + } + return enum_option_details[option_index]; +} + +bool Config::is_config_option_hidden(size_t option_index) { + return schema.options[option_index].hidden || hidden_options.contains(option_index); +} + +} diff --git a/librecomp/src/config_option.cpp b/librecomp/src/config_option.cpp new file mode 100644 index 0000000..17174d1 --- /dev/null +++ b/librecomp/src/config_option.cpp @@ -0,0 +1,73 @@ +#include "librecomp/config.hpp" + +static char make_char_upper(char c) { + if (c >= 'a' && c <= 'z') { + c -= 'a' - 'A'; + } + return c; +} + +static bool case_insensitive_compare(const std::string& a, const std::string& b) { + if (a.size() != b.size()) { + return false; + } + for (size_t i = 0; i < a.size(); i++) { + if (make_char_upper(a[i]) != make_char_upper(b[i])) { + return false; + } + } + return true; +} + +namespace recomp::config { + + // ConfigOptionEnum + std::vector::const_iterator ConfigOptionEnum::find_option_from_string(const std::string& option_key) const { + return std::find_if(options.begin(), options.end(), [option_key](const ConfigOptionEnumOption& opt) { + return case_insensitive_compare(opt.key, option_key); + }); + }; + + std::vector::const_iterator ConfigOptionEnum::find_option_from_value(uint32_t value) const { + return std::find_if(options.begin(), options.end(), [value](const ConfigOptionEnumOption& opt) { + return opt.value == value; + }); + } + + bool ConfigOptionEnum::can_add_option(const std::string& option_key, uint32_t option_value) const { + return options.size() == 0 || ( + find_option_from_string(option_key) == options.end() && + find_option_from_value(option_value) == options.end()); + } + + // ConfigOptionDependency + void ConfigOptionDependency::add_option_dependency(size_t dependent_option_index, size_t source_option_index, std::vector &values) { + if (!option_to_dependencies.contains(source_option_index)) { + option_to_dependencies[source_option_index] = {}; + } + option_to_dependencies[source_option_index].insert(dependent_option_index); + dependency_to_values[dependent_option_index] = values; + } + + std::unordered_map ConfigOptionDependency::check_option_dependencies(size_t source_option_index, ConfigValueVariant value) { + std::unordered_map result{}; + if (!option_to_dependencies.contains(source_option_index)) { + return result; + } + + std::unordered_set &dependencies = option_to_dependencies[source_option_index]; + for (auto &dep : dependencies) { + bool is_match = false; + for (auto &check_value : dependency_to_values[dep]) { + if (value == check_value) { + is_match = true; + break; + } + } + + result[dep] = is_match; + } + + return result; + } +} diff --git a/librecomp/src/mod_config_api.cpp b/librecomp/src/mod_config_api.cpp index 98869b8..79995cb 100644 --- a/librecomp/src/mod_config_api.cpp +++ b/librecomp/src/mod_config_api.cpp @@ -3,7 +3,7 @@ #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)); + recomp::config::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); } @@ -19,7 +19,7 @@ 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)); + recomp::config::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx)); if (uint32_t* as_u32 = std::get_if(&val)) { ctx->f0.d = double(*as_u32); } @@ -52,7 +52,7 @@ void return_string(uint8_t* rdram, recomp_context* ctx, const StringType& str) { } 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)); + recomp::config::ConfigValueVariant val = recomp::mods::get_mod_config_value(mod_index, _arg_string<0>(rdram, ctx)); if (std::string* as_string = std::get_if(&val)) { return_string(rdram, ctx, *as_string); } diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp index 54e7946..9adffe2 100644 --- a/librecomp/src/mod_manifest.cpp +++ b/librecomp/src/mod_manifest.cpp @@ -332,17 +332,20 @@ 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 = "default"; +constexpr std::string_view config_schema_matches_key = "matches"; +constexpr std::string_view config_schema_hidden_from_key = "hidden_from"; +constexpr std::string_view config_schema_disabled_from_key = "disabled_from"; -std::unordered_map config_option_map{ - { "Enum", recomp::mods::ConfigOptionType::Enum}, - { "Number", recomp::mods::ConfigOptionType::Number}, - { "String", recomp::mods::ConfigOptionType::String}, - { "Bool", recomp::mods::ConfigOptionType::Bool}, +std::unordered_map config_option_map{ + { "Enum", recomp::config::ConfigOptionType::Enum}, + { "Number", recomp::config::ConfigOptionType::Number}, + { "String", recomp::config::ConfigOptionType::String}, + { "Bool", recomp::config::ConfigOptionType::Bool}, }; -recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::json &config_schema_json, recomp::mods::ModManifest &ret, std::string &error_param) { +recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::json &config_schema_json, recomp::config::Config *ret, std::string &error_param) { using json = nlohmann::json; - recomp::mods::ConfigOption option; + recomp::config::ConfigOption option; auto id = config_schema_json.find(config_schema_id_key); if (id != config_schema_json.end()) { if (!get_to(*id, option.id)) { @@ -399,25 +402,34 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j } switch (option.type) { - case recomp::mods::ConfigOptionType::Enum: + case recomp::config::ConfigOptionType::Enum: { - recomp::mods::ConfigOptionEnum option_enum; + recomp::config::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)) { + std::vector option_key_list; + if (!get_to_vec(*options, option_key_list)) { error_param = config_schema_options_key; return recomp::mods::ModOpenError::IncorrectConfigSchemaType; } + + for (uint32_t i = 0; i < static_cast(option_key_list.size()); i++) { + if (!option_enum.can_add_option(option_key_list[i], i)) { + error_param = config_schema_options_key; + return recomp::mods::ModOpenError::DuplicateEnumStrings; + } + option_enum.options.push_back({i, option_key_list[i]}); + } } 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); + auto it = option_enum.find_option_from_string(default_value_string); if (it != option_enum.options.end()) { - option_enum.default_value = uint32_t(it - option_enum.options.begin()); + option_enum.default_value = it->value; } else { error_param = config_schema_default_key; @@ -434,9 +446,9 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j } break; - case recomp::mods::ConfigOptionType::Number: + case recomp::config::ConfigOptionType::Number: { - recomp::mods::ConfigOptionNumber option_number; + recomp::config::ConfigOptionNumber option_number; auto min = config_schema_json.find(config_schema_min_key); if (min != config_schema_json.end()) { @@ -497,9 +509,9 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j option.variant = option_number; } break; - case recomp::mods::ConfigOptionType::String: + case recomp::config::ConfigOptionType::String: { - recomp::mods::ConfigOptionString option_string; + recomp::config::ConfigOptionString option_string; auto default_value = config_schema_json.find(config_schema_default_key); if (default_value != config_schema_json.end()) { @@ -512,9 +524,9 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j option.variant = option_string; } break; - case recomp::mods::ConfigOptionType::Bool: + case recomp::config::ConfigOptionType::Bool: { - recomp::mods::ConfigOptionBool option_bool; + recomp::config::ConfigOptionBool option_bool; auto default_value = config_schema_json.find(config_schema_default_key); if (default_value != config_schema_json.end()) { @@ -531,13 +543,129 @@ recomp::mods::ModOpenError parse_manifest_config_schema_option(const nlohmann::j break; } - ret.config_schema.options_by_id.emplace(option.id, ret.config_schema.options.size()); - ret.config_schema.options.emplace_back(option); + if (ret != nullptr) { + ret->add_option(option); + } return recomp::mods::ModOpenError::Good; } -recomp::mods::ModOpenError recomp::mods::parse_manifest(ModManifest& ret, const std::vector& manifest_data, std::string& error_param) { +std::vector parse_config_option_dependency_matches(const nlohmann::json &matches_object, const recomp::config::ConfigOption &source_option) { + std::vector matches = {}; + + switch (source_option.type) { + case recomp::config::ConfigOptionType::None: + default: + //! ERROR: Source option type is invalid + return matches; + case recomp::config::ConfigOptionType::Enum: { + std::vector enum_values = {}; + if (!get_to_vec(matches_object, enum_values)) { + //! ERROR: failed to get array of strings in matches + return matches; + } + + auto &option_enum = get(source_option.variant); + for (auto &value_str : enum_values) { + auto it = option_enum.find_option_from_string(value_str); + if (it == option_enum.options.end()) { + //! ERROR: one of the matches specified doesn't exist in the source enum + return matches; + } + matches.push_back(it->value); + } + break; + } + case recomp::config::ConfigOptionType::String: + //! ERROR: string dependencies are unsupported + return matches; + case recomp::config::ConfigOptionType::Number: + //! ERROR: numerical dependencies are unsupported + return matches; + case recomp::config::ConfigOptionType::Bool: { + bool bool_condition; + if (!get_to(matches_object, bool_condition)) { + //! ERROR: Failed to get boolean from dependency matches + return matches; + } + matches.push_back(bool_condition); + break; + } + } + return matches; +} + +recomp::mods::ModOpenError parse_config_option_dependencies(const nlohmann::json &config_schema_json, recomp::config::Config &config) { + using json = nlohmann::json; + + auto id = config_schema_json.find(config_schema_id_key); + std::string option_id; + if (id != config_schema_json.end()) { + if (!get_to(*id, option_id)) { + return recomp::mods::ModOpenError::Good; + } + } + + recomp::config::ConfigSchema schema = config.get_config_schema(); + + size_t option_index = schema.options_by_id[option_id]; + recomp::config::ConfigOption &option = schema.options[option_index]; + + auto conditional_add_dependency = [config_schema_json, schema, option, option_index](recomp::config::ConfigOptionDependency &dependency, const std::string_view &dep_key) -> bool { + auto find_it = config_schema_json.find(dep_key); + if (find_it == config_schema_json.end()) { + return true; + } + + auto& dependency_json = *find_it; + if (!dependency_json.is_object()) { + //! ERROR: Dependency malformed + return false; + } + + std::string source_dependency_id; + auto find_dep_id = dependency_json.find(config_schema_id_key); + if (find_dep_id == dependency_json.end()) { + //! ERROR: Could not find source dependency id + return false; + } + + if (!get_to(*find_dep_id, source_dependency_id)) { + //! ERROR: Failed to get source dependency id + return false; + } + if (schema.options_by_id.contains(source_dependency_id) == false) { + //! ERROR: Failed to find specified source dependency in schema + return false; + } + + size_t source_dependency_index = schema.options_by_id.at(source_dependency_id); + auto &source_option = schema.options.at(source_dependency_index); + + auto find_matches = dependency_json.find(config_schema_matches_key); + if (find_matches == dependency_json.end()) { + //! ERROR: Failed to find matches + return false; + } + auto matches = parse_config_option_dependency_matches(*find_matches, source_option); + if (matches.empty()) { + //! ERROR: Could not find valid data in matches + return false; + } + + dependency.add_option_dependency(option_index, source_dependency_index, matches); + return true; + }; + + bool disable_success = conditional_add_dependency(schema.disable_dependencies, config_schema_disabled_from_key); + if (!disable_success) return recomp::mods::ModOpenError::InvalidDisableOptionDependency; + bool hidden_success = conditional_add_dependency(schema.hidden_dependencies, config_schema_hidden_from_key); + if (!hidden_success) return recomp::mods::ModOpenError::InvalidHiddenOptionDependency; + + return recomp::mods::ModOpenError::Good; +} + +recomp::mods::ModOpenError recomp::mods::parse_manifest(ModManifest& ret, const std::vector& manifest_data, std::string& error_param, recomp::config::Config *config) { using json = nlohmann::json; json manifest_json = json::parse(manifest_data.begin(), manifest_data.end(), nullptr, false); @@ -589,6 +717,11 @@ recomp::mods::ModOpenError recomp::mods::parse_manifest(ModManifest& ret, const return current_error; } + if (config != nullptr) { + config->set_id(ret.mod_id); + config->set_mod_version(ret.version.to_string()); + } + // Authors current_error = try_get_vec(ret.authors, manifest_json, authors_key, true, error_param); if (current_error != ModOpenError::Good) { @@ -681,11 +814,23 @@ recomp::mods::ModOpenError recomp::mods::parse_manifest(ModManifest& ret, const } for (const json &option : *options) { - ModOpenError open_error = parse_manifest_config_schema_option(option, ret, error_param); + ModOpenError open_error = parse_manifest_config_schema_option(option, config, error_param); if (open_error != ModOpenError::Good) { return open_error; } } + + // Parse option dependencies after all options have been added + // Requires config to not be null + if (config != nullptr) { + for (const json &option : *options) { + ModOpenError dep_error = parse_config_option_dependencies(option, *config); + if (dep_error != ModOpenError::Good) { + error_param = dep_error == ModOpenError::InvalidDisableOptionDependency ? config_schema_disabled_from_key : config_schema_hidden_from_key; + return dep_error; + } + } + } } else { error_param = config_schema_options_key; @@ -696,93 +841,44 @@ recomp::mods::ModOpenError recomp::mods::parse_manifest(ModManifest& ret, const return 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. +bool parse_mod_config_storage(const std::string &expected_mod_id, recomp::config::Config &config) { + return config.load_config([expected_mod_id](nlohmann::json &config_json) { + 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 mod ID is not a string. + // The configuration file doesn't have a mod ID. 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. - 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; + + 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; } - switch (option.type) { - case recomp::mods::ConfigOptionType::Enum: - 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; - case recomp::mods::ConfigOptionType::Number: - if (option_json->is_number()) { - config_storage.value_map[option.id] = option_json->template get(); - } - - break; - case recomp::mods::ConfigOptionType::String: { - if (get_to(*option_json, value_str)) { - config_storage.value_map[option.id] = value_str; - } - - break; - } - case recomp::mods::ConfigOptionType::Bool: { - if (option_json->is_boolean()) { - config_storage.value_map[option.id] = option_json->get(); - } - - break; - } - default: - assert(false && "Unknown option type."); - break; - } - } - - return true; + return true; + }); } recomp::mods::ModOpenError recomp::mods::ModContext::open_mod_from_manifest(ModManifest& manifest, std::string& error_param, const std::vector& supported_content_types, bool requires_manifest) { + recomp::config::Config mod_config; { bool exists; std::vector manifest_data = manifest.file_handle->read_file("mod.json", exists); @@ -820,7 +916,7 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod_from_manifest(ModM } } else { - ModOpenError parse_error = parse_manifest(manifest, manifest_data, error_param); + ModOpenError parse_error = parse_manifest(manifest, manifest_data, error_param, &mod_config); if (parse_error != ModOpenError::Good) { return parse_error; } @@ -869,9 +965,7 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod_from_manifest(ModM } // Read the mod config if it exists. - ConfigStorage config_storage; - 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); + parse_mod_config_storage(manifest.mod_id, mod_config); // Read the mod thumbnail if it exists. static const std::string thumbnail_dds_name = "thumb.dds"; @@ -883,7 +977,7 @@ recomp::mods::ModOpenError recomp::mods::ModContext::open_mod_from_manifest(ModM } // Store the loaded mod manifest in a new mod handle. - add_opened_mod(std::move(manifest), std::move(config_storage), std::move(game_indices), std::move(detected_content_types), std::move(thumbnail_data)); + add_opened_mod(std::move(manifest), std::move(mod_config), std::move(game_indices), std::move(detected_content_types), std::move(thumbnail_data)); return ModOpenError::Good; } @@ -978,6 +1072,8 @@ std::string recomp::mods::error_to_string(ModOpenError error) { return "Duplicate mod found"; case ModOpenError::WrongGame: return "Mod is for a different game"; + case ModOpenError::DuplicateEnumStrings: + return "Duplicate enum strings found in mod.json (enum strings are case insensitive)"; } return "Unknown mod opening error: " + std::to_string((int)error); } diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index c1a4545..ad50319 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -266,9 +266,9 @@ 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, std::vector&& thumbnail) : +recomp::mods::ModHandle::ModHandle(const ModContext& context, ModManifest&& manifest, recomp::config::Config&& config, std::vector&& game_indices, std::vector&& content_types, std::vector&& thumbnail) : manifest(std::move(manifest)), - config_storage(std::move(config_storage)), + config(std::move(config)), code_handle(), recompiler_context{std::make_unique()}, content_types{std::move(content_types)}, @@ -597,12 +597,12 @@ 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, std::vector&& thumbnail) { +void recomp::mods::ModContext::add_opened_mod(ModManifest&& manifest, config::Config&& config, 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_by_filename.emplace(manifest.mod_root_path.filename().native(), mod_index); - 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.emplace_back(*this, std::move(manifest), std::move(config), std::move(game_indices), std::move(detected_content_types), std::move(thumbnail)); opened_mods_order.emplace_back(mod_index); } @@ -646,49 +646,14 @@ void recomp::mods::ModContext::close_mods() { 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) { +bool save_mod_config_storage(const std::string &mod_id, const recomp::Version &mod_version, const recomp::config::Config *config, nlohmann::json &storage_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(); - - 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; - case recomp::mods::ConfigOptionType::Bool: - 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); + config_json["storage"] = storage_json; + return config->save_config_json(config_json); } bool parse_mods_config(const std::filesystem::path &path, std::unordered_set &enabled_mods, std::vector &mod_order) { @@ -734,12 +699,13 @@ bool save_mods_config(const std::filesystem::path &path, const std::unordered_se void recomp::mods::ModContext::dirty_mod_configuration_thread_process() { using namespace std::chrono_literals; + using json = nlohmann::json; + 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_map pending_mod_configs; + std::unordered_map pending_mod_storage_json; std::unordered_set config_enabled_mods; std::vector config_mod_order; bool pending_config_save = false; @@ -776,16 +742,14 @@ void recomp::mods::ModContext::dirty_mod_configuration_thread_process() { 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; + pending_mod_configs[id] = &mod.config; + pending_mod_storage_json[id] = mod.config.get_json_config(); } } } for (const std::string &id : pending_mods) { - 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_mod_configs[id]->save_config_json(pending_mod_storage_json[id]); } pending_mods.clear(); @@ -1422,7 +1386,7 @@ void recomp::mods::ModContext::set_mod_index(const std::string &mod_game_id, con mod_configuration_thread_queue.enqueue(ModConfigQueueSave()); } -const recomp::mods::ConfigSchema &recomp::mods::ModContext::get_mod_config_schema(const std::string &mod_id) const { +const recomp::config::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()) { @@ -1430,7 +1394,7 @@ const recomp::mods::ConfigSchema &recomp::mods::ModContext::get_mod_config_schem } const ModHandle &mod = opened_mods[find_it->second]; - return mod.manifest.config_schema; + return mod.config.get_config_schema(); } const std::vector &recomp::mods::ModContext::get_mod_thumbnail(const std::string &mod_id) const { @@ -1444,7 +1408,7 @@ const std::vector &recomp::mods::ModContext::get_mod_thumbnail(const std:: return mod.thumbnail; } -void recomp::mods::ModContext::set_mod_config_value(size_t mod_index, 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 recomp::config::ConfigValueVariant &value) { // Check that the mod exists. if (mod_index >= opened_mods.size()) { return; @@ -1452,48 +1416,13 @@ void recomp::mods::ModContext::set_mod_config_value(size_t mod_index, const std: 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()) { - // 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; - case ConfigOptionType::Bool: - if (std::holds_alternative(value)) { - mod.config_storage.value_map[option_id] = value; - } - - break; - default: - assert(false && "Unknown config option type."); - return; - } - } + mod.config.set_option_value(option_id, value); // Notify the asynchronous thread it should save the configuration for this mod. 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) { +void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const recomp::config::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()) { @@ -1503,7 +1432,7 @@ void recomp::mods::ModContext::set_mod_config_value(const std::string &mod_id, c 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) const { +recomp::config::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(size_t mod_index, const std::string &option_id) const { // Check that the mod exists. if (mod_index >= opened_mods.size()) { return std::monostate(); @@ -1511,35 +1440,10 @@ recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value( 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()) { - 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; - case ConfigOptionType::Bool: - return std::get(option.variant).default_value; - default: - assert(false && "Unknown config option type."); - return std::monostate(); - } - } + return mod.config.get_option_value(option_id); } -recomp::mods::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(const std::string &mod_id, const std::string &option_id) const { +recomp::config::ConfigValueVariant recomp::mods::ModContext::get_mod_config_value(const std::string &mod_id, const std::string &option_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()) { diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index 8a03dbd..c103823 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -68,6 +68,10 @@ void recomp::register_config_path(std::filesystem::path path) { config_path = path; } +std::filesystem::path recomp::get_config_path() { + return config_path; +} + bool recomp::register_game(const recomp::GameEntry& entry) { // TODO verify that there's no game with this ID already. { @@ -560,7 +564,7 @@ bool recomp::mods::is_mod_auto_enabled(const std::string& mod_id) { return mod_context->is_mod_auto_enabled(mod_id); } -const recomp::mods::ConfigSchema &recomp::mods::get_mod_config_schema(const std::string &mod_id) { +const recomp::config::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); } @@ -570,22 +574,22 @@ 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) { +void recomp::mods::set_mod_config_value(size_t mod_index, const std::string &option_id, const recomp::config::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) { +void recomp::mods::set_mod_config_value(const std::string &mod_id, const std::string &option_id, const recomp::config::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) { +recomp::config::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) { +recomp::config::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); }