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