Add support for MS Store's Fallout 4 DLC

The Microsoft Store installs Fallout 4 DLC to separate directories
outside of the game's install path. The relative paths are fixed: users
cannot customise the install paths (aside from the folder within which
all games are installed on a drive), and renaming the DLC's path or its
Content directory causes an error when trying to launch the game.

The game scans these additional data paths similarly to how it scans
the main data path: it doesn't only load the DLC plugins from their
data paths, it will find non-DLC plugins in them, and DLC plugins can be
moved between them and still be detected. The same is true for BA2
files and other resources.

To support these separate directories:

- GameInterface::IsValidPlugin, GameInterface::LoadPlugins
  and GameInterface::SortPlugins() now take plugin paths instead of
  plugin filenames. Relative paths are interpreted as relative to the
  data path, so the change is backwards-compatible, and absolute paths
  are used as given.
- LoadPlugins now checks that all filenames in the given paths are
  unique, which was previously required but not enforced.
- When scanning for archives, LOOT now scans the DLC data paths before
  scanning the game data path.
- libloadorder and loot-condition-interpreter have been updated to
  support the MS Store Fallout 4 DLC paths. libloadorder has built-in
  support for the Fallout 4 DLCs specifically, so does not need to be
  initialised, while loot-condition-interpreter's support is not
  specific and so it's now passed a list of data paths that includes the
  Fallout 4 DLC paths when appropriate.

When listing the DLC paths together with the game's main data path, the
order they're listed in matches the order in which the game scans them
for plugins, with the game stopping at the first directory in which it
finds a file it's looking for.

This introduces detection of whether or not a game install comes from
the Microsoft Store: like LOOT and libloadorder, libloot detects this by
looking for an appxmanifest.xml file in the install path.

The new Game::SetAdditionalDataPaths() method may be added to
GameInterface in the future: that hasn't been done yet because adding it
will break the ABI.
This commit is contained in:
Oliver Hamlet
2023-05-06 21:16:49 +01:00
parent d9a587065d
commit d7f5a518e1
12 changed files with 397 additions and 50 deletions
+4 -4
View File
@@ -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} &&
+15 -9
View File
@@ -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<std::string>& plugins,
virtual void LoadPlugins(const std::vector<std::string>& 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<std::string> SortPlugins(
const std::vector<std::string>& plugins) = 0;
const std::vector<std::string>& pluginPaths) = 0;
/**
* @}
+138 -32
View File
@@ -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<std::filesystem::path> 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<std::filesystem::path> FindArchives(
const std::filesystem::path& parentPath,
const std::string& archiveFileExtension) {
if (!std::filesystem::is_directory(parentPath)) {
return {};
}
std::vector<std::filesystem::path> 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<ConditionEvaluator>(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<std::filesystem::path>& additionalDataPaths) {
additionalDataPaths_ = additionalDataPaths;
conditionEvaluator_->SetAdditionalDataPaths(additionalDataPaths_);
conditionEvaluator_->ClearConditionCache();
loadOrderHandler_.SetAdditionalDataPaths(additionalDataPaths_);
}
void Game::LoadPlugins(const std::vector<std::string>& plugins,
bool Game::IsValidPlugin(const std::string& pluginPath) const {
return Plugin::IsValid(Type(),
ResolvePluginPath(DataPath(), u8path(pluginPath)));
}
void Game::LoadPlugins(const std::vector<std::string>& 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<std::string> 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<std::string>& 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<std::string>& 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<std::string> Game::SortPlugins(
const std::vector<std::string>& plugins) {
LoadPlugins(plugins, false);
const std::vector<std::string>& pluginPaths) {
LoadPlugins(pluginPaths, false);
std::vector<std::string> 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<std::filesystem::path> 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));
}
}
+8 -3
View File
@@ -55,14 +55,17 @@ public:
const DatabaseInterface& GetDatabase() const;
void SetAdditionalDataPaths(
const std::vector<std::filesystem::path>& 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<std::string>& plugins,
void LoadPlugins(const std::vector<std::string>& pluginPaths,
bool loadHeadersOnly) override;
const PluginInterface* GetPlugin(
@@ -73,7 +76,7 @@ public:
void IdentifyMainMasterFile(const std::string& masterFile) override;
std::vector<std::string> SortPlugins(
const std::vector<std::string>& plugins) override;
const std::vector<std::string>& pluginPaths) override;
void LoadCurrentLoadOrderState() override;
@@ -99,6 +102,8 @@ private:
ApiDatabase database_;
std::string masterFilename_;
std::vector<std::filesystem::path> additionalDataPaths_;
};
}
#endif
+27
View File
@@ -232,6 +232,33 @@ void LoadOrderHandler::SetLoadOrder(
}
}
void LoadOrderHandler::SetAdditionalDataPaths(
const std::vector<std::filesystem::path>& 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<std::string> dataPathStrings;
std::vector<const char*> 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) {
+3
View File
@@ -55,6 +55,9 @@ public:
void SetLoadOrder(const std::vector<std::string>& loadOrder) const;
void SetAdditionalDataPaths(
const std::vector<std::filesystem::path>& dataPaths) const;
private:
void HandleError(const std::string& operation, unsigned int returnCode) const;
+16 -1
View File
@@ -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<std::filesystem::path>& dataPaths) {
std::vector<std::string> dataPathStrings;
std::vector<const char*> 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())
+3
View File
@@ -51,6 +51,9 @@ public:
void RefreshLoadedPluginsState(
const std::vector<const PluginInterface*>& plugins);
void SetAdditionalDataPaths(
const std::vector<std::filesystem::path>& dataPaths);
private:
bool Evaluate(const PluginCleaningData& cleaningData,
const std::string& pluginName);
+148
View File
@@ -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<std::filesystem::path>(
{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<std::filesystem::path>(
{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<std::string>{blankEsm}, newLoadOrder);
}
TEST_P(GameTest, shouldShowBlankEsmAsActiveIfItHasNotBeenLoaded) {
Game game = Game(GetParam(), dataPath.parent_path(), localPath);
game.LoadCurrentLoadOrderState();
@@ -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")}));
}
}
}
@@ -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")}));
}
}
}
+1 -1
View File
@@ -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()),