diff --git a/CMakeLists.txt b/CMakeLists.txt index 06201c84..ca81fad1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -182,6 +182,7 @@ set (LOOT_API_SRC "${CMAKE_BINARY_DIR}/generated/loot_version.cpp" "${CMAKE_SOURCE_DIR}/src/api/metadata_list.cpp" "${CMAKE_SOURCE_DIR}/src/api/masterlist.cpp" "${CMAKE_SOURCE_DIR}/src/api/plugin.cpp" + "${CMAKE_SOURCE_DIR}/src/api/sorting/group_sort.cpp" "${CMAKE_SOURCE_DIR}/src/api/sorting/plugin_sorter.cpp" "${CMAKE_SOURCE_DIR}/src/api/sorting/plugin_sorting_data.cpp" "${CMAKE_SOURCE_DIR}/src/api/helpers/crc.cpp" @@ -233,6 +234,7 @@ set (LOOT_API_HEADERS "${CMAKE_SOURCE_DIR}/include/loot/api.h" "${CMAKE_SOURCE_DIR}/src/api/metadata_list.h" "${CMAKE_SOURCE_DIR}/src/api/masterlist.h" "${CMAKE_SOURCE_DIR}/src/api/plugin.h" + "${CMAKE_SOURCE_DIR}/src/api/sorting/group_sort.h" "${CMAKE_SOURCE_DIR}/src/api/sorting/plugin_sorter.h" "${CMAKE_SOURCE_DIR}/src/api/sorting/plugin_sorting_data.h" "${CMAKE_SOURCE_DIR}/src/api/helpers/git_helper.h" @@ -263,6 +265,7 @@ set (LOOT_TESTS_HEADERS "${CMAKE_SOURCE_DIR}/src/tests/api/internals/game/game_t "${CMAKE_SOURCE_DIR}/src/tests/api/internals/metadata/priority_test.h" "${CMAKE_SOURCE_DIR}/src/tests/api/internals/metadata/tag_test.h" "${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/masterlist_test.h" "${CMAKE_SOURCE_DIR}/src/tests/api/internals/metadata_list_test.h" diff --git a/src/api/sorting/group_sort.cpp b/src/api/sorting/group_sort.cpp new file mode 100644 index 00000000..bb674249 --- /dev/null +++ b/src/api/sorting/group_sort.cpp @@ -0,0 +1,135 @@ +/* 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 "group_sort.h" + +#include +#include +#include + +#include "loot/exception/cyclic_interaction_error.h" + +namespace loot { +typedef boost::adjacency_list GroupGraph; +typedef boost::graph_traits::vertex_descriptor vertex_t; +typedef boost::graph_traits::edge_descriptor edge_t; + +class GroupCycleDetector : public boost::dfs_visitor<> { +public: + void tree_edge(edge_t edge, const GroupGraph& graph) { + auto source = boost::source(edge, graph); + auto name = graph[source]; + + // Check if the plugin already exists in the recorded trail. + auto it = find(begin(trail), end(trail), name); + + if (it != end(trail)) { + // Erase everything from this position onwards, as it doesn't + // contribute to a forward-cycle. + trail.erase(it, end(trail)); + } + + trail.push_back(name); + } + + void back_edge(edge_t edge, const GroupGraph& graph) { + auto source = boost::source(edge, graph); + auto target = boost::target(edge, graph); + + trail.push_back(graph[source]); + std::string backCycle; + auto it = find(begin(trail), end(trail), graph[target]); + for (it; it != end(trail); ++it) { + backCycle += *it + ", "; + } + backCycle.erase(backCycle.length() - 2); + + throw CyclicInteractionError( + graph[source], graph[target], backCycle); + } + +private: + std::vector trail; +}; + +class AfterGroupsVisitor : public boost::dfs_visitor<> { +public: + AfterGroupsVisitor(std::unordered_set& visitedGroups) : visitedGroups_(visitedGroups) {} + void tree_edge(edge_t edge, const GroupGraph& graph) { + auto target = boost::target(edge, graph); + visitedGroups_.insert(graph[target]); + } + + std::unordered_set get_visited_groups() const { + return visitedGroups_; + } +private: + std::unordered_set& visitedGroups_; +}; + +std::unordered_map> GetTransitiveAfterGroups(const std::unordered_set groups) { + GroupGraph graph; + + std::unordered_map groupVertices; + for (const auto& group : groups) { + auto vertex = boost::add_vertex(group.GetName(), graph); + groupVertices.emplace(group.GetName(), vertex); + } + + for (const auto& group : groups) { + for (const auto& otherGroupName : group.GetAfterGroups()) { + auto otherVertex = groupVertices.find(otherGroupName); + if (otherVertex == groupVertices.end()) { + throw std::invalid_argument("The group \"" + otherGroupName + "\" does not exist"); + } + + auto vertex = groupVertices[group.GetName()]; + boost::add_edge(vertex, otherVertex->second, graph); + } + } + + // Check for cycles. + boost::depth_first_search(graph, visitor(GroupCycleDetector())); + + std::unordered_map> transitiveAfterGroups; + for (const vertex_t& vertex : boost::make_iterator_range(boost::vertices(graph))) { + std::unordered_set visitedGroups; + AfterGroupsVisitor afterGroupsVisitor(visitedGroups); + + // Create a color map. + std::vector colorVec(boost::num_vertices(graph)); + auto colorMap = boost::make_iterator_property_map(colorVec.begin(), + boost::get(boost::vertex_index, graph), + colorVec[0]); + + boost::depth_first_visit(graph, vertex, afterGroupsVisitor, colorMap); + transitiveAfterGroups[graph[vertex]] = visitedGroups; + } + + return transitiveAfterGroups; +} +} diff --git a/src/api/sorting/group_sort.h b/src/api/sorting/group_sort.h new file mode 100644 index 00000000..6ba46bb2 --- /dev/null +++ b/src/api/sorting/group_sort.h @@ -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 + . + */ + +#ifndef LOOT_API_SORTING_GROUP_SORT +#define LOOT_API_SORTING_GROUP_SORT + +#include +#include +#include + +#include "loot/metadata/group.h" + +namespace loot { +// Map entries are a group name and names of transitive load after groups. +std::unordered_map> GetTransitiveAfterGroups(const std::unordered_set groups); +} +#endif diff --git a/src/tests/api/internals/main.cpp b/src/tests/api/internals/main.cpp index c9bd6c1b..c8ccf9e0 100644 --- a/src/tests/api/internals/main.cpp +++ b/src/tests/api/internals/main.cpp @@ -45,6 +45,7 @@ #include "tests/api/internals/metadata/priority_test.h" #include "tests/api/internals/metadata/tag_test.h" #include "tests/api/internals/metadata_list_test.h" +#include "tests/api/internals/sorting/group_sort_test.h" #include "tests/api/internals/sorting/plugin_sorter_test.h" #include "tests/api/internals/plugin_test.h" diff --git a/src/tests/api/internals/sorting/group_sort_test.h b/src/tests/api/internals/sorting/group_sort_test.h new file mode 100644 index 00000000..ffd8144c --- /dev/null +++ b/src/tests/api/internals/sorting/group_sort_test.h @@ -0,0 +1,70 @@ +/* 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 +. +*/ + +#ifndef LOOT_TESTS_API_INTERNALS_SORTING_GROUP_SORT_TEST +#define LOOT_TESTS_API_INTERNALS_SORTING_GROUP_SORT_TEST + +#include "api/sorting/group_sort.h" + +#include + +#include "loot/exception/cyclic_interaction_error.h" + +namespace loot { +namespace test { +TEST(GetTransitiveAfterGroups, shouldMapGroupsToTheirTransitiveAfterGroups) { + std::unordered_set groups({ + Group("a"), + Group("b", std::unordered_set({ "a" })), + Group("c", std::unordered_set({ "b" })) + }); + + auto mapped = GetTransitiveAfterGroups(groups); + + EXPECT_TRUE(mapped["a"].empty()); + EXPECT_EQ(std::unordered_set({ "a" }), mapped["b"]); + EXPECT_EQ(std::unordered_set({ "a", "b" }), mapped["c"]); +} + +TEST(GetTransitiveAfterGroups, shouldThrowIfAnAfterGroupDoesNotExist) { + std::unordered_set groups({ + Group("b", std::unordered_set({ "a" })) + }); + + EXPECT_THROW(GetTransitiveAfterGroups(groups), std::invalid_argument); +} + +TEST(GetTransitiveAfterGroups, shouldThrowIfAfterGroupsAreCyclic) { + std::unordered_set groups({ + Group("a", std::unordered_set({ "c" })), + Group("b", std::unordered_set({ "a" })), + Group("c", std::unordered_set({ "b" })) + }); + + EXPECT_THROW(GetTransitiveAfterGroups(groups), CyclicInteractionError); +} +} +} + +#endif