Count override records using masters for TES3 plugins

If a plugin's masters are all present, this makes the sorting logic for
Morrowind match the other supported games. If a plugin's masters are
missing, use the plugin's total record count as the override record
count.

Plugins with missing masters cannot be loaded by the game, reducing the
impact (if any) of the probably-inflated record counts. It's better for
LOOT to be able to sort a slightly wonky load order if there are missing
masters than for it to fail completely, as such plugins may be present
for development and testing reasons.
This commit is contained in:
Oliver Hamlet
2019-09-06 20:13:04 +01:00
parent e98dccac59
commit a3a90062e1
11 changed files with 393 additions and 81 deletions
+2 -1
View File
@@ -82,7 +82,7 @@ set (GTEST_LIBRARIES "${BINARY_DIR}/googlemock/gtest/${CMAKE_CFG_INTDIR}/${CMAKE
ExternalProject_Add(esplugin
PREFIX "external"
URL "https://github.com/Ortham/esplugin/archive/2.1.2.tar.gz"
URL "https://github.com/Ortham/esplugin/archive/3.2.0.tar.gz"
CONFIGURE_COMMAND ""
BUILD_IN_SOURCE 1
BUILD_COMMAND cargo build --release --manifest-path ffi/Cargo.toml --target ${RUST_TARGET} &&
@@ -296,6 +296,7 @@ set (LOOT_TESTS_HEADERS "${CMAKE_SOURCE_DIR}/src/tests/api/internals/game/game_t
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/plugin_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/sorting/group_sort_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/sorting/plugin_sorter_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/sorting/plugin_sorting_data_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/masterlist_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/metadata_list_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/common_game_test_fixture.h"
+5 -3
View File
@@ -58,15 +58,17 @@ same record, i.e. if they both edit the same record or if one edits a record the
other plugin adds.
For each plugin, skip it if it overrides no records, otherwise iterate over all
other plugins. Sorting currently skips adding overlap edges for Morrowind
plugins, because LOOT is unable to distinguish between new and overridden
records in Morrowind plugins, and considers all plugins to override no records.
other plugins.
* If the plugin and other plugin override the same number of records, or do not
overlap, skip the other plugin.
* Otherwise, add an edge from the plugin which overrides more records to the
plugin that overrides fewer records, unless that edge would cause a cycle.
For Morrowind, identifying which records override others requires all of a
plugin's masters to be installed, so if a plugin has missing masters, its total
record count is used in place of its override record count.
Finally, tie-break edges are added to ensure that sorting is consistent. For
each plugin, iterate over all other plugins and add an edge between each pair of
plugins in the direction given by the tie-break comparison function, unless that
+59 -20
View File
@@ -60,7 +60,9 @@ Plugin::Plugin(const GameType gameType,
auto ret = esp_plugin_is_empty(esPlugin.get(), &isEmpty_);
if (ret != ESP_OK) {
throw FileAccessError("Error checking if \"" + name_ + "\" is empty. esplugin error code: " + std::to_string(ret));
throw FileAccessError(
"Error checking if \"" + name_ +
"\" is empty. esplugin error code: " + std::to_string(ret));
}
if (!headerOnly) {
@@ -69,7 +71,9 @@ Plugin::Plugin(const GameType gameType,
ret = esp_plugin_count_override_records(esPlugin.get(),
&numOverrideRecords_);
if (ret != ESP_OK) {
throw FileAccessError("Error counting override records in \"" + name_ + "\". esplugin error code: " + std::to_string(ret));
throw FileAccessError(
"Error counting override records in \"" + name_ +
"\". esplugin error code: " + std::to_string(ret));
}
}
@@ -80,7 +84,8 @@ Plugin::Plugin(const GameType gameType,
logger->error(
"Cannot read plugin file \"{}\". Details: {}", name_, e.what());
}
throw FileAccessError("Cannot read \"" + name_ + "\". Details: " + e.what());
throw FileAccessError("Cannot read \"" + name_ +
"\". Details: " + e.what());
}
}
@@ -91,7 +96,7 @@ float Plugin::GetHeaderVersion() const {
auto ret = esp_plugin_header_version(esPlugin.get(), &version);
if (ret != ESP_OK) {
throw FileAccessError(name_ +
" : esplugin error code: " + std::to_string(ret));
" : esplugin error code: " + std::to_string(ret));
}
return version;
@@ -142,13 +147,12 @@ bool Plugin::IsLightMaster() const {
return isLightMaster;
}
bool Plugin::IsValidAsLightMaster() const
{
bool Plugin::IsValidAsLightMaster() const {
bool isValid;
auto ret = esp_plugin_is_valid_as_light_master(esPlugin.get(), &isValid);
if (ret != ESP_OK) {
throw FileAccessError(name_ +
" : esplugin error code: " + std::to_string(ret));
" : esplugin error code: " + std::to_string(ret));
}
return isValid;
@@ -183,25 +187,58 @@ bool Plugin::DoFormIDsOverlap(const PluginInterface& plugin) const {
return false;
}
size_t Plugin::GetOverlapSize(
const std::vector<std::shared_ptr<const Plugin>> plugins) const {
if (plugins.empty()) {
return 0;
}
std::vector<::Plugin*> esPlugins;
for (const auto& plugin : plugins) {
esPlugins.push_back(plugin->esPlugin.get());
}
size_t overlapSize;
auto ret = esp_plugin_records_overlap_size(
esPlugin.get(), &esPlugins[0], esPlugins.size(), &overlapSize);
if (ret != ESP_OK) {
throw FileAccessError("Error getting overlap size for \"" + name_ +
"\". esplugin error code: " + std::to_string(ret));
}
return overlapSize;
}
size_t Plugin::NumOverrideFormIDs() const { return numOverrideRecords_; }
uint32_t Plugin::GetRecordAndGroupCount() const {
uint32_t recordAndGroupCount = 0;
auto ret =
esp_plugin_record_and_group_count(esPlugin.get(), &recordAndGroupCount);
if (ret != ESP_OK) {
throw FileAccessError("Error getting record and group count for \"" +
name_ +
"\". esplugin error code: " + std::to_string(ret));
}
return recordAndGroupCount;
}
bool Plugin::IsValid(const GameType gameType,
const std::filesystem::path& pluginPath) {
// Check that the file has a valid extension.
if (hasPluginFileExtension(pluginPath.filename().u8string(), gameType)) {
bool isValid;
int returnCode = esp_plugin_is_valid(GetEspluginGameId(gameType),
pluginPath.u8string().c_str(),
true,
&isValid);
pluginPath.u8string().c_str(),
true,
&isValid);
if (returnCode != ESP_OK || !isValid) {
// Try adding .ghost extension.
auto ghostedFilename = pluginPath.u8string() + ".ghost";
returnCode = esp_plugin_is_valid(GetEspluginGameId(gameType),
ghostedFilename.c_str(),
true,
&isValid);
returnCode = esp_plugin_is_valid(
GetEspluginGameId(gameType), ghostedFilename.c_str(), true, &isValid);
}
if (returnCode == ESP_OK && isValid) {
@@ -212,7 +249,7 @@ bool Plugin::IsValid(const GameType gameType,
auto logger = getLogger();
if (logger) {
logger->info("The file \"{}\" is not a valid plugin.",
pluginPath.filename().u8string());
pluginPath.filename().u8string());
}
return false;
@@ -274,11 +311,13 @@ std::string GetArchiveFileExtension(const GameType gameType) {
return ".bsa";
}
std::filesystem::path replaceExtension(std::filesystem::path path, const std::string& newExtension) {
std::filesystem::path replaceExtension(std::filesystem::path path,
const std::string& newExtension) {
return path.replace_extension(std::filesystem::u8path(newExtension));
}
bool equivalent(const std::filesystem::path& path1, const std::filesystem::path& path2) {
bool equivalent(const std::filesystem::path& path1,
const std::filesystem::path& path2) {
// If the paths are identical, they've got to be equivalent,
// it doesn't matter if the paths exist or not.
if (path1 == path2) {
@@ -314,7 +353,8 @@ bool Plugin::LoadsArchive(const GameType gameType,
if (gameType == GameType::tes5) {
// Skyrim plugins only load BSAs that exactly match their basename.
return std::filesystem::exists(replaceExtension(pluginPath, archiveExtension));
return std::filesystem::exists(
replaceExtension(pluginPath, archiveExtension));
} else if (gameType != GameType::tes4 ||
boost::iends_with(pluginPath.filename().u8string(), ".esp")) {
// Oblivion .esp files and FO3, FNV, FO4 plugins can load archives which
@@ -331,8 +371,7 @@ bool Plugin::LoadsArchive(const GameType gameType,
auto bsaPluginFilename =
archivePath.filename().native().substr(0, basenameLength) +
pluginExtension;
auto bsaPluginPath =
pluginPath.parent_path() / bsaPluginFilename;
auto bsaPluginPath = pluginPath.parent_path() / bsaPluginFilename;
if (loot::equivalent(pluginPath, bsaPluginPath)) {
return true;
}
+3
View File
@@ -61,9 +61,12 @@ public:
bool IsEmpty() const;
bool LoadsArchive() const;
bool DoFormIDsOverlap(const PluginInterface& plugin) const;
size_t GetOverlapSize(
const std::vector<std::shared_ptr<const Plugin>> plugins) const;
// Load ordering functions.
size_t NumOverrideFormIDs() const;
uint32_t GetRecordAndGroupCount() const;
// Validity checks.
static bool IsValid(const GameType gameType,
+8 -3
View File
@@ -173,7 +173,8 @@ void PluginSorter::AddPluginVertices(Game& game) {
auto loadOrder = game.GetLoadOrder();
for (const auto& plugin : game.GetCache()->GetPlugins()) {
auto loadedPlugins = game.GetCache()->GetPlugins();
for (const auto& plugin : loadedPlugins) {
auto masterlistMetadata =
game.GetDatabase()
->GetPluginMetadata(plugin->GetName(), false, true)
@@ -182,8 +183,12 @@ void PluginSorter::AddPluginVertices(Game& game) {
->GetPluginUserMetadata(plugin->GetName(), true)
.value_or(PluginMetadata(plugin->GetName()));
auto pluginSortingData =
PluginSortingData(*plugin, masterlistMetadata, userMetadata, loadOrder);
auto pluginSortingData = PluginSortingData(*plugin,
masterlistMetadata,
userMetadata,
loadOrder,
game.Type(),
loadedPlugins);
auto groupName = pluginSortingData.GetGroup();
auto groupIt = groupPlugins.find(groupName);
+51 -5
View File
@@ -31,10 +31,31 @@
#include "api/helpers/text.h"
namespace loot {
PluginSortingData::PluginSortingData(const Plugin& plugin,
const PluginMetadata& masterlistMetadata,
std::vector<std::shared_ptr<const Plugin>> GetPluginsSubset(
const std::set<std::shared_ptr<const Plugin>>& plugins,
const std::vector<std::string>& pluginNames) {
std::vector<std::shared_ptr<const Plugin>> pluginsSubset;
for (const auto& pluginName : pluginNames) {
auto pos = std::find_if(plugins.begin(), plugins.end(), [&](auto plugin) {
return CompareFilenames(plugin->GetName(), pluginName) == 0;
});
if (pos != plugins.end()) {
pluginsSubset.push_back(*pos);
}
}
return pluginsSubset;
}
PluginSortingData::PluginSortingData(
const Plugin& plugin,
const PluginMetadata& masterlistMetadata,
const PluginMetadata& userMetadata,
const std::vector<std::string>& loadOrder) :
const std::vector<std::string>& loadOrder,
const GameType gameType,
const std::set<std::shared_ptr<const Plugin>>& loadedPlugins) :
plugin_(plugin),
masterlistLoadAfter_(masterlistMetadata.GetLoadAfterFiles()),
userLoadAfter_(userMetadata.GetLoadAfterFiles()),
@@ -49,10 +70,35 @@ PluginSortingData::PluginSortingData(const Plugin& plugin,
}
for (size_t i = 0; i < loadOrder.size(); i++) {
if (CompareFilenames(GetName(), loadOrder[i]) == 0) {
if (CompareFilenames(plugin.GetName(), loadOrder[i]) == 0) {
loadOrderIndex_ = i;
}
}
if (gameType == GameType::tes3) {
auto masterNames = plugin.GetMasters();
if (masterNames.empty()) {
numOverrideFormIDs = 0;
} else {
auto masters = GetPluginsSubset(loadedPlugins, masterNames);
if (masters.size() == masterNames.size()) {
numOverrideFormIDs = plugin.GetOverlapSize(masters);
} else {
// Not all masters are loaded, fall back to using the plugin's
// total record count (Morrowind doesn't have groups). This is OK
// because plugins with missing masters can't be loaded by the game,
// so the correctness of their load order positions is less important
// (it may not matter at all, depending on the sophistication/usage of
// merge patches in Morrowind). It's better for LOOT to sort a load
// order with missing masters with potentially poorer results than
// for it to error out, as masters may be missing for a variety of
// development & testing reasons.
numOverrideFormIDs = plugin.GetRecordAndGroupCount();
}
}
} else {
numOverrideFormIDs = plugin.NumOverrideFormIDs();
}
}
std::string PluginSortingData::GetName() const { return plugin_.GetName(); }
@@ -69,7 +115,7 @@ std::vector<std::string> PluginSortingData::GetMasters() const {
}
size_t PluginSortingData::NumOverrideFormIDs() const {
return plugin_.NumOverrideFormIDs();
return numOverrideFormIDs;
}
bool PluginSortingData::DoFormIDsOverlap(
+4 -1
View File
@@ -34,7 +34,9 @@ public:
PluginSortingData(const Plugin& plugin,
const PluginMetadata& masterlistMetadata,
const PluginMetadata& userMetadata,
const std::vector<std::string>& loadOrder);
const std::vector<std::string>& loadOrder,
const GameType gameType,
const std::set<std::shared_ptr<const Plugin>>& loadedPlugins);
std::string GetName() const;
bool IsMaster() const;
@@ -66,6 +68,7 @@ private:
std::set<File> userReq_;
std::optional<size_t> loadOrderIndex_;
size_t numOverrideFormIDs;
};
}
+1
View File
@@ -46,6 +46,7 @@
#include "tests/api/internals/plugin_test.h"
#include "tests/api/internals/sorting/group_sort_test.h"
#include "tests/api/internals/sorting/plugin_sorter_test.h"
#include "tests/api/internals/sorting/plugin_sorting_data_test.h"
TEST(ModuloOperator, shouldConformToTheCpp11Standard) {
// C++11 defines the modulo operator more strongly
+82 -7
View File
@@ -304,11 +304,12 @@ TEST_P(
TEST_P(
PluginTest,
loadsArchiveForAnArchiveThatExactlyMatchesANonAsciiEspFileBasenameShouldReturnTrueForAllGamesExceptMorrowind) {
bool loadsArchive = Plugin(game_.Type(),
game_.GetCache(),
game_.DataPath() / std::filesystem::u8path(nonAsciiEsp),
true)
.LoadsArchive();
bool loadsArchive =
Plugin(game_.Type(),
game_.GetCache(),
game_.DataPath() / std::filesystem::u8path(nonAsciiEsp),
true)
.LoadsArchive();
if (GetParam() == GameType::tes3)
EXPECT_FALSE(loadsArchive);
@@ -320,7 +321,8 @@ TEST_P(
TEST_P(
PluginTest,
loadsArchiveForAnArchiveThatExactlyMatchesAnEspFileBasenameShouldReturnTrueForAllGamesExceptMorrowind) {
bool loadsArchive = Plugin(game_.Type(), game_.GetCache(), game_.DataPath() / blankEsp, true)
bool loadsArchive =
Plugin(game_.Type(), game_.GetCache(), game_.DataPath() / blankEsp, true)
.LoadsArchive();
if (GetParam() == GameType::tes3)
@@ -338,7 +340,8 @@ TEST_P(
true)
.LoadsArchive();
if (GetParam() == GameType::tes3 || GetParam() == GameType::tes4 || GetParam() == GameType::tes5)
if (GetParam() == GameType::tes3 || GetParam() == GameType::tes4 ||
GetParam() == GameType::tes5)
EXPECT_FALSE(loadsArchive);
else
EXPECT_TRUE(loadsArchive);
@@ -500,6 +503,78 @@ TEST_P(PluginTest,
EXPECT_TRUE(plugin2.DoFormIDsOverlap(plugin1));
}
TEST_P(PluginTest, getOverlapSizeShouldCountEachRecordOnce) {
Plugin plugin1(
game_.Type(), game_.GetCache(), game_.DataPath() / blankEsm, false);
Plugin plugin2(game_.Type(),
game_.GetCache(),
game_.DataPath() / blankMasterDependentEsm,
false);
std::vector<std::shared_ptr<const Plugin>> plugins = {
std::make_shared<const Plugin>(plugin2),
std::make_shared<const Plugin>(plugin2)};
EXPECT_EQ(4, plugin1.GetOverlapSize(plugins));
}
TEST_P(PluginTest, getOverlapSizeShouldCheckAgainstAllGivenPlugins) {
Plugin plugin1(
game_.Type(), game_.GetCache(), game_.DataPath() / blankEsm, false);
Plugin plugin2(
game_.Type(), game_.GetCache(), game_.DataPath() / blankEsp, false);
Plugin plugin3(game_.Type(),
game_.GetCache(),
game_.DataPath() / blankMasterDependentEsm,
false);
std::vector<std::shared_ptr<const Plugin>> plugins = {
std::make_shared<const Plugin>(plugin2),
std::make_shared<const Plugin>(plugin3)};
EXPECT_EQ(4, plugin1.GetOverlapSize(plugins));
}
TEST_P(PluginTest,
getOverlapSizeShouldReturnZeroForPluginsWithOnlyHeadersLoaded) {
Plugin plugin1(
game_.Type(), game_.GetCache(), game_.DataPath() / blankEsm, true);
Plugin plugin2(game_.Type(),
game_.GetCache(),
game_.DataPath() / blankMasterDependentEsm,
true);
std::vector<std::shared_ptr<const Plugin>> plugins = {
std::make_shared<const Plugin>(plugin2)};
EXPECT_EQ(0, plugin1.GetOverlapSize(plugins));
}
TEST_P(PluginTest, getOverlapSizeShouldReturnZeroForPluginsThatDoNotOverlap) {
Plugin plugin1(
game_.Type(), game_.GetCache(), game_.DataPath() / blankEsm, false);
Plugin plugin2(
game_.Type(), game_.GetCache(), game_.DataPath() / blankEsp, false);
std::vector<std::shared_ptr<const Plugin>> plugins = {
std::make_shared<const Plugin>(plugin2)};
EXPECT_EQ(0, plugin1.GetOverlapSize(plugins));
}
TEST_P(PluginTest, getRecordAndGroupCountShouldReturnTheHeaderFieldValue) {
Plugin plugin(
game_.Type(), game_.GetCache(), game_.DataPath() / blankEsm, true);
if (GetParam() == GameType::tes3) {
EXPECT_EQ(10, plugin.GetRecordAndGroupCount());
} else if (GetParam() == GameType::tes4) {
EXPECT_EQ(14, plugin.GetRecordAndGroupCount());
} else {
EXPECT_EQ(15, plugin.GetRecordAndGroupCount());
}
}
TEST_P(PluginTest,
hasPluginFileExtensionShouldBeTrueIfFileEndsInDotEspOrDotEsm) {
EXPECT_TRUE(hasPluginFileExtension("file.esp", GetParam()));
@@ -127,7 +127,9 @@ protected:
// but we only have the one so no prefix is necessary.
INSTANTIATE_TEST_CASE_P(,
PluginSorterTest,
::testing::Values(GameType::tes4, GameType::fo4));
::testing::Values(GameType::tes3,
GameType::tes4,
GameType::fo4));
TEST_P(PluginSorterTest, sortingWithNoLoadedPluginsShouldReturnAnEmptyList) {
PluginSorter sorter;
@@ -136,46 +138,6 @@ TEST_P(PluginSorterTest, sortingWithNoLoadedPluginsShouldReturnAnEmptyList) {
EXPECT_TRUE(sorted.empty());
}
TEST_P(PluginSorterTest,
lightMasterFlaggedEspFilesShouldNotBeTreatedAsMasters) {
if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) {
ASSERT_NO_THROW(
std::filesystem::copy(dataPath / blankEsl, dataPath / blankEslEsp));
}
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
auto esp = PluginSortingData(
*dynamic_cast<const Plugin *>(game_.GetPlugin(blankEsp).get()),
PluginMetadata(),
PluginMetadata(),
getLoadOrder());
EXPECT_FALSE(esp.IsMaster());
auto master = PluginSortingData(
*dynamic_cast<const Plugin *>(game_.GetPlugin(blankEsm).get()),
PluginMetadata(),
PluginMetadata(),
getLoadOrder());
EXPECT_TRUE(master.IsMaster());
if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) {
auto lightMaster = PluginSortingData(
*dynamic_cast<const Plugin *>(game_.GetPlugin(blankEsl).get()),
PluginMetadata(),
PluginMetadata(),
getLoadOrder());
EXPECT_TRUE(lightMaster.IsMaster());
auto lightMasterEsp = PluginSortingData(
*dynamic_cast<const Plugin *>(game_.GetPlugin(blankEslEsp).get()),
PluginMetadata(),
PluginMetadata(),
getLoadOrder());
EXPECT_FALSE(lightMasterEsp.IsMaster());
}
}
TEST_P(PluginSorterTest,
sortingShouldNotMakeUnnecessaryChangesToAnExistingLoadOrder) {
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
@@ -0,0 +1,175 @@
/* LOOT
A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
Fallout: New Vegas.
Copyright (C) 2014-2016 WrinklyNinja
This file is part of LOOT.
LOOT is free software: you can redistribute
it and/or modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
LOOT is distributed in the hope that it will
be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with LOOT. If not, see
<https://www.gnu.org/licenses/>.
*/
#ifndef LOOT_TESTS_API_INTERNALS_SORTING_PLUGIN_SORTING_DATA_TEST
#define LOOT_TESTS_API_INTERNALS_SORTING_PLUGIN_SORTING_DATA_TEST
#include "api/sorting/plugin_sorting_data.h"
#include "tests/common_game_test_fixture.h"
namespace loot {
namespace test {
class PluginSortingDataTest : public CommonGameTestFixture {
protected:
PluginSortingDataTest() :
game_(GetParam(), dataPath.parent_path(), localPath),
blankEslEsp("Blank.esl.esp") {}
void loadInstalledPlugins(Game &game_, bool headersOnly) {
std::vector<std::string> plugins({
masterFile,
blankEsm,
blankDifferentEsm,
blankMasterDependentEsm,
blankDifferentMasterDependentEsm,
blankEsp,
blankDifferentEsp,
blankMasterDependentEsp,
blankDifferentMasterDependentEsp,
blankPluginDependentEsp,
blankDifferentPluginDependentEsp,
});
if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) {
plugins.push_back(blankEsl);
if (std::filesystem::exists(dataPath / blankEslEsp)) {
plugins.push_back(blankEslEsp);
}
}
game_.IdentifyMainMasterFile(masterFile);
game_.LoadCurrentLoadOrderState();
game_.LoadPlugins(plugins, headersOnly);
}
Game game_;
const std::string blankEslEsp;
};
// Pass an empty first argument, as it's a prefix for the test instantation,
// but we only have the one so no prefix is necessary.
INSTANTIATE_TEST_CASE_P(,
PluginSortingDataTest,
::testing::Values(GameType::tes3,
GameType::tes4,
GameType::fo4));
TEST_P(PluginSortingDataTest,
lightMasterFlaggedEspFilesShouldNotBeTreatedAsMasters) {
if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) {
ASSERT_NO_THROW(
std::filesystem::copy(dataPath / blankEsl, dataPath / blankEslEsp));
}
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
auto esp = PluginSortingData(
*dynamic_cast<const Plugin *>(game_.GetPlugin(blankEsp).get()),
PluginMetadata(),
PluginMetadata(),
getLoadOrder(),
game_.Type(),
game_.GetCache()->GetPlugins());
EXPECT_FALSE(esp.IsMaster());
auto master = PluginSortingData(
*dynamic_cast<const Plugin *>(game_.GetPlugin(blankEsm).get()),
PluginMetadata(),
PluginMetadata(),
getLoadOrder(),
game_.Type(),
game_.GetCache()->GetPlugins());
EXPECT_TRUE(master.IsMaster());
if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) {
auto lightMaster = PluginSortingData(
*dynamic_cast<const Plugin *>(game_.GetPlugin(blankEsl).get()),
PluginMetadata(),
PluginMetadata(),
getLoadOrder(),
game_.Type(),
game_.GetCache()->GetPlugins());
EXPECT_TRUE(lightMaster.IsMaster());
auto lightMasterEsp = PluginSortingData(
*dynamic_cast<const Plugin *>(game_.GetPlugin(blankEslEsp).get()),
PluginMetadata(),
PluginMetadata(),
getLoadOrder(),
game_.Type(),
game_.GetCache()->GetPlugins());
EXPECT_FALSE(lightMasterEsp.IsMaster());
}
}
TEST_P(PluginSortingDataTest,
numOverrideFormIdsShouldEqualSizeOfOverlapWithThePluginsMasters) {
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
auto plugin =
PluginSortingData(*dynamic_cast<const Plugin *>(
game_.GetPlugin(blankMasterDependentEsm).get()),
PluginMetadata(),
PluginMetadata(),
getLoadOrder(),
game_.Type(),
game_.GetCache()->GetPlugins());
EXPECT_EQ(4, plugin.NumOverrideFormIDs());
}
TEST_P(PluginSortingDataTest,
constructorShouldUseTotalRecordCountAsOverrideFormIdCountForTes3PluginWithAMasterThatIsNotLoaded) {
if (GetParam() != GameType::tes3) {
return;
}
ASSERT_NO_THROW(loadInstalledPlugins(game_, false));
// Pretend that blankEsm isn't loaded.
auto loadedPlugins = game_.GetCache()->GetPlugins();
for (auto it = loadedPlugins.begin(); it != loadedPlugins.end();) {
if ((*it)->GetName() == blankEsm) {
it = loadedPlugins.erase(it);
} else {
++it;
}
}
auto plugin =
PluginSortingData(*dynamic_cast<const Plugin *>(
game_.GetPlugin(blankMasterDependentEsm).get()),
PluginMetadata(),
PluginMetadata(),
getLoadOrder(),
game_.Type(),
loadedPlugins);
EXPECT_EQ(10, plugin.NumOverrideFormIDs());
}
}
}
#endif