diff --git a/librecomp/CMakeLists.txt b/librecomp/CMakeLists.txt index 9f78d4f..9c4bf53 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" @@ -35,6 +36,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/helpers.hpp b/librecomp/include/librecomp/helpers.hpp index 9f02367..d8f5afd 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 @@ -36,6 +38,41 @@ 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 +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 b5c421d..20b7f80 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 @@ -27,6 +30,7 @@ namespace N64Recomp { class Context; struct LiveGeneratorOutput; + class ShimFunction; }; namespace recomp { @@ -55,6 +59,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, @@ -65,6 +72,9 @@ namespace recomp { FailedToParseManifest, InvalidManifestSchema, IncorrectManifestFieldType, + MissingConfigSchemaField, + IncorrectConfigSchemaType, + InvalidConfigSchemaDefault, InvalidVersionString, InvalidMinimumRecompVersionString, InvalidDependencyString, @@ -115,6 +125,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 +171,45 @@ 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; + 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; @@ -176,6 +232,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; @@ -203,6 +260,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 { @@ -244,6 +302,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: @@ -252,12 +324,23 @@ 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); + 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(); 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(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); 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; } @@ -268,14 +351,15 @@ 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); - void add_opened_mod(ModManifest&& manifest, std::vector&& game_indices, std::vector&& detected_content_types); + 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( 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); @@ -285,10 +369,18 @@ 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_set auto_enabled_mods; std::unordered_map patched_funcs; std::unordered_map loaded_mods_by_id; + 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. std::unique_ptr regenerated_code_handle; @@ -299,6 +391,10 @@ 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; ModContentTypeId code_content_type_id; size_t active_game = (size_t)-1; @@ -321,13 +417,15 @@ 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; + std::vector thumbnail; - 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, std::vector&& thumbnail); ModHandle(const ModHandle& rhs) = delete; ModHandle& operator=(const ModHandle& rhs) = delete; ModHandle(ModHandle&& rhs); @@ -457,12 +555,23 @@ namespace recomp { CodeModLoadError validate_api_version(uint32_t api_version, std::string& error_param); - void initialize_mod_recompiler(); + 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); + 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/include/librecomp/overlays.hpp b/librecomp/include/librecomp/overlays.hpp index 40cbb96..35703bd 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/mod_config_api.cpp b/librecomp/src/mod_config_api.cpp new file mode 100644 index 0000000..b1c430a --- /dev/null +++ b/librecomp/src/mod_config_api.cpp @@ -0,0 +1,66 @@ +#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 if (double* as_double = std::get_if(&val)) { + _return(ctx, uint32_t(int32_t(*as_double))); + } + 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 (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 { + 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/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp index 0d71fe9..4ce7a59 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); @@ -131,16 +161,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 +171,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 +309,206 @@ 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 = "default"; + +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 (!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 (!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 (!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); + 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 (!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; + } + 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_by_id.emplace(option.id, ret.config_schema.options.size()); + ret.config_schema.options.emplace_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,9 +610,117 @@ 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; } +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. + 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_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; + } + 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; @@ -520,9 +839,23 @@ 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_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(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; } @@ -547,6 +880,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..4b2ea0d 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -3,12 +3,65 @@ #include #include +#include "librecomp/files.hpp" #include "librecomp/mods.hpp" #include "librecomp/overlays.hpp" #include "librecomp/game.hpp" #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 @@ -213,11 +266,13 @@ 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, 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)}, + thumbnail{ std::move(thumbnail) }, game_indices{std::move(game_indices)} { runtime_toggleable = true; @@ -539,10 +594,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, 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(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); } recomp::mods::ModLoadError recomp::mods::ModContext::load_mod(recomp::mods::ModHandle& mod, std::string& error_param) { @@ -567,10 +624,172 @@ 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(); + 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) { + 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; + 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); +} + +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; + 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; + } + else if (const ModConfigQueueSaveMod* queue_save_mod = std::get_if(&variant)) { + pending_mods.emplace(queue_save_mod->mod_id); + } + }; + + while (active) { + // Wait for at least one mod to require writing. + mod_configuration_thread_queue.wait_dequeue(variant); + handle_variant(variant); + + + // Clear out the entire queue to coalesce all writes with a timeout. + while (active && mod_configuration_thread_queue.wait_dequeue_timed(variant, 1s)) { + handle_variant(variant); + } + + 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_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; + } + } } std::vector recomp::mods::ModContext::scan_mod_folder(const std::filesystem::path& mod_folder) { @@ -611,6 +830,40 @@ 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; + 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); + } + } +} + recomp::mods::ModContext::ModContext() { // Register the code content type. ModContentType code_content_type { @@ -623,6 +876,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); + + 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 +890,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() { + 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) { size_t ret = content_types.size(); @@ -682,8 +940,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; @@ -704,21 +963,94 @@ 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) { + mod_configuration_thread_queue.enqueue(ModConfigQueueSave()); } } @@ -726,11 +1058,15 @@ 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(); } -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; @@ -740,7 +1076,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{}; @@ -881,6 +1218,177 @@ 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); + if (find_it == opened_mods_by_id.end()) { + return empty_schema; + } + + const ModHandle &mod = opened_mods[find_it->second]; + 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(size_t mod_index, const std::string &option_id, const ConfigValueVariant &value) { + // Check that the mod exists. + if (mod_index >= opened_mods.size()) { + return; + } + + 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; + default: + assert(false && "Unknown config option type."); + return; + } + } + + // 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) { + // 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[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; + default: + assert(false && "Unknown config option type."); + return std::monostate(); + } + } +} + +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; +} + +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) { std::vector ret{}; ram_used = 0; @@ -925,7 +1433,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); @@ -1033,7 +1541,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)); @@ -1685,7 +2193,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); @@ -1714,6 +2222,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) { @@ -1856,12 +2371,9 @@ 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(); active_game = (size_t)-1; } - -void recomp::mods::initialize_mod_recompiler() { - N64Recomp::live_recompiler_init(); -} diff --git a/librecomp/src/overlays.cpp b/librecomp/src/overlays.cpp index e807cfa..8c8e5d1 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); diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index da26074..2ac8d46 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,15 +82,29 @@ 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_mods_config_path(config_path / "mods.json"); + mod_context->set_mod_config_directory(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()); } + + 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) { @@ -502,7 +507,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) { @@ -510,11 +515,51 @@ 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 mod_context->is_mod_auto_enabled(mod_id); +} + +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); +} + +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(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); +} + 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); } +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); @@ -643,7 +688,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. @@ -678,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");