diff --git a/docs/LOOT Metadata Syntax.html b/docs/LOOT Metadata Syntax.html index c9840bdc..28bbd793 100644 --- a/docs/LOOT Metadata Syntax.html +++ b/docs/LOOT Metadata Syntax.html @@ -537,6 +537,7 @@ msg: Checksumchecksum("filepath", checksum)Returns true if the calculated CRC-32 checksum of filepath matches checksum, false otherwise. If filepath does not exist, returns false. Versionversion("filepath", "version", comparator)Returns true if the boolean expression (actual version of filepath) comparator version holds true, false otherwise. If filepath does not exist or does not have a version number, its version is assumed to be 0. The comparison is not a straightforward per-character comparison, but instead uses the precedence rules defined by Semantic Versioning, extended to allow leading zeroes, an arbitrary number of release version numbers, case-insensitivity and a wider range of separator characters. Plugin Active Statusactive("filepath")Returns true if filepath is a .esp or .esm file that is both installed and active, false otherwise. + Many Plugins Active Statusmany_active("regex")Returns true if more than one file matching regex is a .esp or .esm file that is both installed and active, false otherwise.

The different types of variable used in the above functions are described in more detail below. diff --git a/src/backend/metadata/condition_grammar.h b/src/backend/metadata/condition_grammar.h index 8dae5a99..a49a4faf 100644 --- a/src/backend/metadata/condition_grammar.h +++ b/src/backend/metadata/condition_grammar.h @@ -87,6 +87,7 @@ public: | ("checksum(" > filePath_ > ',' > qi::hex > ')')[phoenix::bind(&ConditionGrammar::CheckSum, this, qi::labels::_val, qi::labels::_1, qi::labels::_2)] | ("version(" > filePath_ > ',' > quotedStr_ > ',' > comparator_ > ')')[phoenix::bind(&ConditionGrammar::CheckVersion, this, qi::labels::_val, qi::labels::_1, qi::labels::_2, qi::labels::_3)] | ("active(" > filePath_ > ')')[phoenix::bind(&ConditionGrammar::CheckActive, this, qi::labels::_val, qi::labels::_1)] + | ("many_active(" > quotedStr_ > ')')[phoenix::bind(&ConditionGrammar::CheckManyActive, this, qi::labels::_val, qi::labels::_1)] ; quotedStr_ %= '"' > +(char_ - '"') > '"'; @@ -376,6 +377,36 @@ private: BOOST_LOG_TRIVIAL(trace) << "Active check result: " << result; } + void CheckManyActive(bool& result, const std::string& regexStr) const { + result = false; + + BOOST_LOG_TRIVIAL(trace) << "Checking to see if more than one file matching the regex \"" << regexStr << "\" exist."; + + std::pair pathRegex = SplitRegex(regexStr); + + if (game_ == nullptr) + return; + + // Now we have a valid parent path and a regex filename. Check that + // the parent path exists and is a directory. + boost::filesystem::path parent_path = game_->DataPath() / pathRegex.first; + if (!boost::filesystem::exists(parent_path) || !boost::filesystem::is_directory(parent_path)) { + BOOST_LOG_TRIVIAL(trace) << "The path \"" << parent_path << "\" does not exist or is not a directory."; + return; + } + + size_t count = 0; + for (boost::filesystem::directory_iterator itr(parent_path); itr != boost::filesystem::directory_iterator(); ++itr) { + const std::string filename = itr->path().filename().string(); + if (std::regex_match(filename, pathRegex.second) && game_->IsPluginActive(filename)) { + ++count; + BOOST_LOG_TRIVIAL(trace) << "Matching file found: " << itr->path(); + } + } + + result = count > 1; + } + void SyntaxError(Iterator const& /*first*/, Iterator const& last, Iterator const& errorpos, boost::spirit::info const& what) { std::string context(errorpos, last); boost::trim(context); diff --git a/src/tests/backend/metadata/condition_grammar_test.h b/src/tests/backend/metadata/condition_grammar_test.h index 82d925a0..8f959148 100644 --- a/src/tests/backend/metadata/condition_grammar_test.h +++ b/src/tests/backend/metadata/condition_grammar_test.h @@ -561,6 +561,51 @@ TEST_P(ConditionGrammarTest, anActiveConditionWithAPluginThatIsNotActiveShouldEv EXPECT_FALSE(result_); } +TEST_P(ConditionGrammarTest, aManyActiveConditionWithARegexMatchingMoreThanOnePluginThatIsActiveShouldEvaluateToTrue) { + ASSERT_NO_THROW(game_.Init(false, localPath)); + + Grammar grammar(&game_); + std::string condition("many_active(\"Blank( - Different Master Dependent)?\\.es(m|p)\")"); + + success_ = boost::spirit::qi::phrase_parse(std::cbegin(condition), + std::cend(condition), + grammar, + skipper_, + result_); + EXPECT_TRUE(success_); + EXPECT_TRUE(result_); +} + +TEST_P(ConditionGrammarTest, aManyActiveConditionWithARegexMatchingOnlyOnePluginThatIsActiveShouldEvaluateToFalse) { + ASSERT_NO_THROW(game_.Init(false, localPath)); + + Grammar grammar(&game_); + std::string condition("many_active(\"Blank\\.esm\")"); + + success_ = boost::spirit::qi::phrase_parse(std::cbegin(condition), + std::cend(condition), + grammar, + skipper_, + result_); + EXPECT_TRUE(success_); + EXPECT_FALSE(result_); +} + +TEST_P(ConditionGrammarTest, aManyActiveConditionWithARegexMatchingNoPluginsThatAreActiveShouldEvaluateToFalse) { + ASSERT_NO_THROW(game_.Init(false, localPath)); + + Grammar grammar(&game_); + std::string condition("many_active(\"Blank\\.esp\")"); + + success_ = boost::spirit::qi::phrase_parse(std::cbegin(condition), + std::cend(condition), + grammar, + skipper_, + result_); + EXPECT_TRUE(success_); + EXPECT_FALSE(result_); +} + TEST_P(ConditionGrammarTest, aFalseConditionPrecededByANegatorShouldEvaluateToTrue) { Grammar grammar(&game_); std::string condition("not file(\"" + missingEsp + "\")");