From 52c17e3f5c323612d11a1f9f9fdf48ccac0f3a9a Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Wed, 21 Aug 2024 20:16:30 +0100 Subject: [PATCH] Sort blueprint plugins after all others Also validate that no non-blueprint masters or non-masters are expected to load after any blueprint masters. --- include/loot/enum/edge_type.h | 1 + src/api/sorting/plugin_sort.cpp | 141 +++++- src/api/sorting/plugin_sorting_data.cpp | 5 + src/api/sorting/plugin_sorting_data.h | 10 +- .../api/internals/sorting/plugin_graph_test.h | 7 +- .../api/internals/sorting/plugin_sort_test.h | 403 ++++++++++++++++++ .../sorting/plugin_sorting_data_test.h | 39 ++ 7 files changed, 581 insertions(+), 25 deletions(-) diff --git a/include/loot/enum/edge_type.h b/include/loot/enum/edge_type.h index 950cebb9..db7b135c 100644 --- a/include/loot/enum/edge_type.h +++ b/include/loot/enum/edge_type.h @@ -46,6 +46,7 @@ enum struct EdgeType : unsigned int { recordOverlap, assetOverlap, tieBreak, + blueprintMaster, }; } diff --git a/src/api/sorting/plugin_sort.cpp b/src/api/sorting/plugin_sort.cpp index 4a105ad3..1522e281 100644 --- a/src/api/sorting/plugin_sort.cpp +++ b/src/api/sorting/plugin_sort.cpp @@ -78,25 +78,40 @@ std::unordered_map GetGroupsMap( return groupsMap; } +bool IsInRange(const std::vector::const_iterator& begin, + const std::vector::const_iterator& end, + const std::string& name) { + return std::any_of(begin, end, [&](const PluginSortingData& plugin) { + return CompareFilenames(plugin.GetName(), name) == 0; + }); +} + void ValidateSpecificAndHardcodedEdges( const std::vector::const_iterator& begin, + const std::vector::const_iterator& firstBlueprintMaster, const std::vector::const_iterator& firstNonMaster, const std::vector::const_iterator& end, const std::vector& hardcodedPlugins) { const auto isNonMaster = [&](const std::string& name) { - return std::any_of( - firstNonMaster, end, [&](const PluginSortingData& plugin) { - return CompareFilenames(plugin.GetName(), name) == 0; - }); + return IsInRange(firstNonMaster, end, name); + }; + const auto isBlueprintMaster = [&](const std::string& name) { + return IsInRange(firstBlueprintMaster, firstNonMaster, name); }; - for (auto it = begin; it != firstNonMaster; ++it) { + for (auto it = begin; it != firstBlueprintMaster; ++it) { for (const auto& master : it->GetMasters()) { if (isNonMaster(master)) { throw CyclicInteractionError( std::vector{Vertex(master, EdgeType::master), Vertex(it->GetName(), EdgeType::masterFlag)}); } + + if (isBlueprintMaster(master)) { + throw CyclicInteractionError(std::vector{ + Vertex(master, EdgeType::master), + Vertex(it->GetName(), EdgeType::blueprintMaster)}); + } } for (const auto& file : it->GetMasterlistRequirements()) { @@ -106,6 +121,12 @@ void ValidateSpecificAndHardcodedEdges( std::vector{Vertex(name, EdgeType::masterlistRequirement), Vertex(it->GetName(), EdgeType::masterFlag)}); } + + if (isBlueprintMaster(name)) { + throw CyclicInteractionError(std::vector{ + Vertex(name, EdgeType::masterlistRequirement), + Vertex(it->GetName(), EdgeType::blueprintMaster)}); + } } for (const auto& file : it->GetUserRequirements()) { @@ -115,6 +136,12 @@ void ValidateSpecificAndHardcodedEdges( std::vector{Vertex(name, EdgeType::userRequirement), Vertex(it->GetName(), EdgeType::masterFlag)}); } + + if (isBlueprintMaster(name)) { + throw CyclicInteractionError(std::vector{ + Vertex(name, EdgeType::userRequirement), + Vertex(it->GetName(), EdgeType::blueprintMaster)}); + } } for (const auto& file : it->GetMasterlistLoadAfterFiles()) { @@ -124,6 +151,12 @@ void ValidateSpecificAndHardcodedEdges( std::vector{Vertex(name, EdgeType::masterlistLoadAfter), Vertex(it->GetName(), EdgeType::masterFlag)}); } + + if (isBlueprintMaster(name)) { + throw CyclicInteractionError(std::vector{ + Vertex(name, EdgeType::masterlistLoadAfter), + Vertex(it->GetName(), EdgeType::blueprintMaster)}); + } } for (const auto& file : it->GetUserLoadAfterFiles()) { @@ -133,6 +166,58 @@ void ValidateSpecificAndHardcodedEdges( std::vector{Vertex(name, EdgeType::userLoadAfter), Vertex(it->GetName(), EdgeType::masterFlag)}); } + + if (isBlueprintMaster(name)) { + throw CyclicInteractionError(std::vector{ + Vertex(name, EdgeType::userLoadAfter), + Vertex(it->GetName(), EdgeType::blueprintMaster)}); + } + } + } + + for (auto it = firstNonMaster; it != end; ++it) { + for (const auto& master : it->GetMasters()) { + if (isBlueprintMaster(master)) { + throw CyclicInteractionError(std::vector{ + Vertex(master, EdgeType::master), + Vertex(it->GetName(), EdgeType::blueprintMaster)}); + } + } + + for (const auto& file : it->GetMasterlistRequirements()) { + const auto name = std::string(file.GetName()); + if (isBlueprintMaster(name)) { + throw CyclicInteractionError(std::vector{ + Vertex(name, EdgeType::masterlistRequirement), + Vertex(it->GetName(), EdgeType::blueprintMaster)}); + } + } + + for (const auto& file : it->GetUserRequirements()) { + const auto name = std::string(file.GetName()); + if (isBlueprintMaster(name)) { + throw CyclicInteractionError(std::vector{ + Vertex(name, EdgeType::userRequirement), + Vertex(it->GetName(), EdgeType::blueprintMaster)}); + } + } + + for (const auto& file : it->GetMasterlistLoadAfterFiles()) { + const auto name = std::string(file.GetName()); + if (isBlueprintMaster(name)) { + throw CyclicInteractionError(std::vector{ + Vertex(name, EdgeType::masterlistLoadAfter), + Vertex(it->GetName(), EdgeType::blueprintMaster)}); + } + } + + for (const auto& file : it->GetUserLoadAfterFiles()) { + const auto name = std::string(file.GetName()); + if (isBlueprintMaster(name)) { + throw CyclicInteractionError(std::vector{ + Vertex(name, EdgeType::userLoadAfter), + Vertex(it->GetName(), EdgeType::blueprintMaster)}); + } } } @@ -231,26 +316,41 @@ std::vector SortPlugins( // two thirds of all edges added. The cost of each bidirectional search // scales with the number of edges, so reducing edges makes searches // faster. - // As such, sort plugins using two separate graphs for masters and - // non-masters. This means that any edges that go from a non-master to a - // master are effectively ignored, so won't cause cyclic interaction errors. - // Edges going the other way will also effectively be ignored, but that - // shouldn't have a noticeable impact. + // Similarly, blueprint plugins load after all others. + // As such, sort plugins using three separate graphs for masters, + // non-masters and blueprint plugins. This means that any edges that go from a + // non-master to a master are effectively ignored, so won't cause cyclic + // interaction errors. Edges going the other way will also effectively be + // ignored, but that shouldn't have a noticeable impact. const auto firstNonMasterIt = std::stable_partition( pluginsSortingData.begin(), pluginsSortingData.end(), [](const PluginSortingData& plugin) { return plugin.IsMaster(); }); + const auto firstBlueprintPluginIt = + std::stable_partition(pluginsSortingData.begin(), + firstNonMasterIt, + [](const PluginSortingData& plugin) { + return !plugin.IsBlueprintMaster(); + }); + ValidateSpecificAndHardcodedEdges(pluginsSortingData.begin(), + firstBlueprintPluginIt, firstNonMasterIt, pluginsSortingData.end(), earlyLoadingPlugins); - auto newLoadOrder = SortPlugins(pluginsSortingData.begin(), - firstNonMasterIt, - earlyLoadingPlugins, - groupsMap, - predecessorGroupsMap); + auto newMastersLoadOrder = SortPlugins(pluginsSortingData.begin(), + firstBlueprintPluginIt, + earlyLoadingPlugins, + groupsMap, + predecessorGroupsMap); + + const auto newBlueprintMastersLoadOrder = SortPlugins(firstBlueprintPluginIt, + firstNonMasterIt, + earlyLoadingPlugins, + groupsMap, + predecessorGroupsMap); const auto newNonMastersLoadOrder = SortPlugins(firstNonMasterIt, pluginsSortingData.end(), @@ -258,11 +358,14 @@ std::vector SortPlugins( groupsMap, predecessorGroupsMap); - newLoadOrder.insert(newLoadOrder.end(), - newNonMastersLoadOrder.begin(), - newNonMastersLoadOrder.end()); + newMastersLoadOrder.insert(newMastersLoadOrder.end(), + newNonMastersLoadOrder.begin(), + newNonMastersLoadOrder.end()); + newMastersLoadOrder.insert(newMastersLoadOrder.end(), + newBlueprintMastersLoadOrder.begin(), + newBlueprintMastersLoadOrder.end()); - return newLoadOrder; + return newMastersLoadOrder; } std::vector SortPlugins( diff --git a/src/api/sorting/plugin_sorting_data.cpp b/src/api/sorting/plugin_sorting_data.cpp index bd57bacc..0475ba16 100644 --- a/src/api/sorting/plugin_sorting_data.cpp +++ b/src/api/sorting/plugin_sorting_data.cpp @@ -82,6 +82,11 @@ bool PluginSortingData::IsMaster() const { return plugin_ != nullptr && plugin_->IsMaster(); } +bool PluginSortingData::IsBlueprintMaster() const { + return plugin_ != nullptr && plugin_->IsMaster() && + plugin_->IsBlueprintPlugin(); +} + bool PluginSortingData::LoadsArchive() const { return plugin_ != nullptr && plugin_->LoadsArchive(); } diff --git a/src/api/sorting/plugin_sorting_data.h b/src/api/sorting/plugin_sorting_data.h index 66e31db2..db4ac1f6 100644 --- a/src/api/sorting/plugin_sorting_data.h +++ b/src/api/sorting/plugin_sorting_data.h @@ -40,14 +40,14 @@ class PluginSortingData { * PluginSortingData objects must not live longer than the Plugin objects * that they are constructed from. */ - explicit PluginSortingData( - const PluginSortingInterface* plugin, - const PluginMetadata& masterlistMetadata, - const PluginMetadata& userMetadata, - const std::vector& loadOrder); + explicit PluginSortingData(const PluginSortingInterface* plugin, + const PluginMetadata& masterlistMetadata, + const PluginMetadata& userMetadata, + const std::vector& loadOrder); std::string GetName() const; bool IsMaster() const; + bool IsBlueprintMaster() const; bool LoadsArchive() const; std::vector GetMasters() const; size_t GetOverrideRecordCount() const; diff --git a/src/tests/api/internals/sorting/plugin_graph_test.h b/src/tests/api/internals/sorting/plugin_graph_test.h index c4b7339e..4da552f4 100644 --- a/src/tests/api/internals/sorting/plugin_graph_test.h +++ b/src/tests/api/internals/sorting/plugin_graph_test.h @@ -64,7 +64,7 @@ class TestPlugin : public PluginSortingInterface { bool IsUpdatePlugin() const override { return false; } - bool IsBlueprintPlugin() const override { return false; } + bool IsBlueprintPlugin() const override { return isBlueprintMaster_; } bool IsValidAsLightPlugin() const override { return false; } @@ -100,6 +100,10 @@ class TestPlugin : public PluginSortingInterface { void SetIsMaster(bool isMaster) { isMaster_ = isMaster; } + void SetIsBlueprintMaster(bool isBlueprintMaster) { + isBlueprintMaster_ = isBlueprintMaster; + } + void AddOverlappingRecords(const PluginInterface& plugin) { recordsOverlapWith.insert(&plugin); } @@ -122,6 +126,7 @@ class TestPlugin : public PluginSortingInterface { size_t overrideRecordCount_{0}; size_t assetCount_{0}; bool isMaster_{false}; + bool isBlueprintMaster_{false}; }; } diff --git a/src/tests/api/internals/sorting/plugin_sort_test.h b/src/tests/api/internals/sorting/plugin_sort_test.h index bc023fe9..c307614c 100644 --- a/src/tests/api/internals/sorting/plugin_sort_test.h +++ b/src/tests/api/internals/sorting/plugin_sort_test.h @@ -445,6 +445,33 @@ TEST_P(PluginSortTest, EXPECT_EQ(expectedSortedOrder, sorted); } +TEST_P(PluginSortTest, sortingShouldPutBlueprintPluginsAfterAllOthers) { + if (GetParam() != GameType::starfield) { + return; + } + + SetBlueprintFlag(dataPath / blankEsm); + + Game newGame(GetParam(), dataPath.parent_path(), localPath); + ASSERT_NO_THROW(loadInstalledPlugins(newGame, false)); + + std::vector expectedSortedOrder({ + masterFile, + blankDifferentEsm, + blankFullEsm, + blankMasterDependentEsm, + blankMediumEsm, + blankEsl, + blankEsp, + blankDifferentEsp, + blankMasterDependentEsp, + blankEsm, + }); + + const auto sorted = SortPlugins(newGame, newGame.GetLoadOrder()); + EXPECT_EQ(expectedSortedOrder, sorted); +} + TEST_P(PluginSortTest, sortingShouldThrowIfACyclicInteractionIsEncountered) { ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); @@ -617,6 +644,382 @@ TEST_P(PluginSortTest, e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); } } + +TEST_P(PluginSortTest, + sortingShouldThrowIfAMasterEdgeWouldPutABlueprintMasterBeforeAMaster) { + if (GetParam() != GameType::starfield) { + return; + } + + SetBlueprintFlag(dataPath / blankFullEsm); + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankFullEsm, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::master, e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankMasterDependentEsm, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::blueprintMaster, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P(PluginSortTest, + sortingShouldThrowIfAMasterEdgeWouldPutABlueprintMasterBeforeANonMaster) { + if (GetParam() != GameType::starfield) { + return; + } + + // Can't test with the test plugin files, so use the other SortPlugins() + // overload to provide stubs. + const auto esp = GetPlugin(blankMasterDependentEsp); + const auto blueprint = GetPlugin(blankFullEsm); + + esp->AddMaster(blankFullEsm); + blueprint->SetIsMaster(true); + blueprint->SetIsBlueprintMaster(true); + + std::vector pluginsSortingData{ + CreatePluginSortingData(esp->GetName()), + CreatePluginSortingData(blueprint->GetName())}; + + try { + SortPlugins(std::move(pluginsSortingData), {Group()}, {}, {}); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankFullEsm, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::master, e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankMasterDependentEsp, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::blueprintMaster, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P( + PluginSortTest, + sortingShouldThrowIfAMasterlistRequirementEdgeWouldPutABlueprintMasterBeforeAMaster) { + using std::endl; + if (GetParam() != GameType::starfield) { + return; + } + + SetBlueprintFlag(dataPath / blankDifferentEsm); + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + const auto masterlistPath = metadataFilesPath / "masterlist.yaml"; + std::ofstream masterlist(masterlistPath); + masterlist << "plugins:" << endl + << " - name: " << blankEsm << endl + << " req:" << endl + << " - " << blankDifferentEsm << endl; + masterlist.close(); + + game_.GetDatabase().LoadLists(masterlistPath); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::masterlistRequirement, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::blueprintMaster, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P( + PluginSortTest, + sortingShouldThrowIfAMasterlistRequirementEdgeWouldPutABlueprintMasterBeforeANonMaster) { + using std::endl; + if (GetParam() != GameType::starfield) { + return; + } + + SetBlueprintFlag(dataPath / blankDifferentEsm); + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + const auto masterlistPath = metadataFilesPath / "masterlist.yaml"; + std::ofstream masterlist(masterlistPath); + masterlist << "plugins:" << endl + << " - name: " << blankEsp << endl + << " req:" << endl + << " - " << blankDifferentEsm << endl; + masterlist.close(); + + game_.GetDatabase().LoadLists(masterlistPath); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::masterlistRequirement, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsp, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::blueprintMaster, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P( + PluginSortTest, + sortingShouldThrowIfAUserRequirementEdgeWouldPutABlueprintMasterBeforeAMaster) { + if (GetParam() != GameType::starfield) { + return; + } + + SetBlueprintFlag(dataPath / blankDifferentEsm); + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + PluginMetadata plugin(blankEsm); + plugin.SetRequirements({File(blankDifferentEsm)}); + + game_.GetDatabase().SetPluginUserMetadata(plugin); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::userRequirement, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::blueprintMaster, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P( + PluginSortTest, + sortingShouldThrowIfAUserRequirementEdgeWouldPutABlueprintMasterBeforeANonMaster) { + if (GetParam() != GameType::starfield) { + return; + } + + SetBlueprintFlag(dataPath / blankDifferentEsm); + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + PluginMetadata plugin(blankEsp); + plugin.SetRequirements({File(blankDifferentEsm)}); + + game_.GetDatabase().SetPluginUserMetadata(plugin); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::userRequirement, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsp, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::blueprintMaster, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P( + PluginSortTest, + sortingShouldThrowIfAMasterlistLoadAfterEdgeWouldPutABlueprintMasterBeforeAMaster) { + using std::endl; + if (GetParam() != GameType::starfield) { + return; + } + + SetBlueprintFlag(dataPath / blankDifferentEsm); + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + const auto masterlistPath = metadataFilesPath / "masterlist.yaml"; + std::ofstream masterlist(masterlistPath); + masterlist << "plugins:" << endl + << " - name: " << blankEsm << endl + << " after:" << endl + << " - " << blankDifferentEsm << endl; + masterlist.close(); + + game_.GetDatabase().LoadLists(masterlistPath); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::masterlistLoadAfter, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::blueprintMaster, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P( + PluginSortTest, + sortingShouldThrowIfAMasterlistLoadAfterEdgeWouldPutABlueprintMasterBeforeANonMaster) { + using std::endl; + if (GetParam() != GameType::starfield) { + return; + } + + SetBlueprintFlag(dataPath / blankDifferentEsm); + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + const auto masterlistPath = metadataFilesPath / "masterlist.yaml"; + std::ofstream masterlist(masterlistPath); + masterlist << "plugins:" << endl + << " - name: " << blankEsp << endl + << " after:" << endl + << " - " << blankDifferentEsm << endl; + masterlist.close(); + + game_.GetDatabase().LoadLists(masterlistPath); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::masterlistLoadAfter, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsp, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::blueprintMaster, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P( + PluginSortTest, + sortingShouldThrowIfAUserLoadAfterEdgeWouldPutABlueprintMasterBeforeAMaster) { + if (GetParam() != GameType::starfield) { + return; + } + + SetBlueprintFlag(dataPath / blankDifferentEsm); + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + PluginMetadata plugin(blankEsm); + plugin.SetLoadAfterFiles({File(blankDifferentEsm)}); + + game_.GetDatabase().SetPluginUserMetadata(plugin); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::userLoadAfter, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::blueprintMaster, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P( + PluginSortTest, + sortingShouldThrowIfAUserLoadAfterEdgeWouldPutABlueprintMasterBeforeANonMaster) { + if (GetParam() != GameType::starfield) { + return; + } + + SetBlueprintFlag(dataPath / blankDifferentEsm); + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + PluginMetadata plugin(blankEsp); + plugin.SetLoadAfterFiles({File(blankDifferentEsm)}); + + game_.GetDatabase().SetPluginUserMetadata(plugin); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankDifferentEsm, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::userLoadAfter, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsp, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::blueprintMaster, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P( + PluginSortTest, + sortingShouldNotThrowIfAHardcodedEdgeWouldPutABlueprintMasterBeforeAMaster) { + if (GetParam() != GameType::starfield) { + return; + } + // Can't test with the test plugin files, so use the other SortPlugins() + // overload to provide stubs. + const auto esm = GetPlugin(blankEsm); + const auto blueprint = GetPlugin(blankDifferentEsm); + + esm->SetIsMaster(true); + blueprint->SetIsMaster(true); + blueprint->SetIsBlueprintMaster(true); + + std::vector pluginsSortingData{ + CreatePluginSortingData(esm->GetName()), + CreatePluginSortingData(blueprint->GetName())}; + + const auto sorted = SortPlugins( + std::move(pluginsSortingData), {Group()}, {}, {blueprint->GetName()}); + + EXPECT_EQ(std::vector({ + blankEsm, blankDifferentEsm, + }), + sorted); +} + +TEST_P( + PluginSortTest, + sortingShouldNotThrowIfAHardcodedEdgeWouldPutABlueprintMasterBeforeANonMaster) { + if (GetParam() != GameType::starfield) { + return; + } + // Can't test with the test plugin files, so use the other SortPlugins() + // overload to provide stubs. + const auto esm = GetPlugin(blankEsp); + const auto blueprint = GetPlugin(blankDifferentEsm); + + esm->SetIsMaster(true); + blueprint->SetIsMaster(true); + blueprint->SetIsBlueprintMaster(true); + + std::vector pluginsSortingData{ + CreatePluginSortingData(esm->GetName()), + CreatePluginSortingData(blueprint->GetName())}; + + const auto sorted = SortPlugins( + std::move(pluginsSortingData), {Group()}, {}, {blueprint->GetName()}); + + EXPECT_EQ(std::vector({ + blankEsp, + blankDifferentEsm, + }), + sorted); +} } } diff --git a/src/tests/api/internals/sorting/plugin_sorting_data_test.h b/src/tests/api/internals/sorting/plugin_sorting_data_test.h index 3d59bbd7..cc580561 100644 --- a/src/tests/api/internals/sorting/plugin_sorting_data_test.h +++ b/src/tests/api/internals/sorting/plugin_sorting_data_test.h @@ -132,6 +132,45 @@ TEST_P(PluginSortingDataTest, EXPECT_EQ(4, plugin.GetOverrideRecordCount()); } } + +TEST_P(PluginSortingDataTest, + isBlueprintMasterShouldBeTrueIfPluginIsAMasterAndABlueprintPlugin) { + SetBlueprintFlag(dataPath / blankEsm); + SetBlueprintFlag(dataPath / blankEsp); + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + auto plugin = PluginSortingData( + dynamic_cast(game_.GetPlugin(blankEsm)), + PluginMetadata(), + PluginMetadata(), + getLoadOrder()); + if (GetParam() == GameType::starfield) { + EXPECT_TRUE(plugin.IsBlueprintMaster()); + } else { + EXPECT_FALSE(plugin.IsBlueprintMaster()); + } + + plugin = PluginSortingData( + dynamic_cast(game_.GetPlugin(blankDifferentEsm)), + PluginMetadata(), + PluginMetadata(), + getLoadOrder()); + EXPECT_FALSE(plugin.IsBlueprintMaster()); + + plugin = PluginSortingData(dynamic_cast(game_.GetPlugin(blankEsp)), + PluginMetadata(), + PluginMetadata(), + getLoadOrder()); + EXPECT_FALSE(plugin.IsBlueprintMaster()); + + plugin = PluginSortingData( + dynamic_cast(game_.GetPlugin(blankDifferentEsp)), + PluginMetadata(), + PluginMetadata(), + getLoadOrder()); + EXPECT_FALSE(plugin.IsBlueprintMaster()); +} } }