diff --git a/CMakeLists.txt b/CMakeLists.txt index 3cec4efd..8e43cd3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,6 +202,7 @@ set (LOOT_API_SRC "${CMAKE_BINARY_DIR}/generated/loot_version.cpp" "${CMAKE_SOURCE_DIR}/src/api/helpers/crc.cpp" "${CMAKE_SOURCE_DIR}/src/api/helpers/git_helper.cpp" "${CMAKE_SOURCE_DIR}/src/api/helpers/version.cpp" + "${CMAKE_SOURCE_DIR}/src/api/vertex.cpp" "${CMAKE_SOURCE_DIR}/src/api/resource.rc") set (LOOT_API_HEADERS "${CMAKE_SOURCE_DIR}/include/loot/api.h" @@ -213,6 +214,7 @@ set (LOOT_API_HEADERS "${CMAKE_SOURCE_DIR}/include/loot/api.h" "${CMAKE_SOURCE_DIR}/include/loot/exception/file_access_error.h" "${CMAKE_SOURCE_DIR}/include/loot/exception/git_state_error.h" "${CMAKE_SOURCE_DIR}/include/loot/exception/undefined_group_error.h" + "${CMAKE_SOURCE_DIR}/include/loot/enum/edge_type.h" "${CMAKE_SOURCE_DIR}/include/loot/enum/game_type.h" "${CMAKE_SOURCE_DIR}/include/loot/enum/log_level.h" "${CMAKE_SOURCE_DIR}/include/loot/enum/message_type.h" @@ -230,6 +232,7 @@ set (LOOT_API_HEADERS "${CMAKE_SOURCE_DIR}/include/loot/api.h" "${CMAKE_SOURCE_DIR}/include/loot/plugin_interface.h" "${CMAKE_SOURCE_DIR}/include/loot/struct/masterlist_info.h" "${CMAKE_SOURCE_DIR}/include/loot/struct/simple_message.h" + "${CMAKE_SOURCE_DIR}/include/loot/vertex.h" "${CMAKE_SOURCE_DIR}/src/api/api_database.h" "${CMAKE_SOURCE_DIR}/src/api/metadata/condition_evaluator.h" "${CMAKE_SOURCE_DIR}/src/api/metadata/condition_grammar.h" diff --git a/include/loot/database_interface.h b/include/loot/database_interface.h index ad4f7962..4b08e23d 100644 --- a/include/loot/database_interface.h +++ b/include/loot/database_interface.h @@ -29,6 +29,7 @@ #include #include +#include "loot/exception/cyclic_interaction_error.h" #include "loot/metadata/group.h" #include "loot/metadata/message.h" #include "loot/metadata/plugin_metadata.h" @@ -191,7 +192,8 @@ public: * metadata from the masterlist. * @returns An unordered set of Group objects. */ - virtual std::unordered_set GetGroups(bool includeUserMetadata = true) const = 0; + virtual std::unordered_set GetGroups( + bool includeUserMetadata = true) const = 0; /** * @brief Gets the groups that are defined or extended in the loaded userlist. @@ -207,6 +209,25 @@ public: */ virtual void SetUserGroups(const std::unordered_set& groups) = 0; + /** + * @brief Get the "shortest" path between the two given groups according to + * their load after metadata. + * @details The "shortest" path is defined as the path that maximises the + * amount of user metadata involved while minimising the amount of + * masterlist metadata involved. It's not the path involving the + * fewest groups. + * @param fromGroupName + * The name of the source group, that loads earlier. + * @param toGroupName + * The name of the destination group, that loads later. + * @returns A vector of Vertex elements representing the path from the source + * group to the destination group, or an empty vector if no path + * exists. + */ + virtual std::vector GetGroupsPath( + const std::string& fromGroupName, + const std::string& toGroupName) const = 0; + /** * @brief Set the groups diff --git a/include/loot/enum/edge_type.h b/include/loot/enum/edge_type.h new file mode 100644 index 00000000..fbce1722 --- /dev/null +++ b/include/loot/enum/edge_type.h @@ -0,0 +1,50 @@ +/* LOOT + +A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and +Fallout: New Vegas. + +Copyright (C) 2012-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 +. +*/ + +#ifndef LOOT_EDGE_TYPE +#define LOOT_EDGE_TYPE + +/** + * The namespace used by the LOOT API. + */ +namespace loot { +/** + * @brief An enum representing the different possible types of interactions + * between plugins or groups. + */ +enum struct EdgeType : unsigned int { + hardcoded, + masterFlag, + master, + masterlistRequirement, + userRequirement, + masterlistLoadAfter, + userLoadAfter, + group, + overlap, + tieBreak, +}; +} + +#endif diff --git a/include/loot/exception/cyclic_interaction_error.h b/include/loot/exception/cyclic_interaction_error.h index 91e4ce80..93ae95e5 100644 --- a/include/loot/exception/cyclic_interaction_error.h +++ b/include/loot/exception/cyclic_interaction_error.h @@ -26,60 +26,11 @@ #define LOOT_EXCEPTION_CYCLIC_INTERACTION_ERROR #include -#include #include +#include "loot/vertex.h" + namespace loot { -/** - * @brief An enum representing the different possible types of interactions - * between plugins or groups. - */ -enum struct EdgeType : unsigned int { - hardcoded, - masterFlag, - master, - masterlistRequirement, - userRequirement, - masterlistLoadAfter, - userLoadAfter, - group, - overlap, - tieBreak, -}; - -/** - * @brief A class representing a plugin or group vertex in a cyclic interaction - * path, and the type of the interaction with the next vertex in the - * path. - */ -class Vertex { -public: - /** - * @brief Construct a Vertex with the given name and out edge type. - * @param name The name of the plugin or group that this vertex represents. - * @param outEdgeType The type of the edge going out from this vertex. - */ - Vertex(std::string name, EdgeType outEdgeType); - - /** - * @brief Get the name of the plugin or group. - * @return The name of the plugin or group. - */ - std::string GetName() const; - - /** - * @brief Get the type of the edge going to the next vertex. - * @details Each edge goes from the vertex that loads earlier to the vertex - * that loads later. - * @return The edge type. - */ - EdgeType GetTypeOfEdgeToNextVertex() const; - -private: - std::string name_; - EdgeType outEdgeType_; -}; - /** * @brief An exception class thrown if a cyclic interaction is detected when * sorting a load order. diff --git a/include/loot/vertex.h b/include/loot/vertex.h new file mode 100644 index 00000000..d0cdc60a --- /dev/null +++ b/include/loot/vertex.h @@ -0,0 +1,74 @@ +/* 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 +. +*/ + +#ifndef LOOT_VERTEX +#define LOOT_VERTEX + +#include +#include + +#include "loot/api_decorator.h" +#include "loot/enum/edge_type.h" + +namespace loot { +/** + * @brief A class representing a plugin or group vertex in a path, and the + type of the edge to the next vertex in the path if one exists. + */ +class Vertex { +public: + /** + * @brief Construct a Vertex with the given name and no out edge. + * @param name The name of the plugin or group that this vertex represents. + */ + LOOT_API Vertex(std::string name); + + /** + * @brief Construct a Vertex with the given name and out edge type. + * @param name The name of the plugin or group that this vertex represents. + * @param outEdgeType The type of the edge going out from this vertex. + */ + LOOT_API Vertex(std::string name, EdgeType outEdgeType); + + /** + * @brief Get the name of the plugin or group. + * @return The name of the plugin or group. + */ + LOOT_API std::string GetName() const; + + /** + * @brief Get the type of the edge going to the next vertex. + * @details Each edge goes from the vertex that loads earlier to the vertex + * that loads later. + * @return The edge type. + */ + LOOT_API std::optional GetTypeOfEdgeToNextVertex() const; + +private: + std::string name_; + std::optional outEdgeType_; +}; +} + +#endif diff --git a/src/api/api_database.cpp b/src/api/api_database.cpp index 5f39114d..1796e142 100644 --- a/src/api/api_database.cpp +++ b/src/api/api_database.cpp @@ -31,6 +31,7 @@ #include "api/metadata/condition_evaluator.h" #include "api/metadata/yaml/plugin_metadata.h" #include "api/sorting/plugin_sorter.h" +#include "api/sorting/group_sort.h" #include "loot/metadata/group.h" #include "loot/exception/file_access_error.h" @@ -194,10 +195,20 @@ std::unordered_set ApiDatabase::GetGroups(bool includeUserMetadata) const std::unordered_set ApiDatabase::GetUserGroups() const { return userlist_.Groups(); } + void ApiDatabase::SetUserGroups(const std::unordered_set& groups) { userlist_.SetGroups(groups); } + +std::vector ApiDatabase::GetGroupsPath(const std::string& fromGroupName, + const std::string& toGroupName) const { + auto masterlistGroups = GetGroups(false); + auto userGroups = GetUserGroups(); + + return loot::GetGroupsPath(masterlistGroups, userGroups, fromGroupName, toGroupName); +} + std::optional ApiDatabase::GetPluginMetadata(const std::string& plugin, bool includeUserMetadata, bool evaluateConditions) const { diff --git a/src/api/api_database.h b/src/api/api_database.h index 1115ea4f..39823361 100644 --- a/src/api/api_database.h +++ b/src/api/api_database.h @@ -36,6 +36,7 @@ #include "api/metadata_list.h" #include "loot/database_interface.h" #include "loot/enum/game_type.h" +#include "loot/vertex.h" namespace loot { struct ApiDatabase : public DatabaseInterface { @@ -72,6 +73,8 @@ struct ApiDatabase : public DatabaseInterface { std::unordered_set GetGroups(bool includeUserMetadata = true) const; std::unordered_set GetUserGroups() const; void SetUserGroups(const std::unordered_set& groups); + std::vector GetGroupsPath(const std::string& fromGroupName, + const std::string& toGroupName) const; std::optional GetPluginMetadata( const std::string& plugin, diff --git a/src/api/sorting/cyclic_interaction_error.cpp b/src/api/sorting/cyclic_interaction_error.cpp index dd42803d..74c5d25d 100644 --- a/src/api/sorting/cyclic_interaction_error.cpp +++ b/src/api/sorting/cyclic_interaction_error.cpp @@ -24,14 +24,6 @@ #include "loot/exception/cyclic_interaction_error.h" namespace loot { -Vertex::Vertex(std::string name, EdgeType outEdgeType) : - name_(name), - outEdgeType_(outEdgeType) {} - -std::string Vertex::GetName() const { return name_; } - -EdgeType Vertex::GetTypeOfEdgeToNextVertex() const { return outEdgeType_; } - std::string describe(EdgeType edgeType) { switch (edgeType) { case EdgeType::hardcoded: @@ -63,8 +55,11 @@ std::string describe(EdgeType edgeType) { std::string describeCycle(const std::vector& cycle) { std::string text; for (const auto& vertex : cycle) { - text += vertex.GetName() + " --[" + - describe(vertex.GetTypeOfEdgeToNextVertex()) + "]-> "; + text += vertex.GetName(); + if (vertex.GetTypeOfEdgeToNextVertex().has_value()) { + text += " --[" + describe(vertex.GetTypeOfEdgeToNextVertex().value()) + + "]-> "; + } } if (!cycle.empty()) { text += cycle[0].GetName(); diff --git a/src/api/sorting/group_sort.cpp b/src/api/sorting/group_sort.cpp index 0307bf20..4bf019cd 100644 --- a/src/api/sorting/group_sort.cpp +++ b/src/api/sorting/group_sort.cpp @@ -25,6 +25,7 @@ #include "group_sort.h" #include +#include #include #include @@ -70,6 +71,7 @@ typedef boost::adjacency_list::vertex_descriptor vertex_t; typedef boost::graph_traits::edge_descriptor edge_t; +typedef boost::associative_property_map> edge_map_t; class AfterGroupsVisitor : public boost::dfs_visitor<> { public: @@ -97,16 +99,10 @@ std::string join(const std::unordered_set& set) { return output.substr(0, output.length() - 2); } -std::unordered_map> -GetTransitiveAfterGroups(const std::unordered_set& masterlistGroups, - const std::unordered_set& userGroups) { +GroupGraph BuildGraph(const std::unordered_set& masterlistGroups, + const std::unordered_set& userGroups) { GroupGraph graph; - auto logger = getLogger(); - if (logger) { - logger->info("Sorting groups according to their load after data"); - } - std::unordered_map groupVertices; for (const auto& group : masterlistGroups) { auto groupSortingData = GroupSortingData(group.GetName()); @@ -128,6 +124,7 @@ GetTransitiveAfterGroups(const std::unordered_set& masterlistGroups, } } + auto logger = getLogger(); for (const vertex_t& vertex : boost::make_iterator_range(boost::vertices(graph))) { auto group = graph[vertex]; @@ -164,6 +161,19 @@ GetTransitiveAfterGroups(const std::unordered_set& masterlistGroups, } } + return graph; +} + +std::unordered_map> +GetTransitiveAfterGroups(const std::unordered_set& masterlistGroups, + const std::unordered_set& userGroups) { + GroupGraph graph = BuildGraph(masterlistGroups, userGroups); + + auto logger = getLogger(); + if (logger) { + logger->info("Sorting groups according to their load after data"); + } + // Check for cycles. if (logger) { logger->trace("Checking for cycles in the group graph"); @@ -194,4 +204,85 @@ GetTransitiveAfterGroups(const std::unordered_set& masterlistGroups, return transitiveAfterGroups; } + +vertex_t GetVertexByName(const GroupGraph& graph, const std::string& name) { + for (const auto& vertex : + boost::make_iterator_range(boost::vertices(graph))) { + if (graph[vertex].GetName() == name) { + return vertex; + } + } + + auto logger = getLogger(); + if (logger) { + logger->error("Can't find group with name \"{}\"", name); + } + + throw std::invalid_argument("Can't find group with name \"" + name + "\""); +} + + +std::vector GetGroupsPath( + const std::unordered_set& masterlistGroups, + const std::unordered_set& userGroups, + const std::string& fromGroupName, + const std::string& toGroupName) { + GroupGraph graph = BuildGraph(masterlistGroups, userGroups); + + auto logger = getLogger(); + + auto fromVertex = GetVertexByName(graph, fromGroupName); + auto toVertex = GetVertexByName(graph, toGroupName); + + std::map weightMap; + for (const auto& edge : boost::make_iterator_range(boost::edges(graph))) { + if (graph[edge] == EdgeType::userLoadAfter) { + weightMap[edge] = -1000000; // Magnitude is an arbitrarily large number. + } else { + weightMap[edge] = 1; + } + } + + std::vector predecessors(boost::num_vertices(graph)); + std::vector distance(predecessors.size(), + (std::numeric_limits::max)()); + distance[toVertex] = 0; + + bellman_ford_shortest_paths( + graph, + boost::weight_map(edge_map_t(weightMap)) + .predecessor_map(boost::make_iterator_property_map( + predecessors.begin(), get(boost::vertex_index, graph))) + .distance_map(&distance[0]) + .root_vertex(toVertex)); + + std::vector path; + vertex_t currentVertex = fromVertex; + while (currentVertex != toVertex) { + auto nextVertex = predecessors[currentVertex]; + if (nextVertex == currentVertex) { + if (logger) { + logger->error( + "Unreachable vertex {} encountered while looking for vertex {}", + graph[currentVertex].GetName(), + graph[toVertex].GetName()); + } + return std::vector(); + } + + auto pair = boost::edge(nextVertex, currentVertex, graph); + if (!pair.second) { + throw std::runtime_error("Unexpectedly couldn't find edge between \"" + + graph[currentVertex].GetName() + "\" and \"" + + graph[nextVertex].GetName() + "\""); + } + auto vertex = Vertex(graph[currentVertex].GetName(), graph[pair.first]); + path.push_back(vertex); + + currentVertex = nextVertex; + } + path.push_back(Vertex(graph[currentVertex].GetName())); + + return path; +} } diff --git a/src/api/sorting/group_sort.h b/src/api/sorting/group_sort.h index 7825da58..1dde41a2 100644 --- a/src/api/sorting/group_sort.h +++ b/src/api/sorting/group_sort.h @@ -42,10 +42,17 @@ std::unordered_map> GetTransitiveAfterGroups(const std::unordered_set& masterlistGroups, const std::unordered_set& userGroups); +std::vector GetGroupsPath( + const std::unordered_set& masterlistGroups, + const std::unordered_set& userGroups, + const std::string& fromGroupName, + const std::string& toGroupName); + template class CycleDetector : public boost::dfs_visitor<> { public: - void tree_edge(typename boost::graph_traits::edge_descriptor edge, const G& graph) { + void tree_edge(typename boost::graph_traits::edge_descriptor edge, + const G& graph) { auto source = boost::source(edge, graph); auto vertex = Vertex(graph[source].GetName(), graph[edge]); @@ -64,7 +71,8 @@ public: trail.push_back(vertex); } - void back_edge(typename boost::graph_traits::edge_descriptor edge, const G& graph) { + void back_edge(typename boost::graph_traits::edge_descriptor edge, + const G& graph) { auto source = boost::source(edge, graph); auto target = boost::target(edge, graph); diff --git a/src/api/vertex.cpp b/src/api/vertex.cpp new file mode 100644 index 00000000..39ec6e43 --- /dev/null +++ b/src/api/vertex.cpp @@ -0,0 +1,38 @@ +/* LOOT + + A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and + Fallout: New Vegas. + + Copyright (C) 2018 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 + . + */ +#include "loot/vertex.h" + +namespace loot { +Vertex::Vertex(std::string name) : name_(name) {} + +Vertex::Vertex(std::string name, EdgeType outEdgeType) : + name_(name), + outEdgeType_(outEdgeType) {} + +std::string Vertex::GetName() const { return name_; } + +std::optional Vertex::GetTypeOfEdgeToNextVertex() const { + return outEdgeType_; +} +} diff --git a/src/tests/api/interface/database_interface_test.h b/src/tests/api/interface/database_interface_test.h index e08911cb..b353ae39 100644 --- a/src/tests/api/interface/database_interface_test.h +++ b/src/tests/api/interface/database_interface_test.h @@ -496,6 +496,23 @@ TEST_P(DatabaseInterfaceTest, EXPECT_TRUE(groups.find(Group("group4"))->GetAfterGroups().empty()); } +TEST_P(DatabaseInterfaceTest, + getGroupsPathShouldReturnTheShortestPathBetweenTheGivenGroups) { + ASSERT_NO_THROW(GenerateMasterlist()); + ASSERT_NO_THROW(GenerateUserlist()); + + ASSERT_NO_THROW( + db_->LoadLists(masterlistPath, userlistPath_)); + + auto path = db_->GetGroupsPath("group1", "group3"); + + ASSERT_EQ(2, path.size()); + EXPECT_EQ("group1", path[0].GetName()); + EXPECT_EQ(EdgeType::userLoadAfter, path[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ("group3", path[1].GetName()); + EXPECT_FALSE(path[1].GetTypeOfEdgeToNextVertex().has_value()); +} + TEST_P(DatabaseInterfaceTest, getKnownBashTagsShouldReturnAllBashTagsListedInLoadedMetadata) { ASSERT_NO_THROW(GenerateMasterlist()); diff --git a/src/tests/api/internals/sorting/group_sort_test.h b/src/tests/api/internals/sorting/group_sort_test.h index ae289871..9d370f8d 100644 --- a/src/tests/api/internals/sorting/group_sort_test.h +++ b/src/tests/api/internals/sorting/group_sort_test.h @@ -105,6 +105,80 @@ TEST(GetTransitiveAfterGroups, shouldThrowIfAfterGroupsAreCyclic) { } } } + +TEST(GetGroupsPath, shouldThrowIfTheFromGroupDoesNotExist) { + std::unordered_set groups({Group("a", {"c"}), Group("b", {"a"})}); + std::unordered_set userGroups({Group("c", {"b"})}); + + EXPECT_THROW(GetGroupsPath(groups, userGroups, "d", "a"), + std::invalid_argument); +} + +TEST(GetGroupsPath, shouldThrowIfTheToGroupDoesNotExist) { + std::unordered_set groups({Group("a", {"c"}), Group("b", {"a"})}); + std::unordered_set userGroups({Group("c", {"b"})}); + + EXPECT_THROW(GetGroupsPath(groups, userGroups, "a", "d"), + std::invalid_argument); +} + +TEST(GetGroupsPath, + shouldReturnAnEmptyVectorIfThereIsNoPathBetweenTheTwoGroups) { + std::unordered_set groups({Group("a", {}), + Group("b", {"a"}), + Group("c", {"a"}), + Group("d", {"c"}), + Group("e", {"b", "d"})}); + + auto path = GetGroupsPath(groups, {}, "b", "d"); + + EXPECT_TRUE(path.empty()); +} + +TEST(GetGroupsPath, + shouldFindThePathWithTheLeastNumberOfEdgesInAMasterlistOnlyGraph) { + std::unordered_set groups({Group("a", {}), + Group("b", {"a"}), + Group("c", {"a"}), + Group("d", {"c"}), + Group("e", {"b", "d"})}); + + auto path = GetGroupsPath(groups, {}, "a", "e"); + + ASSERT_EQ(3, path.size()); + EXPECT_EQ("a", path[0].GetName()); + EXPECT_EQ(EdgeType::masterlistLoadAfter, + path[0].GetTypeOfEdgeToNextVertex().value()); + EXPECT_EQ("b", path[1].GetName()); + EXPECT_EQ(EdgeType::masterlistLoadAfter, + path[1].GetTypeOfEdgeToNextVertex().value()); + EXPECT_EQ("e", path[2].GetName()); + EXPECT_FALSE(path[2].GetTypeOfEdgeToNextVertex().has_value()); +} + +TEST(GetGroupsPath, + shouldFindThePathWithTheLeastNumberOfEdgesThatContainsUserMetadata) { + std::unordered_set groups({Group("a", {}), + Group("b", {"a"}), + Group("c", {"a"}), + Group("e", {"b", "d"})}); + std::unordered_set userGroups({Group("d", {"c"})}); + + auto path = GetGroupsPath(groups, userGroups, "a", "e"); + + ASSERT_EQ(4, path.size()); + EXPECT_EQ("a", path[0].GetName()); + EXPECT_EQ(EdgeType::masterlistLoadAfter, + path[0].GetTypeOfEdgeToNextVertex().value()); + EXPECT_EQ("c", path[1].GetName()); + EXPECT_EQ(EdgeType::userLoadAfter, + path[1].GetTypeOfEdgeToNextVertex().value()); + EXPECT_EQ("d", path[2].GetName()); + EXPECT_EQ(EdgeType::masterlistLoadAfter, + path[2].GetTypeOfEdgeToNextVertex().value()); + EXPECT_EQ("e", path[3].GetName()); + EXPECT_FALSE(path[3].GetTypeOfEdgeToNextVertex().has_value()); +} } }