Index: binaries/data/mods/public/simulation/templates/template_unit_infantry.xml =================================================================== --- binaries/data/mods/public/simulation/templates/template_unit_infantry.xml +++ binaries/data/mods/public/simulation/templates/template_unit_infantry.xml @@ -1,59 +1,30 @@ + - - 2 - 4 - 15 - - - - 2.5 - 4 - 1000 - Field Palisade Wall - - - - 50.0 - 0.0 - 0.0 - - 2 - - - - 1.0 - - structures/{civ}_civil_centre - structures/{civ}_crannog - structures/{civ}_military_colony - structures/{civ}_house - structures/{civ}_storehouse - structures/{civ}_farmstead - structures/{civ}_field - structures/{civ}_corral - structures/{civ}_outpost - structures/wallset_palisade - structures/{civ}_sentry_tower - structures/{civ}_dock - structures/{civ}_barracks - structures/{civ}_blacksmith - structures/{civ}_temple - structures/{civ}_market - structures/{civ}_defense_tower - structures/{civ}_wallset_stone - structures/{civ}_workshop - structures/{civ}_fortress - structures/{civ}_wonder - - - + + + 2.2 + 1 + 1.1 + + + + + 5 + -Field Jackson Five + + + + 10.0 + + + + + test + 10 - 50 - 0 - 0 - 0 + 10 Index: binaries/data/mods/public/simulation/templates/test.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/templates/test.xml @@ -0,0 +1,26 @@ + + 1.0 + + structures/{civ}_civil_centre + structures/{civ}_crannog + structures/{civ}_military_colony + structures/{civ}_house + structures/{civ}_storehouse + structures/{civ}_farmstead + structures/{civ}_field + structures/{civ}_corral + structures/{civ}_outpost + structures/wallset_palisade + structures/{civ}_sentry_tower + structures/{civ}_dock + structures/{civ}_barracks + structures/{civ}_blacksmith + structures/{civ}_temple + structures/{civ}_market + structures/{civ}_defense_tower + structures/{civ}_wallset_stone + structures/{civ}_workshop + structures/{civ}_fortress + structures/{civ}_wonder + + Index: binaries/data/mods/public/simulation/templates/test_attarm.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/templates/test_attarm.xml @@ -0,0 +1,23 @@ + + + 2 + 4 + 15 + + + + 2.5 + 4 + 1000 + Field Palisade Wall + + + + 50.0 + 0.0 + 0.0 + + 2 + + + Index: binaries/data/mods/public/simulation/templates/test_cost.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/simulation/templates/test_cost.xml @@ -0,0 +1,10 @@ + + 1 + 0 + + 50 + 0 + 0 + 0 + + Index: source/ps/DataTree.h =================================================================== --- /dev/null +++ source/ps/DataTree.h @@ -0,0 +1,118 @@ +/* Copyright (C) 2020 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. 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 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. 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 0 A.D. If not, see . + */ + +#ifndef INCLUDED_DATATREE +#define INCLUDED_DATATREE + +#include "ps/Filesystem.h" + +template +class DataTree : public T +{ +private: + using doctype = typename T::doctype; + using nodetype = typename T::nodetype; + + // This defines the interface of T + void ParseFile(const CVFSFile& input, const VfsPath& filename) { T::ParseFile(input, filename); } + bool CheckForInclude() const { return T::CheckForInclude(); } + + bool IsIncludeNode(const nodetype& node) const { return T::IsIncludeNode(node); } + VfsPath GetIncludePath(const VfsPath& filename, const nodetype& node) const { return T::GetIncludePath(filename, node); } + bool IsReplaceNode(const nodetype& node) const { return T::IsReplaceNode(node); } + + nodetype GetRootNode(const doctype& doc) const { return T::GetRootNode(doc); } + nodetype GetSameChild(const nodetype& node, const nodetype& similar_to) const { return T::GetSameChild(node, similar_to); } + + void UpdateNode(nodetype& node_to_update, const nodetype& reference) { T::UpdateNode(node_to_update, reference); } + void AddChild(nodetype& node, nodetype& child) { T::AddChild(node, child); } + void DeleteNode(nodetype& node) { T::DeleteNode(node); } + void PostMergeProcessing(nodetype& node) { T::PostMergeProcessing(node); } + + PSRETURN MergeNodes(const PIVFS& vfs, const VfsPath& filename, nodetype& node, nodetype merge_from) + { + // Update node attributes + UpdateNode(node, merge_from); + + // Merge mode -> loop through our own nodes, if it exists in merge_from, modulate / replace / else + // If it doesn't, just use as-is. + // Then add all unused node from the target doc. + for (nodetype child = node.begin(); child != node.end(); ++child) + { + nodetype merge_child = GetSameChild(merge_from, child); + if (merge_child && !IsReplaceNode(child) && !IsIncludeNode(child)) + MergeNodes(vfs, filename, child, merge_child); + else + ParseNode(vfs, filename, child); + + if (merge_child) + DeleteNode(merge_child); + } + for (nodetype child : merge_from) + AddChild(node, child); + PostMergeProcessing(node); + return 0; + } + + PSRETURN ParseNode(const PIVFS& vfs, const VfsPath& filename, nodetype& node) + { + // Regular path -> just recurse. + if (!IsIncludeNode(node)) + // Cannot use a range-for loop as the iterator is the node and must be passed by reference below. + for (nodetype child = node.begin(); child != node.end(); ++child) + ParseNode(vfs, filename, child); + else + { + // In this path, we'll load the included DataTree, then recursively merge us into them. + DataTree merge_from; + int err = merge_from.Load(vfs, GetIncludePath(filename, node)); + if (err) + return err; + MergeNodes(vfs, filename, node, GetRootNode(merge_from)); + return 0; + } + return 0; + } +public: + operator doctype() { return GetDoc(); } + doctype GetDoc() { return T::GetDoc(); } + + PSRETURN Load(const PIVFS& vfs, const VfsPath& filename) + { + CVFSFile input; + if (input.Load(vfs, filename)) + { + LOGERROR("CXeromyces: Failed to open XML file %s", filename.string8()); + return 1; + } + + ParseFile(input, filename); + + bool use_include = CheckForInclude(); + + if (!use_include) + return 0; + + // Iterate over nodes. + nodetype root = GetRootNode(GetDoc()); + ParseNode(vfs, filename, root); + + return 0; + } +}; + +#endif // INCLUDED_DATATREE Index: source/ps/DataTreeXML.h =================================================================== --- /dev/null +++ source/ps/DataTreeXML.h @@ -0,0 +1,100 @@ +/* Copyright (C) 2020 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. 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 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. 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 0 A.D. If not, see . + */ + +#ifndef INCLUDED_DATATREEXML +#define INCLUDED_DATATREEXML + +#include "DataTree.h" + +#include + +class DataTreeXMLImpl +{ +protected: + struct NodeWrapper + { + NodeWrapper(xmlNodePtr p) : node(p) {} + + NodeWrapper begin() + { + if (!node->children || node->children->type == XML_ELEMENT_NODE) + return node->children; + return ++NodeWrapper(node->children); + } + NodeWrapper end() { return nullptr; }; + + NodeWrapper begin() const + { + if (!node->children || node->children->type == XML_ELEMENT_NODE) + return node->children; + return ++NodeWrapper(node->children); + } + NodeWrapper end() const { return nullptr; }; + + NodeWrapper& operator++() + { + do + node = node->next; + while (node && node->type != XML_ELEMENT_NODE); + return *this; + } + bool operator!=(const NodeWrapper& o) const { return node != o.node; } + + operator xmlNodePtr() const { return node; } + NodeWrapper& operator*() { return *this; } + xmlNodePtr operator->() const { return node; } + + xmlNodePtr node; + }; + using nodetype = NodeWrapper; + using doctype = xmlDocPtr; + +protected: + ~DataTreeXMLImpl() + { + if (CheckForInclude()) + xmlSaveFile("out.totoro", doc); + if (doc) + xmlFreeDoc(doc); + } + + void ParseFile(const CVFSFile& input, const VfsPath& filename); + bool CheckForInclude() const; + + + bool IsIncludeNode(const nodetype& node) const; + VfsPath GetIncludePath(const VfsPath& filename, const nodetype& node) const; + bool IsReplaceNode(const nodetype& node) const; + + nodetype GetRootNode(const doctype& doc) const; + nodetype GetSameChild(const nodetype& node, const nodetype& similar_to) const; + + void AddChild(nodetype& node, nodetype& child); + void DeleteNode(nodetype& node); + void UpdateNode(nodetype& node_to_update, const nodetype& reference); + + void PostMergeProcessing(nodetype& node); + + doctype GetDoc() { return doc; } + +protected: + doctype doc = nullptr; +}; + +using DataTreeXML = DataTree; + +#endif // INCLUDED_DATATREEXML Index: source/ps/DataTreeXML.cpp =================================================================== --- /dev/null +++ source/ps/DataTreeXML.cpp @@ -0,0 +1,217 @@ +/* Copyright (C) 2020 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. 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 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. 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 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "DataTreeXML.h" + +#include "maths/Fixed.h" + +#include +#include // this isn't in string.hpp in old Boosts + +bool DataTreeXMLImpl::IsIncludeNode(const nodetype& node) const +{ + return node->type == XML_ELEMENT_NODE && + (xmlStrEqual(node->name, (xmlChar*)"include") || + xmlHasProp(node, (xmlChar*)"parent") != nullptr); +; +} + +VfsPath DataTreeXMLImpl::GetIncludePath(const VfsPath& filename, const nodetype& node) const +{ + if (xmlStrEqual(node->name, (xmlChar*)"include")) + return (filename.Parent() / reinterpret_cast(xmlNodeGetContent(node))).ChangeExtension(".xml"); + xmlChar* merge_path = xmlGetProp(node, (xmlChar*)"parent"); + VfsPath ret = (filename.Parent() / reinterpret_cast(merge_path)).ChangeExtension(".xml"); + xmlFree(merge_path); + return ret; +} + +bool DataTreeXMLImpl::IsReplaceNode(const nodetype& node) const +{ + return node->type == XML_ELEMENT_NODE && xmlHasProp(node, (xmlChar*)"replace") != nullptr; +} + +bool DataTreeXMLImpl::CheckForInclude() const +{ + for (xmlNodePtr child = doc->children; child; child = child->next) + if (child->type == XML_PI_NODE && xmlStrEqual(child->name, (xmlChar*)"use_include")) + return true; + return false; + //nodetype root = xmlDocGetRootElement(doc); + //return IsIncludeNode(root); +} + +void DataTreeXMLImpl::ParseFile(const CVFSFile& input, const VfsPath& filename) +{ + doc = xmlReadMemory((const char*)input.GetBuffer(), input.GetBufferSize(), CStrW(filename.string()).ToUTF8().c_str(), NULL, + XML_PARSE_NONET|XML_PARSE_NOCDATA); +} + +DataTreeXMLImpl::nodetype DataTreeXMLImpl::GetRootNode(const doctype& doc) const +{ + return xmlDocGetRootElement(doc); +} + +DataTreeXMLImpl::nodetype DataTreeXMLImpl::GetSameChild(const nodetype& node, const nodetype& similar_to) const +{ + for (xmlNodePtr child = node->children; child; child = child->next) + if (xmlStrEqual(child->name, similar_to->name)) + return child; + return nullptr; +} + + +void DataTreeXMLImpl::AddChild(nodetype& node, nodetype& child) +{ + xmlNodePtr add = xmlDocCopyNode(child, doc, 1); + xmlAddChild(node, add); +} + +void DataTreeXMLImpl::DeleteNode(nodetype& node) +{ + xmlUnlinkNode(node); + xmlFreeNode(node); +} + +void DataTreeXMLImpl::UpdateNode(nodetype& node_to_update, const nodetype& reference) +{ + if (xmlStrEqual(node_to_update->name, (xmlChar*)"include") || xmlHasProp(node_to_update, (xmlChar*)"parent") != nullptr) + { + // Copy the node and its prop, but not children. + xmlNodePtr ref = xmlDocCopyNode(reference, doc, 2); + // Swap exitsting children so they're preserved. + for (xmlNodePtr child = node_to_update->children; child;) + { + xmlNodePtr next = child->next; + if (child->type == XML_ELEMENT_NODE) + { + xmlUnlinkNode(child); + xmlAddChild(ref, child); + } + child = next; + } + // Replace the node. + xmlAddNextSibling(node_to_update, ref); + xmlUnlinkNode(node_to_update); + xmlFreeNode(node_to_update); + if (xmlHasProp(node_to_update, (xmlChar*)"parent") != nullptr) + xmlUnsetProp(node_to_update, (xmlChar*)"parent"); + node_to_update = ref; + return; + } + if (xmlHasProp(node_to_update, (xmlChar*)"op") != nullptr) + { + xmlChar* op = xmlGetProp(node_to_update, (xmlChar*)"op"); + xmlChar* ref_val = xmlNodeGetContent(reference); + xmlChar* op_val = xmlNodeGetContent(node_to_update); + + fixed original_value = fixed::FromString(CStr((const char*)ref_val)); + fixed new_value = fixed::FromString(CStr((const char*)op_val)); + + if (xmlStrEqual(op, (xmlChar*)"add")) + new_value = original_value + new_value; + else if (xmlStrEqual(op, (xmlChar*)"mul")) + new_value = original_value.Multiply(new_value); + else if (xmlStrEqual(op, (xmlChar*)"mul_round")) + new_value = fixed::FromInt(original_value.Multiply(new_value).ToInt_RoundToNearest()); + else + LOGWARNING("Invalid op %s", (const char*)op_val); + + xmlNodeSetContent(node_to_update, (xmlChar*)new_value.ToString().c_str()); + + xmlFree(op_val); + xmlFree(ref_val); + xmlFree(op); + xmlUnsetProp(node_to_update, (xmlChar*)"op"); + return; + } + if (xmlHasProp(node_to_update, (xmlChar*)"datatype") != nullptr) + { + xmlChar* datatype = xmlGetProp(node_to_update, (xmlChar*)"datatype"); + if (xmlStrEqual(datatype, (xmlChar*)"tokens")) + { + // Split into tokens + std::vector oldTokens; + std::vector newTokens; + + xmlChar* reference_content = xmlNodeGetContent(reference); + xmlChar* node_content = xmlNodeGetContent(node_to_update); + std::string old_tokens = std::string((const char*)reference_content); + std::string new_tokens = std::string((const char*)node_content); + + boost::algorithm::split(oldTokens, old_tokens, boost::algorithm::is_space(), boost::algorithm::token_compress_on); + boost::algorithm::split(newTokens, new_tokens, boost::algorithm::is_space(), boost::algorithm::token_compress_on); + + // Merge the two lists + std::vector tokens = oldTokens; + for (const std::string& token : newTokens) + { + if (token[0] == L'-') + { + std::vector::iterator tokenIt = std::find(tokens.begin(), tokens.end(), token.substr(1)); + if (tokenIt != tokens.end()) + tokens.erase(tokenIt); + else + LOGWARNING("[DataTreeXML] Could not remove token '%s' from node '%s'; not present in list nor inherited (possible typo?)", + token.substr(1), (const char*)node_to_update->name/*, sourceIdentifier ? (" in '" + utf8_from_wstring(sourceIdentifier) + "'").c_str() : ""*/); + } + else + { + if (std::find(oldTokens.begin(), oldTokens.end(), token) == oldTokens.end()) + tokens.push_back(token); + } + } + + xmlNodeSetContent(node_to_update, (xmlChar*)boost::algorithm::join(tokens, L" ").c_str()); + + xmlFree(node_content); + xmlFree(reference_content); + } + xmlFree(datatype); + return; + } +} + +void DataTreeXMLImpl::PostMergeProcessing(nodetype& node) +{ + if (xmlHasProp(node, (xmlChar*)"replace") != nullptr) + xmlUnsetProp(node, (xmlChar*)"replace"); + + if (xmlHasProp(node, (xmlChar*)"disable") != nullptr) + { + // Disable node -> drop along with its children. + // We need to point node to the next sibling, if any. + nodetype next = node->next; + DeleteNode(node); + node = next; + } + if (xmlHasProp(node, (xmlChar*)"temporary") != nullptr) + { + // The node was a temporary node that needs to be dropped. + xmlNodePtr newnode = nullptr; + for (xmlNodePtr child = node->children; child;) + { + xmlNodePtr temp = child->next; + newnode = xmlAddPrevSibling(node, child); + child = temp; + } + DeleteNode(node); + node = newnode; + } +} Index: source/ps/XML/Xeromyces.cpp =================================================================== --- source/ps/XML/Xeromyces.cpp +++ source/ps/XML/Xeromyces.cpp @@ -27,6 +27,7 @@ #include "maths/MD5.h" #include "ps/CacheLoader.h" #include "ps/CLogger.h" +#include "ps/DataTreeXML.h" #include "ps/Filesystem.h" #include "RelaxNG.h" #include "Xeromyces.h" @@ -159,35 +160,29 @@ PSRETURN CXeromyces::ConvertFile(const PIVFS& vfs, const VfsPath& filename, const VfsPath& xmbPath, const std::string& validatorName) { - CVFSFile input; - if (input.Load(vfs, filename)) - { - LOGERROR("CXeromyces: Failed to open XML file %s", filename.string8()); - return PSRETURN_Xeromyces_XMLOpenFailed; - } - - xmlDocPtr doc = xmlReadMemory((const char*)input.GetBuffer(), input.GetBufferSize(), CStrW(filename.string()).ToUTF8().c_str(), NULL, - XML_PARSE_NONET|XML_PARSE_NOCDATA); - if (!doc) - { - LOGERROR("CXeromyces: Failed to parse XML file %s", filename.string8()); - return PSRETURN_Xeromyces_XMLParseError; - } - - { - std::lock_guard lock(g_ValidatorCacheLock); - RelaxNGValidator& validator = GetValidator(validatorName); - if (validator.CanValidate() && !validator.ValidateEncoded(doc)) - { - LOGERROR("CXeromyces: failed to validate XML file %s", filename.string8()); - return PSRETURN_Xeromyces_XMLValidationFailed; - } - } - WriteBuffer writeBuffer; - CreateXMB(doc, writeBuffer); - xmlFreeDoc(doc); + { + DataTreeXML xmlDoc; + xmlDoc.Load(vfs, filename); + if (!xmlDoc) + { + LOGERROR("CXeromyces: Failed to parse XML file %s", filename.string8()); + return PSRETURN_Xeromyces_XMLParseError; + } + + { + std::lock_guard lock(g_ValidatorCacheLock); + RelaxNGValidator& validator = GetValidator(validatorName); + if (validator.CanValidate() && !validator.ValidateEncoded(xmlDoc)) + { + LOGERROR("CXeromyces: failed to validate XML file %s", filename.string8()); + return PSRETURN_Xeromyces_XMLValidationFailed; + } + } + + CreateXMB(xmlDoc, writeBuffer); + } // Save the file to disk, so it can be loaded quickly next time. // Don't save if invalid, because we want the syntax error every program start. Index: source/ps/tests/test_DataTree.h =================================================================== --- /dev/null +++ source/ps/tests/test_DataTree.h @@ -0,0 +1,48 @@ +/* Copyright (C) 2019 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. 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 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. 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 0 A.D. If not, see . + */ + +#include "lib/self_test.h" + +#include "ps/DataTree.h" +#include "ps/DataTreeXML.h" +#include "ps/Filesystem.h" + +#include + +class TestDataTree : public CxxTest::TestSuite +{ +public: + void setUp() + { + g_VFS = CreateVfs(); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.datatree")); + TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); + CXeromyces::Startup(); + } + + void tearDown() + { + CXeromyces::Terminate(); + g_VFS.reset(); + DeleteDirectory(DataDir()/"_test.datatree"); + DeleteDirectory(DataDir()/"_testcache"); + } + + void test_xml_tree() + { + } +};