diff --git a/librecomp/include/librecomp/game.hpp b/librecomp/include/librecomp/game.hpp index 9168d6f..6adabfd 100644 --- a/librecomp/include/librecomp/game.hpp +++ b/librecomp/include/librecomp/game.hpp @@ -22,6 +22,28 @@ namespace recomp { std::u8string stored_filename() const; }; + struct Version { + int major = -1; + int minor = -1; + int patch = -1; + std::string suffix; + + std::string to_string() const { + return std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch) + suffix; + } + + static bool from_string(const std::string& str, Version& out); + + auto operator<=>(const Version& rhs) { + if (major != rhs.major) { + return major <=> rhs.major; + } + if (minor != rhs.minor) { + return minor <=> rhs.minor; + } + return patch <=> rhs.patch; + } + }; enum class RomValidationError { Good, FailedToOpen, @@ -41,6 +63,7 @@ namespace recomp { void set_rom_contents(std::vector&& new_rom); void do_rom_read(uint8_t* rdram, gpr ram_address, uint32_t physical_addr, size_t num_bytes); void do_rom_pio(uint8_t* rdram, gpr ram_address, uint32_t physical_addr); + const Version& get_project_version(); /** * The following arguments contain mandatory callbacks that need to be registered (i.e., can't be `nullptr`): @@ -51,6 +74,7 @@ namespace recomp { */ void start( uint32_t rdram_size, + const Version& project_version, ultramodern::renderer::WindowHandle window_handle, const recomp::rsp::callbacks_t& rsp_callbacks, const ultramodern::renderer::callbacks_t& renderer_callbacks, diff --git a/librecomp/include/librecomp/mods.hpp b/librecomp/include/librecomp/mods.hpp index 8b785dc..233d646 100644 --- a/librecomp/include/librecomp/mods.hpp +++ b/librecomp/include/librecomp/mods.hpp @@ -19,6 +19,7 @@ #include "miniz.h" #include "miniz_zip.h" +#include "librecomp/game.hpp" #include "librecomp/recomp.h" #include "librecomp/sections.h" @@ -39,6 +40,8 @@ namespace recomp { InvalidManifestSchema, UnrecognizedManifestField, IncorrectManifestFieldType, + InvalidVersionString, + InvalidMinimumRecompVersionString, MissingManifestField, InnerFileDoesNotExist, DuplicateMod, @@ -50,6 +53,7 @@ namespace recomp { enum class ModLoadError { Good, InvalidGame, + MinimumRecompVersionNotMet, FailedToLoadSyms, FailedToLoadBinary, FailedToLoadNativeCode, @@ -105,15 +109,25 @@ namespace recomp { std::vector exports; }; + struct DependencyDetails { + std::string mod_id; + Version version; + }; + + struct ModDetails { + std::string mod_id; + Version version; + std::vector authors; + std::vector dependencies; + }; + struct ModManifest { std::filesystem::path mod_root_path; std::vector mod_game_ids; std::string mod_id; - - int major_version = -1; - int minor_version = -1; - int patch_version = -1; + Version minimum_recomp_version; + Version version; // These are all relative to the base path for loose mods or inside the zip for zipped mods. std::string binary_path; diff --git a/librecomp/src/mod_manifest.cpp b/librecomp/src/mod_manifest.cpp index c43dd9f..35cc384 100644 --- a/librecomp/src/mod_manifest.cpp +++ b/librecomp/src/mod_manifest.cpp @@ -133,9 +133,8 @@ bool recomp::mods::LooseModFileHandle::file_exists(const std::string& filepath) enum class ManifestField { GameModId, Id, - MajorVersion, - MinorVersion, - PatchVersion, + Version, + MinimumRecompVersion, BinaryPath, BinarySymsPath, RomPatchPath, @@ -145,9 +144,8 @@ enum class ManifestField { const std::string game_mod_id_key = "game_id"; const std::string mod_id_key = "id"; -const std::string major_version_key = "major_version"; -const std::string minor_version_key = "minor_version"; -const std::string patch_version_key = "patch_version"; +const std::string version_key = "version"; +const std::string minimum_recomp_version_key = "minimum_recomp_version"; const std::string binary_path_key = "binary"; const std::string binary_syms_path_key = "binary_syms"; const std::string rom_patch_path_key = "rom_patch"; @@ -155,16 +153,15 @@ const std::string rom_patch_syms_path_key = "rom_patch_syms"; const std::string native_library_paths_key = "native_libraries"; std::unordered_map field_map { - { game_mod_id_key, ManifestField::GameModId }, - { mod_id_key, ManifestField::Id }, - { major_version_key, ManifestField::MajorVersion }, - { minor_version_key, ManifestField::MinorVersion }, - { patch_version_key, ManifestField::PatchVersion }, - { binary_path_key, ManifestField::BinaryPath }, - { binary_syms_path_key, ManifestField::BinarySymsPath }, - { rom_patch_path_key, ManifestField::RomPatchPath }, - { rom_patch_syms_path_key, ManifestField::RomPatchSymsPath }, - { native_library_paths_key, ManifestField::NativeLibraryPaths }, + { game_mod_id_key, ManifestField::GameModId }, + { mod_id_key, ManifestField::Id }, + { version_key, ManifestField::Version }, + { minimum_recomp_version_key, ManifestField::MinimumRecompVersion }, + { binary_path_key, ManifestField::BinaryPath }, + { binary_syms_path_key, ManifestField::BinarySymsPath }, + { rom_patch_path_key, ManifestField::RomPatchPath }, + { rom_patch_syms_path_key, ManifestField::RomPatchSymsPath }, + { native_library_paths_key, ManifestField::NativeLibraryPaths }, }; template @@ -239,22 +236,31 @@ recomp::mods::ModOpenError parse_manifest(recomp::mods::ModManifest& ret, const return recomp::mods::ModOpenError::IncorrectManifestFieldType; } break; - case ManifestField::MajorVersion: - if (!get_to(val, ret.major_version)) { - error_param = key; - return recomp::mods::ModOpenError::IncorrectManifestFieldType; + case ManifestField::Version: + { + const std::string* version_str = val.get_ptr(); + if (version_str == nullptr) { + error_param = key; + return recomp::mods::ModOpenError::IncorrectManifestFieldType; + } + if (!recomp::Version::from_string(*version_str, ret.version)) { + error_param = *version_str; + return recomp::mods::ModOpenError::InvalidVersionString; + } } break; - case ManifestField::MinorVersion: - if (!get_to(val, ret.minor_version)) { - error_param = key; - return recomp::mods::ModOpenError::IncorrectManifestFieldType; - } - break; - case ManifestField::PatchVersion: - if (!get_to(val, ret.patch_version)) { - error_param = key; - return recomp::mods::ModOpenError::IncorrectManifestFieldType; + case ManifestField::MinimumRecompVersion: + { + const std::string* version_str = val.get_ptr(); + if (version_str == nullptr) { + error_param = key; + return recomp::mods::ModOpenError::IncorrectManifestFieldType; + } + if (!recomp::Version::from_string(*version_str, ret.minimum_recomp_version)) { + error_param = *version_str; + return recomp::mods::ModOpenError::InvalidMinimumRecompVersionString; + } + ret.minimum_recomp_version.suffix.clear(); } break; case ManifestField::BinaryPath: @@ -328,16 +334,12 @@ recomp::mods::ModOpenError validate_manifest(const recomp::mods::ModManifest& ma error_param = mod_id_key; return ModOpenError::MissingManifestField; } - if (manifest.major_version == -1) { - error_param = major_version_key; + if (manifest.version.major == -1 || manifest.version.major == -1 || manifest.version.major == -1) { + error_param = version_key; return ModOpenError::MissingManifestField; } - if (manifest.minor_version == -1) { - error_param = minor_version_key; - return ModOpenError::MissingManifestField; - } - if (manifest.patch_version == -1) { - error_param = patch_version_key; + if (manifest.minimum_recomp_version.major == -1 || manifest.minimum_recomp_version.major == -1 || manifest.minimum_recomp_version.major == -1) { + error_param = minimum_recomp_version_key; return ModOpenError::MissingManifestField; } @@ -477,6 +479,10 @@ std::string recomp::mods::error_to_string(ModOpenError error) { return "Unrecognized field in manifest.json"; case ModOpenError::IncorrectManifestFieldType: return "Incorrect type for field in manifest.json"; + case ModOpenError::InvalidVersionString: + return "Invalid version string in manifest.json"; + case ModOpenError::InvalidMinimumRecompVersionString: + return "Invalid minimum recomp version string in manifest.json"; case ModOpenError::MissingManifestField: return "Missing required field in manifest"; case ModOpenError::InnerFileDoesNotExist: @@ -495,6 +501,8 @@ std::string recomp::mods::error_to_string(ModLoadError error) { return "Good"; case ModLoadError::InvalidGame: return "Invalid game"; + case ModLoadError::MinimumRecompVersionNotMet: + return "Mod requires a newer version of this project"; case ModLoadError::FailedToLoadSyms: return "Failed to load mod symbol file"; case ModLoadError::FailedToLoadBinary: diff --git a/librecomp/src/mods.cpp b/librecomp/src/mods.cpp index 65d2e41..cf37a60 100644 --- a/librecomp/src/mods.cpp +++ b/librecomp/src/mods.cpp @@ -314,6 +314,12 @@ void recomp::mods::ModContext::add_opened_mod(ModManifest&& manifest, std::vecto recomp::mods::ModLoadError recomp::mods::ModContext::load_mod(uint8_t* rdram, const std::unordered_map& section_vrom_map, recomp::mods::ModHandle& handle, int32_t load_address, uint32_t& ram_used, std::string& error_param) { using namespace recomp::mods; handle.section_load_addresses.clear(); + + // Check that the mod's minimum recomp version is met. + if (get_project_version() < handle.manifest.minimum_recomp_version) { + error_param = handle.manifest.minimum_recomp_version.to_string(); + return recomp::mods::ModLoadError::MinimumRecompVersionNotMet; + } // Load the mod symbol data from the file provided in the manifest. bool binary_syms_exists = false; @@ -365,7 +371,7 @@ std::vector recomp::mods::ModContext::scan_mo std::vector ret{}; std::error_code ec; for (const auto& mod_path : std::filesystem::directory_iterator{mod_folder, std::filesystem::directory_options::skip_permission_denied, ec}) { - if ((mod_path.is_regular_file() && mod_path.path().extension() == ".zip") || mod_path.is_directory()) { + if ((mod_path.is_regular_file() && mod_path.path().extension() == ".nrm") || mod_path.is_directory()) { printf("Opening mod " PATHFMT "\n", mod_path.path().stem().c_str()); std::string open_error_param; ModOpenError open_error = open_mod(mod_path, open_error_param); @@ -510,27 +516,6 @@ std::vector recomp::mods::ModContext::load_mo return ret; } -bool dependency_version_met(uint8_t major, uint8_t minor, uint8_t patch, uint8_t major_target, uint8_t minor_target, uint8_t patch_target) { - if (major > major_target) { - return true; - } - else if (major < major_target) { - return false; - } - - if (minor > minor_target) { - return true; - } - else if (minor < minor_target) { - return false; - } - - if (patch >= patch_target) { - return true; - } - return false; -} - void recomp::mods::ModContext::check_dependencies(recomp::mods::ModHandle& mod, std::vector>& errors) { errors.clear(); for (N64Recomp::Dependency& cur_dep : mod.recompiler_context->dependencies) { @@ -546,15 +531,18 @@ void recomp::mods::ModContext::check_dependencies(recomp::mods::ModHandle& mod, continue; } - const auto& mod = opened_mods[find_it->second]; - if (!dependency_version_met( - mod.manifest.major_version, mod.manifest.minor_version, mod.manifest.patch_version, - cur_dep.major_version, cur_dep.minor_version, cur_dep.patch_version)) + const ModHandle& dep_mod = opened_mods[find_it->second]; + Version dep_version { + .major = cur_dep.major_version, + .minor = cur_dep.minor_version, + .patch = cur_dep.patch_version + }; + if (dep_version > dep_mod.manifest.version) { std::stringstream error_param_stream{}; error_param_stream << "requires mod \"" << cur_dep.mod_id << "\" " << (int)cur_dep.major_version << "." << (int)cur_dep.minor_version << "." << (int)cur_dep.patch_version << ", got " << - (int)mod.manifest.major_version << "." << (int)mod.manifest.minor_version << "." << (int)mod.manifest.patch_version << ""; + (int)dep_mod.manifest.version.major << "." << (int)dep_mod.manifest.version.minor << "." << (int)dep_mod.manifest.version.patch << ""; errors.emplace_back(ModLoadError::WrongDependencyVersion, error_param_stream.str()); } } diff --git a/librecomp/src/recomp.cpp b/librecomp/src/recomp.cpp index 806f221..2bd3945 100644 --- a/librecomp/src/recomp.cpp +++ b/librecomp/src/recomp.cpp @@ -55,6 +55,8 @@ std::filesystem::path config_path; std::unordered_map game_roms {}; // The global mod context. std::unique_ptr mod_context = std::make_unique(); +// The project's version. +recomp::Version project_version; std::u8string recomp::GameEntry::stored_filename() const { return game_id + u8".z64"; @@ -167,6 +169,70 @@ bool recomp::load_stored_rom(std::u8string& game_id) { return true; } +const recomp::Version& recomp::get_project_version() { + return project_version; +} + +bool recomp::Version::from_string(const std::string& str, Version& out) { + std::array period_indices; + size_t num_periods = 0; + size_t cur_pos = 0; + uint16_t major; + uint16_t minor; + uint16_t patch; + std::string suffix; + + // Find the 2 required periods. + cur_pos = str.find('.', cur_pos); + period_indices[0] = cur_pos; + cur_pos = str.find('.', cur_pos + 1); + period_indices[1] = cur_pos; + + // Check that both were found. + if (period_indices[0] == std::string::npos || period_indices[1] == std::string::npos) { + return false; + } + + // Parse the 3 numbers formed by splitting the string via the periods. + std::array parse_results; + std::array parse_starts { 0, period_indices[0] + 1, period_indices[1] + 1 }; + std::array parse_ends { period_indices[0], period_indices[1], str.size() }; + parse_results[0] = std::from_chars(str.data() + parse_starts[0], str.data() + parse_ends[0], major); + parse_results[1] = std::from_chars(str.data() + parse_starts[1], str.data() + parse_ends[1], minor); + parse_results[2] = std::from_chars(str.data() + parse_starts[2], str.data() + parse_ends[2], patch); + + // Check that the first two parsed correctly. + auto did_parse = [&](size_t i) { + return parse_results[i].ec == std::errc{} && parse_results[i].ptr == str.data() + parse_ends[i]; + }; + + if (!did_parse(0) || !did_parse(1)) { + return false; + } + + // Check that the third had a successful parse, but not necessarily read all the characters. + if (parse_results[2].ec != std::errc{}) { + return false; + } + + // Allow a plus or minus directly after the third number. + if (parse_results[2].ptr != str.data() + parse_ends[2]) { + if (*parse_results[2].ptr == '+' || *parse_results[2].ptr == '-') { + suffix = str.substr(std::distance(str.data(), parse_results[2].ptr)); + } + // Failed to parse, as nothing is allowed directly after the last number besides a plus or minus. + else { + return false; + } + } + + out.major = major; + out.minor = minor; + out.patch = patch; + out.suffix = std::move(suffix); + return true; +} + const std::array first_rom_bytes { 0x80, 0x37, 0x12, 0x40 }; enum class ByteswapType { @@ -477,6 +543,7 @@ bool wait_for_game_started(uint8_t* rdram, recomp_context* context) { void recomp::start( uint32_t rdram_size, + const recomp::Version& version, ultramodern::renderer::WindowHandle window_handle, const recomp::rsp::callbacks_t& rsp_callbacks, const ultramodern::renderer::callbacks_t& renderer_callbacks, @@ -487,6 +554,7 @@ void recomp::start( const ultramodern::error_handling::callbacks_t& error_handling_callbacks, const ultramodern::threads::callbacks_t& threads_callbacks ) { + project_version = version; recomp::check_all_stored_roms(); recomp::rsp::set_callbacks(rsp_callbacks);