diff --git a/doc/en/content-packs.md b/doc/en/content-packs.md index c32d1b03..a41f95f9 100644 --- a/doc/en/content-packs.md +++ b/doc/en/content-packs.md @@ -30,6 +30,11 @@ If prefix is not specified, '!' level will be used. Example: '~randutil' - weak dependency 'randutil'. +Dependency version is indicated after '@' symbol and have operators to restrict acceptable versions. +If version is not specified, '\*' (any) version will be used. + +Example: 'randutil@>=1.0' - dependency 'randutil' which requires version 1.0 or newer. + Example: ```json { diff --git a/doc/ru/content-packs.md b/doc/ru/content-packs.md index 77a78636..4245555f 100644 --- a/doc/ru/content-packs.md +++ b/doc/ru/content-packs.md @@ -32,6 +32,11 @@ Пример: '~randutil' - слабая зависимость 'randutil'. +Версии зависимостей указываются после '@' и имеют операторы для ограничения допустимых версий. +Отсутствие версии зависимости интерпретируется как '\*', т.е. любая версия. + +Пример: 'randutil@>=1.0' - зависимость 'randutil' версии 1.0 и старше. + Пример: ```json { diff --git a/res/layouts/pages/content.xml.lua b/res/layouts/pages/content.xml.lua index 3f1903f3..8ed37630 100644 --- a/res/layouts/pages/content.xml.lua +++ b/res/layouts/pages/content.xml.lua @@ -176,18 +176,105 @@ function place_pack(panel, packinfo, callback, position_func) end end +local Version = {}; + +function Version.matches_pattern(version) + for _, letter in string.gmatch(version, "%.+") do + if type(letter) ~= "number" or letter ~= "." then + return false; + end + + local t = string.split(version, "."); + + return #t == 2 or #t == 3; + end +end + +function Version.__equal(ver1, ver2) + return ver1[1] == ver2[1] and ver1[2] == ver2[2] and ver1[3] == ver2[3]; +end + +function Version.__more(ver1, ver2) + if ver1[1] ~= ver2[1] then return ver1[1] > ver2[1] end; + if ver1[2] ~= ver2[2] then return ver1[2] > ver2[2] end; + return ver1[3] > ver2[3]; +end + +function Version.__less(ver1, ver2) + return Version.__more(ver2, ver1); +end + +function Version.__more_or_equal(ver1, ver2) + return not Version.__less(ver1, ver2); +end + +function Version.__less_or_equal(ver1, ver2) + return not Version.__more(ver1, ver2); +end + +function Version.compare(op, ver1, ver2) + ver1 = string.split(ver1, "."); + ver2 = string.split(ver2, "."); + + if op == "=" then return Version.__equal(ver1, ver2); + elseif op == ">" then return Version.__more(ver1, ver2); + elseif op == "<" then return Version.__less(ver1, ver2); + elseif op == ">=" then return Version.__more_or_equal(ver1, ver2); + elseif op == "<=" then return Version.__less_or_equal(ver1, ver2); + else return false; end +end + +function Version.parse(version) + local op = string.sub(version, 1, 2); + if op == ">=" or op == "=>" then + return ">=", string.sub(version, #op + 1); + elseif op == "<=" or op == "=<" then + return "<=", string.sub(version, #op + 1); + end + + op = string.sub(version, 1, 1); + if op == ">" or op == "<" then + return op, string.sub(version, #op + 1); + end + + return "=", version; +end + +local function compare_version(dependent_version, actual_version) + if Version.matches_pattern(dependent_version) and Version.matches_pattern(actual_version) then + local op, dep_ver = Version.parse_version(dependent_version); + Version.compare(op, dep_ver, actual_version); + elseif dependent_version == "*" or dependent_version == actual_version then + return true; + else + return false; + end +end + function check_dependencies(packinfo) if packinfo.dependencies == nil then return end for i,dep in ipairs(packinfo.dependencies) do - local depid = dep:sub(2,-1) - if dep:sub(1,1) == '!' then + local depid, depver = unpack(string.split(dep:sub(2,-1), "@")) + + if dep:sub(1,1) == '!' then if not table.has(packs_all, depid) then return string.format( "%s (%s)", gui.str("error.dependency-not-found"), depid ) end + + + local dep_pack = pack.get_info(depid); + + if not compare_version(depver, dep_pack.version) then + local op, ver = Version.parse(depver); + + print(string.format("%s: %s !%s %s (%s)", gui.str("error.dependency-version-not-met"), dep_pack.version, op, ver, depid)); + return string.format("%s: %s != %s (%s)", gui.str("error.dependency-version-not-met"), dep_pack.version, ver, depid); + end + if table.has(packs_installed, packinfo.id) then table.insert(required, depid) end diff --git a/res/texts/en_US.txt b/res/texts/en_US.txt index b3be4c92..e35ff99c 100644 --- a/res/texts/en_US.txt +++ b/res/texts/en_US.txt @@ -7,6 +7,7 @@ world.convert-block-layouts=Blocks fields have changes! Convert world files? pack.remove-confirm=Do you want to erase all pack(s) content from the world forever? error.pack-not-found=Could not to find pack error.dependency-not-found=Dependency pack is not found +error.dependency-version-not-met=Dependency pack version is not met. world.delete-confirm=Do you want to delete world forever? world.generators.default=Default world.generators.flat=Flat diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index 5d8d22ca..084e11b3 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -32,6 +32,7 @@ devtools.output=Вывод error.pack-not-found=Не удалось найти пакет error.dependency-not-found=Используемая зависимость не найдена +error.dependency-version-not-met=Версия зависимости не соответствует необходимой pack.remove-confirm=Удалить весь поставляемый паком/паками контент из мира (безвозвратно)? # Подсказки diff --git a/src/content/ContentPack.cpp b/src/content/ContentPack.cpp index ac8c8d59..522b818f 100644 --- a/src/content/ContentPack.cpp +++ b/src/content/ContentPack.cpp @@ -118,21 +118,58 @@ ContentPack ContentPack::read(const io::path& folder) { const auto& dependencies = *found; for (const auto& elem : dependencies) { std::string depName = elem.asString(); - auto level = DependencyLevel::required; + auto level = DependencyLevel::REQUIRED; switch (depName.at(0)) { case '!': depName = depName.substr(1); break; case '?': depName = depName.substr(1); - level = DependencyLevel::optional; + level = DependencyLevel::OPTIONAL; break; case '~': depName = depName.substr(1); - level = DependencyLevel::weak; + level = DependencyLevel::WEAK; break; } - pack.dependencies.push_back({level, depName}); + + std::string depVer = "*"; + std::string depVerOperator = "="; + + size_t versionPos = depName.rfind("@"); + if (versionPos != std::string::npos) { + depVer = depName.substr(versionPos + 1); + depName = depName.substr(0, versionPos); + + if (depVer.size() >= 2) { + std::string op = depVer.substr(0, 2); + std::uint8_t op_size = 0; + + // Two symbol operators + if (op == ">=" || op == "=>" || op == "<=" || op == "=<") { + op_size = 2; + depVerOperator = op; + } + + // One symbol operators + else { + op = depVer.substr(0, 1); + + if (op == ">" || op == "<") { + op_size = 1; + depVerOperator = op; + } + } + + depVer = depVer.substr(op_size); + } else { + if (depVer == ">" || depVer == "<"){ + depVer = "*"; + } + } + } + + pack.dependencies.push_back({level, depName, depVer, depVerOperator}); } } diff --git a/src/content/ContentPack.hpp b/src/content/ContentPack.hpp index f4e44801..1aa3a9ea 100644 --- a/src/content/ContentPack.hpp +++ b/src/content/ContentPack.hpp @@ -20,21 +20,28 @@ public: io::path folder, const std::string& message ); - + std::string getPackId() const; io::path getFolder() const; }; +enum class DependencyVersionOperator { + EQUAL, GREATHER, LESS, + GREATHER_OR_EQUAL, LESS_OR_EQUAL +}; + enum class DependencyLevel { - required, // dependency must be installed - optional, // dependency will be installed if found - weak, // only affects packs order + REQUIRED, // dependency must be installed + OPTIONAL, // dependency will be installed if found + WEAK, // only affects packs order }; /// @brief Content-pack that should be installed earlier the dependent struct DependencyPack { DependencyLevel level; std::string id; + std::string version; + std::string op; }; struct ContentPackStats { diff --git a/src/content/ContentPackVersion.cpp b/src/content/ContentPackVersion.cpp new file mode 100644 index 00000000..7a3d8801 --- /dev/null +++ b/src/content/ContentPackVersion.cpp @@ -0,0 +1,67 @@ +#include "ContentPackVersion.hpp" + +#include +#include +#include + +#include "coders/commons.hpp" + +Version::Version(const std::string& version) { + major = 0; + minor = 0; + patch = 0; + + std::vector parts; + + std::stringstream ss(version); + std::string part; + while (std::getline(ss, part, '.')) { + if (!part.empty()) { + parts.push_back(std::stoi(part)); + } + } + + if (parts.size() > 0) major = parts[0]; + if (parts.size() > 1) minor = parts[1]; + if (parts.size() > 2) patch = parts[2]; +} + +DependencyVersionOperator Version::string_to_operator(const std::string& op) { + if (op == "=") + return DependencyVersionOperator::EQUAL; + else if (op == ">") + return DependencyVersionOperator::GREATHER; + else if (op == "<") + return DependencyVersionOperator::LESS; + else if (op == ">=" || op == "=>") + return DependencyVersionOperator::GREATHER_OR_EQUAL; + else if (op == "<=" || op == "=<") + return DependencyVersionOperator::LESS_OR_EQUAL; + else + return DependencyVersionOperator::EQUAL; +} + +bool isNumber(const std::string& s) { + return !s.empty() && std::all_of(s.begin(), s.end(), ::is_digit); +} + +bool Version::matches_pattern(const std::string& version) { + for (char c : version) { + if (!isdigit(c) && c != '.') { + return false; + } + } + + std::stringstream ss(version); + + std::vector parts; + std::string part; + while (std::getline(ss, part, '.')) { + if (part.empty()) return false; + if (!isNumber(part)) return false; + + parts.push_back(part); + } + + return parts.size() == 2 || parts.size() == 3; +} diff --git a/src/content/ContentPackVersion.hpp b/src/content/ContentPackVersion.hpp new file mode 100644 index 00000000..e922ddd3 --- /dev/null +++ b/src/content/ContentPackVersion.hpp @@ -0,0 +1,57 @@ +#include + +#include "content/ContentPack.hpp" + +class Version { +public: + int major; + int minor; + int patch; + + Version(const std::string& version); + + bool operator==(const Version& other) const { + return major == other.major && minor == other.minor && + patch == other.patch; + } + + bool operator<(const Version& other) const { + if (major != other.major) return major < other.major; + if (minor != other.minor) return minor < other.minor; + return patch < other.patch; + } + + bool operator>(const Version& other) const { + return other < *this; + } + + bool operator>=(const Version& other) const { + return !(*this < other); + } + + bool operator<=(const Version& other) const { + return !(*this > other); + } + + bool process_operator(const std::string& op, const Version& other) const { + auto dep_op = Version::string_to_operator(op); + + switch (dep_op) { + case DependencyVersionOperator::EQUAL: + return *this == other; + case DependencyVersionOperator::GREATHER: + return *this > other; + case DependencyVersionOperator::LESS: + return *this < other; + case DependencyVersionOperator::LESS_OR_EQUAL: + return *this <= other; + case DependencyVersionOperator::GREATHER_OR_EQUAL: + return *this >= other; + default: + return false; + } + } + + static DependencyVersionOperator string_to_operator(const std::string& op); + static bool matches_pattern(const std::string& version); +}; diff --git a/src/content/PacksManager.cpp b/src/content/PacksManager.cpp index 0e9925c8..2d9759cb 100644 --- a/src/content/PacksManager.cpp +++ b/src/content/PacksManager.cpp @@ -3,6 +3,7 @@ #include #include +#include "ContentPackVersion.hpp" #include "util/listutil.hpp" PacksManager::PacksManager() = default; @@ -90,7 +91,7 @@ static bool resolve_dependencies( } auto found = packs.find(dep.id); bool exists = found != packs.end(); - if (!exists && dep.level == DependencyLevel::required) { + if (!exists && dep.level == DependencyLevel::REQUIRED) { throw contentpack_error( dep.id, io::path(), "dependency of '" + pack->id + "'" ); @@ -99,15 +100,32 @@ static bool resolve_dependencies( // ignored for optional or weak dependencies continue; } - if (resolveWeaks && dep.level == DependencyLevel::weak) { + if (resolveWeaks && dep.level == DependencyLevel::WEAK) { // dependency pack is found but not added yet // resolveWeaks is used on second iteration, so it's will not be // added continue; } + auto dep_pack = found -> second; + + if (Version::matches_pattern(dep.version) && Version::matches_pattern(dep_pack.version) + && Version(dep_pack.version) + .process_operator(dep.op, Version(dep.version)) + ) { + // dependency pack version meets the required one + continue; + } else if (dep.version == "*" || dep.version == dep_pack.version){ + // fallback: dependency pack version also meets required one + continue; + } else { + throw contentpack_error( + dep.id, io::path(), "does not meet required version '" + dep.op + dep.version +"' of '" + pack->id + "'" + ); + } + if (!util::contains(allNames, dep.id) && - dep.level != DependencyLevel::weak) { + dep.level != DependencyLevel::WEAK) { allNames.push_back(dep.id); queue.push(&found->second); } diff --git a/src/logic/scripting/lua/libs/libpack.cpp b/src/logic/scripting/lua/libs/libpack.cpp index adc45096..29ceef99 100644 --- a/src/logic/scripting/lua/libs/libpack.cpp +++ b/src/logic/scripting/lua/libs/libpack.cpp @@ -102,19 +102,20 @@ static int l_pack_get_info( auto& dpack = pack.dependencies[i]; std::string prefix; switch (dpack.level) { - case DependencyLevel::required: + case DependencyLevel::REQUIRED: prefix = "!"; break; - case DependencyLevel::optional: + case DependencyLevel::OPTIONAL: prefix = "?"; break; - case DependencyLevel::weak: + case DependencyLevel::WEAK: prefix = "~"; break; default: throw std::runtime_error(""); } - lua::pushfstring(L, "%s%s", prefix.c_str(), dpack.id.c_str()); + + lua::pushfstring(L, "%s%s@%s%s", prefix.c_str(), dpack.id.c_str(), dpack.op.c_str(), dpack.version.c_str()); lua::rawseti(L, i + 1); } lua::setfield(L, "dependencies");