diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a7c8344..08c3f3ff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,7 +100,7 @@ set(LIBGIT2_LIBRARIES "${BINARY_DIR}/${CMAKE_CFG_INTDIR}/${CMAKE_STATIC_LIBRARY_ ExternalProject_Add(libloadorder PREFIX "external" - URL "https://github.com/WrinklyNinja/libloadorder/archive/11.2.2.tar.gz" + URL "https://github.com/WrinklyNinja/libloadorder/archive/11.3.0.tar.gz" CONFIGURE_COMMAND "" BUILD_IN_SOURCE 1 BUILD_COMMAND cargo build --release --manifest-path ffi/Cargo.toml --features ffi-headers --target ${RUST_TARGET} diff --git a/src/api/game/load_order_handler.cpp b/src/api/game/load_order_handler.cpp index 81df7c9b..294bd949 100644 --- a/src/api/game/load_order_handler.cpp +++ b/src/api/game/load_order_handler.cpp @@ -129,6 +129,26 @@ std::vector LoadOrderHandler::GetLoadOrder() const { return loadOrder; } +std::vector LoadOrderHandler::GetImplicitlyActivePlugins() const { + auto logger = getLogger(); + if (logger) { + logger->debug("Getting implicitly active plugins."); + } + + char** pluginArr; + size_t pluginArrSize; + + unsigned int ret = + lo_get_implicitly_active_plugins(gh_, &pluginArr, &pluginArrSize); + + HandleError("get implicitly active plugins", ret); + + std::vector loadOrder(pluginArr, pluginArr + pluginArrSize); + lo_free_string_array(pluginArr, pluginArrSize); + + return loadOrder; +} + void LoadOrderHandler::SetLoadOrder( const std::vector& loadOrder) const { auto logger = getLogger(); diff --git a/src/api/game/load_order_handler.h b/src/api/game/load_order_handler.h index 41b5d3b9..f1b38668 100644 --- a/src/api/game/load_order_handler.h +++ b/src/api/game/load_order_handler.h @@ -48,6 +48,8 @@ public: std::vector GetLoadOrder() const; + std::vector GetImplicitlyActivePlugins() const; + bool IsPluginActive(const std::string& pluginName) const; void SetLoadOrder(const std::vector& loadOrder) const; diff --git a/src/api/sorting/plugin_sorter.cpp b/src/api/sorting/plugin_sorter.cpp index 0532f0e5..d57d2a68 100644 --- a/src/api/sorting/plugin_sorter.cpp +++ b/src/api/sorting/plugin_sorter.cpp @@ -132,6 +132,8 @@ std::vector PluginSorter::Sort(Game& game) { } AddSpecificEdges(); + AddHardcodedPluginEdges(game); + AddGroupEdges(); if (logger_) { @@ -328,6 +330,51 @@ void PluginSorter::AddEdge(const vertex_t& fromVertex, } } +void PluginSorter::AddHardcodedPluginEdges(Game& game) { + if (logger_) { + logger_->trace("Adding hardcoded plugin edges."); + } + + auto implicitlyActivePlugins = + game.GetLoadOrderHandler()->GetImplicitlyActivePlugins(); + + std::set processedPlugins; + for (const auto& plugin : implicitlyActivePlugins) { + auto lowercasedName = boost::locale::to_lower(plugin); + processedPlugins.insert(lowercasedName); + + if (game.Type() == GameType::tes5 && lowercasedName == "update.esm") { + if (logger_) { + logger_->trace( + "Skipping adding hardcoded plugin edges for Update.esm as it does " + "not have a hardcoded position for Skyrim."); + continue; + } + } + + vertex_t pluginVertex; + + if (!GetVertexByName(plugin, pluginVertex)) { + if (logger_) { + logger_->trace( + "Skipping adding harcoded plugin edges for \"{}\" as it is not " + "installed.", + plugin); + } + continue; + } + + vertex_it vit, vitend; + for (tie(vit, vitend) = boost::vertices(graph_); vit != vitend; ++vit) { + auto& graphPlugin = graph_[*vit]; + + if (processedPlugins.count(graphPlugin.GetLowercasedName()) == 0) { + AddEdge(pluginVertex, *vit); + } + } + } +} + void PluginSorter::AddSpecificEdges() { // Add edges for all relationships that aren't overlaps. vertex_it vit, vitend; diff --git a/src/api/sorting/plugin_sorter.h b/src/api/sorting/plugin_sorter.h index e3a0dc8e..208ea38e 100644 --- a/src/api/sorting/plugin_sorter.h +++ b/src/api/sorting/plugin_sorter.h @@ -59,6 +59,7 @@ private: void AddPluginVertices(Game& game); void AddSpecificEdges(); + void AddHardcodedPluginEdges(Game& game); void AddGroupEdges(); void AddOverlapEdges(); void AddTieBreakEdges(); diff --git a/src/api/sorting/plugin_sorting_data.h b/src/api/sorting/plugin_sorting_data.h index 941fdde7..4913b8ba 100644 --- a/src/api/sorting/plugin_sorting_data.h +++ b/src/api/sorting/plugin_sorting_data.h @@ -42,7 +42,8 @@ public: std::unordered_set GetAfterGroupPlugins() const; void SetAfterGroupPlugins(std::unordered_set plugins); - + + using PluginMetadata::GetLowercasedName; using PluginMetadata::GetGroup; using PluginMetadata::GetLoadAfterFiles; using PluginMetadata::GetRequirements; diff --git a/src/tests/api/internals/game/load_order_handler_test.h b/src/tests/api/internals/game/load_order_handler_test.h index 6edcd869..cfc7f701 100644 --- a/src/tests/api/internals/game/load_order_handler_test.h +++ b/src/tests/api/internals/game/load_order_handler_test.h @@ -59,6 +59,38 @@ protected: loadOrderHandler_.Init(GetParam(), dataPath.parent_path(), localPath)); } + std::vector getImplicitlyActivePlugins() { + switch (GetParam()) { + case GameType::tes5: + return {"Skyrim.esm", "Update.esm"}; + case GameType::tes5se: + return {"Skyrim.esm", + "Update.esm", + "Dawnguard.esm", + "Hearthfires.esm", + "Dragonborn.esm"}; + case GameType::tes5vr: + return {"Skyrim.esm", + "Update.esm", + "Dawnguard.esm", + "Hearthfires.esm", + "Dragonborn.esm", + "SkyrimVR.esm"}; + case GameType::fo4: + return {"Fallout4.esm", + "DLCRobot.esm", + "DLCworkshop01.esm", + "DLCCoast.esm", + "DLCworkshop02.esm", + "DLCworkshop03.esm", + "DLCNukaWorld.esm"}; + case GameType::fo4vr: + return {"Fallout4.esm", "Fallout4_VR.esm"}; + default: + return {}; + } + } + LoadOrderHandler loadOrderHandler_; std::vector loadOrderToSet_; }; @@ -139,6 +171,25 @@ TEST_P(LoadOrderHandlerTest, getLoadOrderShouldReturnTheCurrentLoadOrder) { ASSERT_EQ(getLoadOrder(), loadOrderHandler_.GetLoadOrder()); } +TEST_P(LoadOrderHandlerTest, + getImplicitlyActivePluginsShouldThrowIfTheHandlerHasNotBeenInitialised) { + EXPECT_THROW(loadOrderHandler_.GetImplicitlyActivePlugins(), + std::system_error); +} +TEST_P( + LoadOrderHandlerTest, + getImplicitlyActivePluginsShouldReturnValidDataEvenIfStateHasNotBeenLoaded) { + initialiseHandler(); + + ASSERT_EQ(getImplicitlyActivePlugins(), + loadOrderHandler_.GetImplicitlyActivePlugins()); + + loadOrderHandler_.LoadCurrentState(); + + ASSERT_EQ(getImplicitlyActivePlugins(), + loadOrderHandler_.GetImplicitlyActivePlugins()); +} + TEST_P(LoadOrderHandlerTest, setLoadOrderShouldThrowIfTheHandlerHasNotBeenInitialised) { EXPECT_THROW(loadOrderHandler_.SetLoadOrder(loadOrderToSet_), diff --git a/src/tests/api/internals/sorting/plugin_sorter_test.h b/src/tests/api/internals/sorting/plugin_sorter_test.h index f84fe791..d1b92bc2 100644 --- a/src/tests/api/internals/sorting/plugin_sorter_test.h +++ b/src/tests/api/internals/sorting/plugin_sorter_test.h @@ -38,6 +38,7 @@ protected: PluginSorterTest() : game_(GetParam(), dataPath.parent_path(), localPath), masterlistPath_("./userlist.yaml"), + cccPath_(dataPath.parent_path() / getCCCFilename()), blankEslEsp("Blank.esl.esp") {} void TearDown() { @@ -48,6 +49,7 @@ protected: } boost::filesystem::remove(masterlistPath_); + boost::filesystem::remove(cccPath_); } void loadInstalledPlugins(Game &game_, bool headersOnly) { @@ -104,9 +106,32 @@ protected: masterlist.close(); } + std::string getCCCFilename() { + if (GetParam() == GameType::fo4) { + return "Fallout4.ccc"; + } + else { + // Not every game has a .ccc file, but Skyrim SE does, so just assume that. + return "Skyrim.ccc"; + } + } + + void GenerateCCCFile() { + using std::endl; + + if (GetParam() == GameType::fo4) { + boost::filesystem::ofstream ccc(cccPath_); + ccc << blankDifferentEsm << endl + << blankDifferentMasterDependentEsm << endl; + + ccc.close(); + } + } + Game game_; const std::string blankEslEsp; const boost::filesystem::path masterlistPath_; + const boost::filesystem::path cccPath_; }; // Pass an empty first argument, as it's a prefix for the test instantation, @@ -319,7 +344,6 @@ TEST_P( blankEsm, blankMasterDependentEsm, blankDifferentEsm, - masterFile, blankDifferentMasterDependentEsm, blankEsp, blankDifferentEsp, @@ -330,7 +354,10 @@ TEST_P( }); if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) { + expectedSortedOrder.insert(expectedSortedOrder.begin(), masterFile); expectedSortedOrder.insert(expectedSortedOrder.begin() + 5, blankEsl); + } else { + expectedSortedOrder.insert(expectedSortedOrder.begin() + 3, masterFile); } std::vector sorted = ps.Sort(game_); @@ -466,6 +493,38 @@ TEST_P(PluginSorterTest, EXPECT_EQ(expectedSortedOrder, sorted); } +TEST_P(PluginSorterTest, + sortingShouldUseTheGameCCCFileToEnforceHardcodedLoadOrderPositions) { + if (GetParam() != GameType::fo4) { + return; + } + + // Need to generate CCC file then recreate game object as the file is only + // read during intialisation. + GenerateCCCFile(); + Game newGame(GetParam(), dataPath.parent_path(), localPath); + ASSERT_NO_THROW(loadInstalledPlugins(newGame, false)); + + PluginSorter ps; + std::vector expectedSortedOrder({ + masterFile, + blankDifferentEsm, + blankDifferentMasterDependentEsm, + blankEsm, + blankMasterDependentEsm, + blankEsl, + blankEsp, + blankDifferentEsp, + blankMasterDependentEsp, + blankDifferentMasterDependentEsp, + blankPluginDependentEsp, + blankDifferentPluginDependentEsp, + }); + + std::vector sorted = ps.Sort(newGame); + EXPECT_EQ(expectedSortedOrder, sorted); +} + TEST_P(PluginSorterTest, sortingShouldThrowIfACyclicInteractionIsEncountered) { ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); PluginMetadata plugin(blankEsm);