// // EntityTree.cpp // libraries/entities/src // // Created by Brad Hefta-Gaub on 12/4/13. // Copyright 2013 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include #include #include #include "EntityTree.h" #include "EntitySimulation.h" #include "VariantMapToScriptValue.h" #include "AddEntityOperator.h" #include "MovingEntitiesOperator.h" #include "UpdateEntityOperator.h" #include "QVariantGLM.h" #include "EntitiesLogging.h" #include "RecurseOctreeToMapOperator.h" #include "LogHandler.h" #include "EntityEditFilters.h" static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50; const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour // combines the ray cast arguments into a single object class RayArgs { public: // Inputs glm::vec3 origin; glm::vec3 direction; const QVector& entityIdsToInclude; const QVector& entityIdsToDiscard; bool visibleOnly; bool collidableOnly; bool precisionPicking; // Outputs OctreeElementPointer& element; float& distance; BoxFace& face; glm::vec3& surfaceNormal; void** intersectedObject; bool found; }; EntityTree::EntityTree(bool shouldReaverage) : Octree(shouldReaverage), _fbxService(NULL), _simulation(NULL) { resetClientEditStats(); } EntityTree::~EntityTree() { eraseAllOctreeElements(false); } void EntityTree::setEntityScriptSourceWhitelist(const QString& entityScriptSourceWhitelist) { _entityScriptSourceWhitelist = entityScriptSourceWhitelist.split(',', QString::SkipEmptyParts); } void EntityTree::createRootElement() { _rootElement = createNewElement(); } OctreeElementPointer EntityTree::createNewElement(unsigned char* octalCode) { auto newElement = EntityTreeElementPointer(new EntityTreeElement(octalCode)); newElement->setTree(std::static_pointer_cast(shared_from_this())); return std::static_pointer_cast(newElement); } void EntityTree::eraseAllOctreeElements(bool createNewRoot) { emit clearingEntities(); // this would be a good place to clean up our entities... if (_simulation) { _simulation->clearEntities(); } { QWriteLocker locker(&_entityToElementLock); foreach(EntityTreeElementPointer element, _entityToElementMap) { element->cleanupEntities(); } _entityToElementMap.clear(); } Octree::eraseAllOctreeElements(createNewRoot); resetClientEditStats(); clearDeletedEntities(); } bool EntityTree::handlesEditPacketType(PacketType packetType) const { // we handle these types of "edit" packets switch (packetType) { case PacketType::EntityAdd: case PacketType::EntityEdit: case PacketType::EntityErase: case PacketType::EntityPhysics: return true; default: return false; } } /// Adds a new entity item to the tree void EntityTree::postAddEntity(EntityItemPointer entity) { assert(entity); // check to see if we need to simulate this entity.. if (_simulation) { _simulation->addEntity(entity); } if (!entity->isParentIDValid()) { QWriteLocker locker(&_missingParentLock); _missingParent.append(entity); } _isDirty = true; emit addingEntity(entity->getEntityItemID()); // find and hook up any entities with this entity as a (previously) missing parent fixupMissingParents(); } bool EntityTree::updateEntity(const EntityItemID& entityID, const EntityItemProperties& properties, const SharedNodePointer& senderNode) { EntityTreeElementPointer containingElement = getContainingElement(entityID); if (!containingElement) { return false; } EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); if (!existingEntity) { return false; } return updateEntityWithElement(existingEntity, properties, containingElement, senderNode); } bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperties& properties, const SharedNodePointer& senderNode) { EntityTreeElementPointer containingElement = getContainingElement(entity->getEntityItemID()); if (!containingElement) { return false; } return updateEntityWithElement(entity, properties, containingElement, senderNode); } bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityItemProperties& origProperties, EntityTreeElementPointer containingElement, const SharedNodePointer& senderNode) { EntityItemProperties properties = origProperties; bool allowLockChange; QUuid senderID; if (senderNode.isNull()) { auto nodeList = DependencyManager::get(); allowLockChange = nodeList->isAllowedEditor(); senderID = nodeList->getSessionUUID(); } else { allowLockChange = senderNode->isAllowedEditor(); senderID = senderNode->getUUID(); } if (!allowLockChange && (entity->getLocked() != properties.getLocked())) { qCDebug(entities) << "Refusing disallowed lock adjustment."; return false; } // enforce support for locked entities. If an entity is currently locked, then the only // property we allow you to change is the locked property. if (entity->getLocked()) { if (properties.lockedChanged()) { bool wantsLocked = properties.getLocked(); if (!wantsLocked) { EntityItemProperties tempProperties; tempProperties.setLocked(wantsLocked); tempProperties.setLastEdited(properties.getLastEdited()); bool success; AACube queryCube = entity->getQueryAACube(success); if (!success) { qCDebug(entities) << "Warning -- failed to get query-cube for" << entity->getID(); } UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, queryCube); recurseTreeWithOperator(&theOperator); entity->setProperties(tempProperties); _isDirty = true; } } } else { if (getIsServer()) { bool simulationBlocked = !entity->getSimulatorID().isNull(); if (properties.simulationOwnerChanged()) { QUuid submittedID = properties.getSimulationOwner().getID(); // a legit interface will only submit their own ID or NULL: if (submittedID.isNull()) { if (entity->getSimulatorID() == senderID) { // We only allow the simulation owner to clear their own simulationID's. simulationBlocked = false; properties.clearSimulationOwner(); // clear everything } // else: We assume the sender really did believe it was the simulation owner when it sent } else if (submittedID == senderID) { // the sender is trying to take or continue ownership if (entity->getSimulatorID().isNull()) { // the sender it taking ownership properties.promoteSimulationPriority(RECRUIT_SIMULATION_PRIORITY); simulationBlocked = false; } else if (entity->getSimulatorID() == senderID) { // the sender is asserting ownership simulationBlocked = false; } else { // the sender is trying to steal ownership from another simulator // so we apply the rules for ownership change: // (1) higher priority wins // (2) equal priority wins if ownership filter has expired except... uint8_t oldPriority = entity->getSimulationPriority(); uint8_t newPriority = properties.getSimulationOwner().getPriority(); if (newPriority > oldPriority || (newPriority == oldPriority && properties.getSimulationOwner().hasExpired())) { simulationBlocked = false; } } } else { // the entire update is suspect --> ignore it return false; } } else if (simulationBlocked) { simulationBlocked = senderID != entity->getSimulatorID(); } if (simulationBlocked) { // squash ownership and physics-related changes. properties.setSimulationOwnerChanged(false); properties.setPositionChanged(false); properties.setRotationChanged(false); properties.setVelocityChanged(false); properties.setAngularVelocityChanged(false); properties.setAccelerationChanged(false); properties.setParentIDChanged(false); properties.setParentJointIndexChanged(false); if (wantTerseEditLogging()) { qCDebug(entities) << (senderNode ? senderNode->getUUID() : "null") << "physical edits suppressed"; } } } // else client accepts what the server says QString entityScriptBefore = entity->getScript(); quint64 entityScriptTimestampBefore = entity->getScriptTimestamp(); uint32_t preFlags = entity->getDirtyFlags(); AACube newQueryAACube; if (properties.queryAACubeChanged()) { newQueryAACube = properties.getQueryAACube(); } else { newQueryAACube = entity->getQueryAACube(); } UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, newQueryAACube); recurseTreeWithOperator(&theOperator); entity->setProperties(properties); // if the entity has children, run UpdateEntityOperator on them. If the children have children, recurse QQueue toProcess; foreach (SpatiallyNestablePointer child, entity->getChildren()) { if (child && child->getNestableType() == NestableType::Entity) { toProcess.enqueue(child); } } while (!toProcess.empty()) { EntityItemPointer childEntity = std::static_pointer_cast(toProcess.dequeue()); if (!childEntity) { continue; } EntityTreeElementPointer containingElement = childEntity->getElement(); if (!containingElement) { continue; } bool success; AACube queryCube = childEntity->getQueryAACube(success); if (!success) { QWriteLocker locker(&_missingParentLock); _missingParent.append(childEntity); continue; } if (!childEntity->isParentIDValid()) { QWriteLocker locker(&_missingParentLock); _missingParent.append(childEntity); } UpdateEntityOperator theChildOperator(getThisPointer(), containingElement, childEntity, queryCube); recurseTreeWithOperator(&theChildOperator); foreach (SpatiallyNestablePointer childChild, childEntity->getChildren()) { if (childChild && childChild->getNestableType() == NestableType::Entity) { toProcess.enqueue(childChild); } } } _isDirty = true; uint32_t newFlags = entity->getDirtyFlags() & ~preFlags; if (newFlags) { if (_simulation) { if (newFlags & DIRTY_SIMULATION_FLAGS) { _simulation->changeEntity(entity); } } else { // normally the _simulation clears ALL updateFlags, but since there is none we do it explicitly entity->clearDirtyFlags(); } } QString entityScriptAfter = entity->getScript(); quint64 entityScriptTimestampAfter = entity->getScriptTimestamp(); bool reload = entityScriptTimestampBefore != entityScriptTimestampAfter; if (entityScriptBefore != entityScriptAfter || reload) { emitEntityScriptChanging(entity->getEntityItemID(), reload); // the entity script has changed } } // TODO: this final containingElement check should eventually be removed (or wrapped in an #ifdef DEBUG). containingElement = getContainingElement(entity->getEntityItemID()); if (!containingElement) { qCDebug(entities) << "UNEXPECTED!!!! after updateEntity() we no longer have a containing element??? entityID=" << entity->getEntityItemID(); return false; } return true; } EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer result = NULL; EntityItemProperties props = properties; auto nodeList = DependencyManager::get(); if (!nodeList) { qCDebug(entities) << "EntityTree::addEntity -- can't get NodeList"; return nullptr; } if (!properties.getClientOnly() && getIsClient() && !nodeList->getThisNodeCanRez() && !nodeList->getThisNodeCanRezTmp()) { return nullptr; } bool recordCreationTime = false; if (props.getCreated() == UNKNOWN_CREATED_TIME) { // the entity's creation time was not specified in properties, which means this is a NEW entity // and we must record its creation time recordCreationTime = true; } // You should not call this on existing entities that are already part of the tree! Call updateEntity() EntityTreeElementPointer containingElement = getContainingElement(entityID); if (containingElement) { qCDebug(entities) << "UNEXPECTED!!! ----- don't call addEntity() on existing entity items. entityID=" << entityID << "containingElement=" << containingElement.get(); return result; } // construct the instance of the entity EntityTypes::EntityType type = props.getType(); result = EntityTypes::constructEntityItem(type, entityID, props); if (result) { if (recordCreationTime) { result->recordCreationTime(); } // Recurse the tree and store the entity in the correct tree element AddEntityOperator theOperator(getThisPointer(), result); recurseTreeWithOperator(&theOperator); if (result->getAncestorMissing()) { // we added the entity, but didn't know about all its ancestors, so it went into the wrong place. // add it to a list of entities needing to be fixed once their parents are known. QWriteLocker locker(&_missingParentLock); _missingParent.append(result); } postAddEntity(result); } return result; } void EntityTree::emitEntityScriptChanging(const EntityItemID& entityItemID, bool reload) { emit entityScriptChanging(entityItemID, reload); } void EntityTree::emitEntityServerScriptChanging(const EntityItemID& entityItemID, bool reload) { emit entityServerScriptChanging(entityItemID, reload); } void EntityTree::notifyNewCollisionSoundURL(const QString& newURL, const EntityItemID& entityID) { emit newCollisionSoundURL(QUrl(newURL), entityID); } void EntityTree::setSimulation(EntitySimulationPointer simulation) { this->withWriteLock([&] { if (simulation) { // assert that the simulation's backpointer has already been properly connected assert(simulation->getEntityTree().get() == this); } if (_simulation && _simulation != simulation) { // It's important to clearEntities() on the simulation since taht will update each // EntityItem::_simulationState correctly so as to not confuse the next _simulation. _simulation->clearEntities(); } _simulation = simulation; }); } void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ignoreWarnings) { EntityTreeElementPointer containingElement = getContainingElement(entityID); if (!containingElement) { if (!ignoreWarnings) { qCDebug(entities) << "UNEXPECTED!!!! EntityTree::deleteEntity() entityID doesn't exist!!! entityID=" << entityID; } return; } EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); if (!existingEntity) { if (!ignoreWarnings) { qCDebug(entities) << "UNEXPECTED!!!! don't call EntityTree::deleteEntity() on entity items that don't exist. " "entityID=" << entityID; } return; } if (existingEntity->getLocked() && !force) { if (!ignoreWarnings) { qCDebug(entities) << "ERROR! EntityTree::deleteEntity() trying to delete locked entity. entityID=" << entityID; } return; } unhookChildAvatar(entityID); emit deletingEntity(entityID); // NOTE: callers must lock the tree before using this method DeleteEntityOperator theOperator(getThisPointer(), entityID); recurseTreeWithOperator(&theOperator); processRemovedEntities(theOperator); _isDirty = true; } void EntityTree::unhookChildAvatar(const EntityItemID entityID) { EntityItemPointer entity = findEntityByEntityItemID(entityID); entity->forEachDescendant([&](SpatiallyNestablePointer child) { if (child->getNestableType() == NestableType::Avatar) { child->setParentID(nullptr); } }); } void EntityTree::deleteEntities(QSet entityIDs, bool force, bool ignoreWarnings) { // NOTE: callers must lock the tree before using this method DeleteEntityOperator theOperator(getThisPointer()); foreach(const EntityItemID& entityID, entityIDs) { EntityTreeElementPointer containingElement = getContainingElement(entityID); if (!containingElement) { if (!ignoreWarnings) { qCDebug(entities) << "UNEXPECTED!!!! EntityTree::deleteEntities() entityID doesn't exist!!! entityID=" << entityID; } continue; } EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID); if (!existingEntity) { if (!ignoreWarnings) { qCDebug(entities) << "UNEXPECTED!!!! don't call EntityTree::deleteEntities() on entity items that don't exist. " "entityID=" << entityID; } continue; } if (existingEntity->getLocked() && !force) { if (!ignoreWarnings) { qCDebug(entities) << "ERROR! EntityTree::deleteEntities() trying to delete locked entity. entityID=" << entityID; } continue; } // tell our delete operator about this entityID unhookChildAvatar(entityID); theOperator.addEntityIDToDeleteList(entityID); emit deletingEntity(entityID); } if (theOperator.getEntities().size() > 0) { recurseTreeWithOperator(&theOperator); processRemovedEntities(theOperator); _isDirty = true; } } void EntityTree::processRemovedEntities(const DeleteEntityOperator& theOperator) { quint64 deletedAt = usecTimestampNow(); const RemovedEntities& entities = theOperator.getEntities(); foreach(const EntityToDeleteDetails& details, entities) { EntityItemPointer theEntity = details.entity; if (getIsServer()) { QSet childrenIDs; theEntity->forEachChild([&](SpatiallyNestablePointer child) { if (child->getNestableType() == NestableType::Entity) { childrenIDs += child->getID(); } }); deleteEntities(childrenIDs, true, true); } theEntity->die(); if (getIsServer()) { // set up the deleted entities ID QWriteLocker locker(&_recentlyDeletedEntitiesLock); _recentlyDeletedEntityItemIDs.insert(deletedAt, theEntity->getEntityItemID()); } else { // on the client side, we also remember that we deleted this entity, we don't care about the time trackDeletedEntity(theEntity->getEntityItemID()); } if (_simulation) { _simulation->prepareEntityForDelete(theEntity); } } } class FindNearPointArgs { public: glm::vec3 position; float targetRadius; bool found; EntityItemPointer closestEntity; float closestEntityDistance; }; bool EntityTree::findNearPointOperation(OctreeElementPointer element, void* extraData) { FindNearPointArgs* args = static_cast(extraData); EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); glm::vec3 penetration; bool sphereIntersection = entityTreeElement->getAACube().findSpherePenetration(args->position, args->targetRadius, penetration); // If this entityTreeElement contains the point, then search it... if (sphereIntersection) { EntityItemPointer thisClosestEntity = entityTreeElement->getClosestEntity(args->position); // we may have gotten NULL back, meaning no entity was available if (thisClosestEntity) { glm::vec3 entityPosition = thisClosestEntity->getPosition(); float distanceFromPointToEntity = glm::distance(entityPosition, args->position); // If we're within our target radius if (distanceFromPointToEntity <= args->targetRadius) { // we are closer than anything else we've found if (distanceFromPointToEntity < args->closestEntityDistance) { args->closestEntity = thisClosestEntity; args->closestEntityDistance = distanceFromPointToEntity; args->found = true; } } } // we should be able to optimize this... return true; // keep searching in case children have closer entities } // if this element doesn't contain the point, then none of its children can contain the point, so stop searching return false; } bool findRayIntersectionOp(OctreeElementPointer element, void* extraData) { RayArgs* args = static_cast(extraData); bool keepSearching = true; EntityTreeElementPointer entityTreeElementPointer = std::dynamic_pointer_cast(element); if (entityTreeElementPointer->findRayIntersection(args->origin, args->direction, keepSearching, args->element, args->distance, args->face, args->surfaceNormal, args->entityIdsToInclude, args->entityIdsToDiscard, args->visibleOnly, args->collidableOnly, args->intersectedObject, args->precisionPicking)) { args->found = true; } return keepSearching; } bool EntityTree::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, QVector entityIdsToInclude, QVector entityIdsToDiscard, bool visibleOnly, bool collidableOnly, bool precisionPicking, OctreeElementPointer& element, float& distance, BoxFace& face, glm::vec3& surfaceNormal, void** intersectedObject, Octree::lockType lockType, bool* accurateResult) { RayArgs args = { origin, direction, entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly, precisionPicking, element, distance, face, surfaceNormal, intersectedObject, false }; distance = FLT_MAX; bool requireLock = lockType == Octree::Lock; bool lockResult = withReadLock([&]{ recurseTreeWithOperation(findRayIntersectionOp, &args); }, requireLock); if (accurateResult) { *accurateResult = lockResult; // if user asked to accuracy or result, let them know this is accurate } return args.found; } EntityItemPointer EntityTree::findClosestEntity(glm::vec3 position, float targetRadius) { FindNearPointArgs args = { position, targetRadius, false, NULL, FLT_MAX }; withReadLock([&] { // NOTE: This should use recursion, since this is a spatial operation recurseTreeWithOperation(findNearPointOperation, &args); }); return args.closestEntity; } class FindAllNearPointArgs { public: glm::vec3 position; float targetRadius; QVector entities; }; bool EntityTree::findInSphereOperation(OctreeElementPointer element, void* extraData) { FindAllNearPointArgs* args = static_cast(extraData); glm::vec3 penetration; bool sphereIntersection = element->getAACube().findSpherePenetration(args->position, args->targetRadius, penetration); // If this element contains the point, then search it... if (sphereIntersection) { EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); entityTreeElement->getEntities(args->position, args->targetRadius, args->entities); return true; // keep searching in case children have closer entities } // if this element doesn't contain the point, then none of it's children can contain the point, so stop searching return false; } // NOTE: assumes caller has handled locking void EntityTree::findEntities(const glm::vec3& center, float radius, QVector& foundEntities) { FindAllNearPointArgs args = { center, radius, QVector() }; // NOTE: This should use recursion, since this is a spatial operation recurseTreeWithOperation(findInSphereOperation, &args); // swap the two lists of entity pointers instead of copy foundEntities.swap(args.entities); } class FindEntitiesInCubeArgs { public: FindEntitiesInCubeArgs(const AACube& cube) : _cube(cube), _foundEntities() { } AACube _cube; QVector _foundEntities; }; bool EntityTree::findInCubeOperation(OctreeElementPointer element, void* extraData) { FindEntitiesInCubeArgs* args = static_cast(extraData); if (element->getAACube().touches(args->_cube)) { EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); entityTreeElement->getEntities(args->_cube, args->_foundEntities); return true; } return false; } // NOTE: assumes caller has handled locking void EntityTree::findEntities(const AACube& cube, QVector& foundEntities) { FindEntitiesInCubeArgs args(cube); // NOTE: This should use recursion, since this is a spatial operation recurseTreeWithOperation(findInCubeOperation, &args); // swap the two lists of entity pointers instead of copy foundEntities.swap(args._foundEntities); } class FindEntitiesInBoxArgs { public: FindEntitiesInBoxArgs(const AABox& box) : _box(box), _foundEntities() { } AABox _box; QVector _foundEntities; }; bool EntityTree::findInBoxOperation(OctreeElementPointer element, void* extraData) { FindEntitiesInBoxArgs* args = static_cast(extraData); if (element->getAACube().touches(args->_box)) { EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); entityTreeElement->getEntities(args->_box, args->_foundEntities); return true; } return false; } // NOTE: assumes caller has handled locking void EntityTree::findEntities(const AABox& box, QVector& foundEntities) { FindEntitiesInBoxArgs args(box); // NOTE: This should use recursion, since this is a spatial operation recurseTreeWithOperation(findInBoxOperation, &args); // swap the two lists of entity pointers instead of copy foundEntities.swap(args._foundEntities); } class FindInFrustumArgs { public: ViewFrustum frustum; QVector entities; }; bool EntityTree::findInFrustumOperation(OctreeElementPointer element, void* extraData) { FindInFrustumArgs* args = static_cast(extraData); if (element->isInView(args->frustum)) { EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); entityTreeElement->getEntities(args->frustum, args->entities); return true; } return false; } // NOTE: assumes caller has handled locking void EntityTree::findEntities(const ViewFrustum& frustum, QVector& foundEntities) { FindInFrustumArgs args = { frustum, QVector() }; // NOTE: This should use recursion, since this is a spatial operation recurseTreeWithOperation(findInFrustumOperation, &args); // swap the two lists of entity pointers instead of copy foundEntities.swap(args.entities); } EntityItemPointer EntityTree::findEntityByID(const QUuid& id) { EntityItemID entityID(id); return findEntityByEntityItemID(entityID); } EntityItemPointer EntityTree::findEntityByEntityItemID(const EntityItemID& entityID) /*const*/ { EntityItemPointer foundEntity = NULL; EntityTreeElementPointer containingElement = getContainingElement(entityID); if (containingElement) { foundEntity = containingElement->getEntityWithEntityItemID(entityID); } return foundEntity; } void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList& changedProperties) { static quint64 lastTerseLog = 0; quint64 now = usecTimestampNow(); if (now - lastTerseLog > USECS_PER_SECOND) { qCDebug(entities) << "-------------------------"; } lastTerseLog = now; if (properties.simulationOwnerChanged()) { int simIndex = changedProperties.indexOf("simulationOwner"); if (simIndex >= 0) { SimulationOwner simOwner = properties.getSimulationOwner(); changedProperties[simIndex] = QString("simulationOwner:") + QString::number((int)simOwner.getPriority()); } } if (properties.velocityChanged()) { int index = changedProperties.indexOf("velocity"); if (index >= 0) { glm::vec3 value = properties.getVelocity(); changedProperties[index] = QString("velocity:") + QString::number((int)value.x) + "," + QString::number((int)value.y) + "," + QString::number((int)value.z); } } if (properties.gravityChanged()) { int index = changedProperties.indexOf("gravity"); if (index >= 0) { glm::vec3 value = properties.getGravity(); QString changeHint = "0"; if (value.x + value.y + value.z > 0) { changeHint = "+"; } else if (value.x + value.y + value.z < 0) { changeHint = "-"; } changedProperties[index] = QString("gravity:") + changeHint; } } if (properties.actionDataChanged()) { int index = changedProperties.indexOf("actionData"); if (index >= 0) { QByteArray value = properties.getActionData(); QString changeHint = serializedDynamicsToDebugString(value); changedProperties[index] = QString("actionData:") + changeHint; } } if (properties.collisionlessChanged()) { int index = changedProperties.indexOf("collisionless"); if (index >= 0) { bool value = properties.getCollisionless(); QString changeHint = "0"; if (value) { changeHint = "1"; } changedProperties[index] = QString("collisionless:") + changeHint; } } if (properties.dynamicChanged()) { int index = changedProperties.indexOf("dynamic"); if (index >= 0) { bool value = properties.getDynamic(); QString changeHint = "0"; if (value) { changeHint = "1"; } changedProperties[index] = QString("dynamic:") + changeHint; } } if (properties.lockedChanged()) { int index = changedProperties.indexOf("locked"); if (index >= 0) { bool value = properties.getLocked(); QString changeHint = "0"; if (value) { changeHint = "1"; } changedProperties[index] = QString("locked:") + changeHint; } } if (properties.userDataChanged()) { int index = changedProperties.indexOf("userData"); if (index >= 0) { QString changeHint = properties.getUserData(); changedProperties[index] = QString("userData:") + changeHint; } } if (properties.parentJointIndexChanged()) { int index = changedProperties.indexOf("parentJointIndex"); if (index >= 0) { quint16 value = properties.getParentJointIndex(); changedProperties[index] = QString("parentJointIndex:") + QString::number((int)value); } } if (properties.parentIDChanged()) { int index = changedProperties.indexOf("parentID"); if (index >= 0) { QUuid value = properties.getParentID(); changedProperties[index] = QString("parentID:") + value.toString(); } } if (properties.jointRotationsSetChanged()) { int index = changedProperties.indexOf("jointRotationsSet"); if (index >= 0) { auto value = properties.getJointRotationsSet().size(); changedProperties[index] = QString("jointRotationsSet:") + QString::number((int)value); } } if (properties.jointRotationsChanged()) { int index = changedProperties.indexOf("jointRotations"); if (index >= 0) { auto value = properties.getJointRotations().size(); changedProperties[index] = QString("jointRotations:") + QString::number((int)value); } } if (properties.jointTranslationsSetChanged()) { int index = changedProperties.indexOf("jointTranslationsSet"); if (index >= 0) { auto value = properties.getJointTranslationsSet().size(); changedProperties[index] = QString("jointTranslationsSet:") + QString::number((int)value); } } if (properties.jointTranslationsChanged()) { int index = changedProperties.indexOf("jointTranslations"); if (index >= 0) { auto value = properties.getJointTranslations().size(); changedProperties[index] = QString("jointTranslations:") + QString::number((int)value); } } if (properties.queryAACubeChanged()) { int index = changedProperties.indexOf("queryAACube"); glm::vec3 center = properties.getQueryAACube().calcCenter(); changedProperties[index] = QString("queryAACube:") + QString::number((int)center.x) + "," + QString::number((int)center.y) + "," + QString::number((int)center.z); } if (properties.positionChanged()) { int index = changedProperties.indexOf("position"); glm::vec3 pos = properties.getPosition(); changedProperties[index] = QString("position:") + QString::number((int)pos.x) + "," + QString::number((int)pos.y) + "," + QString::number((int)pos.z); } if (properties.lifetimeChanged()) { int index = changedProperties.indexOf("lifetime"); if (index >= 0) { float value = properties.getLifetime(); changedProperties[index] = QString("lifetime:") + QString::number((int)value); } } } bool EntityTree::filterProperties(EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType) { bool accepted = true; auto entityEditFilters = DependencyManager::get(); if (entityEditFilters) { auto position = existingEntity ? existingEntity->getPosition() : propertiesIn.getPosition(); auto entityID = existingEntity ? existingEntity->getEntityItemID() : EntityItemID(); accepted = entityEditFilters->filter(position, propertiesIn, propertiesOut, wasChanged, filterType, entityID); } return accepted; } void EntityTree::bumpTimestamp(EntityItemProperties& properties) { //fixme put class/header const quint64 LAST_EDITED_SERVERSIDE_BUMP = 1; // usec // also bump up the lastEdited time of the properties so that the interface that created this edit // will accept our adjustment to lifetime back into its own entity-tree. if (properties.getLastEdited() == UNKNOWN_CREATED_TIME) { properties.setLastEdited(usecTimestampNow()); } properties.setLastEdited(properties.getLastEdited() + LAST_EDITED_SERVERSIDE_BUMP); } int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned char* editData, int maxLength, const SharedNodePointer& senderNode) { if (!getIsServer()) { qCDebug(entities) << "UNEXPECTED!!! processEditPacketData() should only be called on a server tree."; return 0; } int processedBytes = 0; bool isAdd = false; // we handle these types of "edit" packets switch (message.getType()) { case PacketType::EntityErase: { QByteArray dataByteArray = QByteArray::fromRawData(reinterpret_cast(editData), maxLength); processedBytes = processEraseMessageDetails(dataByteArray, senderNode); break; } case PacketType::EntityAdd: isAdd = true; // fall through to next case case PacketType::EntityPhysics: case PacketType::EntityEdit: { quint64 startDecode = 0, endDecode = 0; quint64 startLookup = 0, endLookup = 0; quint64 startUpdate = 0, endUpdate = 0; quint64 startCreate = 0, endCreate = 0; quint64 startFilter = 0, endFilter = 0; quint64 startLogging = 0, endLogging = 0; bool suppressDisallowedScript = false; bool isPhysics = message.getType() == PacketType::EntityPhysics; _totalEditMessages++; EntityItemID entityItemID; EntityItemProperties properties; startDecode = usecTimestampNow(); bool validEditPacket = EntityItemProperties::decodeEntityEditPacket(editData, maxLength, processedBytes, entityItemID, properties); endDecode = usecTimestampNow(); EntityItemPointer existingEntity; if (!isAdd) { // search for the entity by EntityItemID startLookup = usecTimestampNow(); existingEntity = findEntityByEntityItemID(entityItemID); endLookup = usecTimestampNow(); if (!existingEntity) { // this is not an add-entity operation, and we don't know about the identified entity. validEditPacket = false; } } if (validEditPacket && !_entityScriptSourceWhitelist.isEmpty() && !properties.getScript().isEmpty()) { bool passedWhiteList = false; // grab a URL representation of the entity script so we can check the host for this script auto entityScriptURL = QUrl::fromUserInput(properties.getScript()); for (const auto& whiteListedPrefix : _entityScriptSourceWhitelist) { auto whiteListURL = QUrl::fromUserInput(whiteListedPrefix); // check if this script URL matches the whitelist domain and, optionally, is beneath the path if (entityScriptURL.host().compare(whiteListURL.host(), Qt::CaseInsensitive) == 0 && entityScriptURL.path().startsWith(whiteListURL.path(), Qt::CaseInsensitive)) { passedWhiteList = true; break; } } if (!passedWhiteList) { if (wantEditLogging()) { qCDebug(entities) << "User [" << senderNode->getUUID() << "] attempting to set entity script not on whitelist, edit rejected"; } // If this was an add, we also want to tell the client that sent this edit that the entity was not added. if (isAdd) { QWriteLocker locker(&_recentlyDeletedEntitiesLock); _recentlyDeletedEntityItemIDs.insert(usecTimestampNow(), entityItemID); validEditPacket = passedWhiteList; } else { suppressDisallowedScript = true; } } } if ((isAdd || properties.lifetimeChanged()) && !senderNode->getCanRez() && senderNode->getCanRezTmp()) { // this node is only allowed to rez temporary entities. if need be, cap the lifetime. if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || properties.getLifetime() > _maxTmpEntityLifetime) { properties.setLifetime(_maxTmpEntityLifetime); bumpTimestamp(properties); } } // If we got a valid edit packet, then it could be a new entity or it could be an update to // an existing entity... handle appropriately if (validEditPacket) { startFilter = usecTimestampNow(); bool wasChanged = false; // Having (un)lock rights bypasses the filter, unless it's a physics result. FilterType filterType = isPhysics ? FilterType::Physics : (isAdd ? FilterType::Add : FilterType::Edit); bool allowed = (!isPhysics && senderNode->isAllowedEditor()) || filterProperties(existingEntity, properties, properties, wasChanged, filterType); if (!allowed) { auto timestamp = properties.getLastEdited(); properties = EntityItemProperties(); properties.setLastEdited(timestamp); } if (!allowed || wasChanged) { bumpTimestamp(properties); // For now, free ownership on any modification. properties.clearSimulationOwner(); } endFilter = usecTimestampNow(); if (existingEntity && !isAdd) { if (suppressDisallowedScript) { bumpTimestamp(properties); properties.setScript(existingEntity->getScript()); } // if the EntityItem exists, then update it startLogging = usecTimestampNow(); if (wantEditLogging()) { qCDebug(entities) << "User [" << senderNode->getUUID() << "] editing entity. ID:" << entityItemID; qCDebug(entities) << " properties:" << properties; } if (wantTerseEditLogging()) { QList changedProperties = properties.listChangedProperties(); fixupTerseEditLogging(properties, changedProperties); qCDebug(entities) << senderNode->getUUID() << "edit" << existingEntity->getDebugName() << changedProperties; } endLogging = usecTimestampNow(); startUpdate = usecTimestampNow(); if (!isPhysics) { properties.setLastEditedBy(senderNode->getUUID()); } updateEntity(entityItemID, properties, senderNode); existingEntity->markAsChangedOnServer(); endUpdate = usecTimestampNow(); _totalUpdates++; } else if (isAdd) { bool failedAdd = !allowed; if (!allowed) { qCDebug(entities) << "Filtered entity add. ID:" << entityItemID; } else if (!senderNode->getCanRez() && !senderNode->getCanRezTmp()) { failedAdd = true; qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID() << "] attempted to add an entity ID:" << entityItemID; } else { // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); properties.setLastEditedBy(senderNode->getUUID()); startCreate = usecTimestampNow(); EntityItemPointer newEntity = addEntity(entityItemID, properties); endCreate = usecTimestampNow(); _totalCreates++; if (newEntity) { newEntity->markAsChangedOnServer(); notifyNewlyCreatedEntity(*newEntity, senderNode); startLogging = usecTimestampNow(); if (wantEditLogging()) { qCDebug(entities) << "User [" << senderNode->getUUID() << "] added entity. ID:" << newEntity->getEntityItemID(); qCDebug(entities) << " properties:" << properties; } if (wantTerseEditLogging()) { QList changedProperties = properties.listChangedProperties(); fixupTerseEditLogging(properties, changedProperties); qCDebug(entities) << senderNode->getUUID() << "add" << entityItemID << changedProperties; } endLogging = usecTimestampNow(); } else { failedAdd = true; qCDebug(entities) << "Add entity failed ID:" << entityItemID; } } if (failedAdd) { // Let client know it failed, so that they don't have an entity that no one else sees. QWriteLocker locker(&_recentlyDeletedEntitiesLock); _recentlyDeletedEntityItemIDs.insert(usecTimestampNow(), entityItemID); } } else { static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex("^Edit failed.*"); qCDebug(entities) << "Edit failed. [" << message.getType() <<"] " << "entity id:" << entityItemID << "existingEntity pointer:" << existingEntity.get(); } } _totalDecodeTime += endDecode - startDecode; _totalLookupTime += endLookup - startLookup; _totalUpdateTime += endUpdate - startUpdate; _totalCreateTime += endCreate - startCreate; _totalLoggingTime += endLogging - startLogging; _totalFilterTime += endFilter - startFilter; break; } default: processedBytes = 0; break; } return processedBytes; } void EntityTree::notifyNewlyCreatedEntity(const EntityItem& newEntity, const SharedNodePointer& senderNode) { _newlyCreatedHooksLock.lockForRead(); for (int i = 0; i < _newlyCreatedHooks.size(); i++) { _newlyCreatedHooks[i]->entityCreated(newEntity, senderNode); } _newlyCreatedHooksLock.unlock(); } void EntityTree::addNewlyCreatedHook(NewlyCreatedEntityHook* hook) { _newlyCreatedHooksLock.lockForWrite(); _newlyCreatedHooks.push_back(hook); _newlyCreatedHooksLock.unlock(); } void EntityTree::removeNewlyCreatedHook(NewlyCreatedEntityHook* hook) { _newlyCreatedHooksLock.lockForWrite(); for (int i = 0; i < _newlyCreatedHooks.size(); i++) { if (_newlyCreatedHooks[i] == hook) { _newlyCreatedHooks.erase(_newlyCreatedHooks.begin() + i); break; } } _newlyCreatedHooksLock.unlock(); } void EntityTree::releaseSceneEncodeData(OctreeElementExtraEncodeData* extraEncodeData) const { extraEncodeData->clear(); } void EntityTree::entityChanged(EntityItemPointer entity) { if (_simulation) { _simulation->changeEntity(entity); } } void EntityTree::fixupMissingParents() { MovingEntitiesOperator moveOperator(getThisPointer()); QList missingParents; { QWriteLocker locker(&_missingParentLock); QMutableVectorIterator iter(_missingParent); while (iter.hasNext()) { EntityItemWeakPointer entityWP = iter.next(); EntityItemPointer entity = entityWP.lock(); if (entity) { if (entity->isParentIDValid()) { iter.remove(); } else { missingParents.append(entity); } } else { // entity was deleted before we found its parent. iter.remove(); } } } for (EntityItemPointer entity : missingParents) { if (entity) { bool queryAACubeSuccess; AACube newCube = entity->getQueryAACube(queryAACubeSuccess); if (queryAACubeSuccess) { // make sure queryAACube encompasses maxAACube bool maxAACubeSuccess; AACube maxAACube = entity->getMaximumAACube(maxAACubeSuccess); if (maxAACubeSuccess && !newCube.contains(maxAACube)) { newCube = maxAACube; } } bool doMove = false; if (entity->isParentIDValid()) { // this entity's parent was previously not known, and now is. Update its location in the EntityTree... doMove = true; } else if (getIsServer() && _avatarIDs.contains(entity->getParentID())) { // this is a child of an avatar, which the entity server will never have // a SpatiallyNestable object for. Add it to a list for cleanup when the avatar leaves. if (!_childrenOfAvatars.contains(entity->getParentID())) { _childrenOfAvatars[entity->getParentID()] = QSet(); } _childrenOfAvatars[entity->getParentID()] += entity->getEntityItemID(); doMove = true; } if (queryAACubeSuccess && doMove) { moveOperator.addEntityToMoveList(entity, newCube); entity->markAncestorMissing(false); } } } if (moveOperator.hasMovingEntities()) { PerformanceTimer perfTimer("recurseTreeWithOperator"); recurseTreeWithOperator(&moveOperator); } } void EntityTree::deleteDescendantsOfAvatar(QUuid avatarID) { if (_childrenOfAvatars.contains(avatarID)) { deleteEntities(_childrenOfAvatars[avatarID]); _childrenOfAvatars.remove(avatarID); } } void EntityTree::update() { fixupMissingParents(); if (_simulation) { withWriteLock([&] { _simulation->updateEntities(); VectorOfEntities pendingDeletes; _simulation->takeEntitiesToDelete(pendingDeletes); if (pendingDeletes.size() > 0) { // translate into list of ID's QSet idsToDelete; for (auto entity : pendingDeletes) { idsToDelete.insert(entity->getEntityItemID()); } // delete these things the roundabout way deleteEntities(idsToDelete, true); } }); } } quint64 EntityTree::getAdjustedConsiderSince(quint64 sinceTime) { return (sinceTime - DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER); } bool EntityTree::hasEntitiesDeletedSince(quint64 sinceTime) { quint64 considerEntitiesSince = getAdjustedConsiderSince(sinceTime); // we can probably leverage the ordered nature of QMultiMap to do this quickly... bool hasSomethingNewer = false; QReadLocker locker(&_recentlyDeletedEntitiesLock); QMultiMap::const_iterator iterator = _recentlyDeletedEntityItemIDs.constBegin(); while (iterator != _recentlyDeletedEntityItemIDs.constEnd()) { if (iterator.key() > considerEntitiesSince) { hasSomethingNewer = true; break; // if we have at least one item, we don't need to keep searching } ++iterator; } #ifdef EXTRA_ERASE_DEBUGGING if (hasSomethingNewer) { int elapsed = usecTimestampNow() - considerEntitiesSince; int difference = considerEntitiesSince - sinceTime; qCDebug(entities) << "EntityTree::hasEntitiesDeletedSince() sinceTime:" << sinceTime << "considerEntitiesSince:" << considerEntitiesSince << "elapsed:" << elapsed << "difference:" << difference; } #endif return hasSomethingNewer; } // called by the server when it knows all nodes have been sent deleted packets void EntityTree::forgetEntitiesDeletedBefore(quint64 sinceTime) { quint64 considerSinceTime = sinceTime - DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER; QSet keysToRemove; QWriteLocker locker(&_recentlyDeletedEntitiesLock); QMultiMap::iterator iterator = _recentlyDeletedEntityItemIDs.begin(); // First find all the keys in the map that are older and need to be deleted while (iterator != _recentlyDeletedEntityItemIDs.end()) { if (iterator.key() <= considerSinceTime) { keysToRemove << iterator.key(); } ++iterator; } // Now run through the keysToRemove and remove them foreach (quint64 value, keysToRemove) { _recentlyDeletedEntityItemIDs.remove(value); } } // TODO: consider consolidating processEraseMessageDetails() and processEraseMessage() int EntityTree::processEraseMessage(ReceivedMessage& message, const SharedNodePointer& sourceNode) { #ifdef EXTRA_ERASE_DEBUGGING qCDebug(entities) << "EntityTree::processEraseMessage()"; #endif withWriteLock([&] { message.seek(sizeof(OCTREE_PACKET_FLAGS) + sizeof(OCTREE_PACKET_SEQUENCE) + sizeof(OCTREE_PACKET_SENT_TIME)); uint16_t numberOfIDs = 0; // placeholder for now message.readPrimitive(&numberOfIDs); if (numberOfIDs > 0) { QSet entityItemIDsToDelete; for (size_t i = 0; i < numberOfIDs; i++) { if (NUM_BYTES_RFC4122_UUID > message.getBytesLeftToRead()) { qCDebug(entities) << "EntityTree::processEraseMessage().... bailing because not enough bytes in buffer"; break; // bail to prevent buffer overflow } QUuid entityID = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID)); #ifdef EXTRA_ERASE_DEBUGGING qCDebug(entities) << " ---- EntityTree::processEraseMessage() contained ID:" << entityID; #endif EntityItemID entityItemID(entityID); entityItemIDsToDelete << entityItemID; if (wantEditLogging() || wantTerseEditLogging()) { qCDebug(entities) << "User [" << sourceNode->getUUID() << "] deleting entity. ID:" << entityItemID; } } deleteEntities(entityItemIDsToDelete, true, true); } }); return message.getPosition(); } // This version skips over the header // NOTE: Caller must lock the tree before calling this. // TODO: consider consolidating processEraseMessageDetails() and processEraseMessage() int EntityTree::processEraseMessageDetails(const QByteArray& dataByteArray, const SharedNodePointer& sourceNode) { #ifdef EXTRA_ERASE_DEBUGGING qCDebug(entities) << "EntityTree::processEraseMessageDetails()"; #endif const unsigned char* packetData = (const unsigned char*)dataByteArray.constData(); const unsigned char* dataAt = packetData; size_t packetLength = dataByteArray.size(); size_t processedBytes = 0; uint16_t numberOfIds = 0; // placeholder for now memcpy(&numberOfIds, dataAt, sizeof(numberOfIds)); dataAt += sizeof(numberOfIds); processedBytes += sizeof(numberOfIds); if (numberOfIds > 0) { QSet entityItemIDsToDelete; for (size_t i = 0; i < numberOfIds; i++) { if (processedBytes + NUM_BYTES_RFC4122_UUID > packetLength) { qCDebug(entities) << "EntityTree::processEraseMessageDetails().... bailing because not enough bytes in buffer"; break; // bail to prevent buffer overflow } QByteArray encodedID = dataByteArray.mid((int)processedBytes, NUM_BYTES_RFC4122_UUID); QUuid entityID = QUuid::fromRfc4122(encodedID); dataAt += encodedID.size(); processedBytes += encodedID.size(); #ifdef EXTRA_ERASE_DEBUGGING qCDebug(entities) << " ---- EntityTree::processEraseMessageDetails() contains id:" << entityID; #endif EntityItemID entityItemID(entityID); entityItemIDsToDelete << entityItemID; if (wantEditLogging() || wantTerseEditLogging()) { qCDebug(entities) << "User [" << sourceNode->getUUID() << "] deleting entity. ID:" << entityItemID; } } deleteEntities(entityItemIDsToDelete, true, true); } return (int)processedBytes; } EntityTreeElementPointer EntityTree::getContainingElement(const EntityItemID& entityItemID) /*const*/ { QReadLocker locker(&_entityToElementLock); EntityTreeElementPointer element = _entityToElementMap.value(entityItemID); return element; } void EntityTree::setContainingElement(const EntityItemID& entityItemID, EntityTreeElementPointer element) { QWriteLocker locker(&_entityToElementLock); if (element) { _entityToElementMap[entityItemID] = element; } else { _entityToElementMap.remove(entityItemID); } } void EntityTree::debugDumpMap() { qCDebug(entities) << "EntityTree::debugDumpMap() --------------------------"; QReadLocker locker(&_entityToElementLock); QHashIterator i(_entityToElementMap); while (i.hasNext()) { i.next(); qCDebug(entities) << i.key() << ": " << i.value().get(); } qCDebug(entities) << "-----------------------------------------------------"; } class ContentsDimensionOperator : public RecurseOctreeOperator { public: virtual bool preRecursion(OctreeElementPointer element) override; virtual bool postRecursion(OctreeElementPointer element) override { return true; } glm::vec3 getDimensions() const { return _contentExtents.size(); } float getLargestDimension() const { return _contentExtents.largestDimension(); } private: Extents _contentExtents; }; bool ContentsDimensionOperator::preRecursion(OctreeElementPointer element) { EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); entityTreeElement->expandExtentsToContents(_contentExtents); return true; } glm::vec3 EntityTree::getContentsDimensions() { ContentsDimensionOperator theOperator; recurseTreeWithOperator(&theOperator); return theOperator.getDimensions(); } float EntityTree::getContentsLargestDimension() { ContentsDimensionOperator theOperator; recurseTreeWithOperator(&theOperator); return theOperator.getLargestDimension(); } class DebugOperator : public RecurseOctreeOperator { public: virtual bool preRecursion(OctreeElementPointer element) override; virtual bool postRecursion(OctreeElementPointer element) override { return true; } }; bool DebugOperator::preRecursion(OctreeElementPointer element) { EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); qCDebug(entities) << "EntityTreeElement [" << entityTreeElement.get() << "]"; entityTreeElement->debugDump(); return true; } void EntityTree::dumpTree() { DebugOperator theOperator; recurseTreeWithOperator(&theOperator); } class PruneOperator : public RecurseOctreeOperator { public: virtual bool preRecursion(OctreeElementPointer element) override { return true; } virtual bool postRecursion(OctreeElementPointer element) override; }; bool PruneOperator::postRecursion(OctreeElementPointer element) { EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); entityTreeElement->pruneChildren(); return true; } void EntityTree::pruneTree() { PruneOperator theOperator; recurseTreeWithOperator(&theOperator); } QVector EntityTree::sendEntities(EntityEditPacketSender* packetSender, EntityTreePointer localTree, float x, float y, float z) { SendEntitiesOperationArgs args; args.packetSender = packetSender; args.ourTree = this; args.otherTree = localTree; args.root = glm::vec3(x, y, z); // If this is called repeatedly (e.g., multiple pastes with the same data), the new elements will clash unless we use new identifiers. // We need to keep a map so that we can map parent identifiers correctly. QHash map; args.map = ↦ withReadLock([&] { recurseTreeWithOperation(sendEntitiesOperation, &args); }); packetSender->releaseQueuedMessages(); return map.values().toVector(); } bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extraData) { SendEntitiesOperationArgs* args = static_cast(extraData); EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); std::function getMapped = [&](EntityItemPointer& item) -> const EntityItemID { EntityItemID oldID = item->getEntityItemID(); if (args->map->contains(oldID)) { // Already been handled (e.g., as a parent of somebody that we've processed). return args->map->value(oldID); } EntityItemID newID = QUuid::createUuid(); args->map->insert(oldID, newID); EntityItemProperties properties = item->getProperties(); EntityItemID oldParentID = properties.getParentID(); if (oldParentID.isInvalidID()) { // no parent properties.setPosition(properties.getPosition() + args->root); } else { EntityItemPointer parentEntity = args->ourTree->findEntityByEntityItemID(oldParentID); if (parentEntity) { // map the parent // Warning: (non-tail) recursion of getMapped could blow the call stack if the parent hierarchy is VERY deep. properties.setParentID(getMapped(parentEntity)); // But do not add root offset in this case. } else { // Should not happen, but let's try to be helpful... item->globalizeProperties(properties, "Cannot find %3 parent of %2 %1", args->root); } } if (!properties.getXNNeighborID().isInvalidID()) { auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXNNeighborID()); if (neighborEntity) { properties.setXNNeighborID(getMapped(neighborEntity)); } } if (!properties.getXPNeighborID().isInvalidID()) { auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXPNeighborID()); if (neighborEntity) { properties.setXPNeighborID(getMapped(neighborEntity)); } } if (!properties.getYNNeighborID().isInvalidID()) { auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYNNeighborID()); if (neighborEntity) { properties.setYNNeighborID(getMapped(neighborEntity)); } } if (!properties.getYPNeighborID().isInvalidID()) { auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYPNeighborID()); if (neighborEntity) { properties.setYPNeighborID(getMapped(neighborEntity)); } } if (!properties.getZNNeighborID().isInvalidID()) { auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZNNeighborID()); if (neighborEntity) { properties.setZNNeighborID(getMapped(neighborEntity)); } } if (!properties.getZPNeighborID().isInvalidID()) { auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZPNeighborID()); if (neighborEntity) { properties.setZPNeighborID(getMapped(neighborEntity)); } } // set creation time to "now" for imported entities properties.setCreated(usecTimestampNow()); properties.markAllChanged(); // so the entire property set is considered new, since we're making a new entity EntityTreeElementPointer entityTreeElement = std::static_pointer_cast(element); EntityTreePointer tree = entityTreeElement->getTree(); // queue the packet to send to the server args->packetSender->queueEditEntityMessage(PacketType::EntityAdd, tree, newID, properties); // also update the local tree instantly (note: this is not our tree, but an alternate tree) if (args->otherTree) { args->otherTree->withWriteLock([&] { args->otherTree->addEntity(newID, properties); }); } return newID; }; entityTreeElement->forEachEntity(getMapped); return true; } bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer element, bool skipDefaultValues, bool skipThoseWithBadParents) { if (! entityDescription.contains("Entities")) { entityDescription["Entities"] = QVariantList(); } QScriptEngine scriptEngine; RecurseOctreeToMapOperator theOperator(entityDescription, element, &scriptEngine, skipDefaultValues, skipThoseWithBadParents); recurseTreeWithOperator(&theOperator); return true; } bool EntityTree::readFromMap(QVariantMap& map) { // map will have a top-level list keyed as "Entities". This will be extracted // and iterated over. Each member of this list is converted to a QVariantMap, then // to a QScriptValue, and then to EntityItemProperties. These properties are used // to add the new entity to the EnitytTree. QVariantList entitiesQList = map["Entities"].toList(); QScriptEngine scriptEngine; if (entitiesQList.length() == 0) { // Empty map or invalidly formed file. return false; } bool success = true; foreach (QVariant entityVariant, entitiesQList) { // QVariantMap --> QScriptValue --> EntityItemProperties --> Entity QVariantMap entityMap = entityVariant.toMap(); QScriptValue entityScriptValue = variantMapToScriptValue(entityMap, scriptEngine); EntityItemProperties properties; EntityItemPropertiesFromScriptValueIgnoreReadOnly(entityScriptValue, properties); EntityItemID entityItemID; if (entityMap.contains("id")) { entityItemID = EntityItemID(QUuid(entityMap["id"].toString())); } else { entityItemID = EntityItemID(QUuid::createUuid()); } EntityItemPointer entity = addEntity(entityItemID, properties); if (!entity) { qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType(); success = false; } } return success; } void EntityTree::resetClientEditStats() { _treeResetTime = usecTimestampNow(); _maxEditDelta = 0; _totalEditDeltas = 0; _totalTrackedEdits = 0; } void EntityTree::trackIncomingEntityLastEdited(quint64 lastEditedTime, int bytesRead) { // we don't want to track all edit deltas, just those edits that have happend // since we connected to this domain. This will filter out all previously created // content and only track new edits if (lastEditedTime > _treeResetTime) { quint64 now = usecTimestampNow(); quint64 sinceEdit = now - lastEditedTime; _totalEditDeltas += sinceEdit; _totalEditBytes += bytesRead; _totalTrackedEdits++; if (sinceEdit > _maxEditDelta) { _maxEditDelta = sinceEdit; } } } void EntityTree::callLoader(EntityItemID entityID) { // this is used to bounce from the networking thread to the main thread this->withWriteLock([&] { EntityItemPointer entity = findEntityByEntityItemID(entityID); if (entity) { entity->loader(); } }); } int EntityTree::getJointIndex(const QUuid& entityID, const QString& name) const { EntityTree* nonConstThis = const_cast(this); EntityItemPointer entity = nonConstThis->findEntityByEntityItemID(entityID); if (!entity) { return -1; } return entity->getJointIndex(name); } QStringList EntityTree::getJointNames(const QUuid& entityID) const { EntityTree* nonConstThis = const_cast(this); EntityItemPointer entity = nonConstThis->findEntityByEntityItemID(entityID); if (!entity) { return QStringList(); } return entity->getJointNames(); }