// Ryzom - MMORPG Framework // Copyright (C) 2010 Winch Gate Property Limited // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #include "db_manager.h" #include "pds_table.h" #include "db_manager_messages.h" #include #include #include #include #include #include using namespace std; using namespace NLMISC; using namespace NLNET; #define CHECK_DB_MGR_INIT(function, returnvalue) \ if (!initialised()) \ { \ nlwarning("CDbManager not initialised, " #function "() forbidden"); \ return returnvalue; \ } /* * Initialised yet? */ bool CDbManager::initialised() { return _Initialised; } CVariable DeltaUpdateRate("pds", "DeltaUpdateRate", "Number of seconds between two delta updates", 10, 0, true); /* * Update manager */ bool CDbManager::update() { H_AUTO(PDS_DbManager_update); CHECK_DB_MGR_INIT(update, false); // update stamp CTableBuffer::updateCommonStamp(); TDatabaseMap::iterator it; CDatabase::checkUpdateRates(); // check evently if database need to write some delta TTime tm = CTime::getLocalTime(); if (tm >= _NextTimeDelta) { CTimestamp starttime = _LastUpdateTime; CTimestamp endtime; endtime.setToCurrent(); std::vector ack; for (it=_DatabaseMap.begin(); it!=_DatabaseMap.end(); ++it) { CDatabase* database = (*it).second; // generate deltas if (!database->buildDelta(starttime, endtime)) nlwarning("failed to build delta for database '%d' '%s'", (*it).first, database->getName().c_str()); // obsolete? since RBS build references and tells PDS of success/failure database->checkReferenceChange(); // acknowledge last updates database->flushUpdates(ack); if (!ack.empty() && database->getMappedService().get() != 0xffff) { CMessage msgack("PD_ACK_UPD"); uint32 databaseId = (*it).first; msgack.serial(databaseId); msgack.serialCont(ack); CUnifiedNetwork::getInstance()->send(database->getMappedService(), msgack); } } _NextTimeDelta = tm - (tm%(DeltaUpdateRate*1000)) + (DeltaUpdateRate*1000); _LastUpdateTime = endtime; } CTimestamp ts; ts.setToCurrent(); // check databases require some delta packing/reference generation for (it=_DatabaseMap.begin(); it!=_DatabaseMap.end(); ++it) { CDatabase* database = (*it).second; database->sendBuildCommands(ts); } // send messages to RBS if ready while (_RBSUp && !_RBSMessages.empty()) { CUnifiedNetwork::getInstance()->send("RBS", *(_RBSMessages.front())); delete _RBSMessages.front(); _RBSMessages.pop_front(); } return true; } /* * Release manager */ bool CDbManager::release() { CHECK_DB_MGR_INIT(release, false); // release all databases deleteAllDatabases(); return true; } // Is manager initialised bool CDbManager::_Initialised = false; // Map of database CDbManager::TDatabaseMap CDbManager::_DatabaseMap; // Map of services CDbManager::TServiceMap CDbManager::_ServiceMap; // Next time to build delta TTime CDbManager::_NextTimeDelta; // Next task uint32 CDbManager::_TaskId = 0; // Messages to send to RBS std::deque CDbManager::_RBSMessages; // Acknowledge to wake std::map > CDbManager::_TaskListeners; // RBS state bool CDbManager::_RBSUp = false; // Last Update timestamp CTimestamp CDbManager::_LastUpdateTime; /* * Create a database entry */ CDatabase* CDbManager::createDatabase(TDatabaseId id, CLog* log) { CHECK_DB_MGR_INIT(createDatabase, false); // check db doesn't exist yet CDatabase* db = getDatabase(id); if (db != NULL) { log->displayNL("Unable to createDatabase() %d, already exists as '%s'", id, db->getName().c_str()); return NULL; } // create database and map it db = new CDatabase(id); _DatabaseMap[id] = db; return db; } /* * Delete a database entry */ bool CDbManager::deleteDatabase(TDatabaseId id, CLog* log) { CHECK_DB_MGR_INIT(deleteDatabase, false); // check db exists TDatabaseMap::iterator it = _DatabaseMap.find(id); if (it == _DatabaseMap.end()) { log->displayNL("Unable to deleteDatabase() %d, not create yet", id); return false; } // get database CDatabase* db = (*it).second; // unmap it (*it).second = NULL; _DatabaseMap.erase(it); // delete it delete db; return true; } /* * Load a database and adapt to the description if needed */ CDatabase* CDbManager::loadDatabase(TDatabaseId id, const string& description, CLog* log) { CHECK_DB_MGR_INIT(loadDatabase, false); nlinfo("CDbManager::loadDatabase(): load/setup database '%d'", id); CDatabase* db = getDatabase(id); // database not loaded yet? if (db == NULL) { // create a memory image db = createDatabase(id, log); if (db == NULL) { log->displayNL("failed to create database '%d'", id); return NULL; } // if can't load database if (!db->loadState()) { nlinfo("CDbManager::loadDatabase(): database '%d' doesn't exist, create new", id); // create a new database with the new description if (!db->createFromScratch(description)) { log->displayNL("failed to create database '%d' from scratch", id); return NULL; } return db; } } CDatabase* adapted = db->adapt(description); if (adapted == NULL) { log->displayNL("failed to adapt database '%s' to new description", db->getName().c_str()); return NULL; } // database changed? if (db != adapted) { // replace old on with new one _DatabaseMap[id] = adapted; // and delete old delete db; } return adapted; } /* * load a database */ bool CDbManager::loadDatabase(TDatabaseId id, CLog* log) { CHECK_DB_MGR_INIT(loadDatabase, false); // check db doesn't exist yet CDatabase* db = getDatabase(id); if (db == NULL) { log->displayNL("Unable to loadDatabase() %d, not created yet", id); return false; } // check database not init'ed if (db->initialised()) { log->displayNL("Unable to loadDatabase() %d, already initialised as '%s'", id, db->getName().c_str()); return false; } return db->loadState(); } /* * get a database entry */ CDatabase* CDbManager::getDatabase(TDatabaseId id) { CHECK_DB_MGR_INIT(getDatabase, NULL); TDatabaseMap::iterator it = _DatabaseMap.find(id); return (it == _DatabaseMap.end() ? NULL : (*it).second); } /* * Set an item in database, located by its table, row and column. * \param datasize is provided for validation check (1, 2, 4 or 8 bytes) * \param dataptr points to raw data, which may be 1, 2, 4 or 8 bytes, as indicated by datasize */ bool CDbManager::set(TDatabaseId id, RY_PDS::TTableIndex table, RY_PDS::TRowIndex row, RY_PDS::TColumnIndex column, uint datasize, const void* dataptr) { CHECK_DB_MGR_INIT(set, false); CDatabase* db = getDatabase(id); if (db == NULL) { nlwarning("Unable to set() value in %d, not created yet", id); return false; } return db->set(table, row, column, datasize, dataptr); } /* * Allocate a row in a database * \param id is the database id to allocate row into * \param table is the specified table * \param row is the specified row in table */ bool CDbManager::allocRow(TDatabaseId id, RY_PDS::TTableIndex table, RY_PDS::TRowIndex row) { CHECK_DB_MGR_INIT(allocRow, false); CDatabase* db = getDatabase(id); if (db == NULL) { nlwarning("Unable to allocRow() '%d' in table '%d' in database '%d', not created yet", row, table, id); return false; } return db->allocate(RY_PDS::CObjectIndex(table, row)); } /* * Deallocate a row in a database * \param id is the database id to deallocate row into * \param table is the specified table * \param row is the specified row in table */ bool CDbManager::deallocRow(TDatabaseId id, RY_PDS::TTableIndex table, RY_PDS::TRowIndex row) { CHECK_DB_MGR_INIT(deallocRow, false); CDatabase* db = getDatabase(id); if (db == NULL) { nlwarning("Unable to deallocRow() '%d' in table '%d' in database '%d', not created yet", row, table, id); return false; } return db->deallocate(RY_PDS::CObjectIndex(table, row)); } /* * Map a row in a table * \param index is the table/row to allocate * \param key is the 64 bits row key * Return true if succeded */ bool CDbManager::mapRow(TDatabaseId id, const RY_PDS::CObjectIndex &index, uint64 key) { CHECK_DB_MGR_INIT(mapRow, false); CDatabase* db = getDatabase(id); if (db == NULL) { nlwarning("Unable to mapRow() '%016"NL_I64"X' to row '%d':'%d' in db '%d' , not created yet", key, index.table(), index.row(), id); return false; } return db->mapRow(index, key); } /* * Unmap a row in a table * \param tableIndex is the table to find row * \param key is the 64 bits row key * Return true if succeded */ bool CDbManager::unmapRow(TDatabaseId id, RY_PDS::TTableIndex tableIndex, uint64 key) { CHECK_DB_MGR_INIT(unmapRow, false); CDatabase* db = getDatabase(id); if (db == NULL) { nlwarning("Unable to unmapRow() '%016"NL_I64"X' in '%d':'%d' in db '%d' , not created yet", key, tableIndex, id); return false; } return db->unmapRow(tableIndex, key); } /* * Release a row in a database * \param id is the database id to release row into * \param table is the specified table * \param row is the specified row in table */ bool CDbManager::releaseRow(TDatabaseId id, RY_PDS::TTableIndex table, RY_PDS::TRowIndex row) { CHECK_DB_MGR_INIT(releaseRow, false); CDatabase* db = getDatabase(id); if (db == NULL) { nlwarning("Unable to releaseRow() '%d' in table '%d' in database '%d', not created yet", row, table, id); return false; } return db->release(RY_PDS::CObjectIndex(table, row)); } /* * Fetch data */ bool CDbManager::fetch(TDatabaseId id, RY_PDS::TTableIndex tableIndex, uint64 key, RY_PDS::CPData &data) { CHECK_DB_MGR_INIT(fetch, false); CDatabase* db = getDatabase(id); if (db == NULL) { nlwarning("Unable to fetch(), db '%d' not created yet", id); return false; } RY_PDS::CObjectIndex index = db->getMappedRow(tableIndex, key); if (!index.isValid()) { // row is not mapped return false; } return db->fetch(index, data); } /* * Add String in Database' string manager */ /* bool CDbManager::addString(TDatabaseId id, const NLMISC::CEntityId& eId, RY_PDS::CPDStringManager::TEntryId pdId, const ucstring& str) { CHECK_DB_MGR_INIT(addString, false); CDatabase* db = getDatabase(id); if (db == NULL) { nlwarning("Unable to addString(), db '%d' not created yet", id); return false; } RY_PDS::CPDStringManager& sm = db->getStringManager(); return sm.setString(eId, pdId, str); } */ /* * Delete all database entries */ bool CDbManager::deleteAllDatabases(CLog* log) { CHECK_DB_MGR_INIT(getDatabase, false); CTimestamp starttime = _LastUpdateTime; CTimestamp endtime; endtime.setToCurrent(); TDatabaseMap::iterator it; for (it=_DatabaseMap.begin(); it!=_DatabaseMap.end(); ++it) { CDatabase* db = (*it).second; if (db == NULL) { log->displayNL("Database '%d' left with as NULL", (*it).first); } else { // flush db if (!db->buildDelta(starttime, endtime)) nlwarning("failed to build delta for database '%d' '%s'", (*it).first, db->getName().c_str()); // delete it delete db; } // unreference it (*it).second = NULL; } _DatabaseMap.clear(); return true; } /* * Parse path into TLocatePath */ bool CDbManager::parsePath(const string &strPath, CLocatePath &lpath) { CLocatePath::TLocatePath &path = lpath.FullPath; lpath.Pos = 0; path.clear(); if (strPath.empty()) { nlwarning("CDbManager::parsePath(): empty path"); return false; } // explode path into nodes formed like 'a_name' or 'an_array[a_key]' or 'a_set' vector nodes; explode(strPath, string("."), nodes, false); uint i; for (i=0; i'), pos); if (end == string::npos) return false; anode.Key = node.substr(pos+1, end-pos-1); } else { anode.Name = node; } path.push_back(anode); } return true; } /* * Locate a column using a path */ CTable::CDataAccessor CDbManager::locate(CLocatePath &path) { CHECK_DB_MGR_INIT(getDatabase, CTable::CDataAccessor()); if (path.end()) return CTable::CDataAccessor(); TDatabaseId id; NLMISC::fromString(path.node().Name, id); if (!path.next()) return CTable::CDataAccessor(); CDatabase* db = getDatabase(id); if (db == NULL) return CTable::CDataAccessor(); CTable* table = const_cast(db->getTable(path.node().Name)); if (table == NULL) return CTable::CDataAccessor(); path.next(); return table->getAccessor(path); } /* * Constructor */ CDbManager::CDbManager() { } /* * Init manager */ bool CDbManager::init() { nlinfo("CDbManager::init(): initialise database engine"); // initial type checking RY_PDS::CPDSLib::checkInternalTypes(); initDbManagerMessages(); uint i; for (i=0; i<256; ++i) _ServiceMap[i] = INVALID_DATABASE_ID; string rootPath = RY_PDS::CPDSLib::getPDSRootDirectory(); if (!CFile::isDirectory(rootPath)) { if (!CFile::createDirectoryTree(rootPath)) { nlwarning("CDbManager::init(): failure, can't create root path '%s', can't start.", rootPath.c_str()); return false; } if (!CFile::setRWAccess(rootPath)) { nlwarning("CDbManager::init(): failure, can't set RW access to path '%s', can't start.", rootPath.c_str()); return false; } } _Initialised = true; vector databases; NLMISC::CPath::getPathContent(rootPath, false, true, false, databases); for (i=0; i 256 || _ServiceMap[serviceId.get()] != INVALID_DATABASE_ID) { nlwarning("CDbManager::mapService(): failed, serviceId '%hu' not valid or service already mapped", serviceId.get()); return false; } _ServiceMap[serviceId.get()] = databaseId; CDatabase* database = getDatabase(databaseId); if (database != NULL) database->mapToService(serviceId); return true; } /* * Unmap Service Id */ bool CDbManager::unmapService(NLNET::TServiceId serviceId) { if (serviceId.get() > 256 || _ServiceMap[serviceId.get()] == INVALID_DATABASE_ID) return false; TDatabaseId id = _ServiceMap[serviceId.get()]; _ServiceMap[serviceId.get()] = INVALID_DATABASE_ID; CDatabase* db = getDatabase(id); if (db != NULL) { db->mapToService(TServiceId(0xffff)); db->releaseAll(); } return true; } /* * Add RBS Task */ NLNET::CMessage& CDbManager::addTask(const std::string& msg, ITaskEventListener* listener, void* arg) { NLNET::CMessage* msgrbs = new NLNET::CMessage(msg); _RBSMessages.push_back(msgrbs); uint32 id = nextTaskId(); msgrbs->serial(id); // add listener to task listeners if (listener != NULL) _TaskListeners[id] = std::make_pair(listener, arg); return *msgrbs; } /* * Notify RBS task success report */ void CDbManager::notifyRBSSuccess(uint32 taskId) { std::map >::iterator it = _TaskListeners.find(taskId); if (it == _TaskListeners.end()) return; // call listener success method ITaskEventListener* listener = (*it).second.first; void* arg = (*it).second.second; listener->taskSuccessful(arg); // and remove task _TaskListeners.erase(it); } /* * Notify RBS task failure report */ void CDbManager::notifyRBSFailure(uint32 taskId) { std::map >::iterator it = _TaskListeners.find(taskId); if (it == _TaskListeners.end()) return; // call listener failure method ITaskEventListener* listener = (*it).second.first; void* arg = (*it).second.second; listener->taskFailed(arg); // and remove task _TaskListeners.erase(it); } /* * Utility commands */ // NLMISC_COMMAND(createDatabase, "create a database using a given id", "") { if (args.size() != 1) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); return CDbManager::createDatabase(databaseId, &log) != NULL; } // NLMISC_COMMAND(deleteDatabase, "delete a database using a given id", "") { if (args.size() != 1) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); return CDbManager::deleteDatabase(databaseId, &log); } // NLMISC_COMMAND(loadDatabase, "load a database using a given id", "") { if (args.size() != 1) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); return CDbManager::loadDatabase(databaseId, &log); } // NLMISC_COMMAND(displayDatabase, "display database info", "") { if (args.size() != 1) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); CDatabase *database = CDbManager::getDatabase(databaseId); if (database == NULL) return false; database->display(&log); return true; } // NLMISC_COMMAND(displayTable, "display table info", " ") { if (args.size() != 2) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); const std::string& tableName = args[1]; CDatabase* database = CDbManager::getDatabase(databaseId); if (database == NULL) return false; const CTable* table = database->getTable(tableName); if (table == NULL) return false; table->display(&log, true, true); return true; } // NLMISC_COMMAND(dumpDeltaFileContent, "duump the content of a delta file", " ") { if (args.size() != 3) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); const std::string& tableName = args[1]; CDatabase* database = CDbManager::getDatabase(databaseId); if (database == NULL) return false; const CTable* table = database->getTable(tableName); if (table == NULL) return false; table->dumpDeltaFileContent(args[2], &log); return true; } // NLMISC_COMMAND(displayRow, "display row values", " [ | ]") { if (args.size() != 2 && args.size() != 3) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); CDatabase* database = CDbManager::getDatabase(databaseId); if (database == NULL) return false; CTable* table = NULL; RY_PDS::TRowIndex rowId; if (args.size() == 3) { const CTable* ctable = database->getTable(args[1]); if (ctable != NULL) { RY_PDS::TTableIndex tableIndex = (RY_PDS::TTableIndex)ctable->getId(); table = database->getNonConstTable(tableIndex); NLMISC::fromString(args[2], rowId); } } else { RY_PDS::CObjectIndex index; index.fromString(args[1].c_str()); table = database->getNonConstTable(index.table()); rowId = index.row(); } if (table == NULL) return false; table->displayRow(rowId, &log, true); return true; } // NLMISC_COMMAND(allocRow, "allocate a row in a table of a given database", " ") { if (args.size() != 3) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); const std::string& tableName = args[1]; RY_PDS::TRowIndex rowId; NLMISC::fromString(args[2], rowId); CDatabase* database = CDbManager::getDatabase(databaseId); if (database == NULL) return false; const CTable* table = database->getTable(tableName); if (table == NULL) return false; return database->allocate(RY_PDS::CObjectIndex((RY_PDS::TTableIndex)table->getId(), rowId)); } // NLMISC_COMMAND(deallocRow, "deallocate a row in a table of a given database", " ") { if (args.size() != 3) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); const std::string& tableName = args[1]; RY_PDS::TRowIndex rowId; NLMISC::fromString(args[2], rowId); CDatabase* database = CDbManager::getDatabase(databaseId); if (database == NULL) return false; const CTable* table = database->getTable(tableName); if (table == NULL) return false; return database->deallocate(RY_PDS::CObjectIndex((RY_PDS::TTableIndex)table->getId(), rowId)); } // NLMISC_COMMAND(mapRow, "map a row in a table of a given database with a 64bits key", " ") { if (args.size() != 4) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); const std::string& tableName = args[1]; RY_PDS::TRowIndex rowId; NLMISC::fromString(args[2], rowId); uint64 key; sscanf(args[3].c_str(), "%"NL_I64"X", &key); CDatabase* database = CDbManager::getDatabase(databaseId); if (database == NULL) return false; const CTable* table = database->getTable(tableName); if (table == NULL) return false; return database->mapRow(RY_PDS::CObjectIndex((RY_PDS::TTableIndex)table->getId(), rowId), key); } // NLMISC_COMMAND(unmapRow, "unmap a row in a table of a given database with a 64bits key", " ") { if (args.size() != 3) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); const std::string& tableName = args[1]; uint64 key; sscanf(args[2].c_str(), "%"NL_I64"X", &key); CDatabase* database = CDbManager::getDatabase(databaseId); if (database == NULL) return false; const CTable* table = database->getTable(tableName); if (table == NULL) return false; return database->unmapRow((RY_PDS::TTableIndex)table->getId(), key); } // NLMISC_COMMAND(setValue, "set a value in table", " ") { if (args.size() != 6) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); const std::string& tableName = args[1]; RY_PDS::TRowIndex rowId; NLMISC::fromString(args[2], rowId); RY_PDS::TColumnIndex colId; NLMISC::fromString(args[3], colId); const std::string& type = args[4]; const std::string& value = args[5]; CDatabase* database = CDbManager::getDatabase(databaseId); if (database == NULL) return false; const CTable* table = database->getTable(tableName); if (table == NULL) return false; return database->set((RY_PDS::TTableIndex)table->getId(), rowId, colId, type, value); } // NLMISC_COMMAND(set, "set a value in table", " [] ") { if (args.size() != 2 && args.size() != 3) return false; CLocatePath path; if (!CDbManager::parsePath(args[0], path)) return false; CTable::CDataAccessor accessor = CDbManager::locate(path); if (!accessor.isValid()) return false; CDatabase* database = const_cast(accessor.table()->getParent()); const CTable* table = accessor.table(); const std::string value = (args.size() == 3 ? args[2] : args[1]); const std::string type = (args.size() == 2 ? getNameFromDataType(accessor.column()->getDataType()) : args[1]); return database->set((RY_PDS::TTableIndex)table->getId(), accessor.row(), (RY_PDS::TColumnIndex)accessor.column()->getId(), type, value); } // NLMISC_COMMAND(get, "get a value in table", "") { if (args.size() != 1) return false; CLocatePath path; if (!CDbManager::parsePath(args[0], path)) return false; CTable::CDataAccessor accessor = CDbManager::locate(path); log.displayNL("%s = '%s'", args[0].c_str(), accessor.valueAsString(1).c_str()); return true; } // //NLMISC_COMMAND(displayStringManager, "display the content of a string manager", "") //{ // if (args.size() != 1) // return false; // // TDatabaseId databaseId; // NLMISC::fromString(args[0], databaseId); // CDatabase* database = CDbManager::getDatabase(databaseId); // // if (database == NULL) // return false; // // database->getStringManager().display(&log); // // return true; //} // NLMISC_COMMAND(dumpToXml, "dump the content of an object into an xml file", " [sint expandDepth=-1(infinite depth)]") { if (args.size() < 3 || args.size() > 4) return false; TDatabaseId databaseId; NLMISC::fromString(args[0], databaseId); CDatabase* database = CDbManager::getDatabase(databaseId); if (database == NULL) return false; RY_PDS::CObjectIndex index; index.fromString(args[1].c_str(), database); if (!index.isValid()) { uint64 key; NLMISC::CEntityId id; id.fromString(args[1].c_str()); if (id == NLMISC::CEntityId::Unknown) { if (sscanf(args[1].c_str(), "%"NL_I64"u", &key) != 1) { log.displayNL("id '%s' is not recognized as an EntityId, an ObjectIndex nor a 64 bits raw key", args[1].c_str()); return false; } } else { key = id.getRawId(); } std::set indexes; if (!database->searchObjectIndex(key, indexes)) { log.displayNL("no object matching key %s found", args[1].c_str()); return false; } if (indexes.size() > 1) { log.displayNL("%d objects match key '%s', please select the correct ObjectIndex below", indexes.size(), args[1].c_str()); std::set::iterator it; for (it=indexes.begin(); it!=indexes.end(); ++it) log.displayNL("%s", it->toString(database).c_str()); return true; } index = *(indexes.begin()); } COFile ofile; if (!ofile.open(args[2])) return false; COXml oxml; if (!oxml.init(&ofile)) return false; sint expandDepth = -1; if (args.size() == 4) { NLMISC::fromString(args[3], expandDepth); } database->dumpToXml(index, oxml, expandDepth); return true; }