Skip to content
This repository has been archived by the owner on Aug 4, 2023. It is now read-only.

Add "explicitMerge" ModOp #174

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

Shad0wlife
Copy link

This is effectively a more reliable merge with order independent patching (except for items with the same name, they are still targeted in their order relative to each other). The code for this modop is actually the same size or smaller compared to the "old" merge, at least when removing the comments.

Unlike "merge", "explicitMerge" requires the content of the ModOp tag to explicitly mirror the full structure of the targeted node (hence the name). No nodes will be removed or added, only the given pcdata nodes will be replaced.

This kind of "merge alternative" is especially useful, if we decide to close #173 due to the added complexity there.

A possible addition to this feature could be deprecating the "old" merge, or warning when it gets used for its reduced resilience to updates.

This is effectively a more reliable merge with order independent patching (except for items with the same name, they are still targeted in their order relative to each other).

Unlike merge, this ModOp requires the content of the ModOp tag to explicitly mirror the full structure of the targeted node (hence the name). No nodes will be removed or added, only the given pcdata nodes will be replaced.
@Shad0wlife
Copy link
Author

This is currently functionally identical to #187 , only that it's named explicitMerge. If this implementation is preferred over the non-recursive variant there, I can change the modop to be named "patch" as well.

@arduenify
Copy link

@Shad0wlife I didn't care to fork the repository, however this handles the silent fail by skipping the child and continuing on, and most of the code has been cleaned up and commented.

#pragma once

#include "pugixml.hpp"

#include <filesystem>
#include <optional>
#include <string>
#include <vector>
#include <stack>
#include <unordered_set>

namespace fs = std::filesystem;

// Different types XML operations that can be performed
enum class Type { None, Add, AddNextSibling, AddPrevSibling, Remove, Replace, Merge, Patch };

class XmlOperation
{
public:
    // Constructor for the XmlOperation class
    XmlOperation(std::shared_ptr<pugi::xml_document> doc, pugi::xml_node node,
                 std::string guid = "", std::string temp = "", std::string mod_name = "",
                 fs::path game_path = "", fs::path mod_path = "")
        : doc_(doc), node_(node), guid_(guid), temp_(temp), mod_name_(mod_name),
          game_path_(game_path), mod_path_(mod_path)
    {
        ReadPath(node_, guid_, temp_);
        ReadType(node_, mod_name_, game_path_, mod_path_);
    }

    // Getters for the various properties of the XmlOperation
    std::string GetGuid() const { return guid_; }
    std::string GetTemp() const { return temp_; }
    std::string GetModName() const { return mod_name_; }
    fs::path GetGamePath() const { return game_path_; }
    fs::path GetModPath() const { return mod_path_; }
    Type GetType() const { return type_; }

    pugi::xml_node GetContentNode() const { return content_node_; }

    // Functions to apply the XML operation to a target XML tree
    void Apply(std::shared_ptr<pugi::xml_document> doc);

private:
    // The patch operation is applied to the children of the patch and game nodes, recursively. It will skip the patch node if an error is returned for a particular child, rather than adding it to a stack. This will prevent the silent fail due to an empty stack. 
    void RecursivePatch(pugi::xml_node game_node, pugi::xml_node patch_node);

    // Function to apply the patch operation to the children of a patch node
    void PatchOp(pugi::xml_object_range<pugi::xml_node_iterator> patch_node_range, pugi::xml_node game_node);

    // Function to compare a patch node and a game node and apply the patch to the game node
    void PatchNode(pugi::xml_node game_node, pugi::xml_node patch_node);

    // Functions to read and parse the path and type attributes of an XML operation node
    void ReadPath(pugi::xml_node node, std::string guid, std::string temp);
    void ReadType(pugi::xml_node node, std::string mod_name, fs::path game_path, fs::path mod_path);

     // Private member variables for the XmlOperation class
    std::shared_ptr<pugi::xml_document> doc_;
    pugi::xml_node node_;
    std::string guid_;
    std::string temp_;
    std::string mod_name_;
    fs::path game_path_;
    fs::path mod_path_;
    Type type_;
    pugi::xml_node content_node_;
};

void XmlOperation::Apply(std::shared_ptr<pugi::xml_document> doc)
{
    pugi::xml_node game_node = doc->select_single_node(GetTemp().c_str()).node();
    if (!game_node)
    {
        throw std::runtime_error("Node not found: " + GetTemp());
    }

    // Perform the appropriate XML operation based on the type of the XmlOperation
    if (GetType() == XmlOperation::Type::Add)
    {
        for (auto &&node : GetContentNode())
        {
            game_node.append_copy(node);
        }
    }
    else if (GetType() == XmlOperation::Type::AddNextSibling)
    {
        for (auto &&node : GetContentNode())
        {
            game_node = game_node.parent().insert_copy_after(node, game_node);
        }
    }
    else if (GetType() == XmlOperation::Type::AddPrevSibling)
    {
        for (auto &&node : GetContentNode())
        {
            game_node = game_node.parent().insert_copy_before(node, game_node);
        }
    }
    else if (GetType() == XmlOperation::Type::Remove)
    {
        game_node.parent().remove_child(game_node);
    }
    else if (GetType() == XmlOperation::Type::Replace)
    {
        game_node.parent().replace_child(game_node, GetContentNode().first());
    }
    else if (GetType() == XmlOperation::Type::Merge)
    {
        auto content_node = GetContentNode();
        pugi::xml_node patching_node = *content_node.begin();
        RecursivePatch(game_node, patching_node);
    }
    else if (GetType() == XmlOperation::Type::Patch)
    {
        auto content_node = GetContentNode();
        PatchOp(content_node, game_node);
    }
}

void XmlOperation::RecursivePatch(pugi::xml_node game_node, pugi::xml_node patch_node)
{
    try
    {
        // Apply the patch to the current game and patch nodes
        PatchNode(game_node, patch_node);
    } catch (const std::exception& ex)
    {
        // If the patch operation fails, log the error and skip this node
        std::cerr << "Failed to patch node: " << ex.what() << std::endl;
        return;
    }

    // Recursively apply the patch operation to the children of the current game and patch nodes
    for (auto game_child = game_node.first_child(), patch_child = patch_node.first_child();
         game_child && patch_child;
         game_child = game_child.next_sibling(), patch_child = patch_child.next_sibling())
    {
        RecursivePatch(game_child, patch_child);
    }
}

void XmlOperation::PatchOp(pugi::xml_object_range<pugi::xml_node_iterator> patch_node_range, pugi::xml_node game_node)
{
    // Validate that the patch node has exactly one child
    if (patch_node_range.empty())
    {
        throw std::runtime_error("Patch node has no children");
    }
    if (std::next(patch_node_range.begin()) != patch_node_range.end())
    {
        throw std::runtime_error("Patch node has more than one child");
    }

    // Apply the patch to the children of the patch node
    RecursivePatch(game_node, *patch_node_range.begin());
}

void XmlOperation::PatchNode(pugi::xml_node game_node, pugi::xml_node patch_node)
{
     // Validate that the game and patch nodes have the same name
    if (std::string(game_node.name()) != std::string(patch_node.name()))
    {
        throw std::runtime_error("Patch node name does not match game node name");
    }

    // Iterate over the attributes of the patch node and update the corresponding attributes in the game node
    for (auto patch_attr = patch_node.attributes_begin(); patch_attr != patch_node.attributes_end(); ++patch_attr)
    {
        game_node.attribute(patch_attr->name()).set_value(patch_attr->value());
    }

    // Iterate over the children of the patch node and update the corresponding children in the game node
    for (auto patch_child = patch_node.first_child(), game_child = game_node.first_child();
         patch_child && game_child;
         patch_child = patch_child.next_sibling(), game_child = game_child.next_sibling())
    {
        PatchNode(game_child, patch_child);
    }
}

void XmlOperation::ReadPath(pugi::xml_node node, std::string guid, std::string temp)
{
    // Read and parse the path attribute of the XML operation node
    std::string path = node.attribute("path").as_string();
    size_t pos = path.find("{");
    while (pos != std::string::npos)
    {
        size_t end = path.find("}", pos);
        if (end == std::string::npos)
        {
            throw std::runtime_error("Invalid path attribute");
        }

        std::string tag = path.substr(pos + 1, end - pos - 1);
        if (tag == "guid")
        {
            path.replace(pos, end - pos + 1, guid);
        }
        else if (tag == "temp")
        {
            path.replace(pos, end - pos + 1, temp);
        }
        else
        {
            throw std::runtime_error("Invalid path attribute");
        }

        pos = path.find("{", end);
    }

    // Set the temp member variable to the parsed path
    temp_ = path;
}

void XmlOperation::ReadType(pugi::xml_node node, std::string mod_name, fs::path game_path, fs::path mod_path)
{
    // Read and parse the type attribute of the XML operation node
    std::string type = node.attribute("type").as_string();
    if (stricmp(type.c_str(), "add") == 0)
    {
        type_ = Type::Add;
    }
    else if (stricmp(type.c_str(), "patch") == 0)
    {
        type_ = Type::Patch;
    }
    else
    {
        type_ = Type::None;
        offset_data_t offset_data;
        offset_data.mod_name = mod_name;
        offset_data.game_path = game_path;
        offset_data.mod_path = mod_path;
        offset_data.type = type;
        offset_data.temp = temp_;
        offset_data.guid = guid_;
        offset_map[type] = offset_data;
    }
}

@jakobharder
Copy link
Contributor

Implemented as merge on jakobharder:feature/loader10 for now.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants