mirror of
https://github.com/loot/libloot.git
synced 2026-07-27 14:16:01 -07:00
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:
+4
-4
@@ -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} &&
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user