/** Copyright 2013 BlackBerry Inc. Copyright (c) 2015-2017 Chukong Technologies Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Original file from GamePlay3D: http://gameplay3d.org This file was modified to fit the cocos2d-x project */ #include "base/CCProperties.h" #include #include "platform/CCPlatformMacros.h" #include "platform/CCFileUtils.h" #include "math/Vec2.h" #include "math/Vec3.h" #include "math/Vec4.h" #include "math/Mat4.h" #include "math/Quaternion.h" #include "base/ccUTF8.h" #include "base/CCData.h" USING_NS_AX; // Utility functions (shared with SceneLoader). /** @script{ignore} */ void calculateNamespacePath(std::string_view urlString, std::string& fileString, std::vector& namespacePath); /** @script{ignore} */ Properties* getPropertiesFromNamespacePath(Properties* properties, const std::vector& namespacePath); Properties::Properties() : _dataIdx(nullptr), _data(nullptr), _variables(nullptr), _dirPath(nullptr), _parent(nullptr) { _properties.reserve(32); } Properties::Properties(const Properties& copy) : _dataIdx(copy._dataIdx) , _data(copy._data) , _namespace(copy._namespace) , _id(copy._id) , _parentID(copy._parentID) , _properties(copy._properties) , _variables(nullptr) , _dirPath(nullptr) , _parent(copy._parent) { setDirectoryPath(copy._dirPath); for (const auto space : copy._namespaces) { _namespaces.push_back(new Properties(*space)); } rewind(); } Properties::Properties(Data* data, ssize_t* dataIdx) : _dataIdx(dataIdx), _data(data), _variables(NULL), _dirPath(NULL), _parent(NULL) { readProperties(); rewind(); } Properties::Properties(Data* data, ssize_t* dataIdx, std::string_view name, const char* id, const char* parentID, Properties* parent) : _dataIdx(dataIdx), _data(data), _namespace(name), _variables(NULL), _dirPath(NULL), _parent(parent) { if (id) { _id = id; } if (parentID) { _parentID = parentID; } readProperties(); rewind(); } Properties* Properties::createNonRefCounted(std::string_view url) { if (url.empty()) { AXLOGERROR("Attempting to create a Properties object from an empty URL!"); return nullptr; } // Calculate the file and full namespace path from the specified url. auto& urlString = url; std::string fileString; std::vector namespacePath; calculateNamespacePath(urlString, fileString, namespacePath); // data will be released automatically when 'data' goes out of scope // so we pass data as weak pointer auto data = FileUtils::getInstance()->getDataFromFile(fileString); ssize_t dataIdx = 0; Properties* properties = new Properties(&data, &dataIdx); properties->resolveInheritance(); // Get the specified properties object. Properties* p = getPropertiesFromNamespacePath(properties, namespacePath); if (!p) { AXLOGWARN("Failed to load properties from url '%s'.", url.data()); AX_SAFE_DELETE(properties); return nullptr; } // If the loaded properties object is not the root namespace, // then we have to clone it and delete the root namespace // so that we don't leak memory. if (p != properties) { p = p->clone(); AX_SAFE_DELETE(properties); } // XXX // p->setDirectoryPath(FileSystem::getDirectoryName(fileString)); p->setDirectoryPath(""); return p; } static bool isVariable(const char* str, char* outName, size_t outSize) { size_t len = strlen(str); if (len > 3 && str[0] == '$' && str[1] == '{' && str[len - 1] == '}') { size_t size = len - 3; if (size > (outSize - 1)) size = outSize - 1; strncpy(outName, str + 2, len - 3); outName[len - 3] = 0; return true; } return false; } void Properties::readProperties() { AXASSERT(_data->getSize() > 0, "Invalid data"); char line[2048]; char variable[256]; int c; char* name; char* value; char* parentID; char* rc; char* rcc; char* rccc; bool comment = false; while (true) { // Skip whitespace at the start of lines skipWhiteSpace(); // Stop when we have reached the end of the file. if (eof()) break; // Read the next line. rc = readLine(line, 2048); if (rc == NULL) { AXLOGERROR("Error reading line from file."); return; } // Ignore comments if (comment) { // Check for end of multi-line comment at either start or end of line if (strncmp(line, "*/", 2) == 0) comment = false; else { trimWhiteSpace(line); const auto len = strlen(line); if (len >= 2 && strncmp(line + (len - 2), "*/", 2) == 0) comment = false; } } else if (strncmp(line, "/*", 2) == 0) { // Start of multi-line comment (must be at start of line) comment = true; } else if (strncmp(line, "//", 2) != 0) { // If an '=' appears on this line, parse it as a name/value pair. // Note: strchr() has to be called before strtok(), or a backup of line has to be kept. rc = strchr(line, '='); if (rc != NULL) { // First token should be the property name. name = strtok(line, "="); if (name == NULL) { AXLOGERROR("Error parsing properties file: attribute without name."); return; } // Remove white-space from name. name = trimWhiteSpace(name); // Scan for next token, the property's value. value = strtok(NULL, ""); if (value == NULL) { AXLOGERROR("Error parsing properties file: attribute with name ('%s') but no value.", name); return; } // Remove white-space from value. value = trimWhiteSpace(value); // Is this a variable assignment? if (isVariable(name, variable, 256)) { setVariable(variable, value); } else { // Normal name/value pair _properties.push_back(Property(name, value)); } } else { parentID = NULL; // Get the last character on the line (ignoring whitespace). const char* lineEnd = trimWhiteSpace(line) + (strlen(trimWhiteSpace(line)) - 1); // This line might begin or end a namespace, // or it might be a key/value pair without '='. // Check for '{' on same line. rc = strchr(line, '{'); // Check for inheritance: ':' rcc = strchr(line, ':'); // Check for '}' on same line. rccc = strchr(line, '}'); // Get the name of the namespace. name = strtok(line, " \t\n{"); name = trimWhiteSpace(name); if (name == NULL) { AXLOGERROR("Error parsing properties file: failed to determine a valid token for line '%s'.", line); return; } else if (name[0] == '}') { // End of namespace. return; } // Get its ID if it has one. value = strtok(NULL, ":{"); value = trimWhiteSpace(value); // Get its parent ID if it has one. if (rcc != NULL) { parentID = strtok(NULL, "{"); parentID = trimWhiteSpace(parentID); } if (value != NULL && value[0] == '{') { // If the namespace ends on this line, seek back to right before the '}' character. if (rccc && rccc == lineEnd) { if (seekFromCurrent(-1) == false) { AXLOGERROR("Failed to seek back to before a '}' character in properties file."); return; } while (readChar() != '}') { if (seekFromCurrent(-2) == false) { AXLOGERROR("Failed to seek back to before a '}' character in properties file."); return; } } if (seekFromCurrent(-1) == false) { AXLOGERROR("Failed to seek back to before a '}' character in properties file."); return; } } // New namespace without an ID. Properties* space = new Properties(_data, _dataIdx, name, NULL, parentID, this); _namespaces.push_back(space); // If the namespace ends on this line, seek to right after the '}' character. if (rccc && rccc == lineEnd) { if (seekFromCurrent(1) == false) { AXLOGERROR("Failed to seek to immediately after a '}' character in properties file."); return; } } } else { // If '{' appears on the same line. if (rc != NULL) { // If the namespace ends on this line, seek back to right before the '}' character. if (rccc && rccc == lineEnd) { if (seekFromCurrent(-1) == false) { AXLOGERROR("Failed to seek back to before a '}' character in properties file."); return; } while (readChar() != '}') { if (seekFromCurrent(-2) == false) { AXLOGERROR("Failed to seek back to before a '}' character in properties file."); return; } } if (seekFromCurrent(-1) == false) { AXLOGERROR("Failed to seek back to before a '}' character in properties file."); return; } } // Create new namespace. Properties* space = new Properties(_data, _dataIdx, name, value, parentID, this); _namespaces.push_back(space); // If the namespace ends on this line, seek to right after the '}' character. if (rccc && rccc == lineEnd) { if (seekFromCurrent(1) == false) { AXLOGERROR("Failed to seek to immediately after a '}' character in properties file."); return; } } } else { // Find out if the next line starts with "{" skipWhiteSpace(); c = readChar(); if (c == '{') { // Create new namespace. Properties* space = new Properties(_data, _dataIdx, name, value, parentID, this); _namespaces.push_back(space); } else { // Back up from fgetc() if (seekFromCurrent(-1) == false) AXLOGERROR( "Failed to seek backwards a single character after testing if the next line starts " "with '{'."); // Store "name value" as a name/value pair, or even just "name". if (value != NULL) { _properties.push_back(Property(name, value)); } else { _properties.push_back(Property(name, "")); } } } } } } } } Properties::~Properties() { AX_SAFE_DELETE(_dirPath); for (size_t i = 0, count = _namespaces.size(); i < count; ++i) { AX_SAFE_DELETE(_namespaces[i]); } AX_SAFE_DELETE(_variables); } // // Stream simulation // signed char Properties::readChar() { if (eof()) return EOF; return _data->_bytes[(*_dataIdx)++]; } char* Properties::readLine(char* output, int num) { if (eof()) return nullptr; // little optimization: avoid unneeded dereferences const ssize_t dataIdx = *_dataIdx; int i; for (i = 0; i < num && dataIdx + i < _data->_size; i++) { auto c = _data->_bytes[dataIdx + i]; if (c == '\n') break; output[i] = c; } output[i] = '\0'; // restore value *_dataIdx = dataIdx + i; return output; } bool Properties::seekFromCurrent(int offset) { (*_dataIdx) += offset; return (!eof() && *_dataIdx >= 0); } bool Properties::eof() { return (*_dataIdx >= _data->_size); } void Properties::skipWhiteSpace() { signed char c; do { c = readChar(); } while (isspace(c) && c != EOF); // If we are not at the end of the file, then since we found a // non-whitespace character, we put the cursor back in front of it. if (c != EOF) { if (seekFromCurrent(-1) == false) { AXLOGERROR("Failed to seek backwards one character after skipping whitespace."); } } } char* Properties::trimWhiteSpace(char* str) { if (str == NULL) { return str; } // Trim leading space. while (*str != '\0' && isspace(*str)) str++; // All spaces? if (*str == 0) { return str; } // Trim trailing space. char* end = str + strlen(str) - 1; while (end > str && isspace(*end)) end--; // Write new null terminator. *(end + 1) = 0; return str; } void Properties::resolveInheritance(const char* id) { // Namespaces can be defined like so: // "name id : parentID { }" // This method merges data from the parent namespace into the child. // Get a top-level namespace. Properties* derived; if (id) { derived = getNamespace(id); } else { derived = getNextNamespace(); } while (derived) { // If the namespace has a parent ID, find the parent. if (!derived->_parentID.empty()) { Properties* parent = getNamespace(derived->_parentID.c_str()); if (parent) { resolveInheritance(parent->getId()); // Copy the child. Properties* overrides = new Properties(*derived); // Delete the child's data. for (size_t i = 0, count = derived->_namespaces.size(); i < count; i++) { AX_SAFE_DELETE(derived->_namespaces[i]); } // Copy data from the parent into the child. derived->_properties = parent->_properties; derived->_namespaces = std::vector(); std::vector::const_iterator itt; for (const auto space : parent->_namespaces) { derived->_namespaces.push_back(new Properties(*space)); } derived->rewind(); // Take the original copy of the child and override the data copied from the parent. derived->mergeWith(overrides); // Delete the child copy. AX_SAFE_DELETE(overrides); } } // Resolve inheritance within this namespace. derived->resolveInheritance(); // Get the next top-level namespace and check again. if (!id) { derived = getNextNamespace(); } else { derived = NULL; } } } void Properties::mergeWith(Properties* overrides) { AXASSERT(overrides, "Invalid overrides"); // Overwrite or add each property found in child. overrides->rewind(); const char* name = overrides->getNextProperty(); while (name) { this->setString(name, overrides->getString()); name = overrides->getNextProperty(); } this->_propertiesItr = this->_properties.end(); // Merge all common nested namespaces, add new ones. Properties* overridesNamespace = overrides->getNextNamespace(); while (overridesNamespace) { bool merged = false; rewind(); Properties* derivedNamespace = getNextNamespace(); while (derivedNamespace) { if (strcmp(derivedNamespace->getNamespace(), overridesNamespace->getNamespace()) == 0 && strcmp(derivedNamespace->getId(), overridesNamespace->getId()) == 0) { derivedNamespace->mergeWith(overridesNamespace); merged = true; } derivedNamespace = getNextNamespace(); } if (!merged) { // Add this new namespace. Properties* newNamespace = new Properties(*overridesNamespace); this->_namespaces.push_back(newNamespace); this->_namespacesItr = this->_namespaces.end(); } overridesNamespace = overrides->getNextNamespace(); } } const char* Properties::getNextProperty() { if (_propertiesItr == _properties.end()) { // Restart from the beginning _propertiesItr = _properties.begin(); } else { // Move to the next property ++_propertiesItr; } return _propertiesItr == _properties.end() ? NULL : _propertiesItr->name.c_str(); } Properties* Properties::getNextNamespace() { if (_namespacesItr == _namespaces.end()) { // Restart from the beginning _namespacesItr = _namespaces.begin(); } else { ++_namespacesItr; } if (_namespacesItr != _namespaces.end()) { Properties* ns = *_namespacesItr; return ns; } return nullptr; } void Properties::rewind() { _propertiesItr = _properties.end(); _namespacesItr = _namespaces.end(); } Properties* Properties::getNamespace(const char* id, bool searchNames, bool recurse) const { AXASSERT(id, "invalid id"); for (const auto& it : _namespaces) { Properties* p = it; if (strcmp(searchNames ? p->_namespace.c_str() : p->_id.c_str(), id) == 0) return p; if (recurse) { // Search recursively. p = p->getNamespace(id, searchNames, true); if (p) return p; } } return nullptr; } const char* Properties::getNamespace() const { return _namespace.c_str(); } const char* Properties::getId() const { return _id.c_str(); } bool Properties::exists(const char* name) const { if (name == NULL) return false; for (const auto& itr : _properties) { if (itr.name == name) return true; } return false; } static bool isStringNumeric(const char* str) { AXASSERT(str, "invalid str"); // The first character may be '-' if (*str == '-') str++; // The first character after the sign must be a digit if (!isdigit(*str)) return false; str++; // All remaining characters must be digits, with a single decimal (.) permitted unsigned int decimalCount = 0; while (*str) { if (!isdigit(*str)) { if (*str == '.' && decimalCount == 0) { // Max of 1 decimal allowed decimalCount++; } else { return false; } } str++; } return true; } Properties::Type Properties::getType(const char* name) const { const char* value = getString(name); if (!value) { return Properties::NONE; } // Parse the value to determine the format unsigned int commaCount = 0; char* valuePtr = const_cast(value); while ((valuePtr = strchr(valuePtr, ','))) { valuePtr++; commaCount++; } switch (commaCount) { case 0: return isStringNumeric(value) ? Properties::NUMBER : Properties::STRING; case 1: return Properties::VECTOR2; case 2: return Properties::VECTOR3; case 3: return Properties::VECTOR4; case 15: return Properties::MATRIX; default: return Properties::STRING; } } const char* Properties::getString(const char* name, const char* defaultValue) const { char variable[256]; const char* value = NULL; if (name) { // If 'name' is a variable, return the variable value if (isVariable(name, variable, 256)) { return getVariable(variable, defaultValue); } for (const auto& itr : _properties) { if (itr.name == name) { value = itr.value.c_str(); break; } } } else { // No name provided - get the value at the current iterator position if (_propertiesItr != _properties.end()) { value = _propertiesItr->value.c_str(); } } if (value) { // If the value references a variable, return the variable value if (isVariable(value, variable, 256)) return getVariable(variable, defaultValue); return value; } return defaultValue; } bool Properties::setString(const char* name, const char* value) { if (name) { for (auto&& itr : _properties) { if (itr.name == name) { // Update the first property that matches this name itr.value = value ? value : ""; return true; } } // There is no property with this name, so add one _properties.push_back(Property(name, value ? value : "")); } else { // If there's a current property, set its value if (_propertiesItr == _properties.end()) return false; _propertiesItr->value = value ? value : ""; } return true; } bool Properties::getBool(const char* name, bool defaultValue) const { const char* valueString = getString(name); if (valueString) { return (strcmp(valueString, "true") == 0); } return defaultValue; } int Properties::getInt(const char* name) const { const char* valueString = getString(name); if (valueString) { int value; int scanned; scanned = sscanf(valueString, "%d", &value); if (scanned != 1) { AXLOGERROR("Error attempting to parse property '%s' as an integer.", name); return 0; } return value; } return 0; } float Properties::getFloat(const char* name) const { const char* valueString = getString(name); if (valueString) { float value; int scanned; scanned = sscanf(valueString, "%f", &value); if (scanned != 1) { AXLOGERROR("Error attempting to parse property '%s' as a float.", name); return 0.0f; } return value; } return 0.0f; } bool Properties::getMat4(const char* name, Mat4* out) const { AXASSERT(out, "Invalid out"); const char* valueString = getString(name); if (valueString) { float m[16]; int scanned; scanned = sscanf(valueString, "%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f", &m[0], &m[1], &m[2], &m[3], &m[4], &m[5], &m[6], &m[7], &m[8], &m[9], &m[10], &m[11], &m[12], &m[13], &m[14], &m[15]); if (scanned != 16) { AXLOGERROR("Error attempting to parse property '%s' as a matrix.", name); out->setIdentity(); return false; } out->set(m); return true; } out->setIdentity(); return false; } bool Properties::getVec2(const char* name, Vec2* out) const { return parseVec2(getString(name), out); } bool Properties::getVec3(const char* name, Vec3* out) const { return parseVec3(getString(name), out); } bool Properties::getVec4(const char* name, Vec4* out) const { return parseVec4(getString(name), out); } bool Properties::getQuaternionFromAxisAngle(const char* name, Quaternion* out) const { return parseAxisAngle(getString(name), out); } bool Properties::getColor(const char* name, Vec3* out) const { return parseColor(getString(name), out); } bool Properties::getColor(const char* name, Vec4* out) const { return parseColor(getString(name), out); } bool Properties::getPath(const char* name, std::string* path) const { AXASSERT(name && path, "Invalid name or path"); const char* valueString = getString(name); if (valueString) { if (FileUtils::getInstance()->isFileExist(valueString)) { path->assign(valueString); return true; } else { const Properties* prop = this; while (prop != NULL) { // Search for the file path relative to the bundle file const std::string* dirPath = prop->_dirPath; if (dirPath != NULL && !dirPath->empty()) { std::string relativePath = *dirPath; relativePath.append(valueString); if (FileUtils::getInstance()->isFileExist(relativePath)) { path->assign(relativePath); return true; } } prop = prop->_parent; } } } return false; } const char* Properties::getVariable(const char* name, const char* defaultValue) const { if (name == NULL) return defaultValue; // Search for variable in this Properties object if (_variables) { for (size_t i = 0, count = _variables->size(); i < count; ++i) { Property& prop = (*_variables)[i]; if (prop.name == name) return prop.value.c_str(); } } // Search for variable in parent Properties return _parent ? _parent->getVariable(name, defaultValue) : defaultValue; } void Properties::setVariable(const char* name, const char* value) { AXASSERT(name, "Invalid name"); Property* prop = NULL; // Search for variable in this Properties object and parents Properties* current = const_cast(this); while (current) { if (current->_variables) { for (size_t i = 0, count = current->_variables->size(); i < count; ++i) { Property* p = &(*current->_variables)[i]; if (p->name == name) { prop = p; break; } } } current = current->_parent; } if (prop) { // Found an existing property, set it prop->value = value ? value : ""; } else { // Add a new variable with this name if (!_variables) _variables = new std::vector(); _variables->push_back(Property(name, value ? value : "")); } } Properties* Properties::clone() { Properties* p = new Properties(); p->_namespace = _namespace; p->_id = _id; p->_parentID = _parentID; p->_properties = _properties; p->_propertiesItr = p->_properties.end(); p->setDirectoryPath(_dirPath); for (size_t i = 0, count = _namespaces.size(); i < count; i++) { AXASSERT(_namespaces[i], "Invalid namespace"); Properties* child = _namespaces[i]->clone(); p->_namespaces.push_back(child); child->_parent = p; } p->_namespacesItr = p->_namespaces.end(); return p; } void Properties::setDirectoryPath(const std::string* path) { if (path) { setDirectoryPath(*path); } else { AX_SAFE_DELETE(_dirPath); } } void Properties::setDirectoryPath(std::string_view path) { if (_dirPath == NULL) { _dirPath = new std::string(path); } else { _dirPath->assign(path); } } void calculateNamespacePath(std::string_view urlString, std::string& fileString, std::vector& namespacePath) { // If the url references a specific namespace within the file, // calculate the full namespace path to the final namespace. size_t loc = urlString.rfind('#'); if (loc != std::string::npos) { fileString = urlString.substr(0, loc); auto namespacePathString = urlString.substr(loc + 1); while ((loc = namespacePathString.find('/')) != std::string::npos) { namespacePath.push_back(std::string{namespacePathString.substr(0, loc)}); namespacePathString = namespacePathString.substr(loc + 1); } namespacePath.push_back(std::string{namespacePathString}); } else { fileString = urlString; } } Properties* getPropertiesFromNamespacePath(Properties* properties, const std::vector& namespacePath) { // If the url references a specific namespace within the file, // return the specified namespace or notify the user if it cannot be found. if (!namespacePath.empty()) { size_t size = namespacePath.size(); properties->rewind(); Properties* iter = properties->getNextNamespace(); for (size_t i = 0; i < size;) { while (true) { if (iter == NULL) { AXLOGWARN("Failed to load properties object from url."); return nullptr; } if (strcmp(iter->getId(), namespacePath[i].c_str()) == 0) { if (i != size - 1) { properties = iter->getNextNamespace(); iter = properties; } else properties = iter; i++; break; } iter = properties->getNextNamespace(); } } return properties; } else return properties; } bool Properties::parseVec2(const char* str, Vec2* out) { if (str) { float x, y; if (sscanf(str, "%f,%f", &x, &y) == 2) { if (out) out->set(x, y); return true; } else { AXLOGWARN("Error attempting to parse property as a two-dimensional vector: %s", str); } } if (out) out->set(0.0f, 0.0f); return false; } bool Properties::parseVec3(const char* str, Vec3* out) { if (str) { float x, y, z; if (sscanf(str, "%f,%f,%f", &x, &y, &z) == 3) { if (out) out->set(x, y, z); return true; } else { AXLOGWARN("Error attempting to parse property as a three-dimensional vector: %s", str); } } if (out) out->set(0.0f, 0.0f, 0.0f); return false; } bool Properties::parseVec4(const char* str, Vec4* out) { if (str) { float x, y, z, w; if (sscanf(str, "%f,%f,%f,%f", &x, &y, &z, &w) == 4) { if (out) out->set(x, y, z, w); return true; } else { AXLOGWARN("Error attempting to parse property as a four-dimensional vector: %s", str); } } if (out) out->set(0.0f, 0.0f, 0.0f, 0.0f); return false; } bool Properties::parseAxisAngle(const char* str, Quaternion* out) { if (str) { float x, y, z, theta; if (sscanf(str, "%f,%f,%f,%f", &x, &y, &z, &theta) == 4) { if (out) out->set(Vec3(x, y, z), MATH_DEG_TO_RAD(theta)); return true; } else { AXLOGWARN("Error attempting to parse property as an axis-angle rotation: %s", str); } } if (out) out->set(0.0f, 0.0f, 0.0f, 1.0f); return false; } bool Properties::parseColor(const char* str, Vec3* out) { if (str) { if (strlen(str) == 7 && str[0] == '#') { // Read the string into an int as hex. unsigned int color; if (sscanf(str + 1, "%x", &color) == 1) { if (out) out->set(Vec3::fromColor(color)); return true; } else { // Invalid format AXLOGWARN("Error attempting to parse property as an RGB color: %s", str); } } else { // Not a color string. AXLOGWARN("Error attempting to parse property as an RGB color (not specified as a color string): %s", str); } } if (out) out->set(0.0f, 0.0f, 0.0f); return false; } bool Properties::parseColor(const char* str, Vec4* out) { if (str) { if (strlen(str) == 9 && str[0] == '#') { // Read the string into an int as hex. unsigned int color; if (sscanf(str + 1, "%x", &color) == 1) { if (out) out->set(Vec4::fromColor(color)); return true; } else { // Invalid format AXLOGWARN("Error attempting to parse property as an RGBA color: %s", str); } } else { // Not a color string. AXLOGWARN("Error attempting to parse property as an RGBA color (not specified as a color string): %s", str); } } if (out) out->set(0.0f, 0.0f, 0.0f, 0.0f); return false; }