overte-JulianGro/libraries/entities/src/EntityTree.cpp
2016-04-08 13:32:33 -07:00

1484 lines
58 KiB
C++

//
// 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 <PerfStat.h>
#include <QDateTime>
#include <QtScript/QScriptEngine>
#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"
static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50;
EntityTree::EntityTree(bool shouldReaverage) :
Octree(shouldReaverage),
_fbxService(NULL),
_simulation(NULL)
{
resetClientEditStats();
}
EntityTree::~EntityTree() {
eraseAllOctreeElements(false);
}
void EntityTree::createRootElement() {
_rootElement = createNewElement();
}
OctreeElementPointer EntityTree::createNewElement(unsigned char* octalCode) {
auto newElement = EntityTreeElementPointer(new EntityTreeElement(octalCode));
newElement->setTree(std::static_pointer_cast<EntityTree>(shared_from_this()));
return std::static_pointer_cast<OctreeElement>(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:
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;
maybeNotifyNewCollisionSoundURL("", entity->getCollisionSoundURL());
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<NodeList>();
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);
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);
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();
QString collisionSoundURLBefore = entity->getCollisionSoundURL();
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<SpatiallyNestablePointer> toProcess;
foreach (SpatiallyNestablePointer child, entity->getChildren()) {
if (child && child->getNestableType() == NestableType::Entity) {
toProcess.enqueue(child);
}
}
while (!toProcess.empty()) {
EntityItemPointer childEntity = std::static_pointer_cast<EntityItem>(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
}
maybeNotifyNewCollisionSoundURL(collisionSoundURLBefore, entity->getCollisionSoundURL());
}
// 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;
if (getIsClient()) {
// if our Node isn't allowed to create entities in this domain, don't try.
auto nodeList = DependencyManager::get<NodeList>();
if (nodeList && !nodeList->getThisNodeCanRez()) {
return NULL;
}
}
bool recordCreationTime = false;
if (properties.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 = properties.getType();
result = EntityTypes::constructEntityItem(type, entityID, properties);
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, const bool reload) {
emit entityScriptChanging(entityItemID, reload);
}
void EntityTree::maybeNotifyNewCollisionSoundURL(const QString& previousCollisionSoundURL, const QString& nextCollisionSoundURL) {
if (!nextCollisionSoundURL.isEmpty() && (nextCollisionSoundURL != previousCollisionSoundURL)) {
emit newCollisionSoundURL(QUrl(nextCollisionSoundURL));
}
}
void EntityTree::setSimulation(EntitySimulation* 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;
}
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::deleteEntities(QSet<EntityItemID> 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
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<EntityItemID> 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<FindNearPointArgs*>(extraData);
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(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;
}
// combines the ray cast arguments into a single object
class RayArgs {
public:
glm::vec3 origin;
glm::vec3 direction;
OctreeElementPointer& element;
float& distance;
BoxFace& face;
glm::vec3& surfaceNormal;
const QVector<EntityItemID>& entityIdsToInclude;
const QVector<EntityItemID>& entityIdsToDiscard;
void** intersectedObject;
bool found;
bool precisionPicking;
};
bool findRayIntersectionOp(OctreeElementPointer element, void* extraData) {
RayArgs* args = static_cast<RayArgs*>(extraData);
bool keepSearching = true;
EntityTreeElementPointer entityTreeElementPointer = std::dynamic_pointer_cast<EntityTreeElement>(element);
if (entityTreeElementPointer ->findRayIntersection(args->origin, args->direction, keepSearching,
args->element, args->distance, args->face, args->surfaceNormal, args->entityIdsToInclude,
args->entityIdsToDiscard, args->intersectedObject, args->precisionPicking)) {
args->found = true;
}
return keepSearching;
}
bool EntityTree::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
OctreeElementPointer& element, float& distance,
BoxFace& face, glm::vec3& surfaceNormal, const QVector<EntityItemID>& entityIdsToInclude, const QVector<EntityItemID>& entityIdsToDiscard, void** intersectedObject,
Octree::lockType lockType, bool* accurateResult, bool precisionPicking) {
RayArgs args = { origin, direction, element, distance, face, surfaceNormal, entityIdsToInclude, entityIdsToDiscard, intersectedObject, false, precisionPicking };
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<EntityItemPointer> entities;
};
bool EntityTree::findInSphereOperation(OctreeElementPointer element, void* extraData) {
FindAllNearPointArgs* args = static_cast<FindAllNearPointArgs*>(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<EntityTreeElement>(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<EntityItemPointer>& foundEntities) {
FindAllNearPointArgs args = { center, radius, QVector<EntityItemPointer>() };
// 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<EntityItemPointer> _foundEntities;
};
bool EntityTree::findInCubeOperation(OctreeElementPointer element, void* extraData) {
FindEntitiesInCubeArgs* args = static_cast<FindEntitiesInCubeArgs*>(extraData);
if (element->getAACube().touches(args->_cube)) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->getEntities(args->_cube, args->_foundEntities);
return true;
}
return false;
}
// NOTE: assumes caller has handled locking
void EntityTree::findEntities(const AACube& cube, QVector<EntityItemPointer>& 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<EntityItemPointer> _foundEntities;
};
bool EntityTree::findInBoxOperation(OctreeElementPointer element, void* extraData) {
FindEntitiesInBoxArgs* args = static_cast<FindEntitiesInBoxArgs*>(extraData);
if (element->getAACube().touches(args->_box)) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->getEntities(args->_box, args->_foundEntities);
return true;
}
return false;
}
// NOTE: assumes caller has handled locking
void EntityTree::findEntities(const AABox& box, QVector<EntityItemPointer>& 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);
}
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<QString>& 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();
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("velocity:") + changeHint;
}
}
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 = serializedActionsToDebugString(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);
}
}
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;
// we handle these types of "edit" packets
switch (message.getType()) {
case PacketType::EntityErase: {
QByteArray dataByteArray = QByteArray::fromRawData(reinterpret_cast<const char*>(editData), maxLength);
processedBytes = processEraseMessageDetails(dataByteArray, senderNode);
break;
}
case PacketType::EntityAdd:
case PacketType::EntityEdit: {
quint64 startDecode = 0, endDecode = 0;
quint64 startLookup = 0, endLookup = 0;
quint64 startUpdate = 0, endUpdate = 0;
quint64 startCreate = 0, endCreate = 0;
quint64 startLogging = 0, endLogging = 0;
_totalEditMessages++;
EntityItemID entityItemID;
EntityItemProperties properties;
startDecode = usecTimestampNow();
bool validEditPacket = EntityItemProperties::decodeEntityEditPacket(editData, maxLength, processedBytes,
entityItemID, properties);
endDecode = usecTimestampNow();
// 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) {
// search for the entity by EntityItemID
startLookup = usecTimestampNow();
EntityItemPointer existingEntity = findEntityByEntityItemID(entityItemID);
endLookup = usecTimestampNow();
if (existingEntity && message.getType() == PacketType::EntityEdit) {
// 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<QString> changedProperties = properties.listChangedProperties();
fixupTerseEditLogging(properties, changedProperties);
qCDebug(entities) << senderNode->getUUID() << "edit" <<
existingEntity->getDebugName() << changedProperties;
}
endLogging = usecTimestampNow();
startUpdate = usecTimestampNow();
updateEntity(entityItemID, properties, senderNode);
existingEntity->markAsChangedOnServer();
endUpdate = usecTimestampNow();
_totalUpdates++;
} else if (message.getType() == PacketType::EntityAdd) {
if (senderNode->getCanRez()) {
// this is a new entity... assign a new entityID
properties.setCreated(properties.getLastEdited());
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<QString> changedProperties = properties.listChangedProperties();
fixupTerseEditLogging(properties, changedProperties);
qCDebug(entities) << senderNode->getUUID() << "add" << entityItemID << changedProperties;
}
endLogging = usecTimestampNow();
}
} else {
qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID()
<< "] attempted to add an entity.";
}
} 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;
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 {
for (auto extraData : extraEncodeData->values()) {
EntityTreeElementExtraEncodeData* thisExtraEncodeData = static_cast<EntityTreeElementExtraEncodeData*>(extraData);
delete thisExtraEncodeData;
}
extraEncodeData->clear();
}
void EntityTree::entityChanged(EntityItemPointer entity) {
if (_simulation) {
_simulation->changeEntity(entity);
}
}
void EntityTree::fixupMissingParents() {
MovingEntitiesOperator moveOperator(getThisPointer());
QList<EntityItemPointer> missingParents;
{
QWriteLocker locker(&_missingParentLock);
QMutableVectorIterator<EntityItemWeakPointer> 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<EntityItemID>();
}
_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<EntityItemID> 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<quint64, QUuid>::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;
qDebug() << "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<quint64> keysToRemove;
QWriteLocker locker(&_recentlyDeletedEntitiesLock);
QMultiMap<quint64, QUuid>::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
qDebug() << "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<EntityItemID> 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
qDebug() << " ---- 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
qDebug() << "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<EntityItemID> 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
qDebug() << " ---- 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<EntityItemID, EntityTreeElementPointer> 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);
virtual bool postRecursion(OctreeElementPointer element) { return true; }
float getLargestDimension() const { return _contentExtents.largestDimension(); }
private:
Extents _contentExtents;
};
bool ContentsDimensionOperator::preRecursion(OctreeElementPointer element) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->expandExtentsToContents(_contentExtents);
return true;
}
float EntityTree::getContentsLargestDimension() {
ContentsDimensionOperator theOperator;
recurseTreeWithOperator(&theOperator);
return theOperator.getLargestDimension();
}
class DebugOperator : public RecurseOctreeOperator {
public:
virtual bool preRecursion(OctreeElementPointer element);
virtual bool postRecursion(OctreeElementPointer element) { return true; }
};
bool DebugOperator::preRecursion(OctreeElementPointer element) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(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) { return true; }
virtual bool postRecursion(OctreeElementPointer element);
};
bool PruneOperator::postRecursion(OctreeElementPointer element) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->pruneChildren();
return true;
}
void EntityTree::pruneTree() {
PruneOperator theOperator;
recurseTreeWithOperator(&theOperator);
}
QVector<EntityItemID> 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<EntityItemID, EntityItemID> map;
args.map = &map;
withReadLock([&] {
recurseTreeWithOperation(sendEntitiesOperation, &args);
});
packetSender->releaseQueuedMessages();
return map.values().toVector();
}
bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extraData) {
SendEntitiesOperationArgs* args = static_cast<SendEntitiesOperationArgs*>(extraData);
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
std::function<const EntityItemID(EntityItemPointer&)> 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();
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);
}
}
properties.markAllChanged(); // so the entire property set is considered new, since we're making a new entity
// queue the packet to send to the server
args->packetSender->queueEditEntityMessage(PacketType::EntityAdd, 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);
});
}
args->map->insert(oldID, newID);
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;
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();
}
}
return true;
}
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
EntityItemPointer entity = findEntityByEntityItemID(entityID);
if (entity) {
entity->loader();
}
}
int EntityTree::getJointIndex(const QUuid& entityID, const QString& name) const {
EntityTree* nonConstThis = const_cast<EntityTree*>(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<EntityTree*>(this);
EntityItemPointer entity = nonConstThis->findEntityByEntityItemID(entityID);
if (!entity) {
return QStringList();
}
return entity->getJointNames();
}