From c36eb4b0a6a89997d2f328b9d79ea95fe21d0254 Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Sat, 2 Jun 2018 22:22:42 +0100 Subject: [PATCH] Enforce hardcoded plugin positions during sorting It's very verbose to enforce this through masterlist metadata, and the data is available through libloadorder. Don't treat Update.esm for the original Skyrim as having a hardcoded position, as it doesn't even though it's implicitly active. --- CMakeLists.txt | 2 +- src/api/game/load_order_handler.cpp | 20 ++++++ src/api/game/load_order_handler.h | 2 + src/api/sorting/plugin_sorter.cpp | 47 ++++++++++++++ src/api/sorting/plugin_sorter.h | 1 + src/api/sorting/plugin_sorting_data.h | 3 +- .../internals/game/load_order_handler_test.h | 51 ++++++++++++++++ .../internals/sorting/plugin_sorter_test.h | 61 ++++++++++++++++++- 8 files changed, 184 insertions(+), 3 deletions(-) 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);