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