From 0b18a3529284f0ac830d0368f8616f191882de78 Mon Sep 17 00:00:00 2001 From: Dario Date: Sun, 19 Jan 2025 23:33:31 -0300 Subject: [PATCH] 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 9df4e77..0000000 --- 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 6bf004a..4dcbea4 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 0d71fe9..515c365 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 ac45252..0d4fe3e 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 a64da68..08d324d 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);