Changeset View
Changeset View
Standalone View
Standalone View
source/simulation2/system/ParamNode.cpp
Show All 19 Lines | |||||
#include "ParamNode.h" | #include "ParamNode.h" | ||||
#include "lib/utf8.h" | #include "lib/utf8.h" | ||||
#include "ps/CLogger.h" | #include "ps/CLogger.h" | ||||
#include "ps/CStr.h" | #include "ps/CStr.h" | ||||
#include "ps/Filesystem.h" | #include "ps/Filesystem.h" | ||||
#include "ps/XML/Xeromyces.h" | #include "ps/XML/Xeromyces.h" | ||||
#include "scriptinterface/ScriptInterface.h" | #include "scriptinterface/ScriptInterface.h" | ||||
#include "scriptinterface/ScriptExtraHeaders.h" | |||||
#include <sstream> | #include <sstream> | ||||
#include <unordered_map> | |||||
#include <string_view> | |||||
#include <boost/algorithm/string.hpp> | #include <boost/algorithm/string.hpp> | ||||
static CParamNode g_NullNode(false); | static CParamNode g_NullNode(false); | ||||
CParamNode::CParamNode(bool isOk) : | class CParamNode::Impl | ||||
m_IsOk(isOk) | |||||
{ | { | ||||
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*/) | static ChildrenMap::iterator Find(ChildrenMap& children, const std::string& name, u32 index) | ||||
{ | { | ||||
ret.ApplyLayer(xmb, xmb.GetRoot(), sourceIdentifier); | 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) | static size_t Count(ChildrenMap& children, const std::string& name) | ||||
{ | { | ||||
CXeromyces xero; | ChildrenMap::const_reverse_iterator it = std::find_if(children.rbegin(), children.rend(), | ||||
PSRETURN ok = xero.Load(g_VFS, path, validatorName); | [&](const CParamNode& node) { return node.m_Name == name; }); | ||||
if (ok != PSRETURN_OK) | if (it == children.rend()) | ||||
return; // (Xeromyces already logged an error) | 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*/) | static void SortChildren(CParamNode& node, bool recursive); | ||||
/** | |||||
* The ApplyLayer family of functions takes some data and applies it on the paramNode. | |||||
*/ | |||||
struct NodeMetadata | |||||
{ | { | ||||
CXeromyces xero; | enum Op { | ||||
PSRETURN ok = xero.LoadString(xml); | INVALID, | ||||
if (ok != PSRETURN_OK) | ADD, | ||||
return ok; | 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); | |||||
static bool HandleTokens(CParamNode& node, const std::string& value); | |||||
static bool HandleOp(CParamNode& node, const std::string& value, NodeMetadata::Op op); | |||||
}; | |||||
ret.ApplyLayer(xero, xero.GetRoot(), sourceIdentifier); | bool CParamNode::Impl::HandleTokens(CParamNode& node, const std::string &value) | ||||
{ | |||||
node.ResetScriptVal(); | |||||
return PSRETURN_OK; | // Split into tokens | ||||
} | std::vector<std::string> oldTokens; | ||||
std::vector<std::string> 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); | |||||
void CParamNode::ApplyLayer(const XMBFile& xmb, const XMBElement& element, const wchar_t* sourceIdentifier /*= NULL*/) | // Merge the two lists | ||||
std::vector<std::string> tokens = oldTokens; | |||||
for (size_t i = 0; i < newTokens.size(); ++i) | |||||
{ | |||||
if (newTokens[i][0] == L'-') | |||||
{ | |||||
if (newTokens[i].size() == 1) | |||||
{ | { | ||||
ResetScriptVal(); | // 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<std::string>::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]); | |||||
} | |||||
} | |||||
std::string name = xmb.GetElementString(element.GetNodeName()); // TODO: is GetElementString inefficient? | node.m_Value = boost::algorithm::join(tokens, " "); | ||||
CStr value = element.GetText(); | return true; | ||||
} | |||||
bool hasSetValue = false; | 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 | // Look for special attributes | ||||
int at_disable = xmb.GetAttributeID("disable"); | int at_disable = xmb.GetAttributeID("disable"); | ||||
int at_replace = xmb.GetAttributeID("replace"); | int at_replace = xmb.GetAttributeID("replace"); | ||||
int at_filtered = xmb.GetAttributeID("filtered"); | int at_filtered = xmb.GetAttributeID("filtered"); | ||||
int at_merge = xmb.GetAttributeID("merge"); | int at_merge = xmb.GetAttributeID("merge"); | ||||
int at_op = xmb.GetAttributeID("op"); | int at_op = xmb.GetAttributeID("op"); | ||||
int at_datatype = xmb.GetAttributeID("datatype"); | int at_datatype = xmb.GetAttributeID("datatype"); | ||||
enum op { | |||||
INVALID, | // Store metadata on how to process this element. | ||||
ADD, | NodeMetadata metadata; | ||||
MUL, | |||||
MUL_ROUND | // Look for attributes that modify how this node's value & children should be applied on the ParamNode. | ||||
} op = INVALID; | |||||
bool replacing = false; | |||||
bool filtering = false; | |||||
bool merging = false; | |||||
{ | |||||
XERO_ITER_ATTR(element, attr) | XERO_ITER_ATTR(element, attr) | ||||
{ | { | ||||
if (attr.Name == at_disable) | if (attr.Name == at_disable) | ||||
{ | { | ||||
m_Childs.erase(name); | // The "disable" attribute should not be seen, as the corresponding nodes are not applied. | ||||
return; | // 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; | |||||
} | } | ||||
else if (attr.Name == at_replace) | else if (attr.Name == at_replace) | ||||
{ | { | ||||
m_Childs.erase(name); | std::string name = node.m_Name; | ||||
replacing = true; | u32 index = node.m_Index; | ||||
node = CParamNode(); | |||||
node.m_Name = name; | |||||
node.m_Index = index; | |||||
} | } | ||||
else if (attr.Name == at_filtered) | else if (attr.Name == at_merge) | ||||
{ | { | ||||
filtering = true; | metadata.merging = true; | ||||
} | } | ||||
else if (attr.Name == at_merge) | else if (attr.Name == at_filtered) | ||||
{ | { | ||||
if (m_Childs.find(name) == m_Childs.end()) | metadata.filtering = true; | ||||
return; | |||||
merging = true; | |||||
} | } | ||||
else if (attr.Name == at_op) | else if (attr.Name == at_op) | ||||
{ | { | ||||
if (attr.Value == "add") | if (attr.Value == "add") | ||||
op = ADD; | metadata.op = NodeMetadata::ADD; | ||||
else if (attr.Value == "mul") | else if (attr.Value == "mul") | ||||
op = MUL; | metadata.op = NodeMetadata::MUL; | ||||
else if (attr.Value == "mul_round") | else if (attr.Value == "mul_round") | ||||
op = MUL_ROUND; | metadata.op = NodeMetadata::MUL_ROUND; | ||||
else | else | ||||
LOGWARNING("Invalid op '%ls'", attr.Value); | { | ||||
LOGERROR("Invalid op '%ls'", attr.Value); | |||||
return false; | |||||
} | } | ||||
} | } | ||||
else if (attr.Name == at_datatype && attr.Value == "tokens") | |||||
metadata.tokens = true; | |||||
} | } | ||||
if (metadata.tokens) | |||||
{ | { | ||||
XERO_ITER_ATTR(element, attr) | if (!Impl::HandleTokens(node, element.GetText())) | ||||
return false; | |||||
} | |||||
else if (metadata.op != NodeMetadata::INVALID) | |||||
{ | |||||
if (!Impl::HandleOp(node, element.GetText(), metadata.op)) | |||||
return false; | |||||
} | |||||
else | |||||
{ | { | ||||
if (attr.Name == at_datatype && attr.Value == "tokens") | CStr value = element.GetText(); | ||||
// Annoying edge case in XML: when merging over nodes without children (e.g. <Node>Value</Node>, <Node merge=""/>), | |||||
// 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()) | |||||
{ | { | ||||
CParamNode& node = m_Childs[name]; | node.ResetScriptVal(); | ||||
node.m_Value = value; | |||||
} | |||||
} | |||||
// Split into tokens | // Add the element's attributes, prefixing names with "@" | ||||
std::vector<std::string> oldTokens; | XERO_ITER_ATTR(element, attr) | ||||
std::vector<std::string> newTokens; | { | ||||
if (!replacing && !node.m_Value.empty()) // ignore the old tokens if replace="" was given | // Skip special attributes | ||||
boost::algorithm::split(oldTokens, node.m_Value, boost::algorithm::is_space(), boost::algorithm::token_compress_on); | if (attr.Name == at_replace || attr.Name == at_op || attr.Name == at_merge || attr.Name == at_filtered) | ||||
if (!value.empty()) | continue; | ||||
boost::algorithm::split(newTokens, value, boost::algorithm::is_space(), boost::algorithm::token_compress_on); | // Add any others | ||||
node.ResetScriptVal(); | |||||
std::string attrName = xmb.GetAttributeString(attr.Name); | |||||
Impl::TryEmplace(node, "@" + attrName, 0).m_Value = attr.Value; | |||||
} | |||||
// Merge the two lists | using ChildrenMap = CParamNode::ChildrenMap; | ||||
std::vector<std::string> tokens = oldTokens; | ChildrenMap childs; | ||||
for (size_t i = 0; i < newTokens.size(); ++i) | |||||
// 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<int, int> counts; | |||||
XERO_ITER_EL(element, child) | |||||
{ | { | ||||
if (newTokens[i][0] == '-') | // 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) | |||||
{ | { | ||||
std::vector<std::string>::iterator tokenIt = std::find(tokens.begin(), tokens.end(), newTokens[i].substr(1)); | if (attr.Name == at_disable) | ||||
if (tokenIt != tokens.end()) | { | ||||
tokens.erase(tokenIt); | ChildrenMap::iterator it = Impl::Find(node.m_Children, name, count); | ||||
else | if (it != node.m_Children.end()) | ||||
LOGWARNING("[ParamNode] Could not remove token '%s' from node '%s'%s; not present in list nor inherited (possible typo?)", | node.m_Children.erase(it); | ||||
newTokens[i].substr(1), name, sourceIdentifier ? (" in '" + utf8_from_wstring(sourceIdentifier) + "'").c_str() : ""); | // TODO: shift children's index down by one. | ||||
skipApplying = true; | |||||
break; | |||||
} | } | ||||
else | else if (attr.Name == at_merge) | ||||
{ | { | ||||
if (std::find(oldTokens.begin(), oldTokens.end(), newTokens[i]) == oldTokens.end()) | ChildrenMap::iterator it = Impl::Find(node.m_Children, name, count); | ||||
tokens.push_back(newTokens[i]); | if (it == node.m_Children.end()) | ||||
{ | |||||
skipApplying = true; | |||||
break; | |||||
} | |||||
} | } | ||||
} | } | ||||
if (skipApplying) | |||||
continue; | |||||
node.m_Value = boost::algorithm::join(tokens, " "); | // NB: if a child with this name/index doesn't exist, it will be created, but the index may _not equal_ 'count'. | ||||
hasSetValue = true; | // (this is because of 'disable' nodes and other operations). | ||||
break; | 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++; | |||||
} | } | ||||
// Add this element as a child node | return true; | ||||
CParamNode& node = m_Childs[name]; | } | ||||
if (op != INVALID) | |||||
bool CParamNode::Impl::ApplyLayer(CParamNode& node, const ScriptInterface& scriptInterface, JS::HandleValue value) | |||||
{ | { | ||||
// TODO: Support parsing of data types other than fixed; log warnings in other cases | ScriptRequest rq(scriptInterface); | ||||
fixed oldval = node.ToFixed(); | |||||
fixed mod = fixed::FromString(value); | |||||
switch (op) | std::vector<std::string> props; | ||||
if (!scriptInterface.EnumeratePropertyNames(value, true, props)) | |||||
{ | { | ||||
case ADD: | ScriptException::Raise(rq, "Failed to enumerate component properties."); | ||||
node.m_Value = (oldval + mod).ToString(); | 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; | break; | ||||
case MUL: | } | ||||
node.m_Value = oldval.Multiply(mod).ToString(); | case JSTYPE_STRING: | ||||
case JSTYPE_NUMBER: | |||||
{ | |||||
ScriptInterface::FromJSVal(rq, child, Impl::TryEmplace(node, prop, index).m_Value); | |||||
break; | break; | ||||
case MUL_ROUND: | } | ||||
node.m_Value = fixed::FromInt(oldval.Multiply(mod).ToInt_RoundToNearest()).ToString(); | 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; | break; | ||||
} | |||||
default: | default: | ||||
break; | { | ||||
LOGERROR("Unsupported JS construct when parsing ParamNode"); | |||||
return false; | |||||
} | } | ||||
hasSetValue = true; | } | ||||
} | |||||
return true; | |||||
} | } | ||||
if (!hasSetValue && !merging) | CParamNode::CParamNode(bool isOk) : | ||||
node.m_Value = value; | m_IsOk(isOk) | ||||
{ | |||||
m_Index = 0; | |||||
} | |||||
// We also need to reset node's script val, even if it has no children | void CParamNode::LoadXML(CParamNode& ret, const VfsPath& path, const std::string& validatorName) | ||||
// or if the attributes change. | { | ||||
node.ResetScriptVal(); | CXeromyces xero; | ||||
PSRETURN ok = xero.Load(g_VFS, path, validatorName); | |||||
if (ok != PSRETURN_OK) | |||||
return; // (Xeromyces already logged an error) | |||||
// For the filtered case | LoadXML(ret, xero, path.string().c_str()); | ||||
ChildrenMap childs; | } | ||||
// Recurse through the element's children | void CParamNode::LoadXML(CParamNode& ret, const XMBFile& xmb, const wchar_t* sourceIdentifier /*= NULL*/) | ||||
XERO_ITER_EL(element, child) | |||||
{ | |||||
node.ApplyLayer(xmb, child, sourceIdentifier); | |||||
if (filtering) | |||||
{ | { | ||||
std::string childname = xmb.GetElementString(child.GetNodeName()); | std::string name = xmb.GetElementString(xmb.GetRoot().GetNodeName()); // TODO: is GetElementString inefficient? | ||||
if (node.m_Childs.find(childname) != node.m_Childs.end()) | CParamNode& root = Impl::TryEmplace(ret, name, 0); | ||||
childs[childname] = std::move(node.m_Childs[childname]); | if (!Impl::ApplyLayer(root, xmb, xmb.GetRoot())) | ||||
LOGERROR("Error when loading XML file '%s' into ParamNode", sourceIdentifier ? utf8_from_wstring(sourceIdentifier) : "(unknown)"); | |||||
} | } | ||||
} | |||||
if (filtering) | |||||
node.m_Childs.swap(childs); | |||||
// Add the element's attributes, prefixing names with "@" | PSRETURN CParamNode::LoadXMLString(CParamNode& ret, const char* xml, const wchar_t* sourceIdentifier /*=NULL*/) | ||||
XERO_ITER_ATTR(element, attr) | |||||
{ | { | ||||
// Skip special attributes | CXeromyces xero; | ||||
if (attr.Name == at_replace || attr.Name == at_op || attr.Name == at_merge || attr.Name == at_filtered) | PSRETURN ok = xero.LoadString(xml); | ||||
continue; | if (ok != PSRETURN_OK) | ||||
// Add any others | return ok; | ||||
std::string attrName = xmb.GetAttributeString(attr.Name); | |||||
node.m_Childs["@" + attrName].m_Value = attr.Value; | 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); | return Impl::ApplyLayer(ret, scriptInterface, value); | ||||
if (it == m_Childs.end()) | |||||
return g_NullNode; | |||||
return it->second; | |||||
} | } | ||||
bool CParamNode::IsOk() const | bool CParamNode::IsOk() const | ||||
{ | { | ||||
return m_IsOk; | return m_IsOk; | ||||
} | } | ||||
const std::wstring CParamNode::ToWString() const | const std::wstring CParamNode::ToWString() const | ||||
Show All 29 Lines | |||||
bool CParamNode::ToBool() const | bool CParamNode::ToBool() const | ||||
{ | { | ||||
if (m_Value == "true") | if (m_Value == "true") | ||||
return true; | return true; | ||||
else | else | ||||
return false; | 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 | 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 CParamNode::EscapeXMLString(const std::string& str) | ||||
{ | { | ||||
std::string ret; | std::string ret; | ||||
ret.reserve(str.size()); | ret.reserve(str.size()); | ||||
// TODO: would be nice to check actual v1.0 XML codepoints, | // TODO: would be nice to check actual v1.0 XML codepoints, | ||||
// but our UTF8 validation routines are lacking. | // but our UTF8 validation routines are lacking. | ||||
for (size_t i = 0; i < str.size(); ++i) | for (size_t i = 0; i < str.size(); ++i) | ||||
{ | { | ||||
Show All 20 Lines | std::string CParamNode::ToXMLString() const | ||||
ToXMLString(strm); | ToXMLString(strm); | ||||
return strm.str(); | return strm.str(); | ||||
} | } | ||||
void CParamNode::ToXMLString(std::ostream& strm) const | void CParamNode::ToXMLString(std::ostream& strm) const | ||||
{ | { | ||||
strm << m_Value; | strm << m_Value; | ||||
ChildrenMap::const_iterator it = m_Childs.begin(); | ChildrenMap::const_iterator it = m_Children.begin(); | ||||
for (; it != m_Childs.end(); ++it) | for (; it != m_Children.end(); ++it) | ||||
{ | { | ||||
// Skip attributes here (they were handled when the caller output the tag) | // 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; | continue; | ||||
strm << "<" << it->first; | strm << "<" << it->m_Name; | ||||
// Output the child's attributes first | // Output the child's attributes first | ||||
ChildrenMap::const_iterator cit = it->second.m_Childs.begin(); | ChildrenMap::const_iterator cit = it->m_Children.begin(); | ||||
for (; cit != it->second.m_Childs.end(); ++cit) | 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()); | std::string attrname (cit->m_Name.begin()+1, cit->m_Name.end()); | ||||
strm << " " << attrname << "=\"" << EscapeXMLString(cit->second.m_Value) << "\""; | strm << " " << attrname << "=\"" << EscapeXMLString(cit->m_Value) << "\""; | ||||
} | } | ||||
} | } | ||||
strm << ">"; | strm << ">"; | ||||
it->second.ToXMLString(strm); | it->ToXMLString(strm); | ||||
strm << "</" << it->first << ">"; | strm << "</" << it->m_Name << ">"; | ||||
} | } | ||||
} | } | ||||
void CParamNode::ToJSVal(const ScriptRequest& rq, bool cacheValue, JS::MutableHandleValue ret) const | 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); | ret.set(*m_ScriptVal); | ||||
return; | return; | ||||
} | } | ||||
ConstructJSVal(rq, ret); | ConstructJSVal(rq, ret); | ||||
if (cacheValue) | if (cacheValue) | ||||
m_ScriptVal.reset(new JS::PersistentRootedValue(rq.cx, ret)); | m_ScriptVal = std::make_shared<JS::PersistentRootedValue>(rq.cx, ret); | ||||
} | } | ||||
void CParamNode::ConstructJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret) const | void CParamNode::ConstructJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret) const | ||||
{ | { | ||||
if (m_Childs.empty()) | if (m_Children.empty()) | ||||
{ | { | ||||
// Empty node - map to undefined | // Empty node - map to undefined | ||||
if (m_Value.empty()) | if (m_Value.empty()) | ||||
{ | { | ||||
ret.setUndefined(); | ret.setUndefined(); | ||||
return; | return; | ||||
} | } | ||||
Show All 15 Lines | void CParamNode::ConstructJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret) const | ||||
JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx)); | JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx)); | ||||
if (!obj) | if (!obj) | ||||
{ | { | ||||
ret.setUndefined(); | ret.setUndefined(); | ||||
return; // TODO: report error | return; // TODO: report error | ||||
} | } | ||||
JS::RootedValue childVal(rq.cx); | JS::RootedValue childVal(rq.cx); | ||||
for (std::map<std::string, CParamNode>::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); | it->ConstructJSVal(rq, &childVal); | ||||
if (!JS_SetProperty(rq.cx, obj, it->first.c_str(), childVal)) | if (!JS_SetProperty(rq.cx, obj, it->m_Name.c_str(), childVal)) | ||||
{ | { | ||||
ret.setUndefined(); | ret.setUndefined(); | ||||
return; // TODO: report error | 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 the node has a string too, add that as an extra property | ||||
if (!m_Value.empty()) | if (!m_Value.empty()) | ||||
{ | { | ||||
utf16string text(m_Value.begin(), m_Value.end()); | utf16string text(m_Value.begin(), m_Value.end()); | ||||
JS::RootedString str(rq.cx, JS_AtomizeAndPinUCStringN(rq.cx, reinterpret_cast<const char16_t*>(text.data()), text.length())); | JS::RootedString str(rq.cx, JS_AtomizeAndPinUCStringN(rq.cx, reinterpret_cast<const char16_t*>(text.data()), text.length())); | ||||
if (!str) | if (!str) | ||||
{ | { | ||||
Show All 9 Lines | if (!m_Value.empty()) | ||||
} | } | ||||
} | } | ||||
ret.setObject(*obj); | ret.setObject(*obj); | ||||
} | } | ||||
void CParamNode::ResetScriptVal() | void CParamNode::ResetScriptVal() | ||||
{ | { | ||||
m_ScriptVal = NULL; | m_ScriptVal.reset(); | ||||
} | } |
Wildfire Games · Phabricator