aboutsummaryrefslogtreecommitdiff
path: root/libs/installer_fomod_plus/share/xml
diff options
context:
space:
mode:
Diffstat (limited to 'libs/installer_fomod_plus/share/xml')
-rw-r--r--libs/installer_fomod_plus/share/xml/FomodInfoFile.cpp44
-rw-r--r--libs/installer_fomod_plus/share/xml/FomodInfoFile.h25
-rw-r--r--libs/installer_fomod_plus/share/xml/ModuleConfiguration.cpp352
-rw-r--r--libs/installer_fomod_plus/share/xml/ModuleConfiguration.h305
-rw-r--r--libs/installer_fomod_plus/share/xml/XmlHelper.h17
-rw-r--r--libs/installer_fomod_plus/share/xml/XmlParseException.h10
6 files changed, 753 insertions, 0 deletions
diff --git a/libs/installer_fomod_plus/share/xml/FomodInfoFile.cpp b/libs/installer_fomod_plus/share/xml/FomodInfoFile.cpp
new file mode 100644
index 0000000..d4fed69
--- /dev/null
+++ b/libs/installer_fomod_plus/share/xml/FomodInfoFile.cpp
@@ -0,0 +1,44 @@
+#include "FomodInfoFile.h"
+#include "XmlParseException.h"
+#include <format>
+#include <pugixml.hpp>
+
+#include "stringutil.h"
+
+#include <QFile>
+#include <QString>
+
+bool FomodInfoFile::deserialize(const QString& filePath)
+{
+ QFile file(filePath);
+ if (!file.open(QIODevice::ReadOnly)) {
+ throw XmlParseException(std::format("Failed to open file: {}", filePath.toStdString()));
+ }
+ const QByteArray content = file.readAll();
+ file.close();
+
+ pugi::xml_document doc;
+ if (const pugi::xml_parse_result result = doc.load_buffer(content.constData(), content.size()); !result) {
+ throw XmlParseException(std::format("XML parsed with errors: {}", result.description()));
+ }
+
+ const pugi::xml_node fomodNode = doc.child("fomod");
+ if (!fomodNode) {
+ throw XmlParseException("No <config> node found");
+ }
+
+ name = fomodNode.child("Name").text().as_string();
+ author = fomodNode.child("Author").text().as_string();
+ version = fomodNode.child("Version").text().as_string();
+ website = fomodNode.child("Website").text().as_string();
+ description = fomodNode.child("Description").text().as_string();
+
+ trim({ name, author, version, website, description });
+
+ for (pugi::xml_node groupNode : fomodNode.child("Groups").children("element")) {
+ groups.emplace_back(groupNode.text().as_string());
+ }
+
+ return true;
+
+} \ No newline at end of file
diff --git a/libs/installer_fomod_plus/share/xml/FomodInfoFile.h b/libs/installer_fomod_plus/share/xml/FomodInfoFile.h
new file mode 100644
index 0000000..d2c2a23
--- /dev/null
+++ b/libs/installer_fomod_plus/share/xml/FomodInfoFile.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include <qstring.h>
+#include <string>
+#include <vector>
+
+class FomodInfoFile {
+public:
+ bool deserialize(const QString &filePath);
+
+ [[nodiscard]] const std::string& getName() const { return name; }
+ [[nodiscard]] const std::string& getAuthor() const { return author; }
+ [[nodiscard]] const std::string& getVersion() const { return version; }
+ [[nodiscard]] const std::string& getWebsite() const { return website; }
+ [[nodiscard]] const std::string& getDescription() const { return description; }
+ [[nodiscard]] const std::vector<std::string>& getGroups() const { return groups; }
+
+private:
+ std::string name;
+ std::string author;
+ std::string version;
+ std::string website;
+ std::string description;
+ std::vector<std::string> groups;
+}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/share/xml/ModuleConfiguration.cpp b/libs/installer_fomod_plus/share/xml/ModuleConfiguration.cpp
new file mode 100644
index 0000000..64d6b0b
--- /dev/null
+++ b/libs/installer_fomod_plus/share/xml/ModuleConfiguration.cpp
@@ -0,0 +1,352 @@
+#include "ModuleConfiguration.h"
+
+#include <format>
+
+#include "XmlHelper.h"
+#include "XmlParseException.h"
+#include "stringutil.h"
+
+#include <QFile>
+#include <QString>
+
+using namespace StringConstants::FomodFiles;
+
+static GroupTypeEnum groupTypeFromString(const std::string& groupType)
+{
+ if (groupType == "SelectAny")
+ return SelectAny;
+ if (groupType == "SelectAll")
+ return SelectAll;
+ if (groupType == "SelectExactlyOne")
+ return SelectExactlyOne;
+ if (groupType == "SelectAtMostOne")
+ return SelectAtMostOne;
+ if (groupType == "SelectAtLeastOne")
+ return SelectAtLeastOne;
+ return SelectAny; // is this a sane default? probably
+}
+
+PluginTypeEnum pluginTypeFromString(const std::string& typeStr)
+{
+ if (typeStr == TYPE_REQUIRED)
+ return PluginTypeEnum::Required;
+ if (typeStr == TYPE_OPTIONAL)
+ return PluginTypeEnum::Optional;
+ if (typeStr == TYPE_RECOMMENDED)
+ return PluginTypeEnum::Recommended;
+ if (typeStr == TYPE_NOT_USABLE)
+ return PluginTypeEnum::NotUsable;
+ if (typeStr == TYPE_COULD_BE_USABLE)
+ return PluginTypeEnum::CouldBeUsable;
+ return PluginTypeEnum::Optional;
+}
+
+template <typename T>
+bool deserializeList(pugi::xml_node& node, const char* childName, std::vector<T>& list)
+{
+ for (pugi::xml_node childNode : node.children(childName)) {
+ T item;
+ item.deserialize(childNode);
+ list.push_back(item);
+ }
+ return true;
+}
+
+/*
+ * NOTE: We call 'trim()' on all error-prone fields that rely on user-input. I'm assuming these FOMODs are created with
+ * the FOMOD creation tool, so I won't trim things like flags that the tool sets for the user.
+ */
+
+bool FileDependency::deserialize(pugi::xml_node& node)
+{
+ file = node.attribute("file").as_string();
+ trim(file);
+ // ReSharper disable once CppTooWideScopeInitStatement
+ // Do not use 'auto' for these. it will break equality checks
+ const std::string stateStr = node.attribute("state").as_string();
+
+ if (stateStr == "Missing")
+ state = FileDependencyTypeEnum::Missing;
+ else if (stateStr == "Inactive")
+ state = FileDependencyTypeEnum::Inactive;
+ else if (stateStr == "Active")
+ state = FileDependencyTypeEnum::Active;
+ return true;
+}
+
+bool FlagDependency::deserialize(pugi::xml_node& node)
+{
+ flag = node.attribute("flag").as_string();
+ value = node.attribute("value").as_string();
+ trim({ flag, value });
+ return true;
+}
+
+bool GameDependency::deserialize(pugi::xml_node& node)
+{
+ version = node.attribute("version").as_string();
+ trim(version);
+ return true;
+}
+
+bool CompositeDependency::deserialize(pugi::xml_node& node)
+{
+
+ // this could EITHER have a dependencies child or the dependencies are here.
+ // turns out they could have both.
+ pugi::xml_node possibleNode = node;
+
+ // If the dependencies are all right inside, just use the root node as the dependency base.
+ // This looks hacky but accommodates both _nested_ dependencies for plugins, and extremely simple ones for step visibility.
+ if (node.child("dependencies") && !node.child("fileDependency") && !node.child("flagDependency") &&!node.child("gameDependency")) {
+ possibleNode = node.child("dependencies");
+ }
+
+ deserializeList(possibleNode, "fileDependency", fileDependencies);
+ deserializeList(possibleNode, "flagDependency", flagDependencies);
+ deserializeList(possibleNode, "gameDependency", gameDependencies);
+ deserializeList(possibleNode, "dependencies", nestedDependencies);
+
+ operatorType = OperatorTypeEnum::AND; // safest default.
+
+ if (const std::string operatorStr = possibleNode.attribute("operator").as_string(); operatorStr == "Or") {
+ operatorType = OperatorTypeEnum::OR;
+ }
+
+ return true;
+}
+
+bool DependencyPattern::deserialize(pugi::xml_node& node)
+{
+ if (!node)
+ return false;
+ pugi::xml_node dependenciesNode = node.child("dependencies");
+ dependencies.deserialize(dependenciesNode);
+
+ const pugi::xml_node typeNode = node.child("type");
+ type = pluginTypeFromString(typeNode.attribute("name").as_string());
+ return true;
+}
+
+bool DependencyPatternList::deserialize(pugi::xml_node& node)
+{
+ return deserializeList(node, "pattern", patterns);
+}
+
+bool DependencyPluginType::deserialize(pugi::xml_node& node)
+{
+ pugi::xml_node patternsNode = node.child("patterns");
+ const pugi::xml_node defaultTypeNode = node.child("defaultType");
+ defaultType = pluginTypeFromString(defaultTypeNode.attribute("name").as_string());
+ patterns.deserialize(patternsNode);
+ return true;
+}
+
+bool TypeDescriptor::deserialize(pugi::xml_node& node)
+{
+ pugi::xml_node dependencyTypeNode = node.child("dependencyType");
+ dependencyType.deserialize(dependencyTypeNode);
+ const pugi::xml_node typeNode = node.child("type");
+ type = pluginTypeFromString(typeNode.attribute("name").as_string());
+ return true;
+}
+
+bool Image::deserialize(pugi::xml_node& node)
+{
+ path = node.attribute("path").as_string();
+ return true;
+}
+
+bool HeaderImage::deserialize(pugi::xml_node& node)
+{
+ path = node.attribute("path").as_string();
+ showImage = node.attribute("showImage").as_bool();
+ showFade = node.attribute("showFade").as_bool();
+ height = node.attribute("height").as_int();
+ return true;
+}
+
+bool FileList::deserialize(pugi::xml_node& node)
+{
+ for (pugi::xml_node childNode : node.children()) {
+ if (std::string(childNode.name()) == "folder"
+ || std::string(childNode.name()) == "file") {
+ File file;
+ file.deserialize(childNode);
+ files.emplace_back(file);
+ }
+ }
+ return true;
+}
+
+bool ConditionalFileInstallPattern::deserialize(pugi::xml_node& node)
+{
+ pugi::xml_node dependenciesNode = node.child("dependencies");
+ pugi::xml_node filesNode = node.child("files");
+
+ dependencies.deserialize(dependenciesNode);
+ files.deserialize(filesNode);
+
+ return true;
+}
+
+// <flag name="2">On</flag>
+bool ConditionFlag::deserialize(pugi::xml_node& node)
+{
+ name = node.attribute("name").as_string();
+ value = node.child_value(); //
+ return true;
+}
+
+bool ConditionFlagList::deserialize(pugi::xml_node& node)
+{
+ return deserializeList(node, "flag", flags);
+}
+
+bool File::deserialize(pugi::xml_node& node)
+{
+ source = node.attribute("source").as_string();
+ // destination = node.attribute("destination").as_string();
+ priority = node.attribute("priority").as_int();
+ isFolder = strcmp(node.name(), "folder") == 0;
+ if (auto attr = node.attribute("destination"); attr) {
+ destination = attr.as_string();
+ } else {
+ destination = std::nullopt;
+ }
+ return true;
+}
+
+
+bool Plugin::deserialize(pugi::xml_node& node)
+{
+ pugi::xml_node imageNode = node.child("image");
+ pugi::xml_node typeDescriptorNode = node.child("typeDescriptor");
+ pugi::xml_node conditionFlagsNode = node.child("conditionFlags");
+ pugi::xml_node filesNode = node.child("files");
+
+ // Description is optional in the schema; guard against null C strings from pugixml.
+ if (const pugi::xml_node descNode = node.child("description")) {
+ if (const char* rawDesc = descNode.text().as_string()) {
+ description = rawDesc;
+ }
+ }
+ description = trim(description); // Find a better way to do this eventually.
+ image.deserialize(imageNode);
+ typeDescriptor.deserialize(typeDescriptorNode);
+ name = node.attribute("name").as_string();
+ name = trim(name);
+ conditionFlags.deserialize(conditionFlagsNode);
+ files.deserialize(filesNode);
+ return true;
+}
+
+bool PluginList::deserialize(pugi::xml_node& node)
+{
+ deserializeList(node, "plugin", plugins);
+ order = XmlHelper::getOrderType(node.attribute("order").as_string(), OrderTypeEnum::Ascending);
+
+ // Sort the plugins based on the specified order
+ std::ranges::sort(plugins, [this](const Plugin& a, const Plugin& b) {
+ if (order == OrderTypeEnum::Ascending) {
+ return a.name < b.name;
+ }
+ if (order == OrderTypeEnum::Descending) {
+ return a.name > b.name;
+ }
+ return false; // Default case, no sorting
+ });
+
+ return true;
+}
+
+bool Group::deserialize(pugi::xml_node& node)
+{
+ pugi::xml_node pluginsNode = node.child("plugins");
+ plugins.deserialize(pluginsNode);
+ name = node.attribute("name").as_string();
+ type = groupTypeFromString(node.attribute("type").as_string());
+ return true;
+}
+
+bool GroupList::deserialize(pugi::xml_node& node)
+{
+ deserializeList(node, "group", groups);
+ order = XmlHelper::getOrderType(node.attribute("order").as_string());
+
+ // Sort the groups based on the specified order
+ std::ranges::sort(groups, [this](const Group& a, const Group& b) {
+ if (order == OrderTypeEnum::Ascending) {
+ return a.name < b.name;
+ }
+ if (order == OrderTypeEnum::Descending) {
+ return a.name > b.name;
+ }
+ return false; // Default case, no sorting
+ });
+ return true;
+}
+
+bool InstallStep::deserialize(pugi::xml_node& node)
+{
+ pugi::xml_node visibleNode = node.child("visible");
+ pugi::xml_node optionalFileGroupsNode = node.child("optionalFileGroups");
+ visible.deserialize(visibleNode);
+ optionalFileGroups.deserialize(optionalFileGroupsNode);
+ name = node.attribute("name").as_string();
+ return true;
+}
+
+bool ConditionalFileInstall::deserialize(pugi::xml_node& node)
+{
+ pugi::xml_node patternsNode = node.child("patterns");
+ deserializeList(patternsNode, "pattern", patterns);
+ return true;
+}
+
+bool StepList::deserialize(pugi::xml_node& node)
+{
+ deserializeList(node, "installStep", installSteps);
+ order = XmlHelper::getOrderType(node.attribute("order").as_string());
+ return true;
+}
+
+bool ModuleConfiguration::deserialize(const QString& filePath)
+{
+ QFile file(filePath);
+ if (!file.open(QIODevice::ReadOnly)) {
+ throw XmlParseException(std::format("Failed to open file: {}", filePath.toStdString()));
+ }
+ const QByteArray content = file.readAll();
+ file.close();
+
+ pugi::xml_document doc;
+ if (const pugi::xml_parse_result result = doc.load_buffer(content.constData(), content.size()); !result) {
+ throw XmlParseException(std::format("XML parsed with errors: {}", result.description()));
+ }
+
+ const pugi::xml_node configNode = doc.child("config");
+ if (!configNode) {
+ throw XmlParseException("No <config> node found");
+ }
+
+ moduleName = configNode.child("moduleName").text().as_string();
+
+ moduleImage = HeaderImage();
+ pugi::xml_node moduleImageNode = configNode.child("moduleImage");
+ moduleImage.deserialize(moduleImageNode);
+
+ pugi::xml_node moduleDependenciesNode = configNode.child("moduleDependencies");
+ moduleDependencies.deserialize(moduleDependenciesNode);
+
+ pugi::xml_node requiredInstallFilesNode = configNode.child("requiredInstallFiles");
+ requiredInstallFiles.deserialize(requiredInstallFilesNode);
+
+ pugi::xml_node installStepsNode = configNode.child("installSteps");
+ installSteps.deserialize(installStepsNode);
+
+ pugi::xml_node conditionalFileInstallsNode = configNode.child("conditionalFileInstalls");
+ conditionalFileInstalls.deserialize(conditionalFileInstallsNode);
+
+ return true;
+}
diff --git a/libs/installer_fomod_plus/share/xml/ModuleConfiguration.h b/libs/installer_fomod_plus/share/xml/ModuleConfiguration.h
new file mode 100644
index 0000000..a0a017b
--- /dev/null
+++ b/libs/installer_fomod_plus/share/xml/ModuleConfiguration.h
@@ -0,0 +1,305 @@
+#pragma once
+
+#include <iostream>
+#include <optional>
+#include <pugixml.hpp>
+#include <qstring.h>
+#include <string>
+#include <vector>
+
+class XmlDeserializable {
+public:
+ virtual ~XmlDeserializable() = default;
+
+ virtual bool deserialize(pugi::xml_node& node) = 0;
+
+protected:
+ XmlDeserializable() = default;
+};
+
+enum GroupTypeEnum {
+ SelectAny,
+ SelectAll,
+ SelectExactlyOne,
+ SelectAtMostOne,
+ SelectAtLeastOne
+};
+
+enum class OperatorTypeEnum {
+ AND,
+ OR
+};
+
+enum class OrderTypeEnum {
+ Explicit,
+ Ascending,
+ Descending
+};
+
+enum class FileDependencyTypeEnum {
+ Missing,
+ Inactive,
+ Active,
+ UNKNOWN_STATE
+};
+
+template <typename T>
+class OrderedContents {
+public:
+ OrderTypeEnum order;
+
+ OrderedContents() : order(OrderTypeEnum::Ascending) {}
+ explicit OrderedContents(const OrderTypeEnum orderType): order(orderType) {}
+
+ template <typename Accessor>
+ bool compare(const T& a, const T& b, Accessor accessor) const
+ {
+ switch (order) {
+ case OrderTypeEnum::Ascending:
+ return accessor(a) < accessor(b);
+ case OrderTypeEnum::Descending:
+ return accessor(a) > accessor(b);
+ case OrderTypeEnum::Explicit:
+ default:
+ return false; // No sorting for explicit order
+ }
+ }
+};
+
+enum class PluginTypeEnum {
+ Recommended,
+ Required,
+ Optional,
+ NotUsable,
+ CouldBeUsable,
+ UNKNOWN
+};
+
+
+inline std::ostream& operator<<(std::ostream& os, const PluginTypeEnum& type)
+{
+ switch (type) {
+ case PluginTypeEnum::Recommended:
+ os << "Recommended";
+ break;
+ case PluginTypeEnum::Required:
+ os << "Required";
+ break;
+ case PluginTypeEnum::Optional:
+ os << "Optional";
+ break;
+ case PluginTypeEnum::NotUsable:
+ os << "NotUsable";
+ break;
+ case PluginTypeEnum::CouldBeUsable:
+ os << "CouldBeUsable";
+ break;
+ default: ;
+ }
+ return os;
+}
+
+class PluginType final : public XmlDeserializable {
+public:
+ PluginTypeEnum name = PluginTypeEnum::Optional; // sane default
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class FileDependency final : public XmlDeserializable {
+public:
+ std::string file;
+ FileDependencyTypeEnum state = FileDependencyTypeEnum::UNKNOWN_STATE;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class FlagDependency final : public XmlDeserializable {
+public:
+ std::string flag;
+ std::string value;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class GameDependency final : public XmlDeserializable {
+public:
+ std::string version;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class CompositeDependency final : public XmlDeserializable {
+public:
+ std::vector<FileDependency> fileDependencies;
+ std::vector<FlagDependency> flagDependencies;
+ std::vector<GameDependency> gameDependencies;
+ std::vector<CompositeDependency> nestedDependencies;
+ OperatorTypeEnum operatorType = OperatorTypeEnum::AND; // safest default.
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class DependencyPattern final : public XmlDeserializable {
+public:
+ CompositeDependency dependencies;
+ PluginTypeEnum type;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+
+class DependencyPatternList final : public XmlDeserializable {
+public:
+ std::vector<DependencyPattern> patterns;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class DependencyPluginType final : public XmlDeserializable {
+public:
+ std::optional<PluginTypeEnum> defaultType;
+ DependencyPatternList patterns;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class TypeDescriptor final : public XmlDeserializable {
+public:
+ DependencyPluginType dependencyType;
+ PluginTypeEnum type;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class Image final : public XmlDeserializable {
+public:
+ std::string path;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class HeaderImage final : public XmlDeserializable {
+public:
+ std::string path;
+ bool showImage;
+ bool showFade;
+ int height;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class File final : public XmlDeserializable {
+public:
+ std::string source;
+ std::optional<std::string> destination;
+ int priority{ 0 };
+ bool isFolder;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+
+class FileList final : public XmlDeserializable {
+public:
+ std::vector<File> files;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class ConditionalFileInstallPattern final : public XmlDeserializable {
+public:
+ CompositeDependency dependencies;
+ FileList files;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+// <flag name="2">On</flag>
+class ConditionFlag final : public XmlDeserializable {
+public:
+ std::string name;
+ std::string value;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class ConditionFlagList final : public XmlDeserializable {
+public:
+ std::vector<ConditionFlag> flags;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class Plugin final : public XmlDeserializable {
+public:
+ std::string description;
+ Image image;
+ TypeDescriptor typeDescriptor;
+ std::string name;
+ ConditionFlagList conditionFlags;
+ FileList files;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class PluginList final : public XmlDeserializable, public OrderedContents<Plugin> {
+public:
+ std::vector<Plugin> plugins;
+ OrderTypeEnum order;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class Group final : public XmlDeserializable {
+public:
+ PluginList plugins;
+ std::string name;
+ GroupTypeEnum type;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class GroupList final : public XmlDeserializable, public OrderedContents<Group> {
+public:
+ std::vector<Group> groups;
+ OrderTypeEnum order;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class InstallStep final : public XmlDeserializable {
+public:
+ CompositeDependency visible;
+ GroupList optionalFileGroups;
+ std::string name;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class ConditionalFileInstall final : public XmlDeserializable {
+public:
+ std::vector<ConditionalFileInstallPattern> patterns;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class StepList final : public XmlDeserializable, public OrderedContents<InstallStep> {
+public:
+ std::vector<InstallStep> installSteps;
+ OrderTypeEnum order;
+
+ bool deserialize(pugi::xml_node& node) override;
+};
+
+class ModuleConfiguration {
+public:
+ std::string moduleName;
+ HeaderImage moduleImage;
+ CompositeDependency moduleDependencies;
+ FileList requiredInstallFiles;
+ StepList installSteps;
+ ConditionalFileInstall conditionalFileInstalls;
+
+ bool deserialize(const QString& filePath);
+}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/share/xml/XmlHelper.h b/libs/installer_fomod_plus/share/xml/XmlHelper.h
new file mode 100644
index 0000000..6934a86
--- /dev/null
+++ b/libs/installer_fomod_plus/share/xml/XmlHelper.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include "ModuleConfiguration.h"
+
+class XmlHelper {
+public:
+ static OrderTypeEnum getOrderType(const std::string& orderType, OrderTypeEnum defaultOrder = OrderTypeEnum::Explicit)
+ {
+ if (orderType == "Explicit")
+ return OrderTypeEnum::Explicit;
+ if (orderType == "Ascending")
+ return OrderTypeEnum::Ascending;
+ if (orderType == "Descending")
+ return OrderTypeEnum::Descending;
+ return defaultOrder; // Ascending for plugins, Explicit for groups
+ }
+}; \ No newline at end of file
diff --git a/libs/installer_fomod_plus/share/xml/XmlParseException.h b/libs/installer_fomod_plus/share/xml/XmlParseException.h
new file mode 100644
index 0000000..6406f3b
--- /dev/null
+++ b/libs/installer_fomod_plus/share/xml/XmlParseException.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <stdexcept>
+#include <string>
+
+class XmlParseException final : public std::runtime_error {
+public:
+ explicit XmlParseException(const std::string& message)
+ : std::runtime_error(message) {}
+};