Add DatabaseInterface::GetGroupsPath()

It returns the path between the two given groups that maximises user
metadata and minimises masterlist metadata involved.
This commit is contained in:
Oliver Hamlet
2018-10-20 12:48:24 +01:00
parent 26b34b2d3b
commit 6264b8fa6d
13 changed files with 408 additions and 72 deletions
+3
View File
@@ -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"
+22 -1
View File
@@ -29,6 +29,7 @@
#include <string>
#include <vector>
#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<Group> GetGroups(bool includeUserMetadata = true) const = 0;
virtual std::unordered_set<Group> 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<Group>& 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<Vertex> GetGroupsPath(
const std::string& fromGroupName,
const std::string& toGroupName) const = 0;
/**
* @brief Set the groups
+50
View File
@@ -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
<https://www.gnu.org/licenses/>.
*/
#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
@@ -26,60 +26,11 @@
#define LOOT_EXCEPTION_CYCLIC_INTERACTION_ERROR
#include <stdexcept>
#include <string>
#include <vector>
#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.
+74
View File
@@ -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
<https://www.gnu.org/licenses/>.
*/
#ifndef LOOT_VERTEX
#define LOOT_VERTEX
#include <optional>
#include <string>
#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<EdgeType> GetTypeOfEdgeToNextVertex() const;
private:
std::string name_;
std::optional<EdgeType> outEdgeType_;
};
}
#endif
+11
View File
@@ -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<Group> ApiDatabase::GetGroups(bool includeUserMetadata) const
std::unordered_set<Group> ApiDatabase::GetUserGroups() const {
return userlist_.Groups();
}
void ApiDatabase::SetUserGroups(const std::unordered_set<Group>& groups) {
userlist_.SetGroups(groups);
}
std::vector<Vertex> 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<PluginMetadata> ApiDatabase::GetPluginMetadata(const std::string& plugin,
bool includeUserMetadata,
bool evaluateConditions) const {
+3
View File
@@ -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<Group> GetGroups(bool includeUserMetadata = true) const;
std::unordered_set<Group> GetUserGroups() const;
void SetUserGroups(const std::unordered_set<Group>& groups);
std::vector<Vertex> GetGroupsPath(const std::string& fromGroupName,
const std::string& toGroupName) const;
std::optional<PluginMetadata> GetPluginMetadata(
const std::string& plugin,
+5 -10
View File
@@ -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<Vertex>& 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();
+99 -8
View File
@@ -25,6 +25,7 @@
#include "group_sort.h"
#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/bellman_ford_shortest_paths.hpp>
#include <boost/graph/depth_first_search.hpp>
#include <boost/graph/graph_traits.hpp>
@@ -70,6 +71,7 @@ typedef boost::adjacency_list<boost::vecS,
GroupGraph;
typedef boost::graph_traits<GroupGraph>::vertex_descriptor vertex_t;
typedef boost::graph_traits<GroupGraph>::edge_descriptor edge_t;
typedef boost::associative_property_map<std::map<edge_t, int>> edge_map_t;
class AfterGroupsVisitor : public boost::dfs_visitor<> {
public:
@@ -97,16 +99,10 @@ std::string join(const std::unordered_set<std::string>& set) {
return output.substr(0, output.length() - 2);
}
std::unordered_map<std::string, std::unordered_set<std::string>>
GetTransitiveAfterGroups(const std::unordered_set<Group>& masterlistGroups,
const std::unordered_set<Group>& userGroups) {
GroupGraph BuildGraph(const std::unordered_set<Group>& masterlistGroups,
const std::unordered_set<Group>& userGroups) {
GroupGraph graph;
auto logger = getLogger();
if (logger) {
logger->info("Sorting groups according to their load after data");
}
std::unordered_map<std::string, vertex_t> groupVertices;
for (const auto& group : masterlistGroups) {
auto groupSortingData = GroupSortingData(group.GetName());
@@ -128,6 +124,7 @@ GetTransitiveAfterGroups(const std::unordered_set<Group>& 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<Group>& masterlistGroups,
}
}
return graph;
}
std::unordered_map<std::string, std::unordered_set<std::string>>
GetTransitiveAfterGroups(const std::unordered_set<Group>& masterlistGroups,
const std::unordered_set<Group>& 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<Group>& 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<Vertex> GetGroupsPath(
const std::unordered_set<Group>& masterlistGroups,
const std::unordered_set<Group>& 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<edge_t, int> 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<vertex_t> predecessors(boost::num_vertices(graph));
std::vector<int> distance(predecessors.size(),
(std::numeric_limits<int>::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<Vertex> 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<Vertex>();
}
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;
}
}
+10 -2
View File
@@ -42,10 +42,17 @@ std::unordered_map<std::string, std::unordered_set<std::string>>
GetTransitiveAfterGroups(const std::unordered_set<Group>& masterlistGroups,
const std::unordered_set<Group>& userGroups);
std::vector<Vertex> GetGroupsPath(
const std::unordered_set<Group>& masterlistGroups,
const std::unordered_set<Group>& userGroups,
const std::string& fromGroupName,
const std::string& toGroupName);
template<typename G>
class CycleDetector : public boost::dfs_visitor<> {
public:
void tree_edge(typename boost::graph_traits<G>::edge_descriptor edge, const G& graph) {
void tree_edge(typename boost::graph_traits<G>::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<G>::edge_descriptor edge, const G& graph) {
void back_edge(typename boost::graph_traits<G>::edge_descriptor edge,
const G& graph) {
auto source = boost::source(edge, graph);
auto target = boost::target(edge, graph);
+38
View File
@@ -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
<https://www.gnu.org/licenses/>.
*/
#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<EdgeType> Vertex::GetTypeOfEdgeToNextVertex() const {
return outEdgeType_;
}
}
@@ -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());
@@ -105,6 +105,80 @@ TEST(GetTransitiveAfterGroups, shouldThrowIfAfterGroupsAreCyclic) {
}
}
}
TEST(GetGroupsPath, shouldThrowIfTheFromGroupDoesNotExist) {
std::unordered_set<Group> groups({Group("a", {"c"}), Group("b", {"a"})});
std::unordered_set<Group> userGroups({Group("c", {"b"})});
EXPECT_THROW(GetGroupsPath(groups, userGroups, "d", "a"),
std::invalid_argument);
}
TEST(GetGroupsPath, shouldThrowIfTheToGroupDoesNotExist) {
std::unordered_set<Group> groups({Group("a", {"c"}), Group("b", {"a"})});
std::unordered_set<Group> userGroups({Group("c", {"b"})});
EXPECT_THROW(GetGroupsPath(groups, userGroups, "a", "d"),
std::invalid_argument);
}
TEST(GetGroupsPath,
shouldReturnAnEmptyVectorIfThereIsNoPathBetweenTheTwoGroups) {
std::unordered_set<Group> 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<Group> 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<Group> groups({Group("a", {}),
Group("b", {"a"}),
Group("c", {"a"}),
Group("e", {"b", "d"})});
std::unordered_set<Group> 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());
}
}
}