#include "Base.h" #include "Properties.h" #include "FileSystem.h" #include "Quaternion.h" namespace gameplay { // Utility functions (shared with SceneLoader). /** @script{ignore} */ void calculateNamespacePath(const std::string& urlString, std::string& fileString, std::vector& namespacePath); /** @script{ignore} */ Properties* getPropertiesFromNamespacePath(Properties* properties, const std::vector& namespacePath); Properties::Properties() { } Properties::Properties(const Properties& copy) : _namespace(copy._namespace), _id(copy._id), _parentID(copy._parentID), _properties(copy._properties) { _namespaces = std::vector(); std::vector::const_iterator it; for (it = copy._namespaces.begin(); it < copy._namespaces.end(); ++it) { GP_ASSERT(*it); _namespaces.push_back(new Properties(**it)); } rewind(); } Properties::Properties(FILE* file) { readProperties(file); rewind(); } Properties::Properties(FILE* file, const char* name, const char* id, const char* parentID) : _namespace(name) { if (id) { _id = id; } if (parentID) { _parentID = parentID; } readProperties(file); rewind(); } Properties* Properties::create(const char* url) { if (!url || strlen(url) == 0) { GP_ERROR("Attempting to create a Properties object from an empty URL!"); return NULL; } // Calculate the file and full namespace path from the specified url. std::string urlString = url; std::string fileString; std::vector namespacePath; calculateNamespacePath(urlString, fileString, namespacePath); FILE* file = FileSystem::openFile(fileString.c_str(), "rb"); if (!file) { GP_ERROR("Failed to open file '%s'.", fileString.c_str()); return NULL; } Properties* properties = new Properties(file); properties->resolveInheritance(); fclose(file); // Get the specified properties object. Properties* p = getPropertiesFromNamespacePath(properties, namespacePath); if (!p) { GP_ERROR("Failed to load properties from url '%s'.", url); return NULL; } // 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(); SAFE_DELETE(properties); } return p; } void Properties::readProperties(FILE* file) { GP_ASSERT(file); char line[2048]; int c; char* name; char* value; char* parentID; char* rc; char* rcc; char* rccc; while (true) { skipWhiteSpace(file); // Stop when we have reached the end of the file. if (feof(file)) break; // Read the next line. rc = fgets(line, 2048, file); if (rc == NULL) { GP_ERROR("Error reading line from file."); return; } // Ignore comment, skip line. 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) { // There could be a '}' at the end of the line, ending a namespace. rc = strchr(line, '}'); // First token should be the property name. name = strtok(line, "="); if (name == NULL) { GP_ERROR("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) { GP_ERROR("Error parsing properties file: attribute with name ('%s') but no value.", name); return; } // Remove white-space from value. value = trimWhiteSpace(value); // Store name/value pair. _properties[name] = value; if (rc != NULL) { // End of namespace. return; } } 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) { GP_ERROR("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 (fseek(file, -1, SEEK_CUR) != 0) { GP_ERROR("Failed to seek back to before a '}' character in properties file."); return; } while (fgetc(file) != '}') { if (fseek(file, -2, SEEK_CUR) != 0) { GP_ERROR("Failed to seek back to before a '}' character in properties file."); return; } } if (fseek(file, -1, SEEK_CUR) != 0) { GP_ERROR("Failed to seek back to before a '}' character in properties file."); return; } } // New namespace without an ID. Properties* space = new Properties(file, name, NULL, parentID); _namespaces.push_back(space); // If the namespace ends on this line, seek to right after the '}' character. if (rccc && rccc == lineEnd) { if (fseek(file, 1, SEEK_CUR) != 0) { GP_ERROR("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 (fseek(file, -1, SEEK_CUR) != 0) { GP_ERROR("Failed to seek back to before a '}' character in properties file."); return; } while (fgetc(file) != '}') { if (fseek(file, -2, SEEK_CUR) != 0) { GP_ERROR("Failed to seek back to before a '}' character in properties file."); return; } } if (fseek(file, -1, SEEK_CUR) != 0) { GP_ERROR("Failed to seek back to before a '}' character in properties file."); return; } } // Create new namespace. Properties* space = new Properties(file, name, value, parentID); _namespaces.push_back(space); // If the namespace ends on this line, seek to right after the '}' character. if (rccc && rccc == lineEnd) { if (fseek(file, 1, SEEK_CUR) != 0) { GP_ERROR("Failed to seek to immediately after a '}' character in properties file."); return; } } } else { // Find out if the next line starts with "{" skipWhiteSpace(file); c = fgetc(file); if (c == '{') { // Create new namespace. Properties* space = new Properties(file, name, value, parentID); _namespaces.push_back(space); } else { // Back up from fgetc() if (fseek(file, -1, SEEK_CUR) != 0) GP_ERROR("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[name] = value; } else { _properties[name] = std::string(); } } } } } } } } Properties::~Properties() { unsigned int count = _namespaces.size(); for (unsigned int i = 0; i < count; ++i) { SAFE_DELETE(_namespaces[i]); } } void Properties::skipWhiteSpace(FILE* file) { int c; do { c = fgetc(file); } while (isspace(c)); // 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 (fseek(file, -1, SEEK_CUR) != 0) { GP_ERROR("Failed to seek backwards one character after skipping whitespace."); } } } char* Properties::trimWhiteSpace(char *str) { if (str == NULL) { return str; } char *end; // Trim leading space. while (isspace(*str)) str++; // All spaces? if (*str == 0) { return str; } // Trim trailing space. 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. unsigned int count = derived->_namespaces.size(); for (unsigned int i = 0; i < count; i++) { 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 (itt = parent->_namespaces.begin(); itt < parent->_namespaces.end(); ++itt) { GP_ASSERT(*itt); derived->_namespaces.push_back(new Properties(**itt)); } derived->rewind(); // Take the original copy of the child and override the data copied from the parent. derived->mergeWith(overrides); // Delete the child copy. 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) { GP_ASSERT(overrides); // Overwrite or add each property found in child. char* value = new char[255]; overrides->rewind(); const char* name = overrides->getNextProperty(&value); while (name) { this->_properties[name] = value; name = overrides->getNextProperty(&value); } SAFE_DELETE_ARRAY(value); 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(char** value) { if (_propertiesItr == _properties.end()) { // Restart from the beginning _propertiesItr = _properties.begin(); } else { // Move to the next property ++_propertiesItr; } if (_propertiesItr != _properties.end()) { const std::string& name = _propertiesItr->first; if (!name.empty()) { if (value) { strcpy(*value, _propertiesItr->second.c_str()); } return name.c_str(); } } return NULL; } 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 NULL; } void Properties::rewind() { _propertiesItr = _properties.end(); _namespacesItr = _namespaces.end(); } Properties* Properties::getNamespace(const char* id, bool searchNames) const { GP_ASSERT(id); Properties* ret = NULL; std::vector::const_iterator it; for (it = _namespaces.begin(); it < _namespaces.end(); ++it) { ret = *it; if (strcmp(searchNames ? ret->_namespace.c_str() : ret->_id.c_str(), id) == 0) { return ret; } // Search recursively. ret = ret->getNamespace(id, searchNames); if (ret != NULL) { return ret; } } return ret; } 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 { GP_ASSERT(name); return _properties.find(name) != _properties.end(); } static const bool isStringNumeric(const char* str) { GP_ASSERT(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 { if (name) { std::map::const_iterator itr = _properties.find(name); if (itr != _properties.end()) { return itr->second.c_str(); } } else { if (_propertiesItr != _properties.end()) { return _propertiesItr->second.c_str(); } } return NULL; } 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) { GP_ERROR("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) { GP_ERROR("Error attempting to parse property '%s' as a float.", name); return 0.0f; } return value; } return 0.0f; } long Properties::getLong(const char* name) const { const char* valueString = getString(name); if (valueString) { long value; int scanned; scanned = sscanf(valueString, "%ld", &value); if (scanned != 1) { GP_ERROR("Error attempting to parse property '%s' as a long integer.", name); return 0L; } return value; } return 0L; } bool Properties::getMatrix(const char* name, Matrix* out) const { GP_ASSERT(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) { GP_ERROR("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::getVector2(const char* name, Vector2* out) const { GP_ASSERT(out); const char* valueString = getString(name); if (valueString) { float x, y; int scanned; scanned = sscanf(valueString, "%f,%f", &x, &y); if (scanned != 2) { GP_ERROR("Error attempting to parse property '%s' as a two-dimensional vector.", name); out->set(0.0f, 0.0f); return false; } out->set(x, y); return true; } out->set(0.0f, 0.0f); return false; } bool Properties::getVector3(const char* name, Vector3* out) const { GP_ASSERT(out); const char* valueString = getString(name); if (valueString) { float x, y, z; int scanned; scanned = sscanf(valueString, "%f,%f,%f", &x, &y, &z); if (scanned != 3) { GP_ERROR("Error attempting to parse property '%s' as a three-dimensional vector.", name); out->set(0.0f, 0.0f, 0.0f); return false; } out->set(x, y, z); return true; } out->set(0.0f, 0.0f, 0.0f); return false; } bool Properties::getVector4(const char* name, Vector4* out) const { GP_ASSERT(out); const char* valueString = getString(name); if (valueString) { float x, y, z, w; int scanned; scanned = sscanf(valueString, "%f,%f,%f,%f", &x, &y, &z, &w); if (scanned != 4) { GP_ERROR("Error attempting to parse property '%s' as a four-dimensional vector.", name); out->set(0.0f, 0.0f, 0.0f, 0.0f); return false; } out->set(x, y, z, w); return true; } out->set(0.0f, 0.0f, 0.0f, 0.0f); return false; } bool Properties::getQuaternionFromAxisAngle(const char* name, Quaternion* out) const { GP_ASSERT(out); const char* valueString = getString(name); if (valueString) { float x, y, z, theta; int scanned; scanned = sscanf(valueString, "%f,%f,%f,%f", &x, &y, &z, &theta); if (scanned != 4) { GP_ERROR("Error attempting to parse property '%s' as an axis-angle rotation.", name); out->set(0.0f, 0.0f, 0.0f, 1.0f); return false; } out->set(Vector3(x, y, z), MATH_DEG_TO_RAD(theta)); return true; } out->set(0.0f, 0.0f, 0.0f, 1.0f); return false; } bool Properties::getColor(const char* name, Vector3* out) const { GP_ASSERT(out); const char* valueString = getString(name); if (valueString) { if (strlen(valueString) != 7 || valueString[0] != '#') { // Not a color string. GP_ERROR("Error attempting to parse property '%s' as an RGB color (not specified as a color string).", name); out->set(0.0f, 0.0f, 0.0f); return false; } // Read the string into an int as hex. unsigned int color; if (sscanf(valueString+1, "%x", &color) != 1) { GP_ERROR("Error attempting to parse property '%s' as an RGB color.", name); out->set(0.0f, 0.0f, 0.0f); return false; } out->set(Vector3::fromColor(color)); return true; } out->set(0.0f, 0.0f, 0.0f); return false; } bool Properties::getColor(const char* name, Vector4* out) const { GP_ASSERT(out); const char* valueString = getString(name); if (valueString) { if (strlen(valueString) != 9 || valueString[0] != '#') { // Not a color string. GP_ERROR("Error attempting to parse property '%s' as an RGBA color (not specified as a color string).", name); out->set(0.0f, 0.0f, 0.0f, 0.0f); return false; } // Read the string into an int as hex. unsigned int color; if (sscanf(valueString+1, "%x", &color) != 1) { GP_ERROR("Error attempting to parse property '%s' as an RGBA color.", name); out->set(0.0f, 0.0f, 0.0f, 0.0f); return false; } out->set(Vector4::fromColor(color)); return true; } out->set(0.0f, 0.0f, 0.0f, 0.0f); return false; } Properties* Properties::clone() { Properties* p = new Properties(); p->_namespace = _namespace; p->_id = _id; p->_parentID = _parentID; p->_properties = _properties; p->_propertiesItr = p->_properties.end(); unsigned int count = _namespaces.size(); for (unsigned int i = 0; i < count; i++) { GP_ASSERT(_namespaces[i]); p->_namespaces.push_back(_namespaces[i]->clone()); } p->_namespacesItr = p->_namespaces.end(); return p; } void calculateNamespacePath(const std::string& 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); std::string namespacePathString = urlString.substr(loc + 1); while ((loc = namespacePathString.find("/")) != std::string::npos) { namespacePath.push_back(namespacePathString.substr(0, loc)); namespacePathString = namespacePathString.substr(loc + 1); } namespacePath.push_back(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.size() > 0) { unsigned int size = namespacePath.size(); properties->rewind(); Properties* iter = properties->getNextNamespace(); for (unsigned int i = 0; i < size;) { while (true) { if (iter == NULL) { GP_ERROR("Failed to load properties object from url."); return NULL; } 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; } }