diff --git a/CMakeLists.txt b/CMakeLists.txt index 84b95ee6..dfb02ea5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,8 +87,8 @@ endif() ExternalProject_Add(libloadorder PREFIX "external" - URL "https://github.com/Ortham/libloadorder/archive/14.0.0.tar.gz" - URL_HASH "SHA256=ee235eeb6bbef6f73050a35190c6c10f7a1fbdb3fd32a7ed7ff7b7727844b30f" + URL "https://github.com/Ortham/libloadorder/archive/14.1.0.tar.gz" + URL_HASH "SHA256=9ff9c73612bc9e375759e122bfd56a4a45d8a4ca36837f610c05ab52fef9d67b" CONFIGURE_COMMAND "" BUILD_IN_SOURCE 1 BUILD_COMMAND cargo build --release --manifest-path ffi/Cargo.toml --target ${RUST_TARGET} && @@ -105,8 +105,8 @@ endif() ExternalProject_Add(loot-condition-interpreter PREFIX "external" - URL "https://github.com/loot/loot-condition-interpreter/archive/2.3.1.tar.gz" - URL_HASH "SHA256=e0f5533bf113c2ed48e2249b241e01c1521d9bbf615d01581870db9948b21278" + URL "https://github.com/loot/loot-condition-interpreter/archive/2.4.0.tar.gz" + URL_HASH "SHA256=7c1d42636d8b10ae2b4dc3fced4a0b25112caf5e489a8f8ef0c915b9dd1189e1" CONFIGURE_COMMAND "" BUILD_IN_SOURCE 1 BUILD_COMMAND cargo build --release --manifest-path ffi/Cargo.toml --target ${RUST_TARGET} && diff --git a/include/loot/game_interface.h b/include/loot/game_interface.h index aa6cbab6..09f6b525 100644 --- a/include/loot/game_interface.h +++ b/include/loot/game_interface.h @@ -57,25 +57,29 @@ public: * @details The validity check is not exhaustive: it checks that the file * extension is ``.esm`` or ``.esp`` (after trimming any ``.ghost`` * extension), and that the ``TES4`` header can be parsed. - * @param plugin - * The filename of the file to check. + * @param pluginPath + * The path to the file to check. Relative paths are resolved relative + * to the game's plugins directory, while absolute paths are used + * as given. * @returns True if the file is a valid plugin, false otherwise. */ - virtual bool IsValidPlugin(const std::string& plugin) const = 0; + virtual bool IsValidPlugin(const std::string& pluginPath) const = 0; /** * @brief Parses plugins and loads their data. * @details Any previously-loaded plugin data is discarded when this function * is called. - * @param plugins - * The filenames of the plugins to load. + * @param pluginPaths + * The plugin paths to load. Relative paths are resolved relative to + * the game's plugins directory, while absolute paths are used as + * given. Each plugin filename must be unique within the vector. * @param loadHeadersOnly * If true, only the plugins' ``TES4`` headers are loaded. If false, * all records in the plugins are parsed, apart from the main master * file if it has been identified by a previous call to * ``IdentifyMainMasterFile()``. */ - virtual void LoadPlugins(const std::vector& plugins, + virtual void LoadPlugins(const std::vector& pluginPaths, bool loadHeadersOnly) = 0; /** @@ -118,13 +122,15 @@ public: * applied to the load order used by the game. This function does * not load or evaluate the masterlist or userlist. * @param plugins - * A vector of filenames of the plugins to sort, in their current - * load order. + * The plugin paths to sort, in their current load order. Relative + * paths are resolved relative to the game's plugins directory, while + * absolute paths are used as given. Each plugin filename must be + * unique within the vector. * @returns A vector of the given plugin filenames in their sorted load * order. */ virtual std::vector SortPlugins( - const std::vector& plugins) = 0; + const std::vector& pluginPaths) = 0; /** * @} diff --git a/src/api/game/game.cpp b/src/api/game/game.cpp index 367480b7..1dbb18f1 100644 --- a/src/api/game/game.cpp +++ b/src/api/game/game.cpp @@ -50,6 +50,85 @@ using std::filesystem::u8path; +namespace { +using loot::GameType; + +// The Microsoft Store installs Fallout 4 DLCs to directories outside of the +// game's install path. These directories have fixed paths relative to the +// game install path (renaming them causes the game launch to fail, or not +// find the DLC files). +constexpr const char* MS_FO4_AUTOMATRON_DATA_PATH = + "../../Fallout 4- Automatron (PC)/Content/Data"; +constexpr const char* MS_FO4_CONTRAPTIONS_DATA_PATH = + "../../Fallout 4- Contraptions Workshop (PC)/Content/Data"; +constexpr const char* MS_FO4_FAR_HARBOR_DATA_PATH = + "../../Fallout 4- Far Harbor (PC)/Content/Data"; +constexpr const char* MS_FO4_TEXTURE_PACK_DATA_PATH = + "../../Fallout 4- High Resolution Texture Pack/Content/Data"; +constexpr const char* MS_FO4_NUKA_WORLD_DATA_PATH = + "../../Fallout 4- Nuka-World (PC)/Content/Data"; +constexpr const char* MS_FO4_VAULT_TEC_DATA_PATH = + "../../Fallout 4- Vault-Tec Workshop (PC)/Content/Data"; +constexpr const char* MS_FO4_WASTELAND_DATA_PATH = + "../../Fallout 4- Wasteland Workshop (PC)/Content/Data"; + +bool IsMicrosoftStoreGame(const std::filesystem::path& gamePath) { + return std::filesystem::exists(gamePath / "appxmanifest.xml"); +} + +std::vector GetAdditionalDataPaths( + const GameType gameType, + const std::filesystem::path& dataPath) { + const auto gamePath = dataPath.parent_path(); + + if (gameType == GameType::fo4 && IsMicrosoftStoreGame(gamePath)) { + // All DLC directories are listed before the main data path because DLC + // plugins in those directories override any in the main data path. + return {gamePath / MS_FO4_AUTOMATRON_DATA_PATH, + gamePath / MS_FO4_NUKA_WORLD_DATA_PATH, + gamePath / MS_FO4_WASTELAND_DATA_PATH, + gamePath / MS_FO4_TEXTURE_PACK_DATA_PATH, + gamePath / MS_FO4_VAULT_TEC_DATA_PATH, + gamePath / MS_FO4_FAR_HARBOR_DATA_PATH, + gamePath / MS_FO4_CONTRAPTIONS_DATA_PATH, + dataPath}; + } + + return {}; +} + +std::filesystem::path ResolvePluginPath( + const std::filesystem::path& dataPath, + const std::filesystem::path& pluginPath) { + return pluginPath.is_absolute() ? pluginPath : dataPath / pluginPath; +} + +std::vector FindArchives( + const std::filesystem::path& parentPath, + const std::string& archiveFileExtension) { + if (!std::filesystem::is_directory(parentPath)) { + return {}; + } + + std::vector archivePaths; + + for (std::filesystem::directory_iterator it(parentPath); + it != std::filesystem::directory_iterator(); + ++it) { + // This is only correct for ASCII strings, but that's all that + // GetArchiveFileExtension() can return. It's a lot faster than the more + // generally-correct approach of testing file path equivalence when + // there are a lot of entries in DataPath(). + if (it->is_regular_file() && + boost::iends_with(it->path().u8string(), archiveFileExtension)) { + archivePaths.push_back(it->path()); + } + } + + return archivePaths; +} +} + namespace loot { Game::Game(const GameType gameType, const std::filesystem::path& gamePath, @@ -59,7 +138,10 @@ Game::Game(const GameType gameType, loadOrderHandler_(type_, gamePath_, localDataPath), conditionEvaluator_( std::make_shared(Type(), DataPath())), - database_(ApiDatabase(conditionEvaluator_)) {} + database_(ApiDatabase(conditionEvaluator_)), + additionalDataPaths_(GetAdditionalDataPaths(Type(), DataPath())) { + conditionEvaluator_->SetAdditionalDataPaths(additionalDataPaths_); +} GameType Game::Type() const { return type_; } @@ -85,29 +167,51 @@ const DatabaseInterface& Game::GetDatabase() const { return database_; } DatabaseInterface& Game::GetDatabase() { return database_; } -bool Game::IsValidPlugin(const std::string& plugin) const { - return Plugin::IsValid(Type(), DataPath() / u8path(plugin)); +void Game::SetAdditionalDataPaths( + const std::vector& additionalDataPaths) { + additionalDataPaths_ = additionalDataPaths; + + conditionEvaluator_->SetAdditionalDataPaths(additionalDataPaths_); + conditionEvaluator_->ClearConditionCache(); + loadOrderHandler_.SetAdditionalDataPaths(additionalDataPaths_); } -void Game::LoadPlugins(const std::vector& plugins, +bool Game::IsValidPlugin(const std::string& pluginPath) const { + return Plugin::IsValid(Type(), + ResolvePluginPath(DataPath(), u8path(pluginPath))); +} + +void Game::LoadPlugins(const std::vector& pluginPaths, bool loadHeadersOnly) { const auto logger = getLogger(); - // First validate the plugins (the validity check is done in parallel because + // Check that all plugin filenames are unique. + std::unordered_set filenames; + for (const auto& pluginPath : pluginPaths) { + const auto filename = + NormalizeFilename(u8path(pluginPath).filename().u8string()); + const auto inserted = filenames.insert(filename).second; + if (!inserted) { + throw std::invalid_argument("The filename \"" + filename + + "\" is not unique."); + } + } + + // Validate the plugins (the validity check is done in parallel because // it's relatively slow). const auto invalidPluginIt = std::find_if(std::execution::par_unseq, - plugins.cbegin(), - plugins.cend(), - [this](const std::string& pluginName) { + pluginPaths.cbegin(), + pluginPaths.cend(), + [this](const std::string& pluginPath) { try { - return !IsValidPlugin(pluginName); + return !IsValidPlugin(pluginPath); } catch (...) { return true; } }); - if (invalidPluginIt != plugins.end()) { + if (invalidPluginIt != pluginPaths.end()) { throw std::invalid_argument("\"" + *invalidPluginIt + "\" is not a valid plugin"); } @@ -126,16 +230,17 @@ void Game::LoadPlugins(const std::vector& plugins, const auto masterPath = DataPath() / u8path(masterFilename_); std::for_each( std::execution::par_unseq, - plugins.begin(), - plugins.end(), - [&](const std::string& pluginName) { + pluginPaths.begin(), + pluginPaths.end(), + [&](const std::string& pluginPathString) { try { const auto endIt = - boost::iends_with(pluginName, GHOST_FILE_EXTENSION) - ? pluginName.end() - GHOST_FILE_EXTENSION_LENGTH - : pluginName.end(); + boost::iends_with(pluginPathString, GHOST_FILE_EXTENSION) + ? pluginPathString.end() - GHOST_FILE_EXTENSION_LENGTH + : pluginPathString.end(); - auto pluginPath = DataPath() / u8path(pluginName.begin(), endIt); + const auto pluginPath = ResolvePluginPath( + DataPath(), u8path(pluginPathString.begin(), endIt)); const bool loadHeader = loadHeadersOnly || loot::equivalent(pluginPath, masterPath); @@ -144,7 +249,7 @@ void Game::LoadPlugins(const std::vector& plugins, if (logger) { logger->error( "Caught exception while trying to add {} to the cache: {}", - pluginName, + pluginPathString, e.what()); } } @@ -171,11 +276,17 @@ void Game::IdentifyMainMasterFile(const std::string& masterFile) { } std::vector Game::SortPlugins( - const std::vector& plugins) { - LoadPlugins(plugins, false); + const std::vector& pluginPaths) { + LoadPlugins(pluginPaths, false); + + std::vector loadOrder; + for (const auto& pluginPath : pluginPaths) { + const auto filename = u8path(pluginPath).filename().u8string(); + loadOrder.push_back(filename); + } // Sort plugins into their load order. - return loot::SortPlugins(*this, plugins); + return loot::SortPlugins(*this, loadOrder); } void Game::LoadCurrentLoadOrderState() { @@ -208,19 +319,14 @@ void Game::CacheArchives() { const auto archiveFileExtension = GetArchiveFileExtension(Type()); std::set archivePaths; - for (std::filesystem::directory_iterator it(DataPath()); - it != std::filesystem::directory_iterator(); - ++it) { - // This is only correct for ASCII strings, but that's all that - // GetArchiveFileExtension() can return. It's a lot faster than the more - // generally-correct approach of testing file path equivalence when - // there are a lot of entries in DataPath(). - if (it->is_regular_file() && - boost::iends_with(it->path().u8string(), archiveFileExtension)) { - archivePaths.insert(it->path()); - } + for (const auto& parentPath : additionalDataPaths_) { + const auto archives = FindArchives(parentPath, archiveFileExtension); + archivePaths.insert(archives.begin(), archives.end()); } + const auto archives = FindArchives(DataPath(), archiveFileExtension); + archivePaths.insert(archives.begin(), archives.end()); + cache_.CacheArchivePaths(std::move(archivePaths)); } } diff --git a/src/api/game/game.h b/src/api/game/game.h index 5ec022cb..e8730ce8 100644 --- a/src/api/game/game.h +++ b/src/api/game/game.h @@ -55,14 +55,17 @@ public: const DatabaseInterface& GetDatabase() const; + void SetAdditionalDataPaths( + const std::vector& additionalDataPaths); + // Game Interface Methods // //////////////////////////// DatabaseInterface& GetDatabase() override; - bool IsValidPlugin(const std::string& plugin) const override; + bool IsValidPlugin(const std::string& pluginPath) const override; - void LoadPlugins(const std::vector& plugins, + void LoadPlugins(const std::vector& pluginPaths, bool loadHeadersOnly) override; const PluginInterface* GetPlugin( @@ -73,7 +76,7 @@ public: void IdentifyMainMasterFile(const std::string& masterFile) override; std::vector SortPlugins( - const std::vector& plugins) override; + const std::vector& pluginPaths) override; void LoadCurrentLoadOrderState() override; @@ -99,6 +102,8 @@ private: ApiDatabase database_; std::string masterFilename_; + + std::vector additionalDataPaths_; }; } #endif diff --git a/src/api/game/load_order_handler.cpp b/src/api/game/load_order_handler.cpp index 676cb734..953fee1b 100644 --- a/src/api/game/load_order_handler.cpp +++ b/src/api/game/load_order_handler.cpp @@ -232,6 +232,33 @@ void LoadOrderHandler::SetLoadOrder( } } +void LoadOrderHandler::SetAdditionalDataPaths( + const std::vector& dataPaths) const { + auto logger = getLogger(); + if (logger) { + logger->debug("Setting additional data paths:"); + for (const auto& dataPath : dataPaths) { + logger->debug("\t{}", dataPath.u8string()); + } + } + + std::vector dataPathStrings; + std::vector dataPathCStrings; + for (const auto& dataPath : dataPaths) { + dataPathStrings.push_back(dataPath.u8string()); + dataPathCStrings.push_back(dataPathStrings.back().c_str()); + } + + const unsigned int ret = lo_set_additional_plugins_directories( + gh_.get(), dataPathCStrings.data(), dataPathCStrings.size()); + + HandleError("set additional data paths", ret); + + if (logger) { + logger->debug("Additional data paths set successfully."); + } +} + void LoadOrderHandler::HandleError(const std::string& operation, unsigned int returnCode) const { if (returnCode == LIBLO_OK || returnCode == LIBLO_WARN_LO_MISMATCH) { diff --git a/src/api/game/load_order_handler.h b/src/api/game/load_order_handler.h index 32a08d85..225b1c77 100644 --- a/src/api/game/load_order_handler.h +++ b/src/api/game/load_order_handler.h @@ -55,6 +55,9 @@ public: void SetLoadOrder(const std::vector& loadOrder) const; + void SetAdditionalDataPaths( + const std::vector& dataPaths) const; + private: void HandleError(const std::string& operation, unsigned int returnCode) const; diff --git a/src/api/metadata/condition_evaluator.cpp b/src/api/metadata/condition_evaluator.cpp index 03233537..6feeb360 100644 --- a/src/api/metadata/condition_evaluator.cpp +++ b/src/api/metadata/condition_evaluator.cpp @@ -90,7 +90,8 @@ ConditionEvaluator::ConditionEvaluator(const GameType gameType, // This probably isn't correct for API users other than LOOT. // But that probably doesn't matter, as the only things conditional // on LOOT's version are LOOT-specific messages. - auto lootPath = std::filesystem::absolute("LOOT.exe"); + const auto lootPath = std::filesystem::absolute("LOOT.exe"); + int result = lci_state_create(&state, mapGameType(gameType), dataPath.u8string().c_str(), @@ -242,6 +243,20 @@ void ConditionEvaluator::RefreshLoadedPluginsState( HandleError("fill CRC cache for condition evaluation", result); } +void ConditionEvaluator::SetAdditionalDataPaths( + const std::vector& dataPaths) { + std::vector dataPathStrings; + std::vector dataPathCStrings; + for (const auto& dataPath : dataPaths) { + dataPathStrings.push_back(dataPath.u8string()); + dataPathCStrings.push_back(dataPathStrings.back().c_str()); + } + + int result = lci_state_set_additional_data_paths( + lciState_.get(), dataPathCStrings.data(), dataPathCStrings.size()); + HandleError("create state object for condition evaluation", result); +} + bool ConditionEvaluator::Evaluate(const PluginCleaningData& cleaningData, const std::string& pluginName) { if (pluginName.empty()) diff --git a/src/api/metadata/condition_evaluator.h b/src/api/metadata/condition_evaluator.h index 70ecbf2b..ad1c33cb 100644 --- a/src/api/metadata/condition_evaluator.h +++ b/src/api/metadata/condition_evaluator.h @@ -51,6 +51,9 @@ public: void RefreshLoadedPluginsState( const std::vector& plugins); + void SetAdditionalDataPaths( + const std::vector& dataPaths); + private: bool Evaluate(const PluginCleaningData& cleaningData, const std::string& pluginName); diff --git a/src/tests/api/internals/game/game_test.h b/src/tests/api/internals/game/game_test.h index 7697887e..b6f976df 100644 --- a/src/tests/api/internals/game/game_test.h +++ b/src/tests/api/internals/game/game_test.h @@ -95,6 +95,72 @@ TEST_P(GameTest, constructingShouldNotThrowIfGameAndLocalPathsAreNotEmpty) { EXPECT_NO_THROW(Game(GetParam(), dataPath.parent_path(), localPath)); } +TEST_P( + GameTest, + constructingForAMicrosoftStoreFallout4InstallShouldSetExternalPathsForTheDlcs) { + if (GetParam() != GameType::fo4) { + return; + } + + const auto touch = [](const std::filesystem::path& path) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream out(path); + out.close(); + }; + + // Create the file that indicates it's a Microsoft Store install. + touch(dataPath.parent_path() / "appxmanifest.xml"); + + // Create a few external files. + const auto pluginPath = + dataPath.parent_path() / + "../../Fallout 4- Automatron (PC)/Content/Data/DLCRobot.esm"; + const auto ba2Path1 = + dataPath.parent_path() / + "../../Fallout 4- Far Harbor (PC)/Content/Data/DLCCoast - Main.ba2"; + const auto ba2Path2 = + dataPath.parent_path() / + "../../Fallout 4- Nuka-World (PC)/Content/Data/DLCNukaWorld " + "- Voices_it.ba2"; + touch(pluginPath); + touch(ba2Path1); + touch(ba2Path2); + + Game game = Game(GetParam(), dataPath.parent_path(), localPath); + + EXPECT_NO_THROW(loadInstalledPlugins(game, true)); + + const auto archivePaths = game.GetCache().GetArchivePaths(); + + EXPECT_EQ(std::set( + {ba2Path1, ba2Path2, dataPath / blankArchive}), + archivePaths); + + PluginMetadata metadata(blankEsm); + metadata.SetLoadAfterFiles( + {File("DLCRobot.esm", "", "file(\"DLCRobot.esm\")")}); + game.GetDatabase().SetPluginUserMetadata(metadata); + + const auto evaluatedMetadata = + game.GetDatabase().GetPluginUserMetadata(blankEsm, true).value(); + EXPECT_FALSE(evaluatedMetadata.GetLoadAfterFiles().empty()); +} + +TEST_P(GameTest, isValidPluginShouldResolveRelativePathsRelativeToDataPath) { + const Game game(GetParam(), dataPath.parent_path(), localPath); + + game.IsValidPlugin("../" + dataPath.filename().u8string() + "/" + blankEsm); +} + +TEST_P(GameTest, isValidPluginShouldUseAbsolutePathsAsGiven) { + const Game game(GetParam(), dataPath.parent_path(), localPath); + + ASSERT_TRUE(dataPath.is_absolute()); + + const auto path = dataPath / std::filesystem::u8path(blankEsm); + game.IsValidPlugin(path.u8string()); +} + TEST_P( GameTest, loadPluginsWithHeadersOnlyTrueShouldLoadTheHeadersOfAllInstalledPlugins) { @@ -164,6 +230,43 @@ TEST_P( EXPECT_EQ(expected, game.GetCache().GetArchivePaths()); } +TEST_P(GameTest, loadPluginsShouldFindArchivesInExternalDataPaths) { + const auto touch = [](const std::filesystem::path& path) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream out(path); + out.close(); + }; + + // Create a couple of external archive files. + const std::string archiveFileExtension = + GetParam() == GameType::fo4 || GetParam() == GameType::fo4vr ? ".ba2" + : ".bsa"; + + const auto ba2Path1 = + dataPath.parent_path() / + ("../../Fallout 4- Far Harbor (PC)/Content/Data/DLCCoast - Main" + + archiveFileExtension); + const auto ba2Path2 = + dataPath.parent_path() / + ("../../Fallout 4- Nuka-World (PC)/Content/Data/DLCNukaWorld " + "- Voices_it" + + archiveFileExtension); + touch(ba2Path1); + touch(ba2Path2); + + Game game = Game(GetParam(), dataPath.parent_path(), localPath); + + game.SetAdditionalDataPaths({ba2Path1.parent_path(), ba2Path2.parent_path()}); + + EXPECT_NO_THROW(loadInstalledPlugins(game, true)); + + const auto archivePaths = game.GetCache().GetArchivePaths(); + + EXPECT_EQ(std::set( + {ba2Path1, ba2Path2, dataPath / blankArchive}), + archivePaths); +} + TEST_P(GameTest, loadPluginsShouldClearTheArchivesCacheBeforeFindingArchives) { Game game = Game(GetParam(), dataPath.parent_path(), localPath); @@ -186,6 +289,51 @@ TEST_P( EXPECT_NO_THROW(loadInstalledPlugins(game, false)); } +TEST_P(GameTest, + loadPluginsShouldThrowIfGivenVectorElementsWithTheSameFilename) { + Game game = Game(GetParam(), dataPath.parent_path(), localPath); + + const auto dataPluginPath = dataPath / std::filesystem::u8path(blankEsm); + const auto sourcePluginPath = + getSourcePluginsPath() / std::filesystem::u8path(blankEsm); + + EXPECT_THROW( + game.LoadPlugins({dataPluginPath.u8string(), sourcePluginPath.u8string()}, + true), + std::invalid_argument); +} + +TEST_P(GameTest, loadPluginsShouldResolveRelativePathsRelativeToDataPath) { + Game game = Game(GetParam(), dataPath.parent_path(), localPath); + + const auto relativePath = + "../" + dataPath.filename().u8string() + "/" + blankEsm; + + game.LoadPlugins({relativePath}, true); + + EXPECT_NE(nullptr, game.GetPlugin(blankEsm)); +} + +TEST_P(GameTest, loadPluginsShouldUseAbsolutePathsAsGiven) { + Game game = Game(GetParam(), dataPath.parent_path(), localPath); + + const auto absolutePath = dataPath / std::filesystem::u8path(blankEsm); + + game.LoadPlugins({absolutePath.u8string()}, true); + + EXPECT_NE(nullptr, game.GetPlugin(blankEsm)); +} + +TEST_P(GameTest, sortPluginsShouldHandlePluginPathsThatAreNotJustFilenames) { + Game game = Game(GetParam(), dataPath.parent_path(), localPath); + + const auto absolutePath = dataPath / std::filesystem::u8path(blankEsm); + + const auto newLoadOrder = game.SortPlugins({absolutePath.u8string()}); + + EXPECT_EQ(std::vector{blankEsm}, newLoadOrder); +} + TEST_P(GameTest, shouldShowBlankEsmAsActiveIfItHasNotBeenLoaded) { Game game = Game(GetParam(), dataPath.parent_path(), localPath); game.LoadCurrentLoadOrderState(); diff --git a/src/tests/api/internals/game/load_order_handler_test.h b/src/tests/api/internals/game/load_order_handler_test.h index 706df1bb..baca7bdb 100644 --- a/src/tests/api/internals/game/load_order_handler_test.h +++ b/src/tests/api/internals/game/load_order_handler_test.h @@ -214,6 +214,18 @@ TEST_P(LoadOrderHandlerTest, setLoadOrderShouldSetTheLoadOrder) { EXPECT_EQ(loadOrderToSet_, getLoadOrder()); } + +TEST_P(LoadOrderHandlerTest, setExternalPluginPathsShouldAcceptAnEmptyVector) { + auto loadOrderHandler = createHandler(); + EXPECT_NO_THROW(loadOrderHandler.SetAdditionalDataPaths({})); +} + +TEST_P(LoadOrderHandlerTest, + setExternalPluginPathsShouldAcceptANonEmptyVector) { + auto loadOrderHandler = createHandler(); + EXPECT_NO_THROW(loadOrderHandler.SetAdditionalDataPaths( + {std::filesystem::u8path("a"), std::filesystem::u8path("b")})); +} } } diff --git a/src/tests/api/internals/metadata/condition_evaluator_test.h b/src/tests/api/internals/metadata/condition_evaluator_test.h index cdeaf18b..8179f8d4 100644 --- a/src/tests/api/internals/metadata/condition_evaluator_test.h +++ b/src/tests/api/internals/metadata/condition_evaluator_test.h @@ -122,6 +122,17 @@ TEST_P(ConditionEvaluatorTest, EXPECT_TRUE(evaluator_.Evaluate("file(\"" + blankEsm + "\")")); } +TEST_P(ConditionEvaluatorTest, evaluateShouldUseAllGivenDataPaths) { + ASSERT_FALSE( + evaluator_.Evaluate("file(\"" + localPath.filename().u8string() + "\")")); + + evaluator_.ClearConditionCache(); + evaluator_.SetAdditionalDataPaths({localPath.parent_path()}); + + EXPECT_TRUE( + evaluator_.Evaluate("file(\"" + localPath.filename().u8string() + "\")")); +} + TEST_P(ConditionEvaluatorTest, evaluateFileConditionShouldReturnTrueForANonAsciiFileThatExists) { EXPECT_TRUE(evaluator_.Evaluate("file(\"" + nonAsciiEsm + "\")")); @@ -256,6 +267,17 @@ TEST_P( EXPECT_FALSE(evaluator_.Evaluate(condition)); } + +TEST_P(ConditionEvaluatorTest, + setAdditionalDataPathsShouldAcceptAnEmptyVector) { + EXPECT_NO_THROW(evaluator_.SetAdditionalDataPaths({})); +} + +TEST_P(ConditionEvaluatorTest, + setAdditionalDataPathsShouldAcceptANonEmptyVector) { + EXPECT_NO_THROW(evaluator_.SetAdditionalDataPaths( + {std::filesystem::u8path("a"), std::filesystem::u8path("b")})); +} } } diff --git a/src/tests/common_game_test_fixture.h b/src/tests/common_game_test_fixture.h index a94ff1d7..22c793c1 100644 --- a/src/tests/common_game_test_fixture.h +++ b/src/tests/common_game_test_fixture.h @@ -57,7 +57,7 @@ protected: french("fr"), german("de"), missingPath(rootTestPath / "missing"), - dataPath(rootTestPath / "game" / getPluginsFolder()), + dataPath(rootTestPath / "games" / "game" / getPluginsFolder()), localPath(rootTestPath / "local" / "game"), metadataFilesPath(rootTestPath / "metadata"), masterFile(getMasterFile()),