mirror of
https://github.com/loot/libloot.git
synced 2026-07-27 14:16:01 -07:00
Implement support for OpenMW
Most of the complexity is handled by libloadorder, but it's worth noting that: - The game path is OpenMW's install path, not Morrowind's - OpenMW doesn't force master-flagged plugins to load before others - OpenMW doesn't provide a way to record the load order of inactive plugins - .omwgame and .omwaddon plugins are equivalent to .esm and .esp respectively, while .omwscripts plugins have a completely different format with none of the metadata that libloot uses. - OpenMW effectively relies on additional data paths to load Morrowind's (and mods') files, and the last directory listed that contains a given filename is used to load a file with that filename, with the main data path effectively being the first listed. - I've disabled support for ghosted plugins for OpenMW because it makes the multi-path stuff more confusing and may not provide any benefit.
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
LOOT is a plugin load order optimisation tool for Starfield, TES III: Morrowind, TES IV: Oblivion, TES V: Skyrim, TES V: Skyrim Special Edition, Fallout 3, Fallout: New Vegas, Fallout 4 and Fallout 4 VR. It is designed to assist mod users in avoiding detrimental conflicts, by automatically calculating a load order that satisfies all plugin dependencies and maximises each plugin's impact on the user's game.
|
||||
LOOT is a plugin load order optimisation tool for Starfield, TES III: Morrowind, TES IV: Oblivion, TES V: Skyrim, TES V: Skyrim Special Edition, TES V: Skyrim VR, Fallout 3, Fallout: New Vegas, Fallout 4, Fallout 4 VR and OpenMW. It is designed to assist mod users in avoiding detrimental conflicts, by automatically calculating a load order that satisfies all plugin dependencies and maximises each plugin's impact on the user's game.
|
||||
|
||||
LOOT also provides some load order error checking, including checks for requirements, incompatibilities and cyclic dependencies. In addition, it provides a large number of plugin-specific usage notes, bug warnings and Bash Tag suggestions.
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ every time, so that it gives consistent results.
|
||||
After that, three graphs are created, and the plugins are added to them as
|
||||
vertices in their sorted order:
|
||||
|
||||
- One graph contains plugins that are not masters (e.g. they don't have their
|
||||
master flag set - for some games they must also not have the ``.esm`` file
|
||||
extension).
|
||||
- One graph contains plugins that are not masters (meaning master plugins in
|
||||
their own right, not that they are listed as a master within another plugin's
|
||||
header).
|
||||
- One graph contains plugins that are blueprint masters: blueprint masters are
|
||||
a type of plugin specific to Starfield, so for all other games this graph will
|
||||
be empty.
|
||||
@@ -49,7 +49,7 @@ than to enforce those relationships within a single graph.
|
||||
|
||||
A consequence of using three separate graphs is that any plugin data or metadata
|
||||
that involves a pair of plugins that go in different graphs will be silently
|
||||
ignored. For example: if plugin A is master-flagged and plugin B is not, and
|
||||
ignored. For example: if plugin A is a master and plugin B is not, and
|
||||
plugin A has metadata saying it must load after plugin B, then that metadata
|
||||
will be ignored because the two plugins are sorted independently, as if the
|
||||
other plugin is not installed.
|
||||
@@ -224,8 +224,8 @@ Combine the load orders
|
||||
|
||||
Finally, the sorted load orders are combined in this order:
|
||||
|
||||
1. master-flagged plugins
|
||||
2. non-master-flagged plugins
|
||||
1. master plugins
|
||||
2. non-master plugins
|
||||
3. blueprint master plugins
|
||||
|
||||
That gives the complete sorted load order.
|
||||
|
||||
@@ -106,7 +106,7 @@ There are several conditions that can be tested for using the functions detailed
|
||||
|
||||
.. describe:: is_master(file_path path)
|
||||
|
||||
Returns true if ``path`` is an installed master plugin, and false otherwise.
|
||||
Returns true if ``path`` is an installed master plugin, and false otherwise. This returns false for all OpenMW plugins, as OpenMW does not force master plugins to load before others.
|
||||
|
||||
.. describe:: checksum(file_path path, checksum expected_checksum)
|
||||
|
||||
|
||||
+7
-5
@@ -98,11 +98,13 @@ LOOT_API bool IsCompatible(const unsigned int major,
|
||||
* The relative or absolute path to the directory containing the
|
||||
* game's executable.
|
||||
* @param game_local_path
|
||||
* The relative or absolute path to the game's folder in
|
||||
* `%%LOCALAPPDATA%` or an empty path. If an empty path, the API will
|
||||
* attempt to look up the path that `%%LOCALAPPDATA%` corresponds to.
|
||||
* This parameter is provided so that systems lacking that environmental
|
||||
* variable (eg. Linux) can still use the API.
|
||||
* The relative or absolute path to the game's local data folder, or an
|
||||
* empty path. The local data folder is usually in `%%LOCALAPPDATA%`, but
|
||||
* Morrowind has no local data folder and OpenMW's is in the user's
|
||||
* My Games folder on Windows and in `$HOME/.config` on Linux. If an
|
||||
* empty path is provided, the API will attempt to look up the relevant
|
||||
* local data path, which may fail in some situations (e.g. when running
|
||||
* libloot natively on Linux for a game other than Morrowind or OpenMW).
|
||||
* @returns The new game handle.
|
||||
*/
|
||||
LOOT_API std::unique_ptr<GameInterface> CreateGameHandle(
|
||||
|
||||
@@ -50,7 +50,9 @@ enum struct GameType : unsigned int {
|
||||
/** The Elder Scrolls III: Morrowind */
|
||||
tes3,
|
||||
/** Starfield */
|
||||
starfield
|
||||
starfield,
|
||||
/** OpenMW */
|
||||
openmw
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,11 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Gets the currently-set additional data paths.
|
||||
* @details Only Fallout 4 installed from the Microsoft Store is configured
|
||||
* with any additional data paths by default, as its DLC directories
|
||||
* are installed outside of the Fallout 4 install path.
|
||||
* @details The following games are configured with additional data paths by
|
||||
* default:
|
||||
* - Fallout 4, when installed from the Microsoft Store
|
||||
* - Starfield
|
||||
* - OpenMW
|
||||
*/
|
||||
virtual std::vector<std::filesystem::path> GetAdditionalDataPaths() const = 0;
|
||||
|
||||
@@ -52,8 +54,9 @@ public:
|
||||
* @details The additional data paths are used when interacting with the load
|
||||
* order, evaluating conditions and scanning for archives (BSA/BA2
|
||||
* depending on the game). Additional data paths are used in the
|
||||
* order they are given, and take precedence over the game's main
|
||||
* data path.
|
||||
* order they are given (except with OpenMW, which checks them in
|
||||
* reverse order), and take precedence over the game's main data
|
||||
* path.
|
||||
*/
|
||||
virtual void SetAdditionalDataPaths(
|
||||
const std::vector<std::filesystem::path>& additionalDataPaths) = 0;
|
||||
@@ -87,9 +90,9 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Check if a file is a valid plugin.
|
||||
* @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.
|
||||
* @details The validity check is not exhaustive: it generally checks that the
|
||||
* file is a valid plugin file extension for the game and that its
|
||||
* header (if applicable) can be parsed.
|
||||
* @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
|
||||
@@ -107,10 +110,9 @@ public:
|
||||
* 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()``.
|
||||
* If true, only the plugins' 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::filesystem::path>& pluginPaths,
|
||||
@@ -219,6 +221,9 @@ public:
|
||||
|
||||
/**
|
||||
* @brief Set the game's load order.
|
||||
* @details There is no way to persist the load order of inactive OpenMW
|
||||
* plugins, so setting an OpenMW load order will have no effect if
|
||||
* the relative order of active plugins is unchanged.
|
||||
* @param loadOrder
|
||||
* A vector of plugin filenames sorted in the load order to set.
|
||||
*/
|
||||
|
||||
@@ -44,7 +44,8 @@ public:
|
||||
/**
|
||||
* Get the plugin's filename.
|
||||
* @return The plugin filename. If the plugin was ghosted when it was loaded,
|
||||
* this filename will be without the .ghost suffix.
|
||||
* this filename will be without the .ghost suffix, unless the game is
|
||||
* OpenMW, in which case ghosted plugins are not supported.
|
||||
*/
|
||||
virtual std::string GetName() const = 0;
|
||||
|
||||
@@ -88,8 +89,19 @@ public:
|
||||
virtual std::optional<uint32_t> GetCRC() const = 0;
|
||||
|
||||
/**
|
||||
* Check if the plugin's master flag is set.
|
||||
* @return True if the master flag is set, false otherwise.
|
||||
* Check if the plugin is a master plugin.
|
||||
*
|
||||
* What causes a plugin to be a master plugin varies by game, but is usually
|
||||
* indicated by the plugin having its master flag set and/or by its file
|
||||
* extension. However, OpenMW uses neither for determining plugins' load order
|
||||
* so all OpenMW plugins are treated as non-masters.
|
||||
*
|
||||
* The term "master" is potentially confusing: a plugin A may not be a *master
|
||||
* plugin*, but may still be a *master of* another plugin by being listed as
|
||||
* such in that plugin's header record. Master plugins are sometimes referred
|
||||
* to as *master files* or simply *masters*, while the other meaning is always
|
||||
* referenced in relation to another plugin.
|
||||
* @return True if the plugin is a master plugin, false otherwise.
|
||||
*/
|
||||
virtual bool IsMaster() const = 0;
|
||||
|
||||
|
||||
@@ -54,6 +54,8 @@ const char* DescribeGameType(GameType gameType) {
|
||||
return "The Elder Scrolls III: Morrowind";
|
||||
case GameType::starfield:
|
||||
return "Starfield";
|
||||
case GameType::openmw:
|
||||
return "OpenMW";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
+50
-34
@@ -89,6 +89,7 @@ bool IsMicrosoftStoreInstall(const GameType gameType,
|
||||
case GameType::tes5:
|
||||
case GameType::tes5vr:
|
||||
case GameType::fo4vr:
|
||||
case GameType::openmw:
|
||||
return false;
|
||||
default:
|
||||
throw std::logic_error("Unrecognised game type");
|
||||
@@ -115,42 +116,20 @@ std::filesystem::path GetUserDocumentsPath(
|
||||
#endif
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> GetAdditionalDataPaths(
|
||||
const GameType gameType,
|
||||
const std::filesystem::path& dataPath,
|
||||
const std::filesystem::path& gameLocalPath) {
|
||||
const auto gamePath = dataPath.parent_path();
|
||||
|
||||
if (gameType == GameType::fo4 &&
|
||||
IsMicrosoftStoreInstall(gameType, gamePath)) {
|
||||
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};
|
||||
}
|
||||
|
||||
if (gameType == GameType::starfield) {
|
||||
return {GetUserDocumentsPath(gameLocalPath) / "My Games" / "Starfield" /
|
||||
"Data"};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path ResolvePluginPath(
|
||||
GameType gameType,
|
||||
const std::filesystem::path& dataPath,
|
||||
const std::filesystem::path& pluginPath) {
|
||||
auto absolutePath =
|
||||
pluginPath.is_absolute() ? pluginPath : dataPath / pluginPath;
|
||||
|
||||
// In case the plugin is ghosted.
|
||||
if (!std::filesystem::exists(absolutePath)) {
|
||||
if (gameType != GameType::openmw && !std::filesystem::exists(absolutePath)) {
|
||||
const auto logger = loot::getLogger();
|
||||
if (logger) {
|
||||
logger->debug("Could not find plugin at {}, adding {} file extension", absolutePath.u8string(), loot::GHOST_FILE_EXTENSION);
|
||||
logger->debug("Could not find plugin at {}, adding {} file extension",
|
||||
absolutePath.u8string(),
|
||||
loot::GHOST_FILE_EXTENSION);
|
||||
}
|
||||
absolutePath += loot::GHOST_FILE_EXTENSION;
|
||||
}
|
||||
@@ -182,6 +161,35 @@ std::vector<std::filesystem::path> FindArchives(
|
||||
|
||||
return archivePaths;
|
||||
}
|
||||
|
||||
std::filesystem::path FindPlugin(
|
||||
GameType gameType,
|
||||
const std::filesystem::path& dataPath,
|
||||
const std::vector<std::filesystem::path>& additionalDataPaths,
|
||||
const std::string& pluginName) {
|
||||
const auto relativePath = std::filesystem::u8path(pluginName);
|
||||
|
||||
const auto finder = [&](const auto& path) {
|
||||
const auto resolvedPath = ResolvePluginPath(gameType, path, relativePath);
|
||||
return std::filesystem::exists(resolvedPath);
|
||||
};
|
||||
|
||||
if (gameType == GameType::openmw) {
|
||||
const auto it = std::find_if(
|
||||
additionalDataPaths.rbegin(), additionalDataPaths.rend(), finder);
|
||||
if (it != additionalDataPaths.rend()) {
|
||||
return *it;
|
||||
}
|
||||
} else {
|
||||
const auto it = std::find_if(
|
||||
additionalDataPaths.begin(), additionalDataPaths.end(), finder);
|
||||
if (it != additionalDataPaths.end()) {
|
||||
return *it;
|
||||
}
|
||||
}
|
||||
|
||||
return dataPath / relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
namespace loot {
|
||||
@@ -193,9 +201,8 @@ Game::Game(const GameType gameType,
|
||||
loadOrderHandler_(type_, gamePath_, localDataPath),
|
||||
conditionEvaluator_(
|
||||
std::make_shared<ConditionEvaluator>(GetType(), DataPath())),
|
||||
database_(ApiDatabase(conditionEvaluator_)),
|
||||
additionalDataPaths_(
|
||||
::GetAdditionalDataPaths(GetType(), DataPath(), localDataPath)) {
|
||||
database_(ApiDatabase(conditionEvaluator_)) {
|
||||
additionalDataPaths_ = loadOrderHandler_.GetAdditionalDataPaths();
|
||||
conditionEvaluator_->SetAdditionalDataPaths(additionalDataPaths_);
|
||||
}
|
||||
|
||||
@@ -204,6 +211,8 @@ GameType Game::GetType() const { return type_; }
|
||||
std::filesystem::path Game::DataPath() const {
|
||||
if (type_ == GameType::tes3) {
|
||||
return gamePath_ / "Data Files";
|
||||
} else if (type_ == GameType::openmw) {
|
||||
return gamePath_ / "resources" / "vfs";
|
||||
} else {
|
||||
return gamePath_ / "Data";
|
||||
}
|
||||
@@ -237,7 +246,8 @@ void Game::SetAdditionalDataPaths(
|
||||
}
|
||||
|
||||
bool Game::IsValidPlugin(const std::filesystem::path& pluginPath) const {
|
||||
return Plugin::IsValid(GetType(), ResolvePluginPath(DataPath(), pluginPath));
|
||||
return Plugin::IsValid(GetType(),
|
||||
ResolvePluginPath(GetType(), DataPath(), pluginPath));
|
||||
}
|
||||
|
||||
void Game::LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
|
||||
@@ -285,7 +295,12 @@ void Game::LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
|
||||
logger->trace("Starting plugin loading.");
|
||||
}
|
||||
|
||||
const auto masterPath = DataPath() / u8path(masterFilename_);
|
||||
const auto masterPath = GetType() == GameType::openmw
|
||||
? FindPlugin(GetType(),
|
||||
DataPath(),
|
||||
GetAdditionalDataPaths(),
|
||||
masterFilename_)
|
||||
: DataPath() / u8path(masterFilename_);
|
||||
std::for_each(
|
||||
std::execution::par_unseq,
|
||||
pluginPaths.begin(),
|
||||
@@ -293,7 +308,7 @@ void Game::LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
|
||||
[&](const std::filesystem::path& pluginPath) {
|
||||
try {
|
||||
const auto resolvedPluginPath =
|
||||
ResolvePluginPath(DataPath(), pluginPath);
|
||||
ResolvePluginPath(GetType(), DataPath(), pluginPath);
|
||||
|
||||
const bool loadHeader =
|
||||
loadHeadersOnly ||
|
||||
@@ -312,7 +327,8 @@ void Game::LoadPlugins(const std::vector<std::filesystem::path>& pluginPaths,
|
||||
});
|
||||
|
||||
if (!loadHeadersOnly &&
|
||||
(GetType() == GameType::tes3 || GetType() == GameType::starfield)) {
|
||||
(GetType() == GameType::tes3 || GetType() == GameType::openmw ||
|
||||
GetType() == GameType::starfield)) {
|
||||
auto plugins = cache_.GetPlugins();
|
||||
const auto pluginsMetadata = Plugin::GetPluginsMetadata(plugins);
|
||||
for (auto& plugin : plugins) {
|
||||
|
||||
@@ -50,6 +50,8 @@ unsigned int mapGameId(GameType gameType) {
|
||||
return LIBLO_GAME_FO4VR;
|
||||
case GameType::starfield:
|
||||
return LIBLO_GAME_STARFIELD;
|
||||
case GameType::openmw:
|
||||
return LIBLO_GAME_OPENMW;
|
||||
default:
|
||||
throw std::logic_error("Unexpected game type");
|
||||
}
|
||||
@@ -208,6 +210,28 @@ std::filesystem::path LoadOrderHandler::GetActivePluginsFilePath() const {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> LoadOrderHandler::GetAdditionalDataPaths() const {
|
||||
const auto logger = getLogger();
|
||||
if (logger) {
|
||||
logger->trace("Getting additional data paths.");
|
||||
}
|
||||
|
||||
char** pathArr = nullptr;
|
||||
size_t pathArrSize = 0;
|
||||
|
||||
const unsigned int ret = lo_get_additional_plugins_directories(gh_.get(), &pathArr, &pathArrSize);
|
||||
|
||||
HandleError("get additional data paths", ret);
|
||||
|
||||
std::vector<std::filesystem::path> loadOrder;
|
||||
for (size_t i = 0; i < pathArrSize; i += 1) {
|
||||
loadOrder.push_back(std::filesystem::u8path(std::string(pathArr[i])));
|
||||
}
|
||||
lo_free_string_array(pathArr, pathArrSize);
|
||||
|
||||
return loadOrder;
|
||||
}
|
||||
|
||||
void LoadOrderHandler::SetLoadOrder(
|
||||
const std::vector<std::string>& loadOrder) const {
|
||||
auto logger = getLogger();
|
||||
|
||||
@@ -51,6 +51,8 @@ public:
|
||||
|
||||
std::filesystem::path GetActivePluginsFilePath() const;
|
||||
|
||||
std::vector<std::filesystem::path> GetAdditionalDataPaths() const;
|
||||
|
||||
bool IsPluginActive(const std::string& pluginName) const;
|
||||
|
||||
void SetLoadOrder(const std::vector<std::string>& loadOrder) const;
|
||||
|
||||
@@ -51,7 +51,8 @@ void HandleError(const std::string operation, int returnCode) {
|
||||
logger->error(err);
|
||||
}
|
||||
|
||||
throw ConditionSyntaxError(returnCode, loot_condition_interpreter_category(), err);
|
||||
throw ConditionSyntaxError(
|
||||
returnCode, loot_condition_interpreter_category(), err);
|
||||
}
|
||||
|
||||
int mapGameType(GameType gameType) {
|
||||
@@ -76,6 +77,8 @@ int mapGameType(GameType gameType) {
|
||||
return LCI_GAME_FALLOUT_4_VR;
|
||||
case GameType::starfield:
|
||||
return LCI_GAME_STARFIELD;
|
||||
case GameType::openmw:
|
||||
return LCI_GAME_OPENMW;
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
"Unrecognised game type encountered while mapping for condition "
|
||||
@@ -90,9 +93,8 @@ ConditionEvaluator::ConditionEvaluator(const GameType gameType,
|
||||
lci_state_destroy)) {
|
||||
lci_state* state = nullptr;
|
||||
|
||||
int result = lci_state_create(&state,
|
||||
mapGameType(gameType),
|
||||
dataPath.u8string().c_str());
|
||||
int result = lci_state_create(
|
||||
&state, mapGameType(gameType), dataPath.u8string().c_str());
|
||||
HandleError("create state object for condition evaluation", result);
|
||||
|
||||
lciState_ = std::unique_ptr<lci_state, decltype(&lci_state_destroy)>(
|
||||
|
||||
+102
-14
@@ -135,6 +135,7 @@ std::vector<std::filesystem::path> FindAssociatedArchives(
|
||||
const std::filesystem::path& pluginPath) {
|
||||
switch (gameType) {
|
||||
case GameType::tes3:
|
||||
case GameType::openmw:
|
||||
return {};
|
||||
case GameType::tes5:
|
||||
// Skyrim (non-SE) plugins can only load BSAs that have exactly the same
|
||||
@@ -229,18 +230,24 @@ Plugin::Plugin(const GameType gameType,
|
||||
const GameCache& gameCache,
|
||||
std::filesystem::path pluginPath,
|
||||
const bool headerOnly) :
|
||||
name_(TrimDotGhostExtension(pluginPath.filename().u8string())),
|
||||
name_(gameType == GameType::openmw
|
||||
? pluginPath.filename().u8string()
|
||||
: TrimDotGhostExtension(pluginPath.filename().u8string())),
|
||||
esPlugin(
|
||||
std::unique_ptr<::Plugin, decltype(&esp_plugin_free)>(nullptr,
|
||||
esp_plugin_free)),
|
||||
ignoreMasterFlag_(gameType == GameType::openmw),
|
||||
isEmpty_(true) {
|
||||
auto logger = getLogger();
|
||||
|
||||
try {
|
||||
Load(pluginPath, gameType, headerOnly);
|
||||
if (gameType != GameType::openmw ||
|
||||
pluginPath.extension() != ".omwscripts") {
|
||||
Load(pluginPath, gameType, headerOnly);
|
||||
|
||||
auto ret = esp_plugin_is_empty(esPlugin.get(), &isEmpty_);
|
||||
HandleEspluginError(ret, "check if \"{}\" is empty", name_);
|
||||
auto ret = esp_plugin_is_empty(esPlugin.get(), &isEmpty_);
|
||||
HandleEspluginError(ret, "check if \"{}\" is empty", name_);
|
||||
}
|
||||
|
||||
archivePaths_ = FindAssociatedArchives(gameType, gameCache, pluginPath);
|
||||
|
||||
@@ -284,6 +291,10 @@ Plugin::Plugin(const GameType gameType,
|
||||
}
|
||||
|
||||
void Plugin::ResolveRecordIds(Vec_PluginMetadata* pluginsMetadata) const {
|
||||
if (esPlugin == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto ret = esp_plugin_resolve_record_ids(esPlugin.get(), pluginsMetadata);
|
||||
HandleEspluginError(ret, "resolve the record IDs of \"{}\"", name_);
|
||||
}
|
||||
@@ -291,6 +302,10 @@ void Plugin::ResolveRecordIds(Vec_PluginMetadata* pluginsMetadata) const {
|
||||
std::string Plugin::GetName() const { return name_; }
|
||||
|
||||
std::optional<float> Plugin::GetHeaderVersion() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
float version = 0.0f;
|
||||
|
||||
const auto ret = esp_plugin_header_version(esPlugin.get(), &version);
|
||||
@@ -308,6 +323,10 @@ std::optional<std::string> Plugin::GetVersion() const {
|
||||
}
|
||||
|
||||
std::vector<std::string> Plugin::GetMasters() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return {};
|
||||
}
|
||||
|
||||
char** masters = nullptr;
|
||||
size_t numMasters = 0;
|
||||
const auto ret = esp_plugin_masters(esPlugin.get(), &masters, &numMasters);
|
||||
@@ -325,6 +344,10 @@ std::vector<Tag> Plugin::GetBashTags() const { return tags_; }
|
||||
std::optional<uint32_t> Plugin::GetCRC() const { return crc_; }
|
||||
|
||||
bool Plugin::IsMaster() const {
|
||||
if (ignoreMasterFlag_ || esPlugin == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isMaster = false;
|
||||
const auto ret = esp_plugin_is_master(esPlugin.get(), &isMaster);
|
||||
HandleEspluginError(ret, "check if \"{}\" is a master", name_);
|
||||
@@ -333,6 +356,10 @@ bool Plugin::IsMaster() const {
|
||||
}
|
||||
|
||||
bool Plugin::IsLightPlugin() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isLightPlugin = false;
|
||||
const auto ret = esp_plugin_is_light_plugin(esPlugin.get(), &isLightPlugin);
|
||||
HandleEspluginError(ret, "check if \"{}\" is a light plugin", name_);
|
||||
@@ -341,6 +368,10 @@ bool Plugin::IsLightPlugin() const {
|
||||
}
|
||||
|
||||
bool Plugin::IsMediumPlugin() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isMediumPlugin = false;
|
||||
const auto ret = esp_plugin_is_medium_plugin(esPlugin.get(), &isMediumPlugin);
|
||||
HandleEspluginError(ret, "check if \"{}\" is a medium plugin", name_);
|
||||
@@ -349,6 +380,10 @@ bool Plugin::IsMediumPlugin() const {
|
||||
}
|
||||
|
||||
bool Plugin::IsUpdatePlugin() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isUpdatePlugin = false;
|
||||
const auto ret = esp_plugin_is_update_plugin(esPlugin.get(), &isUpdatePlugin);
|
||||
HandleEspluginError(ret, "check if \"{}\" is an update plugin", name_);
|
||||
@@ -357,6 +392,10 @@ bool Plugin::IsUpdatePlugin() const {
|
||||
}
|
||||
|
||||
bool Plugin::IsBlueprintPlugin() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isBlueprintPlugin = false;
|
||||
const auto ret =
|
||||
esp_plugin_is_blueprint_plugin(esPlugin.get(), &isBlueprintPlugin);
|
||||
@@ -366,6 +405,10 @@ bool Plugin::IsBlueprintPlugin() const {
|
||||
}
|
||||
|
||||
bool Plugin::IsValidAsLightPlugin() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isValid = false;
|
||||
const auto ret =
|
||||
esp_plugin_is_valid_as_light_plugin(esPlugin.get(), &isValid);
|
||||
@@ -375,6 +418,10 @@ bool Plugin::IsValidAsLightPlugin() const {
|
||||
}
|
||||
|
||||
bool Plugin::IsValidAsMediumPlugin() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isValid = false;
|
||||
const auto ret =
|
||||
esp_plugin_is_valid_as_medium_plugin(esPlugin.get(), &isValid);
|
||||
@@ -385,6 +432,10 @@ bool Plugin::IsValidAsMediumPlugin() const {
|
||||
}
|
||||
|
||||
bool Plugin::IsValidAsUpdatePlugin() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isValid = false;
|
||||
const auto ret =
|
||||
esp_plugin_is_valid_as_update_plugin(esPlugin.get(), &isValid);
|
||||
@@ -399,9 +450,17 @@ bool Plugin::IsEmpty() const { return isEmpty_; }
|
||||
bool Plugin::LoadsArchive() const { return !archivePaths_.empty(); }
|
||||
|
||||
bool Plugin::DoRecordsOverlap(const PluginInterface& plugin) const {
|
||||
if (esPlugin == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
auto& otherPlugin = dynamic_cast<const Plugin&>(plugin);
|
||||
|
||||
if (otherPlugin.esPlugin == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool doPluginsOverlap = false;
|
||||
const auto ret = esp_plugin_do_records_overlap(
|
||||
esPlugin.get(), otherPlugin.esPlugin.get(), &doPluginsOverlap);
|
||||
@@ -424,6 +483,10 @@ bool Plugin::DoRecordsOverlap(const PluginInterface& plugin) const {
|
||||
}
|
||||
|
||||
size_t Plugin::GetOverrideRecordCount() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t overrideRecordCount;
|
||||
const auto ret =
|
||||
esp_plugin_count_override_records(esPlugin.get(), &overrideRecordCount);
|
||||
@@ -433,6 +496,10 @@ size_t Plugin::GetOverrideRecordCount() const {
|
||||
}
|
||||
|
||||
uint32_t Plugin::GetRecordAndGroupCount() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t recordAndGroupCount = 0;
|
||||
const auto ret =
|
||||
esp_plugin_record_and_group_count(esPlugin.get(), &recordAndGroupCount);
|
||||
@@ -515,6 +582,10 @@ void Plugin::Load(const std::filesystem::path& path,
|
||||
}
|
||||
|
||||
std::string Plugin::GetDescription() const {
|
||||
if (esPlugin == nullptr) {
|
||||
return "";
|
||||
}
|
||||
|
||||
char* description = nullptr;
|
||||
const auto ret = esp_plugin_description(esPlugin.get(), &description);
|
||||
HandleEspluginError(ret, "read the description of \"{}\"", name_);
|
||||
@@ -530,7 +601,7 @@ std::string Plugin::GetDescription() const {
|
||||
}
|
||||
|
||||
std::unique_ptr<Vec_PluginMetadata, decltype(&esp_plugins_metadata_free)>
|
||||
Plugin::GetPluginsMetadata(std::vector<const Plugin*> plugins) {
|
||||
Plugin::GetPluginsMetadata(const std::vector<const Plugin*>& plugins) {
|
||||
if (plugins.empty()) {
|
||||
return std::unique_ptr<Vec_PluginMetadata,
|
||||
decltype(&esp_plugins_metadata_free)>(
|
||||
@@ -540,7 +611,10 @@ Plugin::GetPluginsMetadata(std::vector<const Plugin*> plugins) {
|
||||
std::vector<const ::Plugin*> esPlugins;
|
||||
esPlugins.reserve(plugins.size());
|
||||
for (const auto& plugin : plugins) {
|
||||
esPlugins.push_back(plugin->esPlugin.get());
|
||||
const auto esPlugin = plugin->esPlugin.get();
|
||||
if (esPlugin != nullptr) {
|
||||
esPlugins.push_back(plugin->esPlugin.get());
|
||||
}
|
||||
}
|
||||
|
||||
Vec_PluginMetadata* pluginsMetadata = nullptr;
|
||||
@@ -566,6 +640,7 @@ std::string GetArchiveFileExtension(const GameType gameType) {
|
||||
unsigned int Plugin::GetEspluginGameId(GameType gameType) {
|
||||
switch (gameType) {
|
||||
case GameType::tes3:
|
||||
case GameType::openmw:
|
||||
return ESP_GAME_MORROWIND;
|
||||
case GameType::tes4:
|
||||
return ESP_GAME_OBLIVION;
|
||||
@@ -589,18 +664,31 @@ unsigned int Plugin::GetEspluginGameId(GameType gameType) {
|
||||
}
|
||||
|
||||
bool hasPluginFileExtension(std::string filename, GameType gameType) {
|
||||
if (boost::iends_with(filename, GHOST_FILE_EXTENSION)) {
|
||||
if (gameType != GameType::openmw &&
|
||||
boost::iends_with(filename, GHOST_FILE_EXTENSION)) {
|
||||
filename =
|
||||
filename.substr(0, filename.length() - GHOST_FILE_EXTENSION_LENGTH);
|
||||
}
|
||||
|
||||
bool isEspOrEsm = boost::iends_with(filename, ".esp") ||
|
||||
boost::iends_with(filename, ".esm");
|
||||
bool isEsl = (gameType == GameType::fo4 || gameType == GameType::fo4vr ||
|
||||
gameType == GameType::tes5se || gameType == GameType::tes5vr ||
|
||||
gameType == GameType::starfield) &&
|
||||
boost::iends_with(filename, ".esl");
|
||||
if (boost::iends_with(filename, ".esp") ||
|
||||
boost::iends_with(filename, ".esm")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isEspOrEsm || isEsl;
|
||||
if (gameType == GameType::openmw &&
|
||||
(boost::iends_with(filename, ".omwaddon") ||
|
||||
boost::iends_with(filename, ".omwgame") ||
|
||||
boost::iends_with(filename, ".omwscripts"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((gameType == GameType::fo4 || gameType == GameType::fo4vr ||
|
||||
gameType == GameType::tes5se || gameType == GameType::tes5vr ||
|
||||
gameType == GameType::starfield) &&
|
||||
boost::iends_with(filename, ".esl")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -93,8 +93,7 @@ public:
|
||||
|
||||
static std::unique_ptr<Vec_PluginMetadata,
|
||||
decltype(&esp_plugins_metadata_free)>
|
||||
GetPluginsMetadata(
|
||||
std::vector<const Plugin*>);
|
||||
GetPluginsMetadata(const std::vector<const Plugin*>& plugins);
|
||||
|
||||
private:
|
||||
void Load(const std::filesystem::path& path,
|
||||
@@ -106,8 +105,10 @@ private:
|
||||
|
||||
std::string name_;
|
||||
std::unique_ptr<::Plugin, decltype(&esp_plugin_free)> esPlugin;
|
||||
bool isEmpty_; // Does the plugin contain any records other than the TES4
|
||||
// header?
|
||||
bool ignoreMasterFlag_{false};
|
||||
bool isEmpty_{false}; // Does the plugin contain any records other than the
|
||||
// TES4
|
||||
// header?
|
||||
std::optional<std::string> version_; // Obtained from description field.
|
||||
std::optional<uint32_t> crc_;
|
||||
std::vector<Tag> tags_;
|
||||
|
||||
@@ -95,7 +95,8 @@ INSTANTIATE_TEST_SUITE_P(,
|
||||
GameType::fo4vr,
|
||||
GameType::tes5vr,
|
||||
GameType::tes3,
|
||||
GameType::starfield));
|
||||
GameType::starfield,
|
||||
GameType::openmw));
|
||||
|
||||
TEST_P(CreateGameHandleTest,
|
||||
shouldSucceedIfPassedValidParametersWithRelativePaths) {
|
||||
@@ -124,7 +125,7 @@ TEST_P(CreateGameHandleTest, shouldSucceedIfPassedALocalPathThatDoesNotExist) {
|
||||
|
||||
TEST_P(CreateGameHandleTest, shouldThrowIfPassedALocalPathThatIsNotADirectory) {
|
||||
EXPECT_THROW(CreateGameHandle(GetParam(), gamePath, dataPath / blankEsm),
|
||||
std::invalid_argument);
|
||||
std::invalid_argument);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
@@ -83,7 +83,8 @@ INSTANTIATE_TEST_SUITE_P(,
|
||||
GameType::fo4vr,
|
||||
GameType::tes5vr,
|
||||
GameType::tes3,
|
||||
GameType::starfield));
|
||||
GameType::starfield,
|
||||
GameType::openmw));
|
||||
|
||||
TEST_P(GameInterfaceTest, setAdditionalDataPathsShouldDoThat) {
|
||||
const auto paths = std::vector<std::filesystem::path>{
|
||||
@@ -135,7 +136,16 @@ TEST_P(
|
||||
}
|
||||
|
||||
TEST_P(GameInterfaceTest, loadPluginsShouldTrimDotGhostFileExtensions) {
|
||||
handle_->LoadPlugins({blankMasterDependentEsm + ".ghost"}, true);
|
||||
if (GetParam() == GameType::openmw) {
|
||||
// Ghosting is not supported for OpenMW.
|
||||
EXPECT_THROW(
|
||||
handle_->LoadPlugins({blankMasterDependentEsm + ".ghost"}, true),
|
||||
std::invalid_argument);
|
||||
return;
|
||||
} else {
|
||||
handle_->LoadPlugins({blankMasterDependentEsm + ".ghost"}, true);
|
||||
}
|
||||
|
||||
EXPECT_EQ(1, handle_->GetLoadedPlugins().size());
|
||||
|
||||
ASSERT_NO_THROW(handle_->GetPlugin(blankMasterDependentEsm));
|
||||
@@ -245,11 +255,32 @@ TEST_P(GameInterfaceTest, getLoadOrderShouldReturnTheCurrentLoadOrder) {
|
||||
std::filesystem::remove(dataPath / std::filesystem::u8path(nonAsciiEsm));
|
||||
|
||||
// Set no additional data paths to avoid picking up non-test plugins on PCs
|
||||
// which have Starfield or Fallout 4 installed.
|
||||
handle_->SetAdditionalDataPaths({});
|
||||
// which have Starfield or Fallout 4 installed. Don't clear the additional
|
||||
// data paths for OpenMW because they come from test config.
|
||||
if (GetParam() != GameType::openmw) {
|
||||
handle_->SetAdditionalDataPaths({});
|
||||
}
|
||||
|
||||
handle_->LoadCurrentLoadOrderState();
|
||||
ASSERT_EQ(getLoadOrder(), handle_->GetLoadOrder());
|
||||
|
||||
if (GetParam() == GameType::openmw) {
|
||||
ASSERT_EQ(std::vector<std::string>({
|
||||
blankDifferentEsm,
|
||||
blankDifferentMasterDependentEsm,
|
||||
blankDifferentEsp,
|
||||
blankDifferentPluginDependentEsp,
|
||||
blankMasterDependentEsm,
|
||||
blankMasterDependentEsp,
|
||||
blankEsp,
|
||||
blankPluginDependentEsp,
|
||||
masterFile,
|
||||
blankEsm,
|
||||
blankDifferentMasterDependentEsp,
|
||||
}),
|
||||
handle_->GetLoadOrder());
|
||||
} else {
|
||||
ASSERT_EQ(getLoadOrder(), handle_->GetLoadOrder());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(GameInterfaceTest, setLoadOrderShouldSetTheLoadOrder) {
|
||||
@@ -257,8 +288,11 @@ TEST_P(GameInterfaceTest, setLoadOrderShouldSetTheLoadOrder) {
|
||||
std::filesystem::remove(dataPath / std::filesystem::u8path(nonAsciiEsm));
|
||||
|
||||
// Set no additional data paths to avoid picking up non-test plugins on PCs
|
||||
// which have Starfield or Fallout 4 installed.
|
||||
handle_->SetAdditionalDataPaths({});
|
||||
// which have Starfield or Fallout 4 installed. Don't clear the additional
|
||||
// data paths for OpenMW because they come from test config.
|
||||
if (GetParam() != GameType::openmw) {
|
||||
handle_->SetAdditionalDataPaths({});
|
||||
}
|
||||
|
||||
handle_->LoadCurrentLoadOrderState();
|
||||
|
||||
@@ -278,6 +312,20 @@ TEST_P(GameInterfaceTest, setLoadOrderShouldSetTheLoadOrder) {
|
||||
blankEsp,
|
||||
blankMasterDependentEsp,
|
||||
};
|
||||
} else if (GetParam() == GameType::openmw) {
|
||||
loadOrder = {
|
||||
blankDifferentMasterDependentEsm,
|
||||
blankDifferentPluginDependentEsp,
|
||||
blankDifferentEsm,
|
||||
blankDifferentEsp,
|
||||
blankMasterDependentEsm,
|
||||
blankMasterDependentEsp,
|
||||
blankPluginDependentEsp,
|
||||
blankEsp,
|
||||
masterFile,
|
||||
blankDifferentMasterDependentEsp,
|
||||
blankEsm,
|
||||
};
|
||||
} else {
|
||||
loadOrder = {
|
||||
masterFile,
|
||||
@@ -302,11 +350,15 @@ TEST_P(GameInterfaceTest, setLoadOrderShouldSetTheLoadOrder) {
|
||||
|
||||
EXPECT_EQ(loadOrder, handle_->GetLoadOrder());
|
||||
|
||||
if (gameSupportsEsl) {
|
||||
loadOrder.erase(std::begin(loadOrder));
|
||||
}
|
||||
// It's not possible to persist the load order of inactive plugins for
|
||||
// OpenMW.
|
||||
if (GetParam() != GameType::openmw) {
|
||||
if (gameSupportsEsl) {
|
||||
loadOrder.erase(std::begin(loadOrder));
|
||||
}
|
||||
|
||||
EXPECT_EQ(loadOrder, getLoadOrder());
|
||||
EXPECT_EQ(loadOrder, getLoadOrder());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,8 @@ INSTANTIATE_TEST_SUITE_P(,
|
||||
GameType::fo4vr,
|
||||
GameType::tes5vr,
|
||||
GameType::tes3,
|
||||
GameType::starfield));
|
||||
GameType::starfield,
|
||||
GameType::openmw));
|
||||
|
||||
TEST_P(GameTest, constructingShouldStoreTheGivenValues) {
|
||||
Game game = Game(GetParam(), gamePath, localPath);
|
||||
@@ -70,7 +71,7 @@ TEST_P(GameTest, constructingShouldStoreTheGivenValues) {
|
||||
#ifndef _WIN32
|
||||
TEST_P(GameTest,
|
||||
constructingShouldThrowOnLinuxIfLocalPathIsNotGivenExceptForMorrowind) {
|
||||
if (GetParam() == GameType::tes3) {
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
|
||||
EXPECT_NO_THROW(Game(GetParam(), gamePath));
|
||||
} else {
|
||||
EXPECT_THROW(Game(GetParam(), gamePath), std::system_error);
|
||||
@@ -118,6 +119,9 @@ TEST_P(
|
||||
"My Games" / "Starfield" / "Data";
|
||||
EXPECT_TRUE(boost::ends_with(game.GetAdditionalDataPaths()[0].u8string(),
|
||||
expectedSuffix.u8string()));
|
||||
} else if (GetParam() == GameType::openmw) {
|
||||
EXPECT_EQ(std::vector<std::filesystem::path>{localPath / "data"},
|
||||
game.GetAdditionalDataPaths());
|
||||
} else {
|
||||
EXPECT_TRUE(game.GetAdditionalDataPaths().empty());
|
||||
}
|
||||
@@ -195,6 +199,23 @@ TEST_P(GameTest, isValidPluginShouldUseAbsolutePathsAsGiven) {
|
||||
EXPECT_TRUE(game.IsValidPlugin(path));
|
||||
}
|
||||
|
||||
TEST_P(
|
||||
GameTest,
|
||||
isValidPluginShouldTryGhostedPathIfGivenPluginDoesNotExistExceptForOpenMW) {
|
||||
const Game game(GetParam(), gamePath, localPath);
|
||||
|
||||
if (GetParam() == GameType::openmw) {
|
||||
// This wasn't done for OpenMW during common setup.
|
||||
const auto pluginPath =
|
||||
game.DataPath() / (blankMasterDependentEsm + ".ghost");
|
||||
std::filesystem::rename(dataPath / blankMasterDependentEsm, pluginPath);
|
||||
|
||||
EXPECT_FALSE(game.IsValidPlugin(blankMasterDependentEsm));
|
||||
} else {
|
||||
EXPECT_TRUE(game.IsValidPlugin(blankMasterDependentEsm));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(
|
||||
GameTest,
|
||||
loadPluginsWithHeadersOnlyTrueShouldLoadTheHeadersOfAllInstalledPlugins) {
|
||||
@@ -289,8 +310,8 @@ TEST_P(GameTest, loadPluginsShouldFindArchivesInAdditionalDataPaths) {
|
||||
archiveFileExtension);
|
||||
const auto ba2Path2 =
|
||||
gamePath / ("../../Fallout 4- Nuka-World (PC)/Content/Data/DLCNukaWorld "
|
||||
"- Voices_it" +
|
||||
archiveFileExtension);
|
||||
"- Voices_it" +
|
||||
archiveFileExtension);
|
||||
touch(ba2Path1);
|
||||
touch(ba2Path2);
|
||||
|
||||
@@ -371,10 +392,11 @@ TEST_P(
|
||||
|
||||
std::filesystem::remove(dataPath / pluginName);
|
||||
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::starfield) {
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw ||
|
||||
GetParam() == GameType::starfield) {
|
||||
try {
|
||||
game.LoadPlugins({blankMasterDependentEsm}, false);
|
||||
FAIL();
|
||||
FAIL();
|
||||
} catch (const std::system_error& e) {
|
||||
EXPECT_EQ(ESP_ERROR_PLUGIN_METADATA_NOT_FOUND, e.code().value());
|
||||
EXPECT_EQ(esplugin_category(), e.code().category());
|
||||
|
||||
@@ -57,6 +57,8 @@ protected:
|
||||
|
||||
std::vector<std::string> getEarlyLoadingPlugins() {
|
||||
switch (GetParam()) {
|
||||
case GameType::openmw:
|
||||
return {"builtin.omwscripts"};
|
||||
case GameType::tes5:
|
||||
return {"Skyrim.esm"};
|
||||
case GameType::tes5se:
|
||||
@@ -111,7 +113,8 @@ INSTANTIATE_TEST_SUITE_P(,
|
||||
GameType::fo3,
|
||||
GameType::fonv,
|
||||
GameType::fo4,
|
||||
GameType::tes5se));
|
||||
GameType::tes5se,
|
||||
GameType::openmw));
|
||||
|
||||
TEST_P(LoadOrderHandlerTest, constructorShouldThrowIfNoGamePathIsSet) {
|
||||
EXPECT_THROW(LoadOrderHandler(GetParam(), ""), std::invalid_argument);
|
||||
@@ -129,7 +132,7 @@ TEST_P(LoadOrderHandlerTest, constructorShouldNotThrowIfNoLocalPathIsSet) {
|
||||
#else
|
||||
TEST_P(LoadOrderHandlerTest,
|
||||
constructorShouldNotThrowIfNoLocalPathIsSetAndGameTypeIsMorrowind) {
|
||||
if (GetParam() == GameType::tes3) {
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
|
||||
EXPECT_NO_THROW(LoadOrderHandler(GetParam(), gamePath));
|
||||
} else {
|
||||
EXPECT_THROW(LoadOrderHandler(GetParam(), gamePath), std::system_error);
|
||||
@@ -179,7 +182,24 @@ TEST_P(LoadOrderHandlerTest, getLoadOrderShouldReturnTheCurrentLoadOrder) {
|
||||
auto loadOrderHandler = createHandler();
|
||||
loadOrderHandler.LoadCurrentState();
|
||||
|
||||
ASSERT_EQ(getLoadOrder(), loadOrderHandler.GetLoadOrder());
|
||||
if (GetParam() == GameType::openmw) {
|
||||
EXPECT_EQ(std::vector<std::string>({
|
||||
blankDifferentEsm,
|
||||
blankDifferentMasterDependentEsm,
|
||||
blankDifferentEsp,
|
||||
blankDifferentPluginDependentEsp,
|
||||
blankMasterDependentEsm,
|
||||
blankMasterDependentEsp,
|
||||
blankEsp,
|
||||
blankPluginDependentEsp,
|
||||
masterFile,
|
||||
blankEsm,
|
||||
blankDifferentMasterDependentEsp,
|
||||
}),
|
||||
loadOrderHandler.GetLoadOrder());
|
||||
} else {
|
||||
ASSERT_EQ(getLoadOrder(), loadOrderHandler.GetLoadOrder());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(LoadOrderHandlerTest,
|
||||
@@ -209,6 +229,45 @@ TEST_P(LoadOrderHandlerTest,
|
||||
loadOrderHandler.GetEarlyLoadingPlugins());
|
||||
}
|
||||
|
||||
TEST_P(LoadOrderHandlerTest, getAdditionalDataPathsShouldReturnValidData) {
|
||||
if (GetParam() == GameType::fo4) {
|
||||
// Create the file that indicates it's a Microsoft Store install.
|
||||
touch(gamePath / "appxmanifest.xml");
|
||||
}
|
||||
|
||||
auto loadOrderHandler = createHandler();
|
||||
|
||||
if (GetParam() == GameType::fo4) {
|
||||
const auto basePath = gamePath / ".." / "..";
|
||||
EXPECT_EQ(std::vector<std::filesystem::path>(
|
||||
{basePath / "Fallout 4- Automatron (PC)" / "Content" / "Data",
|
||||
basePath / "Fallout 4- Nuka-World (PC)" / "Content" / "Data",
|
||||
basePath / "Fallout 4- Wasteland Workshop (PC)" / "Content" /
|
||||
"Data",
|
||||
basePath / "Fallout 4- High Resolution Texture Pack" /
|
||||
"Content" / "Data",
|
||||
basePath / "Fallout 4- Vault-Tec Workshop (PC)" / "Content" /
|
||||
"Data",
|
||||
basePath / "Fallout 4- Far Harbor (PC)" / "Content" / "Data",
|
||||
basePath / "Fallout 4- Contraptions Workshop (PC)" /
|
||||
"Content" / "Data"}),
|
||||
loadOrderHandler.GetAdditionalDataPaths());
|
||||
} else if (GetParam() == GameType::starfield) {
|
||||
ASSERT_EQ(1, loadOrderHandler.GetAdditionalDataPaths().size());
|
||||
|
||||
const auto expectedSuffix = std::filesystem::u8path("Documents") /
|
||||
"My Games" / "Starfield" / "Data";
|
||||
EXPECT_TRUE(boost::ends_with(
|
||||
loadOrderHandler.GetAdditionalDataPaths()[0].u8string(),
|
||||
expectedSuffix.u8string()));
|
||||
} else if (GetParam() == GameType::openmw) {
|
||||
EXPECT_EQ(std::vector<std::filesystem::path>{localPath / "data"},
|
||||
loadOrderHandler.GetAdditionalDataPaths());
|
||||
} else {
|
||||
EXPECT_TRUE(loadOrderHandler.GetAdditionalDataPaths().empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(LoadOrderHandlerTest, setLoadOrderShouldSetTheLoadOrder) {
|
||||
auto loadOrderHandler = createHandler();
|
||||
loadOrderHandler.LoadCurrentState();
|
||||
@@ -218,7 +277,14 @@ TEST_P(LoadOrderHandlerTest, setLoadOrderShouldSetTheLoadOrder) {
|
||||
if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se)
|
||||
loadOrderToSet_.erase(begin(loadOrderToSet_));
|
||||
|
||||
EXPECT_EQ(loadOrderToSet_, getLoadOrder());
|
||||
if (GetParam() == GameType::openmw) {
|
||||
// Can't set the load order positions of inactive plugins,
|
||||
// this reads what libloadorder has cached in memory instead of
|
||||
// what was actually saved.
|
||||
EXPECT_EQ(loadOrderToSet_, loadOrderHandler.GetLoadOrder());
|
||||
} else {
|
||||
EXPECT_EQ(loadOrderToSet_, getLoadOrder());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(LoadOrderHandlerTest, setExternalPluginPathsShouldAcceptAnEmptyVector) {
|
||||
|
||||
@@ -91,7 +91,8 @@ INSTANTIATE_TEST_SUITE_P(,
|
||||
GameType::fo3,
|
||||
GameType::fonv,
|
||||
GameType::fo4,
|
||||
GameType::tes5se));
|
||||
GameType::tes5se,
|
||||
GameType::openmw));
|
||||
|
||||
TEST_P(ConditionEvaluatorTest,
|
||||
evaluateShouldReturnTrueForAnEmptyConditionString) {
|
||||
|
||||
@@ -85,7 +85,7 @@ protected:
|
||||
dataPath / blankMasterDependentArchive);
|
||||
ASSERT_TRUE(
|
||||
std::filesystem::exists(dataPath / blankMasterDependentArchive));
|
||||
} else if (GetParam() == GameType::tes3) {
|
||||
} else if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
|
||||
touch(dataPath / blankArchive);
|
||||
|
||||
blankMasterDependentArchive = "Blank - Master Dependent.bsa";
|
||||
@@ -191,7 +191,26 @@ INSTANTIATE_TEST_SUITE_P(,
|
||||
GameType::fo4vr,
|
||||
GameType::tes5vr,
|
||||
GameType::tes3,
|
||||
GameType::starfield));
|
||||
GameType::starfield,
|
||||
GameType::openmw));
|
||||
|
||||
TEST_P(PluginTest, constructorShouldTrimGhostExtensionExceptForOpenMW) {
|
||||
const auto pluginPath =
|
||||
game_.DataPath() / (blankMasterDependentEsm + ".ghost");
|
||||
|
||||
if (GetParam() == GameType::openmw) {
|
||||
// This wasn't done for OpenMW during common setup.
|
||||
std::filesystem::rename(dataPath / blankMasterDependentEsm, pluginPath);
|
||||
}
|
||||
|
||||
Plugin plugin(game_.GetType(), game_.GetCache(), pluginPath, true);
|
||||
|
||||
if (GetParam() == GameType::openmw) {
|
||||
EXPECT_EQ(pluginPath.filename().u8string(), plugin.GetName());
|
||||
} else {
|
||||
EXPECT_EQ(blankMasterDependentEsm, plugin.GetName());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(PluginTest, loadingShouldHandleNonAsciiFilenamesCorrectly) {
|
||||
Plugin plugin(game_.GetType(),
|
||||
@@ -209,11 +228,15 @@ TEST_P(PluginTest, loadingHeaderOnlyShouldReadHeaderData) {
|
||||
|
||||
EXPECT_EQ(blankEsm, plugin.GetName());
|
||||
EXPECT_TRUE(plugin.GetMasters().empty());
|
||||
EXPECT_TRUE(plugin.IsMaster());
|
||||
if (GetParam() == GameType::openmw) {
|
||||
EXPECT_FALSE(plugin.IsMaster());
|
||||
} else {
|
||||
EXPECT_TRUE(plugin.IsMaster());
|
||||
}
|
||||
EXPECT_FALSE(plugin.IsEmpty());
|
||||
EXPECT_EQ("5.0", plugin.GetVersion());
|
||||
|
||||
if (GetParam() == GameType::tes3) {
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
|
||||
EXPECT_FLOAT_EQ(1.2f, plugin.GetHeaderVersion().value());
|
||||
} else if (GetParam() == GameType::tes4) {
|
||||
EXPECT_FLOAT_EQ(0.8f, plugin.GetHeaderVersion().value());
|
||||
@@ -237,11 +260,15 @@ TEST_P(PluginTest, loadingWholePluginShouldReadHeaderData) {
|
||||
|
||||
EXPECT_EQ(blankEsm, plugin.GetName());
|
||||
EXPECT_TRUE(plugin.GetMasters().empty());
|
||||
EXPECT_TRUE(plugin.IsMaster());
|
||||
if (GetParam() == GameType::openmw) {
|
||||
EXPECT_FALSE(plugin.IsMaster());
|
||||
} else {
|
||||
EXPECT_TRUE(plugin.IsMaster());
|
||||
}
|
||||
EXPECT_FALSE(plugin.IsEmpty());
|
||||
EXPECT_EQ("5.0", plugin.GetVersion());
|
||||
|
||||
if (GetParam() == GameType::tes3) {
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
|
||||
EXPECT_FLOAT_EQ(1.2f, plugin.GetHeaderVersion().value());
|
||||
} else if (GetParam() == GameType::tes4) {
|
||||
EXPECT_FLOAT_EQ(0.8f, plugin.GetHeaderVersion().value());
|
||||
@@ -253,12 +280,13 @@ TEST_P(PluginTest, loadingWholePluginShouldReadHeaderData) {
|
||||
}
|
||||
|
||||
TEST_P(PluginTest, loadingWholePluginShouldReadFields) {
|
||||
Plugin plugin(game_.GetType(),
|
||||
game_.GetCache(),
|
||||
game_.DataPath() / (blankMasterDependentEsm + ".ghost"),
|
||||
false);
|
||||
const auto pluginName = GetParam() == GameType::openmw
|
||||
? blankMasterDependentEsm
|
||||
: blankMasterDependentEsm + ".ghost";
|
||||
Plugin plugin(
|
||||
game_.GetType(), game_.GetCache(), game_.DataPath() / pluginName, false);
|
||||
|
||||
if (GetParam() == GameType::tes3) {
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
|
||||
Plugin master(
|
||||
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsm, false);
|
||||
const auto pluginsMetadata = Plugin::GetPluginsMetadata({&master});
|
||||
@@ -297,6 +325,30 @@ TEST_P(PluginTest, loadingANonMasterPluginShouldReadTheMasterFlagAsFalse) {
|
||||
EXPECT_FALSE(plugin.IsMaster());
|
||||
}
|
||||
|
||||
TEST_P(PluginTest, loadingWholePluginShouldSucceedForOpenMWPlugins) {
|
||||
const auto omwgame = "Blank.omwgame";
|
||||
const auto omwaddon = "Blank.omwaddon";
|
||||
const auto omwscripts = "Blank.omwscripts";
|
||||
|
||||
std::filesystem::rename(dataPath / blankEsm, dataPath / omwgame);
|
||||
std::filesystem::rename(dataPath / blankEsp, dataPath / omwaddon);
|
||||
std::ofstream out(dataPath / omwscripts);
|
||||
out.close();
|
||||
|
||||
EXPECT_NO_THROW(
|
||||
Plugin(game_.GetType(), game_.GetCache(), dataPath / omwgame, false));
|
||||
EXPECT_NO_THROW(
|
||||
Plugin(game_.GetType(), game_.GetCache(), dataPath / omwaddon, false));
|
||||
if (GetParam() == GameType::openmw) {
|
||||
EXPECT_NO_THROW(Plugin(
|
||||
game_.GetType(), game_.GetCache(), dataPath / omwscripts, false));
|
||||
} else {
|
||||
EXPECT_THROW(
|
||||
Plugin(game_.GetType(), game_.GetCache(), dataPath / omwscripts, false),
|
||||
std::system_error);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(
|
||||
PluginTest,
|
||||
isLightPluginShouldBeTrueForAPluginWithEslFileExtensionForFallout4AndSkyrimSeAndFalseOtherwise) {
|
||||
@@ -399,7 +451,8 @@ TEST_P(
|
||||
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsm, true)
|
||||
.LoadsArchive();
|
||||
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::tes4)
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw ||
|
||||
GetParam() == GameType::tes4)
|
||||
EXPECT_FALSE(loadsArchive);
|
||||
else
|
||||
EXPECT_TRUE(loadsArchive);
|
||||
@@ -416,7 +469,8 @@ TEST_P(
|
||||
true)
|
||||
.LoadsArchive();
|
||||
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::starfield)
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw ||
|
||||
GetParam() == GameType::starfield)
|
||||
EXPECT_FALSE(loadsArchive);
|
||||
else
|
||||
EXPECT_TRUE(loadsArchive);
|
||||
@@ -431,7 +485,7 @@ TEST_P(
|
||||
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsp, true)
|
||||
.LoadsArchive();
|
||||
|
||||
if (GetParam() == GameType::tes3)
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw)
|
||||
EXPECT_FALSE(loadsArchive);
|
||||
else
|
||||
EXPECT_TRUE(loadsArchive);
|
||||
@@ -597,10 +651,12 @@ TEST_P(PluginTest,
|
||||
doRecordsOverlapShouldReturnFalseForTwoPluginsWithOnlyHeadersLoaded) {
|
||||
Plugin plugin1(
|
||||
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsm, true);
|
||||
Plugin plugin2(game_.GetType(),
|
||||
game_.GetCache(),
|
||||
game_.DataPath() / (blankMasterDependentEsm + ".ghost"),
|
||||
true);
|
||||
|
||||
const auto pluginName = GetParam() == GameType::openmw
|
||||
? blankMasterDependentEsm
|
||||
: blankMasterDependentEsm + ".ghost";
|
||||
Plugin plugin2(
|
||||
game_.GetType(), game_.GetCache(), game_.DataPath() / pluginName, true);
|
||||
|
||||
EXPECT_FALSE(plugin1.DoRecordsOverlap(plugin2));
|
||||
EXPECT_FALSE(plugin2.DoRecordsOverlap(plugin1));
|
||||
@@ -626,13 +682,14 @@ TEST_P(PluginTest,
|
||||
doRecordsOverlapShouldReturnTrueIfOnePluginOverridesTheOthersRecords) {
|
||||
const auto plugin1Name =
|
||||
GetParam() == GameType::starfield ? blankFullEsm : blankEsm;
|
||||
const auto plugin2Name = GetParam() == GameType::openmw
|
||||
? blankMasterDependentEsm
|
||||
: blankMasterDependentEsm + ".ghost";
|
||||
|
||||
Plugin plugin1(
|
||||
game_.GetType(), game_.GetCache(), game_.DataPath() / plugin1Name, false);
|
||||
Plugin plugin2(game_.GetType(),
|
||||
game_.GetCache(),
|
||||
game_.DataPath() / (blankMasterDependentEsm + ".ghost"),
|
||||
false);
|
||||
Plugin plugin2(
|
||||
game_.GetType(), game_.GetCache(), game_.DataPath() / plugin2Name, false);
|
||||
|
||||
if (GetParam() == GameType::starfield) {
|
||||
plugin1.ResolveRecordIds(nullptr);
|
||||
@@ -649,7 +706,7 @@ TEST_P(PluginTest, getRecordAndGroupCountShouldReturnTheHeaderFieldValue) {
|
||||
Plugin plugin(
|
||||
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsm, true);
|
||||
|
||||
if (GetParam() == GameType::tes3) {
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
|
||||
EXPECT_EQ(10u, plugin.GetRecordAndGroupCount());
|
||||
} else if (GetParam() == GameType::tes4) {
|
||||
EXPECT_EQ(14u, plugin.GetRecordAndGroupCount());
|
||||
@@ -665,7 +722,7 @@ TEST_P(PluginTest,
|
||||
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsp, false)
|
||||
.GetAssetCount();
|
||||
|
||||
if (GetParam() == GameType::tes3) {
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
|
||||
EXPECT_EQ(0, assetCount);
|
||||
} else if (GetParam() == GameType::fo4 || GetParam() == GameType::fo4vr ||
|
||||
GetParam() == GameType::starfield) {
|
||||
@@ -690,7 +747,7 @@ TEST_P(PluginTest,
|
||||
game_.GetType(), game_.GetCache(), game_.DataPath() / blankEsp, false);
|
||||
OtherPluginType plugin2;
|
||||
|
||||
if (GetParam() == GameType::tes3) {
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
|
||||
EXPECT_FALSE(plugin1.DoAssetsOverlap(plugin2));
|
||||
} else {
|
||||
EXPECT_THROW(plugin1.DoAssetsOverlap(plugin2), std::invalid_argument);
|
||||
@@ -734,7 +791,7 @@ TEST_P(PluginTest,
|
||||
game_.DataPath() / blankMasterDependentEsp,
|
||||
false);
|
||||
|
||||
if (GetParam() == GameType::tes3) {
|
||||
if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw) {
|
||||
// Morrowind plugins can't load assets.
|
||||
EXPECT_FALSE(plugin1.DoAssetsOverlap(plugin2));
|
||||
EXPECT_FALSE(plugin2.DoAssetsOverlap(plugin1));
|
||||
@@ -744,16 +801,20 @@ TEST_P(PluginTest,
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(PluginTest,
|
||||
hasPluginFileExtensionShouldBeTrueIfFileEndsInDotEspOrDotEsm) {
|
||||
class HasPluginFileExtensionTest : public ::testing::TestWithParam<GameType> {};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(,
|
||||
HasPluginFileExtensionTest,
|
||||
::testing::ValuesIn(ALL_GAME_TYPES));
|
||||
|
||||
TEST_P(HasPluginFileExtensionTest, shouldBeTrueIfFileEndsInDotEspOrDotEsm) {
|
||||
EXPECT_TRUE(hasPluginFileExtension("file.esp", GetParam()));
|
||||
EXPECT_TRUE(hasPluginFileExtension("file.esm", GetParam()));
|
||||
EXPECT_FALSE(hasPluginFileExtension("file.bsa", GetParam()));
|
||||
}
|
||||
|
||||
TEST_P(
|
||||
PluginTest,
|
||||
hasPluginFileExtensionShouldBeTrueIfFileEndsInDotEslOnlyForFallout4AndLater) {
|
||||
TEST_P(HasPluginFileExtensionTest,
|
||||
shouldBeTrueIfFileEndsInDotEslOnlyForFallout4AndLater) {
|
||||
bool result = hasPluginFileExtension("file.esl", GetParam());
|
||||
|
||||
EXPECT_EQ(GetParam() == GameType::fo4 || GetParam() == GameType::fo4vr ||
|
||||
@@ -763,6 +824,26 @@ TEST_P(
|
||||
result);
|
||||
}
|
||||
|
||||
TEST_P(HasPluginFileExtensionTest, shouldTrimGhostExtensionExceptForOpenMW) {
|
||||
if (GetParam() == GameType::openmw) {
|
||||
EXPECT_FALSE(hasPluginFileExtension("file.esp.ghost", GetParam()));
|
||||
EXPECT_FALSE(hasPluginFileExtension("file.esm.ghost", GetParam()));
|
||||
} else {
|
||||
EXPECT_TRUE(hasPluginFileExtension("file.esp.ghost", GetParam()));
|
||||
EXPECT_TRUE(hasPluginFileExtension("file.esm.ghost", GetParam()));
|
||||
}
|
||||
EXPECT_FALSE(hasPluginFileExtension("file.bsa.ghost", GetParam()));
|
||||
}
|
||||
|
||||
TEST_P(HasPluginFileExtensionTest, shouldRecogniseOpenMWPluginExtensions) {
|
||||
EXPECT_EQ(GetParam() == GameType::openmw,
|
||||
hasPluginFileExtension("file.omwgame", GetParam()));
|
||||
EXPECT_EQ(GetParam() == GameType::openmw,
|
||||
hasPluginFileExtension("file.omwaddon", GetParam()));
|
||||
EXPECT_EQ(GetParam() == GameType::openmw,
|
||||
hasPluginFileExtension("file.omwscripts", GetParam()));
|
||||
}
|
||||
|
||||
TEST(equivalent, shouldReturnTrueIfGivenEqualPathsThatExist) {
|
||||
auto path1 = std::filesystem::path("./testing-plugins/LICENSE");
|
||||
auto path2 = std::filesystem::path("./testing-plugins/LICENSE");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user