mirror of
				https://github.com/N64Recomp/N64ModernRuntime.git
				synced 2025-10-30 08:02:29 +00:00 
			
		
		
		
	Implement mod configuration, mod reordering, and extended exports (#95)
* init recomp config_store * Use a custom hash class to enable hetereogenous lookup * Added config registry/option files * switch to using usings * dropdown config type * Added TextField option type * parse/validate button config type * wip callback registry * Add auto enabled. * Cleanup. * Add support for config schema. * Add float arg1 helpers * Config storage for mods. * Proper enum parsing. * Persist mod order and enable. * Enable new mods by default. * Mods directory. * Parse thumbnail when opening mods. * Auto-enabled mods. * Implement extended function exports that pass the caller mod's index as an extra argument * Fix mod configs not saving and default value not getting parsed * Implement API to allow mods to read their config values * Fix config value parsing to allow integral values for double fields * Change construction of ModConfigQueueSaveMod. * Fix N64Recomp commit after rebase --------- Co-authored-by: Dario <dariosamo@gmail.com> Co-authored-by: thecozies <79979276+thecozies@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									ec56fb39b0
								
							
						
					
					
						commit
						2ed84f46c5
					
				
					 9 changed files with 1181 additions and 54 deletions
				
			
		|  | @ -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" | ||||
|  |  | |||
|  | @ -1,6 +1,8 @@ | |||
| #ifndef __RECOMP_HELPERS__ | ||||
| #define __RECOMP_HELPERS__ | ||||
| 
 | ||||
| #include <string> | ||||
| 
 | ||||
| #include "recomp.h" | ||||
| #include <ultramodern/ultra64.h> | ||||
| 
 | ||||
|  | @ -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 <int arg_index> | ||||
| std::string _arg_string(uint8_t* rdram, recomp_context* ctx) { | ||||
|     PTR(char) str = _arg<arg_index, PTR(char)>(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 <typename T> | ||||
| void _return(recomp_context* ctx, T val) { | ||||
|     static_assert(sizeof(T) <= 4 && "Only 32-bit value returns supported currently"); | ||||
|  |  | |||
|  | @ -13,6 +13,9 @@ | |||
| #include <array> | ||||
| #include <cstddef> | ||||
| #include <variant> | ||||
| #include <mutex> | ||||
| 
 | ||||
| #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<recomp::mods::HookDefinition> | |||
| 
 | ||||
| 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<char> read_file(const std::string& filepath, bool& exists) const = 0; | ||||
|  | @ -154,6 +171,45 @@ namespace recomp { | |||
|             Version version; | ||||
|         }; | ||||
| 
 | ||||
|         struct ConfigOptionEnum { | ||||
|             std::vector<std::string> 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<ConfigOptionEnum, ConfigOptionNumber, ConfigOptionString> ConfigOptionVariant; | ||||
| 
 | ||||
|         struct ConfigOption { | ||||
|             std::string id; | ||||
|             std::string name; | ||||
|             std::string description; | ||||
|             ConfigOptionType type; | ||||
|             ConfigOptionVariant variant; | ||||
|         }; | ||||
| 
 | ||||
|         struct ConfigSchema { | ||||
|             std::vector<ConfigOption> options; | ||||
|             std::unordered_map<std::string, size_t> options_by_id; | ||||
|         }; | ||||
| 
 | ||||
|         typedef std::variant<std::monostate, uint32_t, double, std::string> ConfigValueVariant; | ||||
| 
 | ||||
|         struct ConfigStorage { | ||||
|             std::unordered_map<std::string, ConfigValueVariant> value_map; | ||||
|         }; | ||||
| 
 | ||||
|         struct ModDetails { | ||||
|             std::string mod_id; | ||||
|             std::string display_name; | ||||
|  | @ -176,6 +232,7 @@ namespace recomp { | |||
|             std::vector<std::string> authors; | ||||
|             std::vector<Dependency> dependencies; | ||||
|             std::unordered_map<std::string, size_t> dependencies_by_id; | ||||
|             ConfigSchema config_schema; | ||||
|             Version minimum_recomp_version; | ||||
|             Version version; | ||||
|             bool runtime_toggleable; | ||||
|  | @ -203,6 +260,7 @@ namespace recomp { | |||
|         }; | ||||
| 
 | ||||
|         std::vector<ModDetails> 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<ModConfigQueueSaveMod, ModConfigQueueSave, ModConfigQueueEnd> ModConfigQueueVariant; | ||||
| 
 | ||||
|         class LiveRecompilerCodeHandle; | ||||
|         class ModContext { | ||||
|         public: | ||||
|  | @ -252,12 +324,23 @@ namespace recomp { | |||
| 
 | ||||
|             void register_game(const std::string& mod_game_id); | ||||
|             std::vector<ModOpenErrorDetails> 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<ModLoadErrorDetails> load_mods(const GameEntry& game_entry, uint8_t* rdram, int32_t load_address, uint32_t& ram_used); | ||||
|             void unload_mods(); | ||||
|             std::vector<ModDetails> 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<char> &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<ModContentTypeId>& 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<std::pair<ModLoadError, std::string>>& errors); | ||||
|             CodeModLoadError init_mod_code(uint8_t* rdram, const std::unordered_map<uint32_t, uint16_t>& 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<recomp_func_t*, recomp::overlays::BasePatchedFunction>& base_patched_funcs, std::string& error_param); | ||||
|             void add_opened_mod(ModManifest&& manifest, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& detected_content_types); | ||||
|             CodeModLoadError resolve_code_dependencies(ModHandle& mod, size_t mod_index, const std::unordered_map<recomp_func_t*, recomp::overlays::BasePatchedFunction>& base_patched_funcs, std::string& error_param); | ||||
|             void add_opened_mod(ModManifest&& manifest, ConfigStorage&& config_storage, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& detected_content_types, std::vector<char>&& thumbnail); | ||||
|             void close_mods(); | ||||
|             std::vector<ModLoadErrorDetails> regenerate_with_hooks( | ||||
|                 const std::vector<std::pair<HookDefinition, size_t>>& sorted_unprocessed_hooks, | ||||
|                 const std::unordered_map<uint32_t, uint16_t>& section_vrom_map, | ||||
|                 const std::unordered_map<recomp_func_t*, overlays::BasePatchedFunction>& base_patched_funcs, | ||||
|                 std::span<const uint8_t> 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<std::string, size_t> mod_game_ids; | ||||
|             std::vector<ModHandle> opened_mods; | ||||
|             std::unordered_map<std::string, size_t> opened_mods_by_id; | ||||
|             std::vector<size_t> opened_mods_order; | ||||
|             std::mutex opened_mods_mutex; | ||||
|             std::unordered_set<std::string> mod_ids; | ||||
|             std::unordered_set<std::string> enabled_mods; | ||||
|             std::unordered_set<std::string> auto_enabled_mods; | ||||
|             std::unordered_map<recomp_func_t*, PatchData> patched_funcs; | ||||
|             std::unordered_map<std::string, size_t> loaded_mods_by_id; | ||||
|             std::unique_ptr<std::thread> mod_configuration_thread; | ||||
|             moodycamel::BlockingConcurrentQueue<ModConfigQueueVariant> mod_configuration_thread_queue; | ||||
|             std::filesystem::path mods_config_path; | ||||
|             std::filesystem::path mod_config_directory; | ||||
|             std::mutex mod_config_storage_mutex; | ||||
|             std::vector<size_t> loaded_code_mods; | ||||
|             // Code handle for vanilla code that was regenerated to add hooks.
 | ||||
|             std::unique_ptr<LiveRecompilerCodeHandle> 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<bool> processed_hook_slots; | ||||
|             // Generated shim functions to use for implementing shim exports.
 | ||||
|             std::vector<std::unique_ptr<N64Recomp::ShimFunction>> shim_functions; | ||||
|             ConfigSchema empty_schema; | ||||
|             std::vector<char> 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<ModCodeHandle> code_handle; | ||||
|             std::unique_ptr<N64Recomp::Context> recompiler_context; | ||||
|             std::vector<uint32_t> section_load_addresses; | ||||
|             // Content types present in this mod.
 | ||||
|             std::vector<ModContentTypeId> content_types; | ||||
|             std::vector<char> thumbnail; | ||||
| 
 | ||||
|             ModHandle(const ModContext& context, ModManifest&& manifest, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& content_types); | ||||
|             ModHandle(const ModContext& context, ModManifest&& manifest, ConfigStorage&& config_storage, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& content_types, std::vector<char>&& 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<char> &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<ModContentTypeId>& content_types, bool requires_manifest); | ||||
| 
 | ||||
| 
 | ||||
|         void register_config_exports(); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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(); | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										66
									
								
								librecomp/src/mod_config_api.cpp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								librecomp/src/mod_config_api.cpp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<uint32_t>(&val)) { | ||||
|         _return(ctx, *as_u32); | ||||
|     } | ||||
|     else if (double* as_double = std::get_if<double>(&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<uint32_t>(&val)) { | ||||
|         ctx->f0.d = double(*as_u32); | ||||
|     } | ||||
|     else if (double* as_double = std::get_if<double>(&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<std::string>(&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<uint8_t*>(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); | ||||
| } | ||||
|  | @ -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<std::string, ManifestField> 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 <typename T1, typename T2> | ||||
| bool get_to(const nlohmann::json& val, T2& out) { | ||||
|  | @ -298,6 +309,206 @@ recomp::mods::ModOpenError try_get_vec(std::vector<T2>& 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<std::string, recomp::mods::ConfigOptionType> 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<json::string_t>(*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<json::string_t>(*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<json::string_t>(*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<json::string_t>(*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<std::string>(*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<json::string_t>(*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<double>(); | ||||
|             } | ||||
| 
 | ||||
|             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<double>(); | ||||
|             } | ||||
|              | ||||
|             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<double>(); | ||||
|             } | ||||
| 
 | ||||
|             auto precision = config_schema_json.find(config_schema_precision_key); | ||||
|             if (precision != config_schema_json.end()) { | ||||
|                 int64_t precision_int64; | ||||
|                 if (get_to<int64_t>(*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<bool>(*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<double>(); | ||||
|             } | ||||
| 
 | ||||
|             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<json::string_t>(*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<char>& 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<json::string_t>(*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<json::string_t>(*option_json, value_str)) { | ||||
|                 const recomp::mods::ConfigOptionEnum &option_enum = std::get<recomp::mods::ConfigOptionEnum>(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<double>(); | ||||
|             } | ||||
| 
 | ||||
|             break; | ||||
|         case recomp::mods::ConfigOptionType::String: { | ||||
|             if (get_to<json::string_t>(*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<ModContentTypeId>& 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<char> 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: | ||||
|  |  | |||
|  | @ -3,12 +3,65 @@ | |||
| #include <sstream> | ||||
| #include <functional> | ||||
| 
 | ||||
| #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 <typename T1, typename T2> | ||||
| bool get_to_vec(const nlohmann::json& val, std::vector<T2>& out) { | ||||
|     const nlohmann::json::array_t* ptr = val.get_ptr<const nlohmann::json::array_t*>(); | ||||
|     if (ptr == nullptr) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     out.clear(); | ||||
| 
 | ||||
|     for (const nlohmann::json& cur_val : *ptr) { | ||||
|         const T1* temp_ptr = cur_val.get_ptr<const T1*>(); | ||||
|         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<size_t>&& game_indices, std::vector<ModContentTypeId>&& content_types) : | ||||
| recomp::mods::ModHandle::ModHandle(const ModContext& context, ModManifest&& manifest, ConfigStorage&& config_storage, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& content_types, std::vector<char>&& thumbnail) : | ||||
|     manifest(std::move(manifest)), | ||||
|     config_storage(std::move(config_storage)), | ||||
|     code_handle(), | ||||
|     recompiler_context{std::make_unique<N64Recomp::Context>()}, | ||||
|     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<size_t>&& game_indices, std::vector<ModContentTypeId>&& detected_content_types) { | ||||
| void recomp::mods::ModContext::add_opened_mod(ModManifest&& manifest, ConfigStorage&& config_storage, std::vector<size_t>&& game_indices, std::vector<ModContentTypeId>&& detected_content_types, std::vector<char>&& 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<recomp::mods::ConfigOptionEnum>(config_option.variant).options[std::get<uint32_t>(it.second)]; | ||||
|             break; | ||||
|         case recomp::mods::ConfigOptionType::Number: | ||||
|             storage_json[it.first] = std::get<double>(it.second); | ||||
|             break; | ||||
|         case recomp::mods::ConfigOptionType::String: | ||||
|             storage_json[it.first] = std::get<std::string>(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<std::string> &enabled_mods, std::vector<std::string> &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<std::string> enabled_mods_vector; | ||||
|         if (get_to_vec<std::string>(*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<std::string>(*mod_order_json, mod_order); | ||||
|     } | ||||
| 
 | ||||
|     return true; | ||||
| } | ||||
| 
 | ||||
| bool save_mods_config(const std::filesystem::path &path, const std::unordered_set<std::string> &enabled_mods, const std::vector<std::string> &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<std::string> pending_mods; | ||||
|     std::unordered_map<std::string, ConfigStorage> pending_mod_storage; | ||||
|     std::unordered_map<std::string, ConfigSchema> pending_mod_schema; | ||||
|     std::unordered_map<std::string, Version> pending_mod_version; | ||||
|     std::unordered_set<std::string> config_enabled_mods; | ||||
|     std::vector<std::string> 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<ModConfigQueueEnd>(&variant) != nullptr) { | ||||
|             active = false; | ||||
|         } | ||||
|         else if (std::get_if<ModConfigQueueSave>(&variant) != nullptr) { | ||||
|             pending_config_save = true; | ||||
|         } | ||||
|         else if (const ModConfigQueueSaveMod* queue_save_mod = std::get_if<ModConfigQueueSaveMod>(&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::ModOpenErrorDetails> recomp::mods::ModContext::scan_mod_folder(const std::filesystem::path& mod_folder) { | ||||
|  | @ -611,6 +830,40 @@ std::vector<recomp::mods::ModOpenErrorDetails> recomp::mods::ModContext::scan_mo | |||
|     return ret; | ||||
| } | ||||
| 
 | ||||
| void recomp::mods::ModContext::load_mods_config() { | ||||
|     std::unordered_set<std::string> config_enabled_mods; | ||||
|     std::vector<std::string> config_mod_order; | ||||
|     std::vector<bool> 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<size_t> 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<std::thread>(&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<std::string> 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<std::string> new_auto_enabled_mods; | ||||
|             for (const std::string &enabled_mod_id : enabled_mods) { | ||||
|                 std::vector<std::string> 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::ModDetails> recomp::mods::ModContext::get_mod_details(const std::string& mod_game_id) { | ||||
| std::vector<recomp::mods::ModDetails> recomp::mods::ModContext::get_mod_details(const std::string &mod_game_id) { | ||||
|     std::vector<ModDetails> ret{}; | ||||
|     bool all_games = mod_game_id.empty(); | ||||
|     size_t game_index = (size_t)-1; | ||||
|  | @ -740,7 +1076,8 @@ std::vector<recomp::mods::ModDetails> 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<Dependency> 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<char> &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<uint32_t>(value)) { | ||||
|                 if (std::get<uint32_t>(value) < std::get<ConfigOptionEnum>(option.variant).options.size()) { | ||||
|                     mod.config_storage.value_map[option_id] = value; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             break; | ||||
|         case ConfigOptionType::Number: | ||||
|             if (std::holds_alternative<double>(value)) { | ||||
|                 mod.config_storage.value_map[option_id] = value; | ||||
|             } | ||||
| 
 | ||||
|             break; | ||||
|         case ConfigOptionType::String: | ||||
|             if (std::holds_alternative<std::string>(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<ConfigOptionEnum>(option.variant).default_value; | ||||
|         case ConfigOptionType::Number: | ||||
|             return std::get<ConfigOptionNumber>(option.variant).default_value; | ||||
|         case ConfigOptionType::String: | ||||
|             return std::get<ConfigOptionString>(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::ModLoadErrorDetails> recomp::mods::ModContext::load_mods(const GameEntry& game_entry, uint8_t* rdram, int32_t load_address, uint32_t& ram_used) { | ||||
|     std::vector<recomp::mods::ModLoadErrorDetails> ret{}; | ||||
|     ram_used = 0; | ||||
|  | @ -925,7 +1433,7 @@ std::vector<recomp::mods::ModLoadErrorDetails> 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::ModLoadErrorDetails> 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<recomp_func_t*, overlays::BasePatchedFunction>& 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<recomp_func_t*, overlays::BasePatchedFunction>& 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<N64Recomp::ShimFunction>(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(); | ||||
| } | ||||
|  |  | |||
|  | @ -38,6 +38,7 @@ static std::unordered_map<uint32_t, uint16_t> patch_code_sections_by_rom{}; | |||
| static std::vector<LoadedSection> loaded_sections{}; | ||||
| static std::unordered_map<int32_t, recomp_func_t*> func_map{}; | ||||
| static std::unordered_map<std::string, recomp_func_t*> base_exports{}; | ||||
| static std::unordered_map<std::string, recomp_func_ext_t*> ext_base_exports{}; | ||||
| static std::unordered_map<std::string, size_t> base_events; | ||||
| static std::unordered_map<uint32_t, recomp_func_t*> 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<uint32_t, recomp_func_t*> 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); | ||||
|  |  | |||
|  | @ -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<recomp::mods::ModOpenErrorDetails> 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<char> &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::ModDetails> 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"); | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 Wiseguy
						Wiseguy