diff --git a/CMakeLists.txt b/CMakeLists.txt
index ce3bff78..90694d36 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -168,6 +168,7 @@ set (LOOT_API_SRC "${CMAKE_BINARY_DIR}/generated/loot_version.cpp"
"${CMAKE_SOURCE_DIR}/src/api/metadata/condition_evaluator.cpp"
"${CMAKE_SOURCE_DIR}/src/api/metadata/conditional_metadata.cpp"
"${CMAKE_SOURCE_DIR}/src/api/metadata/file.cpp"
+ "${CMAKE_SOURCE_DIR}/src/api/metadata/group.cpp"
"${CMAKE_SOURCE_DIR}/src/api/metadata/location.cpp"
"${CMAKE_SOURCE_DIR}/src/api/metadata/message.cpp"
"${CMAKE_SOURCE_DIR}/src/api/metadata/message_content.cpp"
@@ -202,6 +203,7 @@ set (LOOT_API_HEADERS "${CMAKE_SOURCE_DIR}/include/loot/api.h"
"${CMAKE_SOURCE_DIR}/include/loot/loot_version.h"
"${CMAKE_SOURCE_DIR}/include/loot/metadata/conditional_metadata.h"
"${CMAKE_SOURCE_DIR}/include/loot/metadata/file.h"
+ "${CMAKE_SOURCE_DIR}/include/loot/metadata/group.h"
"${CMAKE_SOURCE_DIR}/include/loot/metadata/location.h"
"${CMAKE_SOURCE_DIR}/include/loot/metadata/message.h"
"${CMAKE_SOURCE_DIR}/include/loot/metadata/message_content.h"
@@ -216,6 +218,7 @@ set (LOOT_API_HEADERS "${CMAKE_SOURCE_DIR}/include/loot/api.h"
"${CMAKE_SOURCE_DIR}/src/api/metadata/condition_evaluator.h"
"${CMAKE_SOURCE_DIR}/src/api/metadata/condition_grammar.h"
"${CMAKE_SOURCE_DIR}/src/api/metadata/yaml/file.h"
+ "${CMAKE_SOURCE_DIR}/src/api/metadata/yaml/group.h"
"${CMAKE_SOURCE_DIR}/src/api/metadata/yaml/location.h"
"${CMAKE_SOURCE_DIR}/src/api/metadata/yaml/message.h"
"${CMAKE_SOURCE_DIR}/src/api/metadata/yaml/message_content.h"
@@ -249,6 +252,7 @@ set (LOOT_TESTS_HEADERS "${CMAKE_SOURCE_DIR}/src/tests/api/internals/game/game_t
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/metadata/condition_grammar_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/metadata/conditional_metadata_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/metadata/file_test.h"
+ "${CMAKE_SOURCE_DIR}/src/tests/api/internals/metadata/group_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/metadata/location_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/metadata/message_test.h"
"${CMAKE_SOURCE_DIR}/src/tests/api/internals/metadata/message_content_test.h"
diff --git a/include/loot/metadata/group.h b/include/loot/metadata/group.h
new file mode 100644
index 00000000..73b32f4e
--- /dev/null
+++ b/include/loot/metadata/group.h
@@ -0,0 +1,105 @@
+/* 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_METADATA_GROUP
+#define LOOT_METADATA_GROUP
+
+#include
+#include
+
+#include "loot/api_decorator.h"
+
+namespace loot {
+/**
+ * Represents a group to which plugin metadata objects can belong.
+ */
+class Group {
+public:
+ /**
+ * Construct a Group with the name "default" and an empty set of groups to
+ * load after.
+ * @return A Group object.
+ */
+ LOOT_API Group();
+
+ /**
+ * Construct a File with the given name and an empty set of groups to load
+ * after.
+ * @param name
+ * The group name.
+ * @return A Group object.
+ */
+ LOOT_API Group(const std::string& name);
+
+ /**
+ * Construct a File with the given name and set of groups to load after.
+ * @param name
+ * The group name.
+ * @param afterGroups
+ * The names of groups this group loads after.
+ * @return A Group object.
+ */
+ LOOT_API Group(const std::string& name,
+ const std::unordered_set& afterGroups);
+
+ /**
+ * Check if two Group objects are equal by comparing their names.
+ * @returns True if the names are case-sensitively equal, false otherwise.
+ */
+ LOOT_API bool operator==(const Group& rhs) const;
+
+ /**
+ * Get the name of the group.
+ * @return The group's name.
+ */
+ LOOT_API std::string GetName() const;
+
+ /**
+ * Get the set of groups this group loads after.
+ * @return A set of group names.
+ */
+ LOOT_API std::unordered_set GetAfterGroups() const;
+
+private:
+ std::string name_;
+ std::unordered_set afterGroups_;
+};
+}
+
+namespace std {
+/**
+ * A specialisation of std::hash for loot::Group.
+ */
+template<>
+struct hash {
+ /**
+ * Calculate a hash value for a loot::Group object.
+ * @return The hash generated from the group's name.
+ */
+ size_t operator()(const loot::Group& group) const {
+ return hash()(group.GetName());
+ }
+};
+}
+
+#endif
diff --git a/src/api/metadata/group.cpp b/src/api/metadata/group.cpp
new file mode 100644
index 00000000..742e7d9a
--- /dev/null
+++ b/src/api/metadata/group.cpp
@@ -0,0 +1,50 @@
+/* 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/metadata/group.h"
+
+#include
+
+#include "api/metadata/yaml/group.h"
+
+namespace loot {
+Group::Group() : name_("default") {}
+
+Group::Group(const std::string& name) : name_(name) {}
+
+Group::Group(const std::string& name,
+ const std::unordered_set& afterGroups) :
+ name_(name),
+ afterGroups_(afterGroups) {}
+
+bool Group::operator==(const Group& rhs) const {
+ return name_ == rhs.name_;
+}
+
+std::string Group::GetName() const { return name_; }
+
+std::unordered_set Group::GetAfterGroups() const {
+ return afterGroups_;
+}
+}
diff --git a/src/api/metadata/yaml/group.h b/src/api/metadata/yaml/group.h
new file mode 100644
index 00000000..3b991098
--- /dev/null
+++ b/src/api/metadata/yaml/group.h
@@ -0,0 +1,86 @@
+/* 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_YAML_GROUP
+#define LOOT_YAML_GROUP
+
+#include
+#include
+
+#include
+
+#include "api/metadata/yaml/set.h"
+#include "loot/metadata/group.h"
+
+namespace YAML {
+template<>
+struct convert {
+ static Node encode(const loot::Group& rhs) {
+ Node node;
+ node["name"] = rhs.GetName();
+
+ auto afterGroups = rhs.GetAfterGroups();
+ if (!afterGroups.empty())
+ node["after"] = afterGroups;
+
+ return node;
+ }
+
+ static bool decode(const Node& node, loot::Group& rhs) {
+ if (!node.IsMap())
+ throw RepresentationException(
+ node.Mark(), "bad conversion: 'group' object must be a map");
+
+ if (!node["name"])
+ throw RepresentationException(
+ node.Mark(),
+ "bad conversion: 'name' key missing from 'file' map object");
+
+ std::string name = node["name"].as();
+
+ if (node["after"]) {
+ rhs = loot::Group(name, node["after"].as>());
+ } else {
+ rhs = loot::Group(name);
+ }
+
+ return true;
+ }
+};
+
+inline Emitter& operator<<(Emitter& out, const loot::Group& rhs) {
+ out << BeginMap
+ << Key << "name" << Value << YAML::SingleQuoted << rhs.GetName();
+
+ auto afterGroups = rhs.GetAfterGroups();
+ if (!afterGroups.empty()) {
+ out << Key << "after" << Value << afterGroups;
+ }
+
+ out << EndMap;
+
+ return out;
+}
+}
+
+#endif
diff --git a/src/tests/api/internals/main.cpp b/src/tests/api/internals/main.cpp
index 11e8d67e..6d4330bb 100644
--- a/src/tests/api/internals/main.cpp
+++ b/src/tests/api/internals/main.cpp
@@ -36,6 +36,7 @@
#include "tests/api/internals/metadata/condition_grammar_test.h"
#include "tests/api/internals/metadata/conditional_metadata_test.h"
#include "tests/api/internals/metadata/file_test.h"
+#include "tests/api/internals/metadata/group_test.h"
#include "tests/api/internals/metadata/location_test.h"
#include "tests/api/internals/metadata/message_content_test.h"
#include "tests/api/internals/metadata/message_test.h"
diff --git a/src/tests/api/internals/metadata/group_test.h b/src/tests/api/internals/metadata/group_test.h
new file mode 100644
index 00000000..30fbe986
--- /dev/null
+++ b/src/tests/api/internals/metadata/group_test.h
@@ -0,0 +1,150 @@
+/* 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_TESTS_API_INTERNALS_METADATA_GROUP_TEST
+#define LOOT_TESTS_API_INTERNALS_METADATA_GROUP_TEST
+
+#include "loot/metadata/group.h"
+
+#include
+
+#include "api/metadata/yaml/group.h"
+
+namespace loot {
+namespace test {
+TEST(Group, defaultConstructorShouldCreateDefaultGroup) {
+ Group group;
+
+ EXPECT_EQ("default", group.GetName());
+ EXPECT_TRUE(group.GetAfterGroups().empty());
+}
+
+TEST(Group, nameConstructorShouldCreateNamedGroup) {
+ Group group("group1");
+
+ EXPECT_EQ("group1", group.GetName());
+ EXPECT_TRUE(group.GetAfterGroups().empty());
+}
+
+TEST(Group, allArgsConstructorShouldStoreGivenValues) {
+ Group group("group1", std::unordered_set({"other_group"}));
+
+ EXPECT_EQ("group1", group.GetName());
+ EXPECT_EQ(std::unordered_set({"other_group"}), group.GetAfterGroups());
+}
+
+TEST(Group, groupsWithCaseInsensitiveEqualNameStringsShouldNotBeEqual) {
+ Group group1("name");
+ Group group2("Name");
+
+ EXPECT_FALSE(group1 == group2);
+}
+
+TEST(Group, groupsWithCaseSensitiveEqualNameStringsShouldBeEqual) {
+ Group group1("name");
+ Group group2("name");
+
+ EXPECT_TRUE(group1 == group2);
+}
+
+TEST(Group, groupsWithDifferentNamesShouldBeUnequal) {
+ Group group1("name1");
+ Group group2("name2");
+
+ EXPECT_FALSE(group1 == group2);
+}
+
+TEST(Group, emittingAsYamlShouldOmitAfterKeyIfAfterGroupsIsEmpty) {
+ Group group;
+
+ YAML::Emitter emitter;
+ emitter << group;
+
+ EXPECT_STREQ("name: 'default'", emitter.c_str());
+}
+
+TEST(Group, emittingAsYamlShouldIncludeAfterKeyIfAfterGroupsIsNotEmpty) {
+ Group group("group1", std::unordered_set({ "other_group" }));
+
+ YAML::Emitter emitter;
+ emitter << group;
+
+ EXPECT_STREQ(
+ "name: 'group1'\n"
+ "after:\n"
+ " - other_group",
+ emitter.c_str());
+}
+
+TEST(Group, encodingAsYamlShouldOmitAfterKeyIfAfterGroupsIsEmpty) {
+ Group group;
+ YAML::Node node;
+ node = group;
+
+ EXPECT_EQ("default", node["name"].as());
+ EXPECT_FALSE(node["after"]);
+}
+
+TEST(Group, encodingAsYamlShouldIncludeAfterKeyIfAfterGroupsIsNotEmpty) {
+ Group group("group1", std::unordered_set({ "other_group" }));
+ YAML::Node node;
+ node = group;
+
+ std::unordered_set expectedAfterGroups = { "other_group" };
+ EXPECT_EQ("group1", node["name"].as());
+ EXPECT_EQ(expectedAfterGroups, node["after"].as>());
+}
+
+TEST(Group, decodingFromYamlShouldSetGivenName) {
+ YAML::Node node = YAML::Load("{name: group1}");
+ Group group = node.as();
+
+ EXPECT_EQ("group1", group.GetName());
+ EXPECT_TRUE(group.GetAfterGroups().empty());
+}
+
+TEST(Group, decodingFromYamlShouldSetAfterGroupsIfAnyAreGiven) {
+ YAML::Node node = YAML::Load("{name: group1, after: [ other_group ]}");
+ Group group = node.as();
+
+ std::unordered_set expectedAfterGroups = { "other_group" };
+ EXPECT_EQ("group1", group.GetName());
+ EXPECT_EQ(expectedAfterGroups, group.GetAfterGroups());
+}
+
+TEST(Group, decodingFromYamlShouldThrowIfTheNameKeyIsMissing) {
+ YAML::Node node = YAML::Load("{after: []}");
+
+ EXPECT_THROW(node.as(), YAML::RepresentationException);
+}
+
+TEST(Group, decodingFromYamlShouldThrowIfAListIsGiven) {
+ YAML::Node node = YAML::Load("[0, 1, 2]");
+
+ EXPECT_THROW(node.as(), YAML::RepresentationException);
+}
+}
+}
+
+#endif