Index: source/gui/Scripting/JSInterface_GUIManager.cpp =================================================================== --- source/gui/Scripting/JSInterface_GUIManager.cpp +++ source/gui/Scripting/JSInterface_GUIManager.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -74,6 +74,19 @@ return g_GUI->GetTemplate(templateName); } +CParamNode GetParamNodeAt(const std::wstring& path) +{ + CParamNode node; + CParamNode::LoadXML(node, VfsPath(path), ""); + return node; +} + +CParamNode CreateParamNode(const ScriptInterface& itf, JS::HandleValue val) +{ + CParamNode node; + CParamNode::LoadJS(node, itf, val); + return node; +} void RegisterScriptFunctions(const ScriptRequest& rq) { @@ -84,6 +97,8 @@ ScriptFunction::Register<&ResetCursor>(rq, "ResetCursor"); ScriptFunction::Register<&TemplateExists>(rq, "TemplateExists"); ScriptFunction::Register<&GetTemplate>(rq, "GetTemplate"); + ScriptFunction::Register<&GetParamNodeAt>(rq, "GetParamNodeAt"); + ScriptFunction::Register<&CreateParamNode>(rq, "CreateParamNode"); ScriptFunction::Register<&CGUI::FindObjectByName, &ScriptFunction::ObjectFromCBData>(rq, "GetGUIObjectByName"); ScriptFunction::Register<&CGUI::SetGlobalHotkey, &ScriptFunction::ObjectFromCBData>(rq, "SetGlobalHotkey"); Index: source/ps/TemplateLoader.cpp =================================================================== --- source/ps/TemplateLoader.cpp +++ source/ps/TemplateLoader.cpp @@ -155,6 +155,8 @@ LOGERROR("Failed to load entity template '%s'", templateName.c_str()); return NULL_NODE; } + // Template component order is relevant, both for initialisation & validation, so sort them after loading. + ret.SortChildren(false); return m_TemplateFileData.insert_or_assign(templateName, ret).first->second; } Index: source/simulation2/components/CCmpObstruction.cpp =================================================================== --- source/simulation2/components/CCmpObstruction.cpp +++ source/simulation2/components/CCmpObstruction.cpp @@ -244,12 +244,12 @@ for(CParamNode::ChildrenMap::const_iterator it = clusterMap.begin(); it != clusterMap.end(); ++it) { Shape b; - b.size0 = it->second.GetChild("@width").ToFixed(); - b.size1 = it->second.GetChild("@depth").ToFixed(); + b.size0 = it->GetChild("@width").ToFixed(); + b.size1 = it->GetChild("@depth").ToFixed(); ENSURE(b.size0 > minObstruction); ENSURE(b.size1 > minObstruction); - b.dx = it->second.GetChild("@x").ToFixed(); - b.dz = it->second.GetChild("@z").ToFixed(); + b.dx = it->GetChild("@x").ToFixed(); + b.dz = it->GetChild("@z").ToFixed(); b.da = entity_angle_t::FromInt(0); b.flags = m_Flags; m_Shapes.push_back(b); Index: source/simulation2/components/CCmpPathfinder.cpp =================================================================== --- source/simulation2/components/CCmpPathfinder.cpp +++ source/simulation2/components/CCmpPathfinder.cpp @@ -92,11 +92,10 @@ const CParamNode::ChildrenMap& passClasses = externalParamNode.GetChild("Pathfinder").GetChild("PassabilityClasses").GetChildren(); for (CParamNode::ChildrenMap::const_iterator it = passClasses.begin(); it != passClasses.end(); ++it) { - std::string name = it->first; ENSURE((int)m_PassClasses.size() <= PASS_CLASS_BITS); pass_class_t mask = PASS_CLASS_MASK_FROM_INDEX(m_PassClasses.size()); - m_PassClasses.push_back(PathfinderPassability(mask, it->second)); - m_PassClassMasks[name] = mask; + m_PassClasses.push_back(PathfinderPassability(mask, *it)); + m_PassClassMasks[it->GetName()] = mask; } m_Workers.emplace_back(PathfinderWorker{}); Index: source/simulation2/system/ComponentManager.cpp =================================================================== --- source/simulation2/system/ComponentManager.cpp +++ source/simulation2/system/ComponentManager.cpp @@ -824,19 +824,19 @@ for (CParamNode::ChildrenMap::const_iterator it = tmplChilds.begin(); it != tmplChilds.end(); ++it) { // Ignore attributes on the root element - if (it->first.length() && it->first[0] == '@') + if (it->IsAttribute()) continue; - CComponentManager::ComponentTypeId cid = LookupCID(it->first); + CComponentManager::ComponentTypeId cid = LookupCID(it->GetName()); if (cid == CID__Invalid) { - LOGERROR("Unrecognized component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); + LOGERROR("Unrecognized component type name '%s' in entity template '%s'", it->GetName(), utf8_from_wstring(templateName)); return INVALID_ENTITY; } - if (!AddComponent(handle, cid, it->second)) + if (!AddComponent(handle, cid, *it)) { - LOGERROR("Failed to construct component type name '%s' in entity template '%s'", it->first, utf8_from_wstring(templateName)); + LOGERROR("Failed to construct component type name '%s' in entity template '%s'", it->GetName(), utf8_from_wstring(templateName)); return INVALID_ENTITY; } // TODO: maybe we should delete already-constructed components if one of them fails? Index: source/simulation2/system/ParamNode.h =================================================================== --- source/simulation2/system/ParamNode.h +++ source/simulation2/system/ParamNode.h @@ -24,8 +24,9 @@ #include "ps/Errors.h" #include "scriptinterface/ScriptTypes.h" -#include +#include #include +#include class XMBFile; class XMBElement; @@ -149,8 +150,10 @@ */ class CParamNode { + // Some implementation details are hidden away as methods of this class. + class Impl; public: - typedef std::map ChildrenMap; + typedef std::vector ChildrenMap; /** * Constructs a new, empty node. @@ -184,11 +187,11 @@ static PSRETURN LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier = NULL); /** - * Returns the (unique) child node with the given name, or a node with IsOk() == false if there is none. + * Load the data specified by the Javascript variable @a value into the node @a ret. + * Existing data will be overwritten. This however does not support advanced filtering that XML supports (for now). + * @return true on success, false on failure. */ - const CParamNode& GetChild(const char* name) const; - // (Children are returned as const in order to allow future optimisations, where we assume - // a node is always modified explicitly and not indirectly via its children, e.g. to cache JS::Values) + static bool LoadJS(CParamNode& ret, const ScriptInterface& scriptInterface, JS::HandleValue value); /** * Returns true if this is a valid CParamNode, false if it represents a non-existent node @@ -252,42 +255,47 @@ */ void ToJSVal(const ScriptRequest& rq, bool cacheValue, JS::MutableHandleValue ret) const; + const std::string& GetName() const { return m_Name; } + + bool IsAttribute() const { return !m_Name.empty() && m_Name[0] == '@'; } + /** - * Returns the names/nodes of the children of this node, ordered by name + * Returns the (unique) child node with the given name, or a node with IsOk() == false if there is none. + * Children are returned as const in order to allow future optimisations, where we assume + * a node is always modified explicitly and not indirectly via its children, e.g. to cache JS::Values. + */ + const CParamNode& GetChild(const char* name) const; + + /** + * Returns the names/nodes of the children of this node. */ const ChildrenMap& GetChildren() const; + /** + * Sort children of this node. + */ + void SortChildren(bool recursive); + /** * Escapes a string so that it is well-formed XML content/attribute text. * (Replaces "&" with "&" etc) */ static std::string EscapeXMLString(const std::string& str); - std::string m_Name; - u32 m_Index; - private: - - /** - * Overlays the specified data onto this node. See class documentation for the concept and examples. - * - * @param xmb Representation of the XMB file containing an element with the data to apply. - * @param element Element inside the specified @p xmb file containing the data to apply. - * @param sourceIdentifier Optional; string you can pass along to indicate the source of - * the data getting applied. Used for output to log messages if an error occurs. - */ - void ApplyLayer(const XMBFile& xmb, const XMBElement& element, const wchar_t* sourceIdentifier = NULL); - void ResetScriptVal(); void ConstructJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret) const; + std::string m_Name; + u32 m_Index; std::string m_Value; - ChildrenMap m_Childs; + ChildrenMap m_Children; bool m_IsOk; /** * Caches the ToJSVal script representation of this node. + * shared_ptr to enable 'shallow' copying. */ mutable std::shared_ptr m_ScriptVal; }; Index: source/simulation2/system/ParamNode.cpp =================================================================== --- source/simulation2/system/ParamNode.cpp +++ source/simulation2/system/ParamNode.cpp @@ -25,54 +25,149 @@ #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" #include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/ScriptExtraHeaders.h" #include +#include +#include + #include static CParamNode g_NullNode(false); -CParamNode::CParamNode(bool isOk) : - m_IsOk(isOk) +class CParamNode::Impl { -} +public: + static ChildrenMap::const_iterator Find(const ChildrenMap& children, const std::string& name, u32 index) + { + return std::find_if(children.begin(), children.end(), + [&](const CParamNode& node) { return node.m_Name == name && node.m_Index == index; }); + } -void CParamNode::LoadXML(CParamNode& ret, const XMBFile& xmb, const wchar_t* sourceIdentifier /*= NULL*/) -{ - ret.ApplyLayer(xmb, xmb.GetRoot(), sourceIdentifier); -} + static ChildrenMap::iterator Find(ChildrenMap& children, const std::string& name, u32 index) + { + return std::find_if(children.begin(), children.end(), + [&](const CParamNode& node) { return node.m_Name == name && node.m_Index == index; }); + } -void CParamNode::LoadXML(CParamNode& ret, const VfsPath& path, const std::string& validatorName) -{ - CXeromyces xero; - PSRETURN ok = xero.Load(g_VFS, path, validatorName); - if (ok != PSRETURN_OK) - return; // (Xeromyces already logged an error) + static size_t Count(ChildrenMap& children, const std::string& name) + { + ChildrenMap::const_reverse_iterator it = std::find_if(children.rbegin(), children.rend(), + [&](const CParamNode& node) { return node.m_Name == name; }); + if (it == children.rend()) + return 0; + return it->m_Index + 1; + } - LoadXML(ret, xero, path.string().c_str()); -} + /** + * Acts similarly to std::map::try_emplace - returns the child if it exists already, else adds it and returns it. + * NB: @indexHint is used for lookup, but the newly inserted may have any index. + */ + static CParamNode& TryEmplace(CParamNode& node, std::string name, u32 indexHint) + { + ChildrenMap::iterator it = Find(node.m_Children, name, indexHint); + if (it != node.m_Children.end()) + return *it; + u32 idx = Count(node.m_Children, name); + CParamNode& ret = node.m_Children.emplace_back(); + ret.m_Name = name; + ret.m_Index = idx; + return ret; + } -PSRETURN CParamNode::LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier /*=NULL*/) -{ - CXeromyces xero; - PSRETURN ok = xero.LoadString(xml); - if (ok != PSRETURN_OK) - return ok; + static void SortChildren(CParamNode& node, bool recursive); - ret.ApplyLayer(xero, xero.GetRoot(), sourceIdentifier); + /** + * The ApplyLayer family of functions takes some data and applies it on the paramNode. + */ + struct NodeMetadata + { + enum Op { + INVALID, + ADD, + MUL, + MUL_ROUND + } op = INVALID; + bool filtering = false; + bool merging = false; + bool tokens = false; + }; + static bool ApplyLayer(CParamNode& node, const XMBFile& xmb, const XMBElement& element); + static bool ApplyLayer(CParamNode& node, const ScriptInterface& scriptInterface, JS::HandleValue value); - return PSRETURN_OK; -} + static bool HandleTokens(CParamNode& node, const std::string& value); + static bool HandleOp(CParamNode& node, const std::string& value, NodeMetadata::Op op); +}; -void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element, const wchar_t* sourceIdentifier /*= NULL*/) +bool CParamNode::Impl::HandleTokens(CParamNode& node, const std::string &value) { - ResetScriptVal(); + node.ResetScriptVal(); - std::string name = xmb.GetElementString(element.GetNodeName()); // TODO: is GetElementString inefficient? - CStr value = element.GetText(); + // Split into tokens + std::vector oldTokens; + std::vector newTokens; + if (!node.m_Value.empty()) + boost::algorithm::split(oldTokens, node.m_Value, boost::algorithm::is_space(), boost::algorithm::token_compress_on); + if (!value.empty()) + boost::algorithm::split(newTokens, value, boost::algorithm::is_space(), boost::algorithm::token_compress_on); - bool hasSetValue = false; + // Merge the two lists + std::vector tokens = oldTokens; + for (size_t i = 0; i < newTokens.size(); ++i) + { + if (newTokens[i][0] == L'-') + { + if (newTokens[i].size() == 1) + { + // Probably a typo, warn explicitly and exit. + LOGERROR("[ParamNode] Unexpected white space after '-' in tokens from node '%s'; probable typo", + newTokens[i].substr(1), node.m_Name); + return false; + } + std::vector::iterator tokenIt = std::find(tokens.begin(), tokens.end(), newTokens[i].substr(1)); + if (tokenIt != tokens.end()) + tokens.erase(tokenIt); + else + { + LOGERROR("[ParamNode] Could not remove token '%s' from node '%s'; not present in list nor inherited (possible typo?)", + newTokens[i].substr(1), node.m_Name); + return false; + } + } + else + { + if (std::find(oldTokens.begin(), oldTokens.end(), newTokens[i]) == oldTokens.end()) + tokens.push_back(newTokens[i]); + } + } + node.m_Value = boost::algorithm::join(tokens, " "); + return true; +} + +bool CParamNode::Impl::HandleOp(CParamNode& node, const std::string& value, Impl::NodeMetadata::Op op) +{ + // TODO: Support parsing of data types other than fixed; log warnings in other cases + node.ResetScriptVal(); + fixed oldval = node.ToFixed(); + fixed mod = fixed::FromString(value); + if (op == NodeMetadata::ADD) + node.m_Value = (oldval + mod).ToString(); + else if (op == NodeMetadata::MUL) + node.m_Value = oldval.Multiply(mod).ToString(); + else if (op == NodeMetadata::MUL_ROUND) + node.m_Value = fixed::FromInt(oldval.Multiply(mod).ToInt_RoundToNearest()).ToString(); + else + { + LOGERROR("Unknown op flag"); + return false; + } + return true; +} + +bool CParamNode::Impl::ApplyLayer(CParamNode& node, const XMBFile& xmb, const XMBElement& element) +{ // Look for special attributes int at_disable = xmb.GetAttributeID("disable"); int at_replace = xmb.GetAttributeID("replace"); @@ -80,143 +175,77 @@ int at_merge = xmb.GetAttributeID("merge"); int at_op = xmb.GetAttributeID("op"); int at_datatype = xmb.GetAttributeID("datatype"); - enum op { - INVALID, - ADD, - MUL, - MUL_ROUND - } op = INVALID; - bool replacing = false; - bool filtering = false; - bool merging = false; + + // Store metadata on how to process this element. + NodeMetadata metadata; + + // Look for attributes that modify how this node's value & children should be applied on the ParamNode. + XERO_ITER_ATTR(element, attr) { - XERO_ITER_ATTR(element, attr) + if (attr.Name == at_disable) { - if (attr.Name == at_disable) - { - m_Childs.erase(name); - return; - } - else if (attr.Name == at_replace) - { - m_Childs.erase(name); - replacing = true; - } - else if (attr.Name == at_filtered) - { - filtering = true; - } - else if (attr.Name == at_merge) - { - if (m_Childs.find(name) == m_Childs.end()) - return; - merging = true; - } - else if (attr.Name == at_op) - { - if (attr.Value == "add") - op = ADD; - else if (attr.Value == "mul") - op = MUL; - else if (attr.Value == "mul_round") - op = MUL_ROUND; - else - LOGWARNING("Invalid op '%ls'", attr.Value); - } + // The "disable" attribute should not be seen, as the corresponding nodes are not applied. + // This must have happened because the root node has the disable attribute, which is likely an error - report. + LOGERROR("Cannot disable the root node %s", node.m_Name); + return false; } - } - { - XERO_ITER_ATTR(element, attr) + else if (attr.Name == at_replace) + { + std::string name = node.m_Name; + u32 index = node.m_Index; + node = CParamNode(); + node.m_Name = name; + node.m_Index = index; + } + else if (attr.Name == at_merge) { - if (attr.Name == at_datatype && attr.Value == "tokens") + metadata.merging = true; + } + else if (attr.Name == at_filtered) + { + metadata.filtering = true; + } + else if (attr.Name == at_op) + { + if (attr.Value == "add") + metadata.op = NodeMetadata::ADD; + else if (attr.Value == "mul") + metadata.op = NodeMetadata::MUL; + else if (attr.Value == "mul_round") + metadata.op = NodeMetadata::MUL_ROUND; + else { - CParamNode& node = m_Childs[name]; - - // Split into tokens - std::vector oldTokens; - std::vector newTokens; - if (!replacing && !node.m_Value.empty()) // ignore the old tokens if replace="" was given - boost::algorithm::split(oldTokens, node.m_Value, boost::algorithm::is_space(), boost::algorithm::token_compress_on); - if (!value.empty()) - boost::algorithm::split(newTokens, value, boost::algorithm::is_space(), boost::algorithm::token_compress_on); - - // Merge the two lists - std::vector tokens = oldTokens; - for (size_t i = 0; i < newTokens.size(); ++i) - { - if (newTokens[i][0] == '-') - { - std::vector::iterator tokenIt = std::find(tokens.begin(), tokens.end(), newTokens[i].substr(1)); - if (tokenIt != tokens.end()) - tokens.erase(tokenIt); - else - LOGWARNING("[ParamNode] Could not remove token '%s' from node '%s'%s; not present in list nor inherited (possible typo?)", - newTokens[i].substr(1), name, sourceIdentifier ? (" in '" + utf8_from_wstring(sourceIdentifier) + "'").c_str() : ""); - } - else - { - if (std::find(oldTokens.begin(), oldTokens.end(), newTokens[i]) == oldTokens.end()) - tokens.push_back(newTokens[i]); - } - } - - node.m_Value = boost::algorithm::join(tokens, " "); - hasSetValue = true; - break; + LOGERROR("Invalid op '%ls'", attr.Value); + return false; } } + else if (attr.Name == at_datatype && attr.Value == "tokens") + metadata.tokens = true; } - // Add this element as a child node - CParamNode& node = m_Childs[name]; - if (op != INVALID) + if (metadata.tokens) { - // TODO: Support parsing of data types other than fixed; log warnings in other cases - fixed oldval = node.ToFixed(); - fixed mod = fixed::FromString(value); - - switch (op) - { - case ADD: - node.m_Value = (oldval + mod).ToString(); - break; - case MUL: - node.m_Value = oldval.Multiply(mod).ToString(); - break; - case MUL_ROUND: - node.m_Value = fixed::FromInt(oldval.Multiply(mod).ToInt_RoundToNearest()).ToString(); - break; - default: - break; - } - hasSetValue = true; + if (!Impl::HandleTokens(node, element.GetText())) + return false; } - - if (!hasSetValue && !merging) - node.m_Value = value; - - // We also need to reset node's script val, even if it has no children - // or if the attributes change. - node.ResetScriptVal(); - - // For the filtered case - ChildrenMap childs; - - // Recurse through the element's children - XERO_ITER_EL(element, child) + else if (metadata.op != NodeMetadata::INVALID) + { + if (!Impl::HandleOp(node, element.GetText(), metadata.op)) + return false; + } + else { - node.ApplyLayer(xmb, child, sourceIdentifier); - if (filtering) + CStr value = element.GetText(); + // Annoying edge case in XML: when merging over nodes without children (e.g. Value, ), + // we would usually replace "Value" with the new value: nothing. This is usually not what's wanted + // (since it likely can just be disabled then), so don't replace the value with an empty value in that case. + if (!!metadata.merging || !value.empty()) { - std::string childname = xmb.GetElementString(child.GetNodeName()); - if (node.m_Childs.find(childname) != node.m_Childs.end()) - childs[childname] = std::move(node.m_Childs[childname]); + node.ResetScriptVal(); + node.m_Value = value; } } - if (filtering) - node.m_Childs.swap(childs); - // Add the element's attributes, prefixing names with "@" XERO_ITER_ATTR(element, attr) { @@ -224,17 +253,201 @@ if (attr.Name == at_replace || attr.Name == at_op || attr.Name == at_merge || attr.Name == at_filtered) continue; // Add any others + node.ResetScriptVal(); std::string attrName = xmb.GetAttributeString(attr.Name); - node.m_Childs["@" + attrName].m_Value = attr.Value; + Impl::TryEmplace(node, "@" + attrName, 0).m_Value = attr.Value; } + + using ChildrenMap = CParamNode::ChildrenMap; + ChildrenMap childs; + + // Recurse through the element's children. + node.m_Children.reserve(element.GetChildNodes().size()); + // Keep a count of encountered elements (by name) to know if we need to raise index. + // (use the integer name - it's faster than creating strings). + std::unordered_map counts; + XERO_ITER_EL(element, child) + { + // Greedily reset - it's likely at least one child will change. + node.ResetScriptVal(); + + std::string name = xmb.GetElementString(child.GetNodeName()); // TODO: is GetElementString inefficient? + // Increment count by 1, but return the original value (to use as the index). + int count = counts[child.GetNodeName()]++; + + bool skipApplying = false; + // Some special attributes are handled here for convenience. + XERO_ITER_ATTR(child, attr) + { + if (attr.Name == at_disable) + { + ChildrenMap::iterator it = Impl::Find(node.m_Children, name, count); + if (it != node.m_Children.end()) + node.m_Children.erase(it); + // TODO: shift children's index down by one. + skipApplying = true; + break; + } + else if (attr.Name == at_merge) + { + ChildrenMap::iterator it = Impl::Find(node.m_Children, name, count); + if (it == node.m_Children.end()) + { + skipApplying = true; + break; + } + } + } + if (skipApplying) + continue; + + // NB: if a child with this name/index doesn't exist, it will be created, but the index may _not equal_ 'count'. + // (this is because of 'disable' nodes and other operations). + CParamNode& childNode = Impl::TryEmplace(node, name, count); + if (!Impl::ApplyLayer(childNode, xmb, child)) + return false; + } + + if (metadata.filtering) + for (ChildrenMap::iterator it = node.m_Children.begin(); it != node.m_Children.end();) + { + if (counts.find(xmb.GetElementID(it->m_Name.c_str())) == counts.end()) + { + node.ResetScriptVal(); + it = node.m_Children.erase(it); + } + else + it++; + } + + return true; +} + +bool CParamNode::Impl::ApplyLayer(CParamNode& node, const ScriptInterface& scriptInterface, JS::HandleValue value) +{ + ScriptRequest rq(scriptInterface); + + std::vector props; + if (!scriptInterface.EnumeratePropertyNames(value, true, props)) + { + ScriptException::Raise(rq, "Failed to enumerate component properties."); + return false; + } + + node.m_Children.reserve(props.size()); + for (const std::string& prop : props) + { + JS::RootedValue child(rq.cx); + scriptInterface.GetProperty(value, prop.c_str(), &child); + + bool attrib = !prop.empty() && prop.front() == '@'; + + // Look for the property index if this isn't an attribute + u32 index = 0; + if (!attrib && !prop.empty() && prop.back() == '@') + { + size_t idx = prop.substr(0, prop.size()-1).find_last_of('@'); + if (idx == std::string::npos) + { + LOGERROR("ParamNode properties cannot end with an '@' unless it is an index specifier"); + return false; + } + index = std::strtol(prop.substr(idx, prop.size()-1).c_str(), nullptr, 10); + } + + switch (JS_TypeOfValue(rq.cx, child)) + { + case JSTYPE_UNDEFINED: + case JSTYPE_NULL: + { + Impl::TryEmplace(node, prop, index); + break; + } + case JSTYPE_STRING: + case JSTYPE_NUMBER: + { + ScriptInterface::FromJSVal(rq, child, Impl::TryEmplace(node, prop, index).m_Value); + break; + } + case JSTYPE_OBJECT: + { + if (attrib) + { + LOGERROR("Attribute '%s': ParamNode attributes cannot be objects", prop); + return false; + } + bool isArray = false; + JS::IsArrayObject(rq.cx, child, &isArray); + if (!isArray && !Impl::ApplyLayer(Impl::TryEmplace(node, prop, index), scriptInterface, child)) + return false; + else + { + JS::RootedObject obj(rq.cx); + JS_ValueToObject(rq.cx, child, &obj); + u32 length; + JS::GetArrayLength(rq.cx, obj, &length); + for (size_t i = 0; i < length; ++i) + { + JS::RootedValue arrayChild(rq.cx); + scriptInterface.GetPropertyInt(child, i, &arrayChild); + if (!Impl::ApplyLayer(Impl::TryEmplace(node, prop, i), scriptInterface, arrayChild)) + return false; + } + } + break; + } + default: + { + LOGERROR("Unsupported JS construct when parsing ParamNode"); + return false; + } + } + } + return true; +} + +CParamNode::CParamNode(bool isOk) : +m_IsOk(isOk) +{ + m_Index = 0; +} + +void CParamNode::LoadXML(CParamNode& ret, const VfsPath& path, const std::string& validatorName) +{ + CXeromyces xero; + PSRETURN ok = xero.Load(g_VFS, path, validatorName); + if (ok != PSRETURN_OK) + return; // (Xeromyces already logged an error) + + LoadXML(ret, xero, path.string().c_str()); +} + +void CParamNode::LoadXML(CParamNode& ret, const XMBFile& xmb, const wchar_t* sourceIdentifier /*= NULL*/) +{ + std::string name = xmb.GetElementString(xmb.GetRoot().GetNodeName()); // TODO: is GetElementString inefficient? + CParamNode& root = Impl::TryEmplace(ret, name, 0); + if (!Impl::ApplyLayer(root, xmb, xmb.GetRoot())) + LOGERROR("Error when loading XML file '%s' into ParamNode", sourceIdentifier ? utf8_from_wstring(sourceIdentifier) : "(unknown)"); +} + +PSRETURN CParamNode::LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier /*=NULL*/) +{ + CXeromyces xero; + PSRETURN ok = xero.LoadString(xml); + if (ok != PSRETURN_OK) + return ok; + + std::string name = xero.GetElementString(xero.GetRoot().GetNodeName()); // TODO: is GetElementString inefficient? + CParamNode& root = Impl::TryEmplace(ret, name, 0); + if (!Impl::ApplyLayer(root, xero, xero.GetRoot())) + LOGERROR("Error when loading XML file '%s' into ParamNode", sourceIdentifier ? utf8_from_wstring(sourceIdentifier) : "(unknown)"); + + return PSRETURN_OK; } -const CParamNode& CParamNode::GetChild(const char* name) const +bool CParamNode::LoadJS(CParamNode& ret, const ScriptInterface& scriptInterface, JS::HandleValue value) { - ChildrenMap::const_iterator it = m_Childs.find(name); - if (it == m_Childs.end()) - return g_NullNode; - return it->second; + return Impl::ApplyLayer(ret, scriptInterface, value); } bool CParamNode::IsOk() const @@ -280,11 +493,46 @@ return false; } +const CParamNode& CParamNode::GetChild(const char* name) const +{ + // Return the first child found with that name. + ChildrenMap::const_iterator it = Impl::Find(m_Children, name, 0); + if (it == m_Children.end()) + return g_NullNode; + return *it; +} + const CParamNode::ChildrenMap& CParamNode::GetChildren() const { - return m_Childs; + return m_Children; } +void CParamNode::SortChildren(bool recursive) +{ + // Assume we can only be called from the root paramNode + if (!m_Name.empty() || m_Children.size() != 1) + { + LOGERROR("Cannot sort non-root or empty node"); + return; + } + Impl::SortChildren(m_Children[0], recursive); +} + +void CParamNode::Impl::SortChildren(CParamNode& node, bool recursive) +{ + std::sort(node.m_Children.begin(), node.m_Children.end(), [](const CParamNode& a, const CParamNode& b) { + if (a.m_Name < b.m_Name) + return true; + if (a.m_Name > b.m_Name) + return false; + return a.m_Index < b.m_Index; + }); + if (recursive) + for (CParamNode& child : node.m_Children) + SortChildren(child, recursive); +} + + std::string CParamNode::EscapeXMLString(const std::string& str) { std::string ret; @@ -321,37 +569,37 @@ { strm << m_Value; - ChildrenMap::const_iterator it = m_Childs.begin(); - for (; it != m_Childs.end(); ++it) + ChildrenMap::const_iterator it = m_Children.begin(); + for (; it != m_Children.end(); ++it) { // Skip attributes here (they were handled when the caller output the tag) - if (it->first.length() && it->first[0] == '@') + if (it->m_Name.length() && it->m_Name[0] == '@') continue; - strm << "<" << it->first; + strm << "<" << it->m_Name; // Output the child's attributes first - ChildrenMap::const_iterator cit = it->second.m_Childs.begin(); - for (; cit != it->second.m_Childs.end(); ++cit) + ChildrenMap::const_iterator cit = it->m_Children.begin(); + for (; cit != it->m_Children.end(); ++cit) { - if (cit->first.length() && cit->first[0] == '@') + if (cit->m_Name.length() && cit->m_Name[0] == '@') { - std::string attrname (cit->first.begin()+1, cit->first.end()); - strm << " " << attrname << "=\"" << EscapeXMLString(cit->second.m_Value) << "\""; + std::string attrname (cit->m_Name.begin()+1, cit->m_Name.end()); + strm << " " << attrname << "=\"" << EscapeXMLString(cit->m_Value) << "\""; } } strm << ">"; - it->second.ToXMLString(strm); + it->ToXMLString(strm); - strm << "first << ">"; + strm << "m_Name << ">"; } } void CParamNode::ToJSVal(const ScriptRequest& rq, bool cacheValue, JS::MutableHandleValue ret) const { - if (cacheValue && m_ScriptVal != NULL) + if (cacheValue && m_ScriptVal) { ret.set(*m_ScriptVal); return; @@ -360,12 +608,12 @@ ConstructJSVal(rq, ret); if (cacheValue) - m_ScriptVal.reset(new JS::PersistentRootedValue(rq.cx, ret)); + m_ScriptVal = std::make_shared(rq.cx, ret); } void CParamNode::ConstructJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret) const { - if (m_Childs.empty()) + if (m_Children.empty()) { // Empty node - map to undefined if (m_Value.empty()) @@ -397,15 +645,67 @@ } JS::RootedValue childVal(rq.cx); - for (std::map::const_iterator it = m_Childs.begin(); it != m_Childs.end(); ++it) + + ChildrenMap::const_iterator attrEndIt = m_Children.begin(); + while (attrEndIt != m_Children.end() && !attrEndIt->m_Name.empty() && attrEndIt->m_Name[0] == '@') + ++attrEndIt; + ChildrenMap::const_iterator it = m_Children.begin(); + // First output attributes + for (; it != attrEndIt; ++it) { - it->second.ConstructJSVal(rq, &childVal); - if (!JS_SetProperty(rq.cx, obj, it->first.c_str(), childVal)) + it->ConstructJSVal(rq, &childVal); + if (!JS_SetProperty(rq.cx, obj, it->m_Name.c_str(), childVal)) { ret.setUndefined(); return; // TODO: report error } } + if (it != m_Children.end()) + { + bool allSame = true; + const std::string& ln = it->m_Name; + ChildrenMap::const_iterator it2 = it; + for (; it2 != m_Children.end(); ++it2) + if (it2->m_Name != ln) + { + allSame = false; + break; + } + // Special case: we'll print an array if there are multiple, identically-named members. + if (allSame && it2 - it > 1) + { + JS::RootedValueVector vec(rq.cx); + for (; it != m_Children.end(); ++it) + { + JS::RootedValue objVal(rq.cx); + it->ConstructJSVal(rq, &objVal); + if (!vec.append(objVal)) + return; // TODO: report error + } + childVal.setObjectOrNull(JS::NewArrayObject(rq.cx, vec)); + if (!JS_SetProperty(rq.cx, obj, ln.c_str(), childVal)) + { + ret.setUndefined(); + return; // TODO: report error + } + } + else + { + for (; it != m_Children.end(); ++it) + { + it->ConstructJSVal(rq, &childVal); + std::string name = it->m_Name; + // Wrap the index in "@x@" at the end + if (it->m_Index > 0) + name += "@" + std::to_string(it->m_Index) + "@"; + if (!JS_SetProperty(rq.cx, obj, name.c_str(), childVal)) + { + ret.setUndefined(); + return; // TODO: report error + } + } + } + } // If the node has a string too, add that as an extra property if (!m_Value.empty()) @@ -431,5 +731,5 @@ void CParamNode::ResetScriptVal() { - m_ScriptVal = NULL; + m_ScriptVal.reset(); } Index: source/simulation2/tests/test_CmpTemplateManager.h =================================================================== --- source/simulation2/tests/test_CmpTemplateManager.h +++ source/simulation2/tests/test_CmpTemplateManager.h @@ -80,7 +80,7 @@ TS_ASSERT(actor != NULL); TS_ASSERT_STR_EQUALS(actor->ToXMLString(), "1.0128x128/ellipse.png128x128/ellipse_mask.png" - "example1falsefalsefalse"); + "falsefalsefalseexample1"); } void test_LoadTemplate_scriptcache() @@ -114,19 +114,19 @@ const CParamNode* actor = tempMan->LoadTemplate(ent2, "actor|example1"); ScriptInterface::ToJSVal(rq, &val, &actor->GetChild("VisualActor")); - TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Actor:\"example1\", ActorOnly:(void 0), SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\"})"); + TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\", Actor:\"example1\", ActorOnly:(void 0)})"); const CParamNode* foundation = tempMan->LoadTemplate(ent2, "foundation|actor|example1"); ScriptInterface::ToJSVal(rq, &val, &foundation->GetChild("VisualActor")); - TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Actor:\"example1\", ActorOnly:(void 0), Foundation:(void 0), SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\"})"); + TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\", Actor:\"example1\", ActorOnly:(void 0), Foundation:(void 0)})"); #define GET_FIRST_ELEMENT(n, templateName) \ const CParamNode* n = tempMan->LoadTemplate(ent2, templateName); \ for (CParamNode::ChildrenMap::const_iterator it = n->GetChildren().begin(); it != n->GetChildren().end(); ++it) \ { \ - if (it->first[0] == '@') \ + if (it->IsAttribute()) \ continue; \ - ScriptInterface::ToJSVal(rq, &val, it->second); \ + ScriptInterface::ToJSVal(rq, &val, *it); \ break; \ } Index: source/simulation2/tests/test_ParamNode.h =================================================================== --- source/simulation2/tests/test_ParamNode.h +++ source/simulation2/tests/test_ParamNode.h @@ -79,11 +79,26 @@ TS_ASSERT_EQUALS(node.GetChild("test").GetChild("w").GetChild("@a").ToInt(), 4); } + void test_basicJS() + { + ScriptInterface script("Test", "Test", g_ScriptContext); + ScriptRequest rq(script); + + CParamNode node; + TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, "3test"), PSRETURN_OK); + JS::RootedValue val(rq.cx); + node.ToJSVal(rq, false, &val); + CParamNode node2; + std::string st; + CParamNode::LoadJS(node2, script, val, st); + TS_ASSERT_STR_EQUALS(node2.ToXMLString(), "3test"); + } + void test_ToXMLString() { CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " 3 "), PSRETURN_OK); - TS_ASSERT_STR_EQUALS(node.ToXMLString(), "3"); + TS_ASSERT_STR_EQUALS(node.ToXMLString(), "3"); } void test_overlay_basic() @@ -124,7 +139,7 @@ TestLogger logger; CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " -nonexistenttoken X"), PSRETURN_OK); - TS_ASSERT_STR_EQUALS(node.ToXMLString(), "X"); + TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "Could not remove token 'nonexistenttoken'"); } void test_overlay_remove_empty_token() @@ -132,7 +147,7 @@ TestLogger logger; CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " Y - X "), PSRETURN_OK); - TS_ASSERT_STR_EQUALS(node.ToXMLString(), "Y X"); + TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "Unexpected white space"); } void test_overlay_filtered() @@ -153,16 +168,16 @@ CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " foobar foo "), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " testbaz willnotbeincluded textmore text "), PSRETURN_OK); - TS_ASSERT_STR_EQUALS(node.ToXMLString(), "testbarbazmore texttextfoo"); + TS_ASSERT_STR_EQUALS(node.ToXMLString(), "testbarbazfootextmore text"); } void test_overlay_merge_empty() { - // 'merge' nodes don't change the original value. + // Edge-case: when merging, empty nodes don't overwrite the original value, but non-empty nodes do. CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, "foobar"), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, "skippedreplaced"), PSRETURN_OK); - TS_ASSERT_STR_EQUALS(node.ToXMLString(), "foobar"); + TS_ASSERT_STR_EQUALS(node.ToXMLString(), "fooreplaced"); } void test_overlay_filtered_merge() @@ -170,7 +185,7 @@ CParamNode node; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " 1200 "), PSRETURN_OK); TS_ASSERT_EQUALS(CParamNode::LoadXMLString(node, " bar 1 "), PSRETURN_OK); - TS_ASSERT_STR_EQUALS(node.ToXMLString(), "11200bar"); + TS_ASSERT_STR_EQUALS(node.ToXMLString(), "12001bar"); } void test_overlay_ops()