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:
Oliver Hamlet
2025-02-01 21:36:03 +00:00
parent 2d3192683f
commit 84c0cca534
24 changed files with 698 additions and 173 deletions
+1 -1
View File
@@ -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.
+6 -6
View File
@@ -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.
+1 -1
View File
@@ -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
View File
@@ -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(
+3 -1
View File
@@ -50,7 +50,9 @@ enum struct GameType : unsigned int {
/** The Elder Scrolls III: Morrowind */
tes3,
/** Starfield */
starfield
starfield,
/** OpenMW */
openmw
};
}
+17 -12
View File
@@ -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.
*/
+15 -3
View File
@@ -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;
+2
View File
@@ -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
View File
@@ -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) {
+24
View File
@@ -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();
+2
View File
@@ -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;
+6 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
+63 -11
View File
@@ -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());
}
}
}
}
+28 -6
View File
@@ -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) {
+112 -31
View File
@@ -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