diff options
Diffstat (limited to 'libs/installer_fomod_plus/share/xml')
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) {} +}; |
