diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-14 02:45:12 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-14 02:45:25 -0600 |
| commit | 817e8f5cd26739c69d930d21cd9dc4c0b6e4984e (patch) | |
| tree | 52aed11d6751bb7d20b06acf197d56fac113203d /libs/installer_fomod_plus/share/xml | |
| parent | 51a9f8f197727f00896e5de44569b098923527dd (diff) | |
Add FUSE external mapping support, BG3/Oblivion Remastered fixes, fomod-plus and NaK integration
FUSE VFS now deploys non-data-dir mod mappings (Paks, OBSE, UE4SS, etc.)
via real symlinks and injects file-level data-dir mappings (plugins.txt,
loadorder.txt) into the VFS tree. Fixes game launches for Oblivion
Remastered (Root Builder path resolution, script extender support) and
BG3 (Wine prefix documents directory, file mapper symlinks on Linux).
Vendors mo2-fomod-plus plugin and NaK crate for FOMOD installer and
game finder/runtime support.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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) {} +}; |
