overte/libraries/entities/src/EntityTree.cpp
2019-09-06 17:23:38 -07:00

3200 lines
135 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 "EntityTree.h"
#include <QtCore/QDateTime>
#include <QtCore/QQueue>
#include <openssl/err.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <NetworkingConstants.h>
#include "AccountManager.h"
#include <QJsonObject>
#include <QJsonDocument>
#include <QJsonArray>
#include <QtScript/QScriptEngine>
#include <Extents.h>
#include <PerfStat.h>
#include <Profile.h>
#include <AddressManager.h>
#include "EntitySimulation.h"
#include "VariantMapToScriptValue.h"
#include "AddEntityOperator.h"
#include "UpdateEntityOperator.h"
#include "QVariantGLM.h"
#include "EntitiesLogging.h"
#include "RecurseOctreeToMapOperator.h"
#include "RecurseOctreeToJSONOperator.h"
#include "LogHandler.h"
#include "EntityEditFilters.h"
#include "EntityDynamicFactoryInterface.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
static const QString DOMAIN_UNLIMITED = "domainUnlimited";
EntityTree::EntityTree(bool shouldReaverage) :
Octree(shouldReaverage)
{
resetClientEditStats();
EntityItem::retrieveMarketplacePublicKey();
}
EntityTree::~EntityTree() {
// NOTE: to eraseAllOctreeElements() in this context is useless because
// any OctreeElements in the tree still have shared backpointers to this Tree
// which means the dtor never would have been called in the first place!
//
// I'm keeping this useless commented-out line to remind us:
// we don't need shared pointer overhead for EntityTrees.
// TODO: EntityTreeElement::_tree should be raw back pointer.
// AND: EntityItem::_element should be a raw back pointer.
//eraseAllOctreeElements(false); // KEEP THIS
}
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<EntityTree>(shared_from_this()));
return std::static_pointer_cast<OctreeElement>(newElement);
}
void EntityTree::eraseDomainAndNonOwnedEntities() {
emit clearingEntities();
if (_simulation) {
// local entities are not in the simulation, so we clear ALL
_simulation->clearEntities();
}
this->withWriteLock([&] {
QHash<EntityItemID, EntityItemPointer> savedEntities;
// NOTE: lock the Tree first, then lock the _entityMap.
// It should never be done the other way around.
QReadLocker locker(&_entityMapLock);
foreach(EntityItemPointer entity, _entityMap) {
EntityTreeElementPointer element = entity->getElement();
if (element) {
element->cleanupDomainAndNonOwnedEntities();
}
if (entity->isLocalEntity() || (entity->isAvatarEntity() && entity->getOwningAvatarID() == getMyAvatarSessionUUID())) {
savedEntities[entity->getEntityItemID()] = entity;
} else {
int32_t spaceIndex = entity->getSpaceIndex();
if (spaceIndex != -1) {
// stale spaceIndices will be freed later
_staleProxies.push_back(spaceIndex);
}
}
}
_entityMap.swap(savedEntities);
});
resetClientEditStats();
clearDeletedEntities();
{
QWriteLocker locker(&_needsParentFixupLock);
QVector<EntityItemWeakPointer> needParentFixup;
foreach (EntityItemWeakPointer entityItem, _needsParentFixup) {
auto entity = entityItem.lock();
if (entity && (entity->isLocalEntity() || (entity->isAvatarEntity() && entity->getOwningAvatarID() == getMyAvatarSessionUUID()))) {
needParentFixup.push_back(entityItem);
}
}
_needsParentFixup = needParentFixup;
}
}
void EntityTree::eraseAllOctreeElements(bool createNewRoot) {
emit clearingEntities();
if (_simulation) {
_simulation->clearEntities();
}
QHash<EntityItemID, EntityItemPointer> localMap;
localMap.swap(_entityMap);
this->withWriteLock([&] {
foreach(EntityItemPointer entity, localMap) {
EntityTreeElementPointer element = entity->getElement();
if (element) {
element->cleanupEntities();
}
int32_t spaceIndex = entity->getSpaceIndex();
if (spaceIndex != -1) {
// assume stale spaceIndices will be freed later
_staleProxies.push_back(spaceIndex);
}
}
});
localMap.clear();
Octree::eraseAllOctreeElements(createNewRoot);
resetClientEditStats();
clearDeletedEntities();
{
QWriteLocker locker(&_needsParentFixupLock);
_needsParentFixup.clear();
}
}
void EntityTree::readBitstreamToTree(const unsigned char* bitstream,
uint64_t bufferSizeBytes, ReadBitstreamToTreeParams& args) {
Octree::readBitstreamToTree(bitstream, bufferSizeBytes, args);
// add entities
QHash<EntityItemID, EntityItemPointer>::const_iterator itr;
for (itr = _entitiesToAdd.constBegin(); itr != _entitiesToAdd.constEnd(); ++itr) {
const EntityItemPointer& entityItem = itr.value();
AddEntityOperator theOperator(getThisPointer(), entityItem);
recurseTreeWithOperator(&theOperator);
postAddEntity(entityItem);
}
_entitiesToAdd.clear();
// move entities
if (_entityMover.hasMovingEntities()) {
PerformanceTimer perfTimer("recurseTreeWithOperator");
recurseTreeWithOperator(&_entityMover);
_entityMover.reset();
}
}
int EntityTree::readEntityDataFromBuffer(const unsigned char* data, int bytesLeftToRead, ReadBitstreamToTreeParams& args) {
const unsigned char* dataAt = data;
int bytesRead = 0;
uint16_t numberOfEntities = 0;
int expectedBytesPerEntity = EntityItem::expectedBytes();
args.elementsPerPacket++;
if (bytesLeftToRead >= (int)sizeof(numberOfEntities)) {
// read our entities in....
numberOfEntities = *(uint16_t*)dataAt;
dataAt += sizeof(numberOfEntities);
bytesLeftToRead -= (int)sizeof(numberOfEntities);
bytesRead += sizeof(numberOfEntities);
if (bytesLeftToRead >= (int)(numberOfEntities * expectedBytesPerEntity)) {
for (uint16_t i = 0; i < numberOfEntities; i++) {
int bytesForThisEntity = 0;
EntityItemID entityItemID = EntityItemID::readEntityItemIDFromBuffer(dataAt, bytesLeftToRead);
EntityItemPointer entity = findEntityByEntityItemID(entityItemID);
if (entity) {
QString entityScriptBefore = entity->getScript();
QUuid parentIDBefore = entity->getParentID();
QString entityServerScriptsBefore = entity->getServerScripts();
quint64 entityScriptTimestampBefore = entity->getScriptTimestamp();
bytesForThisEntity = entity->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args);
if (entity->getDirtyFlags()) {
entityChanged(entity);
}
_entityMover.addEntityToMoveList(entity, entity->getQueryAACube());
QString entityScriptAfter = entity->getScript();
QString entityServerScriptsAfter = entity->getServerScripts();
quint64 entityScriptTimestampAfter = entity->getScriptTimestamp();
bool reload = entityScriptTimestampBefore != entityScriptTimestampAfter;
// If the script value has changed on us, or it's timestamp has changed to force
// a reload then we want to send out a script changing signal...
if (reload || entityScriptBefore != entityScriptAfter) {
emitEntityScriptChanging(entityItemID, reload); // the entity script has changed
}
if (reload || entityServerScriptsBefore != entityServerScriptsAfter) {
emitEntityServerScriptChanging(entityItemID, reload); // the entity server script has changed
}
QUuid parentIDAfter = entity->getParentID();
if (parentIDBefore != parentIDAfter) {
addToNeedsParentFixupList(entity);
}
} else {
entity = EntityTypes::constructEntityItem(dataAt, bytesLeftToRead);
if (entity) {
bytesForThisEntity = entity->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args);
// don't add if we've recently deleted....
if (!isDeletedEntity(entityItemID)) {
_entitiesToAdd.insert(entityItemID, entity);
if (entity->getCreated() == UNKNOWN_CREATED_TIME) {
entity->recordCreationTime();
}
#ifdef WANT_DEBUG
} else {
qCDebug(entities) << "Received packet for previously deleted entity [" <<
entityItemID << "] ignoring. (inside " << __FUNCTION__ << ")";
#endif
}
}
}
// Move the buffer forward to read more entities
dataAt += bytesForThisEntity;
bytesLeftToRead -= bytesForThisEntity;
bytesRead += bytesForThisEntity;
}
}
}
return bytesRead;
}
bool EntityTree::handlesEditPacketType(PacketType packetType) const {
// we handle these types of "edit" packets
switch (packetType) {
case PacketType::EntityAdd:
case PacketType::EntityClone:
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);
if (getIsServer()) {
addCertifiedEntityOnServer(entity);
}
// check to see if we need to simulate this entity..
if (_simulation) {
_simulation->addEntity(entity);
}
if (!entity->getParentID().isNull()) {
addToNeedsParentFixupList(entity);
}
_isDirty = true;
// find and hook up any entities with this entity as a (previously) missing parent
fixupNeedsParentFixups();
emit addingEntity(entity->getEntityItemID());
emit addingEntityPointer(entity.get());
}
bool EntityTree::updateEntity(const EntityItemID& entityID, const EntityItemProperties& properties, const SharedNodePointer& senderNode) {
EntityItemPointer entity;
{
QReadLocker locker(&_entityMapLock);
entity = _entityMap.value(entityID);
}
if (!entity) {
return false;
}
return updateEntity(entity, properties, senderNode);
}
bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperties& origProperties,
const SharedNodePointer& senderNode) {
EntityTreeElementPointer containingElement = entity->getElement();
if (!containingElement) {
return false;
}
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);
tempProperties.setLastEdited(properties.getLastEdited());
bool success;
AACube queryCube = entity->getQueryAACube(success);
if (!success) {
qCWarning(entities) << "failed to get query-cube for" << entity->getID();
}
UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, queryCube);
recurseTreeWithOperator(&theOperator);
if (entity->setProperties(tempProperties)) {
emit editingEntityPointer(entity);
}
_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 is taking ownership
if (properties.getSimulationOwner().getPriority() == VOLUNTEER_SIMULATION_PRIORITY) {
// the entity-server always promotes VOLUNTEER to RECRUIT to avoid ownership thrash
// when dynamic objects first activate and multiple participants bid simultaneously
properties.setSimulationPriority(RECRUIT_SIMULATION_PRIORITY);
}
simulationBlocked = false;
} else if (entity->getSimulatorID() == senderID) {
// the sender is asserting ownership, maybe changing priority
simulationBlocked = false;
// the entity-server always promotes VOLUNTEER to RECRUIT to avoid ownership thrash
// when dynamic objects first activate and multiple participants bid simultaneously
if (properties.getSimulationOwner().getPriority() == VOLUNTEER_SIMULATION_PRIORITY) {
properties.setSimulationPriority(RECRUIT_SIMULATION_PRIORITY);
}
} 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
// (3) VOLUNTEER priority is promoted to RECRUIT
uint8_t oldPriority = entity->getSimulationPriority();
uint8_t newPriority = properties.getSimulationOwner().getPriority();
if (newPriority > oldPriority ||
(newPriority == oldPriority && properties.getSimulationOwner().hasExpired())) {
simulationBlocked = false;
if (properties.getSimulationOwner().getPriority() == VOLUNTEER_SIMULATION_PRIORITY) {
properties.setSimulationPriority(RECRUIT_SIMULATION_PRIORITY);
}
}
}
if (!simulationBlocked) {
entity->setSimulationOwnershipExpiry(usecTimestampNow() + MAX_INCOMING_SIMULATION_UPDATE_PERIOD);
}
} else {
// the entire update is suspect --> ignore it
return false;
}
} else if (simulationBlocked) {
simulationBlocked = senderID != entity->getSimulatorID();
if (!simulationBlocked) {
entity->setSimulationOwnershipExpiry(usecTimestampNow() + MAX_INCOMING_SIMULATION_UPDATE_PERIOD);
}
}
if (simulationBlocked) {
// squash ownership and physics-related changes.
// TODO? replace these eight calls with just one?
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);
if (entity->setProperties(properties)) {
emit editingEntityPointer(entity);
}
// 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 childContainingElement = childEntity->getElement();
if (!childContainingElement) {
continue;
}
bool success;
AACube queryCube = childEntity->getQueryAACube(success);
if (!success) {
addToNeedsParentFixupList(childEntity);
continue;
}
if (!childEntity->getParentID().isNull()) {
addToNeedsParentFixupList(childEntity);
}
UpdateEntityOperator theChildOperator(getThisPointer(), childContainingElement, 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 (entity->isSimulated()) {
assert((bool)_simulation);
if (newFlags & DIRTY_SIMULATION_FLAGS) {
_simulation->changeEntity(entity);
}
} else {
// normally the _simulation clears ALL dirtyFlags, but when not possible 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).
if (!entity->getElement()) {
qCWarning(entities) << "EntityTree::updateEntity() we no longer have a containing element for entityID="
<< entity->getEntityItemID();
return false;
}
return true;
}
EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties, bool isClone) {
EntityItemProperties props = properties;
auto nodeList = DependencyManager::get<NodeList>();
if (!nodeList) {
qCDebug(entities) << "EntityTree::addEntity -- can't get NodeList";
return nullptr;
}
if (properties.getEntityHostType() == entity::HostType::DOMAIN && getIsClient() &&
!nodeList->getThisNodeCanRez() && !nodeList->getThisNodeCanRezTmp() &&
!nodeList->getThisNodeCanRezCertified() && !nodeList->getThisNodeCanRezTmpCertified() && !_serverlessDomain && !isClone) {
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) {
qCWarning(entities) << "EntityTree::addEntity() on existing entity item with entityID=" << entityID
<< "containingElement=" << containingElement.get();
return nullptr;
}
// construct the instance of the entity
EntityTypes::EntityType type = props.getType();
EntityItemPointer 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);
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) {
_simulation->clearEntities();
}
_simulation = simulation;
});
}
void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ignoreWarnings) {
EntityTreeElementPointer containingElement = getContainingElement(entityID);
if (!containingElement) {
if (!ignoreWarnings) {
qCWarning(entities) << "EntityTree::deleteEntity() on non-existent entityID=" << entityID;
}
return;
}
EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID);
if (!existingEntity) {
if (!ignoreWarnings) {
qCWarning(entities) << "EntityTree::deleteEntity() on non-existant entity item with entityID=" << entityID;
}
return;
}
if (existingEntity->getLocked() && !force) {
if (!ignoreWarnings) {
qCDebug(entities) << "ERROR! EntityTree::deleteEntity() trying to delete locked entity. entityID=" << entityID;
}
return;
}
cleanupCloneIDs(entityID);
unhookChildAvatar(entityID);
emit deletingEntity(entityID);
emit deletingEntityPointer(existingEntity.get());
// NOTE: callers must lock the tree before using this method
DeleteEntityOperator theOperator(getThisPointer(), entityID);
existingEntity->forEachDescendant([&](SpatiallyNestablePointer descendant) {
auto descendantID = descendant->getID();
theOperator.addEntityIDToDeleteList(descendantID);
emit deletingEntity(descendantID);
EntityItemPointer descendantEntity = std::dynamic_pointer_cast<EntityItem>(descendant);
if (descendantEntity) {
emit deletingEntityPointer(descendantEntity.get());
}
});
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::cleanupCloneIDs(const EntityItemID& entityID) {
EntityItemPointer entity = findEntityByEntityItemID(entityID);
if (entity) {
// remove clone ID from its clone origin's clone ID list if clone origin exists
const QUuid& cloneOriginID = entity->getCloneOriginID();
if (!cloneOriginID.isNull()) {
EntityItemPointer cloneOrigin = findEntityByID(cloneOriginID);
if (cloneOrigin) {
cloneOrigin->removeCloneID(entityID);
}
}
// clear the clone origin ID on any clones that this entity had
const QVector<QUuid>& cloneIDs = entity->getCloneIDs();
foreach(const QUuid& cloneChildID, cloneIDs) {
EntityItemPointer cloneChild = findEntityByEntityItemID(cloneChildID);
if (cloneChild) {
cloneChild->setCloneOriginID(QUuid());
}
}
}
}
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) {
qCWarning(entities) << "EntityTree::deleteEntities() on non-existent entityID=" << entityID;
}
continue;
}
EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID);
if (!existingEntity) {
if (!ignoreWarnings) {
qCWarning(entities) << "EntityTree::deleteEntities() on non-existent entity item with 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
cleanupCloneIDs(entityID);
unhookChildAvatar(entityID);
theOperator.addEntityIDToDeleteList(entityID);
emit deletingEntity(entityID);
emit deletingEntityPointer(existingEntity.get());
}
if (!theOperator.getEntities().empty()) {
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()) {
removeCertifiedEntityOnServer(theEntity);
// set up the deleted entities ID
QWriteLocker recentlyDeletedEntitiesLocker(&_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 (theEntity->isSimulated()) {
_simulation->prepareEntityForDelete(theEntity);
}
int32_t spaceIndex = theEntity->getSpaceIndex();
if (spaceIndex != -1) {
// stale spaceIndices will be freed later
_staleProxies.push_back(spaceIndex);
}
}
}
class RayArgs {
public:
// Inputs
glm::vec3 origin;
glm::vec3 direction;
glm::vec3 invDirection;
const QVector<EntityItemID>& entityIdsToInclude;
const QVector<EntityItemID>& entityIdsToDiscard;
PickFilter searchFilter;
// Outputs
OctreeElementPointer& element;
float& distance;
BoxFace& face;
glm::vec3& surfaceNormal;
QVariantMap& extraInfo;
EntityItemID entityID;
};
bool evalRayIntersectionOp(const OctreeElementPointer& element, void* extraData) {
RayArgs* args = static_cast<RayArgs*>(extraData);
bool keepSearching = true;
EntityTreeElementPointer entityTreeElementPointer = std::static_pointer_cast<EntityTreeElement>(element);
EntityItemID entityID = entityTreeElementPointer->evalRayIntersection(args->origin, args->direction,
args->element, args->distance, args->face, args->surfaceNormal, args->entityIdsToInclude,
args->entityIdsToDiscard, args->searchFilter, args->extraInfo);
if (!entityID.isNull()) {
args->entityID = entityID;
// We recurse OctreeElements in order, so if we hit something, we can stop immediately
keepSearching = false;
}
return keepSearching;
}
float evalRayIntersectionSortingOp(const OctreeElementPointer& element, void* extraData) {
RayArgs* args = static_cast<RayArgs*>(extraData);
EntityTreeElementPointer entityTreeElementPointer = std::static_pointer_cast<EntityTreeElement>(element);
float distance = FLT_MAX;
// If origin is inside the cube, always check this element first
if (entityTreeElementPointer->getAACube().contains(args->origin)) {
distance = 0.0f;
} else {
float boundDistance = FLT_MAX;
BoxFace face;
glm::vec3 surfaceNormal;
if (entityTreeElementPointer->getAACube().findRayIntersection(args->origin, args->direction, args->invDirection, boundDistance, face, surfaceNormal)) {
// Don't add this cell if it's already farther than our best distance so far
if (boundDistance < args->distance) {
distance = boundDistance;
}
}
}
return distance;
}
EntityItemID EntityTree::evalRayIntersection(const glm::vec3& origin, const glm::vec3& direction,
QVector<EntityItemID> entityIdsToInclude, QVector<EntityItemID> entityIdsToDiscard,
PickFilter searchFilter, OctreeElementPointer& element, float& distance,
BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo,
Octree::lockType lockType, bool* accurateResult) {
// calculate dirReciprocal like this rather than with glm's scalar / vec3 template to avoid NaNs.
vec3 dirReciprocal = glm::vec3(direction.x == 0.0f ? 0.0f : 1.0f / direction.x,
direction.y == 0.0f ? 0.0f : 1.0f / direction.y,
direction.z == 0.0f ? 0.0f : 1.0f / direction.z);
RayArgs args = { origin, direction, dirReciprocal, entityIdsToInclude, entityIdsToDiscard,
searchFilter, element, distance, face, surfaceNormal, extraInfo, EntityItemID() };
distance = FLT_MAX;
bool requireLock = lockType == Octree::Lock;
bool lockResult = withReadLock([&]{
recurseTreeWithOperationSorted(evalRayIntersectionOp, evalRayIntersectionSortingOp, &args);
}, requireLock);
if (accurateResult) {
*accurateResult = lockResult; // if user asked to accuracy or result, let them know this is accurate
}
return args.entityID;
}
class ParabolaArgs {
public:
// Inputs
glm::vec3 origin;
glm::vec3 velocity;
glm::vec3 acceleration;
const QVector<EntityItemID>& entityIdsToInclude;
const QVector<EntityItemID>& entityIdsToDiscard;
PickFilter searchFilter;
// Outputs
OctreeElementPointer& element;
float& parabolicDistance;
BoxFace& face;
glm::vec3& surfaceNormal;
QVariantMap& extraInfo;
EntityItemID entityID;
};
bool evalParabolaIntersectionOp(const OctreeElementPointer& element, void* extraData) {
ParabolaArgs* args = static_cast<ParabolaArgs*>(extraData);
bool keepSearching = true;
EntityTreeElementPointer entityTreeElementPointer = std::static_pointer_cast<EntityTreeElement>(element);
EntityItemID entityID = entityTreeElementPointer->evalParabolaIntersection(args->origin, args->velocity, args->acceleration,
args->element, args->parabolicDistance, args->face, args->surfaceNormal, args->entityIdsToInclude,
args->entityIdsToDiscard, args->searchFilter, args->extraInfo);
if (!entityID.isNull()) {
args->entityID = entityID;
// We recurse OctreeElements in order, so if we hit something, we can stop immediately
keepSearching = false;
}
return keepSearching;
}
float evalParabolaIntersectionSortingOp(const OctreeElementPointer& element, void* extraData) {
ParabolaArgs* args = static_cast<ParabolaArgs*>(extraData);
EntityTreeElementPointer entityTreeElementPointer = std::static_pointer_cast<EntityTreeElement>(element);
float distance = FLT_MAX;
// If origin is inside the cube, always check this element first
if (entityTreeElementPointer->getAACube().contains(args->origin)) {
distance = 0.0f;
} else {
float boundDistance = FLT_MAX;
BoxFace face;
glm::vec3 surfaceNormal;
if (entityTreeElementPointer->getAACube().findParabolaIntersection(args->origin, args->velocity, args->acceleration, boundDistance, face, surfaceNormal)) {
// Don't add this cell if it's already farther than our best distance so far
if (boundDistance < args->parabolicDistance) {
distance = boundDistance;
}
}
}
return distance;
}
EntityItemID EntityTree::evalParabolaIntersection(const PickParabola& parabola,
QVector<EntityItemID> entityIdsToInclude, QVector<EntityItemID> entityIdsToDiscard,
PickFilter searchFilter,
OctreeElementPointer& element, glm::vec3& intersection, float& distance, float& parabolicDistance,
BoxFace& face, glm::vec3& surfaceNormal, QVariantMap& extraInfo,
Octree::lockType lockType, bool* accurateResult) {
ParabolaArgs args = { parabola.origin, parabola.velocity, parabola.acceleration, entityIdsToInclude, entityIdsToDiscard,
searchFilter, element, parabolicDistance, face, surfaceNormal, extraInfo, EntityItemID() };
parabolicDistance = FLT_MAX;
distance = FLT_MAX;
bool requireLock = lockType == Octree::Lock;
bool lockResult = withReadLock([&] {
recurseTreeWithOperationSorted(evalParabolaIntersectionOp, evalParabolaIntersectionSortingOp, &args);
}, requireLock);
if (accurateResult) {
*accurateResult = lockResult; // if user asked to accuracy or result, let them know this is accurate
}
if (!args.entityID.isNull()) {
intersection = parabola.origin + parabola.velocity * parabolicDistance + 0.5f * parabola.acceleration * parabolicDistance * parabolicDistance;
distance = glm::distance(intersection, parabola.origin);
}
return args.entityID;
}
class FindClosestEntityArgs {
public:
// Inputs
glm::vec3 position;
float targetRadius;
PickFilter searchFilter;
// Outputs
QUuid closestEntity;
float closestEntityDistance;
};
bool evalClosestEntityOperation(const OctreeElementPointer& element, void* extraData) {
FindClosestEntityArgs* args = static_cast<FindClosestEntityArgs*>(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) {
float closestDistanceSquared = FLT_MAX;
QUuid thisClosestEntity = entityTreeElement->evalClosetEntity(args->position, args->searchFilter, closestDistanceSquared);
// we may have gotten NULL back, meaning no entity was available
if (!thisClosestEntity.isNull()) {
float distanceFromPointToEntity = glm::sqrt(closestDistanceSquared);
// 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;
}
}
}
// 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;
}
// NOTE: assumes caller has handled locking
QUuid EntityTree::evalClosestEntity(const glm::vec3& position, float targetRadius, PickFilter searchFilter) {
FindClosestEntityArgs args = { position, targetRadius, searchFilter, QUuid(), FLT_MAX };
recurseTreeWithOperation(evalClosestEntityOperation, &args);
return args.closestEntity;
}
class FindEntitiesInSphereArgs {
public:
// Inputs
glm::vec3 position;
float targetRadius;
PickFilter searchFilter;
// Outputs
QVector<QUuid> entities;
};
bool evalInSphereOperation(const OctreeElementPointer& element, void* extraData) {
FindEntitiesInSphereArgs* args = static_cast<FindEntitiesInSphereArgs*>(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->evalEntitiesInSphere(args->position, args->targetRadius, args->searchFilter, 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::evalEntitiesInSphere(const glm::vec3& center, float radius, PickFilter searchFilter, QVector<QUuid>& foundEntities) {
FindEntitiesInSphereArgs args = { center, radius, searchFilter, QVector<QUuid>() };
recurseTreeWithOperation(evalInSphereOperation, &args);
foundEntities.swap(args.entities);
}
class FindEntitiesInSphereWithTypeArgs {
public:
// Inputs
glm::vec3 position;
float targetRadius;
EntityTypes::EntityType type;
PickFilter searchFilter;
// Outputs
QVector<QUuid> entities;
};
bool evalInSphereWithTypeOperation(const OctreeElementPointer& element, void* extraData) {
FindEntitiesInSphereWithTypeArgs* args = static_cast<FindEntitiesInSphereWithTypeArgs*>(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->evalEntitiesInSphereWithType(args->position, args->targetRadius, args->type, args->searchFilter, 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::evalEntitiesInSphereWithType(const glm::vec3& center, float radius, EntityTypes::EntityType type, PickFilter searchFilter, QVector<QUuid>& foundEntities) {
FindEntitiesInSphereWithTypeArgs args = { center, radius, type, searchFilter, QVector<QUuid>() };
recurseTreeWithOperation(evalInSphereWithTypeOperation, &args);
foundEntities.swap(args.entities);
}
class FindEntitiesInSphereWithNameArgs {
public:
// Inputs
glm::vec3 position;
float targetRadius;
QString name;
bool caseSensitive;
PickFilter searchFilter;
// Outputs
QVector<QUuid> entities;
};
bool evalInSphereWithNameOperation(const OctreeElementPointer& element, void* extraData) {
FindEntitiesInSphereWithNameArgs* args = static_cast<FindEntitiesInSphereWithNameArgs*>(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->evalEntitiesInSphereWithName(args->position, args->targetRadius, args->name, args->caseSensitive, args->searchFilter, 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::evalEntitiesInSphereWithName(const glm::vec3& center, float radius, const QString& name, bool caseSensitive, PickFilter searchFilter, QVector<QUuid>& foundEntities) {
FindEntitiesInSphereWithNameArgs args = { center, radius, name, caseSensitive, searchFilter, QVector<QUuid>() };
recurseTreeWithOperation(evalInSphereWithNameOperation, &args);
foundEntities.swap(args.entities);
}
class FindEntitiesInCubeArgs {
public:
// Inputs
AACube cube;
PickFilter searchFilter;
// Outputs
QVector<QUuid> entities;
};
bool findInCubeOperation(const 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->evalEntitiesInCube(args->cube, args->searchFilter, args->entities);
return true;
}
return false;
}
// NOTE: assumes caller has handled locking
void EntityTree::evalEntitiesInCube(const AACube& cube, PickFilter searchFilter, QVector<QUuid>& foundEntities) {
FindEntitiesInCubeArgs args { cube, searchFilter, QVector<QUuid>() };
recurseTreeWithOperation(findInCubeOperation, &args);
foundEntities.swap(args.entities);
}
class FindEntitiesInBoxArgs {
public:
// Inputs
AABox box;
PickFilter searchFilter;
// Outputs
QVector<QUuid> entities;
};
bool findInBoxOperation(const 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->evalEntitiesInBox(args->box, args->searchFilter, args->entities);
return true;
}
return false;
}
// NOTE: assumes caller has handled locking
void EntityTree::evalEntitiesInBox(const AABox& box, PickFilter searchFilter, QVector<QUuid>& foundEntities) {
FindEntitiesInBoxArgs args { box, searchFilter, QVector<QUuid>() };
// 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.entities);
}
class FindEntitiesInFrustumArgs {
public:
// Inputs
ViewFrustum frustum;
PickFilter searchFilter;
// Outputs
QVector<QUuid> entities;
};
bool findInFrustumOperation(const OctreeElementPointer& element, void* extraData) {
FindEntitiesInFrustumArgs* args = static_cast<FindEntitiesInFrustumArgs*>(extraData);
if (element->isInView(args->frustum)) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->evalEntitiesInFrustum(args->frustum, args->searchFilter, args->entities);
return true;
}
return false;
}
// NOTE: assumes caller has handled locking
void EntityTree::evalEntitiesInFrustum(const ViewFrustum& frustum, PickFilter searchFilter, QVector<QUuid>& foundEntities) {
FindEntitiesInFrustumArgs args = { frustum, searchFilter, QVector<QUuid>() };
// 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) const {
EntityItemID entityID(id);
return findEntityByEntityItemID(entityID);
}
EntityItemPointer EntityTree::findEntityByEntityItemID(const EntityItemID& entityID) const {
EntityItemPointer foundEntity = nullptr;
{
QReadLocker locker(&_entityMapLock);
foundEntity = _entityMap.value(entityID);
}
if (foundEntity && !foundEntity->getElement()) {
// special case to maintain legacy behavior:
// if the entity is in the map but not in the tree
// then pretend the entity doesn't exist
return EntityItemPointer(nullptr);
} else {
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();
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.privateUserDataChanged()) {
int index = changedProperties.indexOf("privateUserData");
if (index >= 0) {
QString changeHint = properties.getPrivateUserData();
changedProperties[index] = QString("privateUserData:") + 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) + "/" +
QString::number(properties.getQueryAACube().getDimensions().x);
}
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<EntityEditFilters>();
if (entityEditFilters) {
auto position = existingEntity ? existingEntity->getWorldPosition() : propertiesIn.getPosition();
auto entityID = existingEntity ? existingEntity->getEntityItemID() : EntityItemID();
accepted = entityEditFilters->filter(position, propertiesIn, propertiesOut, wasChanged, filterType, entityID, existingEntity);
}
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);
}
bool EntityTree::isScriptInWhitelist(const QString& scriptProperty) {
// grab a URL representation of the entity script so we can check the host for this script
auto entityScriptURL = QUrl::fromUserInput(scriptProperty);
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)) {
return true;
}
}
return false;
}
void EntityTree::addCertifiedEntityOnServer(EntityItemPointer entity) {
QString certID(entity->getCertificateID());
EntityItemID existingEntityItemID;
if (!certID.isEmpty()) {
EntityItemID entityItemID = entity->getEntityItemID();
QWriteLocker locker(&_entityCertificateIDMapLock);
QList<EntityItemID>& entityList = _entityCertificateIDMap[certID]; // inserts it if needed.
if (!entityList.isEmpty() && !entity->getCertificateType().contains(DOMAIN_UNLIMITED)) {
existingEntityItemID = entityList.first(); // we will only care about the first, if any, below.
entityList.removeOne(existingEntityItemID);
}
entityList << entityItemID; // adds to list within hash because entityList is a reference.
qCDebug(entities) << "Certificate ID" << certID << "belongs to" << entityItemID << "total" << entityList.size() << "entities.";
}
// Delete an already-existing entity from the tree if it has the same
// CertificateID as the entity we're trying to add.
if (!existingEntityItemID.isNull()) {
qCDebug(entities) << "Certificate ID" << certID << "already exists on entity with ID"
<< existingEntityItemID << ". Deleting existing entity.";
withWriteLock([&] {
deleteEntity(existingEntityItemID, true);
});
}
}
void EntityTree::removeCertifiedEntityOnServer(EntityItemPointer entity) {
QString certID = entity->getCertificateID();
if (!certID.isEmpty()) {
QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock);
QList<EntityItemID>& entityList = _entityCertificateIDMap[certID];
entityList.removeOne(entity->getEntityItemID());
if (entityList.isEmpty()) {
// hmmm, do we to make it be a hash instead of a list, so that this is faster if you stamp out 1000 of a domainUnlimited?
_entityCertificateIDMap.remove(certID);
}
}
}
void EntityTree::startDynamicDomainVerificationOnServer(float minimumAgeToRemove) {
QReadLocker locker(&_entityCertificateIDMapLock);
QHashIterator<QString, QList<EntityItemID>> i(_entityCertificateIDMap);
qCDebug(entities) << _entityCertificateIDMap.size() << "certificates present.";
while (i.hasNext()) {
i.next();
const auto& certificateID = i.key();
const auto& entityIDs = i.value();
if (entityIDs.isEmpty()) {
continue;
}
// Examine each cert:
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/location");
QJsonObject request;
request["certificate_id"] = certificateID;
networkRequest.setUrl(requestURL);
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
connect(networkReply, &QNetworkReply::finished, this, [this, entityIDs, networkReply, minimumAgeToRemove, certificateID] {
QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object();
jsonObject = jsonObject["data"].toObject();
bool failure = networkReply->error() != QNetworkReply::NoError;
auto failureReason = networkReply->error();
networkReply->deleteLater();
if (failure) {
qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << failureReason
<< "; NOT deleting cert" << certificateID << "More info:" << jsonObject;
return;
}
QString thisDomainID = DependencyManager::get<AddressManager>()->getDomainID().remove(QRegExp("\\{|\\}"));
if (jsonObject["domain_id"].toString() == thisDomainID) {
// Entity belongs here. Nothing to do.
return;
}
// Entity does not belong here:
QList<EntityItemID> retained;
for (int i = 0; i < entityIDs.size(); i++) {
EntityItemID entityID = entityIDs.at(i);
EntityItemPointer entity = findEntityByEntityItemID(entityID);
if (!entity) {
qCDebug(entities) << "Entity undergoing dynamic domain verification is no longer available:" << entityID;
continue;
}
if (entity->getAge() <= minimumAgeToRemove) {
qCDebug(entities) << "Entity failed dynamic domain verification, but was created too recently to necessitate deletion:" << entityID;
retained << entityID;
continue;
}
qCDebug(entities) << "Entity's cert's domain ID" << jsonObject["domain_id"].toString()
<< "doesn't match the current Domain ID" << thisDomainID << "; deleting entity" << entityID;
withWriteLock([&] {
deleteEntity(entityID, true);
});
}
{
QWriteLocker entityCertificateIDMapLocker(&_entityCertificateIDMapLock);
if (retained.isEmpty()) {
qCDebug(entities) << "Removed" << certificateID;
_entityCertificateIDMap.remove(certificateID);
} else {
qCDebug(entities) << "Retained" << retained.size() << "young entities for" << certificateID;
_entityCertificateIDMap[certificateID] = retained;
}
}
});
}
}
void EntityTree::startChallengeOwnershipTimer(const EntityItemID& entityItemID) {
QTimer* _challengeOwnershipTimeoutTimer = new QTimer(this);
connect(this, &EntityTree::killChallengeOwnershipTimeoutTimer, this, [=](const EntityItemID& id) {
if (entityItemID == id && _challengeOwnershipTimeoutTimer) {
_challengeOwnershipTimeoutTimer->stop();
_challengeOwnershipTimeoutTimer->deleteLater();
}
});
connect(_challengeOwnershipTimeoutTimer, &QTimer::timeout, this, [=]() {
qCDebug(entities) << "Ownership challenge timed out, deleting entity" << entityItemID;
withWriteLock([&] {
deleteEntity(entityItemID, true);
});
if (_challengeOwnershipTimeoutTimer) {
_challengeOwnershipTimeoutTimer->stop();
_challengeOwnershipTimeoutTimer->deleteLater();
}
});
_challengeOwnershipTimeoutTimer->setSingleShot(true);
_challengeOwnershipTimeoutTimer->start(5000);
}
QByteArray EntityTree::computeNonce(const EntityItemID& entityID, const QString ownerKey) {
QUuid nonce = QUuid::createUuid(); //random, 5-hex value, separated by "-"
QByteArray nonceBytes = nonce.toByteArray();
QWriteLocker locker(&_entityNonceMapLock);
_entityNonceMap.insert(entityID, QPair<QUuid, QString>(nonce, ownerKey));
return nonceBytes;
}
bool EntityTree::verifyNonce(const EntityItemID& entityID, const QString& nonce) {
QString actualNonce, key;
{
QWriteLocker locker(&_entityNonceMapLock);
QPair<QUuid, QString> sent = _entityNonceMap.take(entityID);
actualNonce = sent.first.toString();
key = sent.second;
}
QString annotatedKey = "-----BEGIN PUBLIC KEY-----\n" + key.insert(64, "\n") + "\n-----END PUBLIC KEY-----\n";
QByteArray hashedActualNonce = QCryptographicHash::hash(QByteArray(actualNonce.toUtf8()), QCryptographicHash::Sha256);
bool verificationSuccess = EntityItemProperties::verifySignature(annotatedKey.toUtf8(), hashedActualNonce, QByteArray::fromBase64(nonce.toUtf8()));
if (verificationSuccess) {
qCDebug(entities) << "Ownership challenge for Entity ID" << entityID << "succeeded.";
} else {
qCDebug(entities) << "Ownership challenge for Entity ID" << entityID << "failed. Actual nonce:" << actualNonce <<
"\nHashed actual nonce (digest):" << hashedActualNonce << "\nSent nonce (signature)" << nonce << "\nKey" << key;
}
return verificationSuccess;
}
void EntityTree::processChallengeOwnershipRequestPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) {
int idByteArraySize;
int textByteArraySize;
int nodeToChallengeByteArraySize;
message.readPrimitive(&idByteArraySize);
message.readPrimitive(&textByteArraySize);
message.readPrimitive(&nodeToChallengeByteArraySize);
QByteArray id(message.read(idByteArraySize));
QByteArray text(message.read(textByteArraySize));
QByteArray nodeToChallenge(message.read(nodeToChallengeByteArraySize));
sendChallengeOwnershipRequestPacket(id, text, nodeToChallenge, sourceNode);
}
void EntityTree::processChallengeOwnershipReplyPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) {
auto nodeList = DependencyManager::get<NodeList>();
int idByteArraySize;
int textByteArraySize;
int challengingNodeUUIDByteArraySize;
message.readPrimitive(&idByteArraySize);
message.readPrimitive(&textByteArraySize);
message.readPrimitive(&challengingNodeUUIDByteArraySize);
QByteArray id(message.read(idByteArraySize));
QByteArray text(message.read(textByteArraySize));
QUuid challengingNode = QUuid::fromRfc4122(message.read(challengingNodeUUIDByteArraySize));
auto challengeOwnershipReplyPacket = NLPacket::create(PacketType::ChallengeOwnershipReply,
idByteArraySize + text.length() + 2 * sizeof(int),
true);
challengeOwnershipReplyPacket->writePrimitive(idByteArraySize);
challengeOwnershipReplyPacket->writePrimitive(text.length());
challengeOwnershipReplyPacket->write(id);
challengeOwnershipReplyPacket->write(text);
nodeList->sendPacket(std::move(challengeOwnershipReplyPacket), *(nodeList->nodeWithUUID(challengingNode)));
}
void EntityTree::sendChallengeOwnershipPacket(const QString& certID, const QString& ownerKey, const EntityItemID& entityItemID, const SharedNodePointer& senderNode) {
// 1. Obtain a nonce
auto nodeList = DependencyManager::get<NodeList>();
QByteArray text = computeNonce(entityItemID, ownerKey);
if (text == "") {
qCDebug(entities) << "CRITICAL ERROR: Couldn't compute nonce. Deleting entity...";
withWriteLock([&] {
deleteEntity(entityItemID, true);
});
} else {
qCDebug(entities) << "Challenging ownership of Cert ID" << certID;
// 2. Send the nonce to the rezzing avatar's node
QByteArray idByteArray = entityItemID.toByteArray();
int idByteArraySize = idByteArray.size();
auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership,
idByteArraySize + text.length() + 2 * sizeof(int),
true);
challengeOwnershipPacket->writePrimitive(idByteArraySize);
challengeOwnershipPacket->writePrimitive(text.length());
challengeOwnershipPacket->write(idByteArray);
challengeOwnershipPacket->write(text);
nodeList->sendPacket(std::move(challengeOwnershipPacket), *senderNode);
// 3. Kickoff a 10-second timeout timer that deletes the entity if we don't get an ownership response in time
if (thread() != QThread::currentThread()) {
QMetaObject::invokeMethod(this, "startChallengeOwnershipTimer", Q_ARG(const EntityItemID&, entityItemID));
return;
} else {
startChallengeOwnershipTimer(entityItemID);
}
}
}
void EntityTree::sendChallengeOwnershipRequestPacket(const QByteArray& id, const QByteArray& text, const QByteArray& nodeToChallenge, const SharedNodePointer& senderNode) {
auto nodeList = DependencyManager::get<NodeList>();
// In this case, Client A is challenging Client B. Client A is inspecting a certified entity that it wants
// to make sure belongs to Avatar B.
QByteArray senderNodeUUID = senderNode->getUUID().toRfc4122();
int idByteArraySize = id.length();
int TextByteArraySize = text.length();
int senderNodeUUIDSize = senderNodeUUID.length();
auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnershipRequest,
idByteArraySize + TextByteArraySize + senderNodeUUIDSize + 3 * sizeof(int),
true);
challengeOwnershipPacket->writePrimitive(idByteArraySize);
challengeOwnershipPacket->writePrimitive(TextByteArraySize);
challengeOwnershipPacket->writePrimitive(senderNodeUUIDSize);
challengeOwnershipPacket->write(id);
challengeOwnershipPacket->write(text);
challengeOwnershipPacket->write(senderNodeUUID);
nodeList->sendPacket(std::move(challengeOwnershipPacket), *(nodeList->nodeWithUUID(QUuid::fromRfc4122(nodeToChallenge))));
}
void EntityTree::validatePop(const QString& certID, const EntityItemID& entityItemID, const SharedNodePointer& senderNode) {
// Start owner verification.
auto nodeList = DependencyManager::get<NodeList>();
// First, asynchronously hit "proof_of_purchase_status?transaction_type=transfer" endpoint.
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkRequest networkRequest;
networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
requestURL.setPath("/api/v1/commerce/proof_of_purchase_status/transfer");
QJsonObject request;
request["certificate_id"] = certID;
networkRequest.setUrl(requestURL);
QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
connect(networkReply, &QNetworkReply::finished, [this, networkReply, entityItemID, certID, senderNode]() {
QJsonObject jsonObject = QJsonDocument::fromJson(networkReply->readAll()).object();
jsonObject = jsonObject["data"].toObject();
if (networkReply->error() == QNetworkReply::NoError) {
if (!jsonObject["invalid_reason"].toString().isEmpty()) {
qCDebug(entities) << "invalid_reason not empty, deleting entity" << entityItemID;
withWriteLock([&] {
deleteEntity(entityItemID, true);
});
} else if (jsonObject["transfer_status"].toArray().first().toString() == "failed") {
qCDebug(entities) << "'transfer_status' is 'failed', deleting entity" << entityItemID;
withWriteLock([&] {
deleteEntity(entityItemID, true);
});
} else {
// Second, challenge ownership of the PoP cert
// (ignore pending status; a failure will be cleaned up during DDV)
sendChallengeOwnershipPacket(certID,
jsonObject["transfer_recipient_key"].toString(),
entityItemID,
senderNode);
}
} else {
qCDebug(entities) << "Call to" << networkReply->url() << "failed with error" << networkReply->error() << "; deleting entity" << entityItemID
<< "More info:" << jsonObject;
withWriteLock([&] {
deleteEntity(entityItemID, true);
});
}
networkReply->deleteLater();
});
}
void EntityTree::processChallengeOwnershipPacket(ReceivedMessage& message, const SharedNodePointer& sourceNode) {
int idByteArraySize;
int textByteArraySize;
message.readPrimitive(&idByteArraySize);
message.readPrimitive(&textByteArraySize);
EntityItemID id(message.read(idByteArraySize));
QString text(message.read(textByteArraySize));
emit killChallengeOwnershipTimeoutTimer(id);
if (!verifyNonce(id, text)) {
withWriteLock([&] {
deleteEntity(id, true);
});
}
}
int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned char* editData, int maxLength,
const SharedNodePointer& senderNode) {
if (!getIsServer()) {
qCWarning(entities) << "EntityTree::processEditPacketData() should only be called on a server tree.";
return 0;
}
int processedBytes = 0;
bool isAdd = false;
bool isClone = false;
// 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::EntityClone:
isClone = true; // fall through to next case
// FALLTHRU
case PacketType::EntityAdd:
isAdd = true; // fall through to next case
// FALLTHRU
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 suppressDisallowedClientScript = false;
bool suppressDisallowedServerScript = false;
bool suppressDisallowedPrivateUserData = false;
bool isPhysics = message.getType() == PacketType::EntityPhysics;
_totalEditMessages++;
EntityItemID entityItemID;
EntityItemProperties properties;
startDecode = usecTimestampNow();
bool validEditPacket = false;
EntityItemID entityIDToClone;
EntityItemPointer entityToClone;
if (isClone) {
QByteArray buffer = QByteArray::fromRawData(reinterpret_cast<const char*>(editData), maxLength);
validEditPacket = EntityItemProperties::decodeCloneEntityMessage(buffer, processedBytes, entityIDToClone, entityItemID);
if (validEditPacket) {
entityToClone = findEntityByEntityItemID(entityIDToClone);
if (entityToClone) {
properties = entityToClone->getProperties();
}
}
} else {
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()) {
bool wasDeletedBecauseOfClientScript = false;
// check the client entity script to make sure its URL is in the whitelist
if (!properties.getScript().isEmpty()) {
bool clientScriptPassedWhitelist = isScriptInWhitelist(properties.getScript());
if (!clientScriptPassedWhitelist) {
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 = false;
wasDeletedBecauseOfClientScript = true;
} else {
suppressDisallowedClientScript = true;
}
}
}
// check all server entity scripts to make sure their URLs are in the whitelist
if (!properties.getServerScripts().isEmpty()) {
bool serverScriptPassedWhitelist = isScriptInWhitelist(properties.getServerScripts());
if (!serverScriptPassedWhitelist) {
if (wantEditLogging()) {
qCDebug(entities) << "User [" << senderNode->getUUID()
<< "] attempting to set server 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) {
// Make sure we didn't already need to send back a delete because the client script failed
// the whitelist check
if (!wasDeletedBecauseOfClientScript) {
QWriteLocker locker(&_recentlyDeletedEntitiesLock);
_recentlyDeletedEntityItemIDs.insert(usecTimestampNow(), entityItemID);
validEditPacket = false;
}
} else {
suppressDisallowedServerScript = true;
}
}
}
}
if (!properties.getPrivateUserData().isEmpty() && validEditPacket && !senderNode->getCanGetAndSetPrivateUserData()) {
if (wantEditLogging()) {
qCDebug(entities) << "User [" << senderNode->getUUID()
<< "] is attempting to set private user data but user isn't allowed; 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 = false;
} else {
suppressDisallowedPrivateUserData = true;
}
}
if (!isClone) {
if ((isAdd || properties.lifetimeChanged()) &&
((!senderNode->getCanRez() && senderNode->getCanRezTmp()) ||
(!senderNode->getCanRezCertified() && senderNode->getCanRezTmpCertified()))) {
// 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 (isAdd && properties.getLocked() && !senderNode->isAllowedEditor()) {
// if a node can't change locks, don't allow it to create an already-locked entity -- automatically
// clear the locked property and allow the unlocked entity to be created.
properties.setLocked(false);
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 (suppressDisallowedClientScript) {
bumpTimestamp(properties);
properties.setScript(existingEntity->getScript());
}
if (suppressDisallowedServerScript) {
bumpTimestamp(properties);
properties.setServerScripts(existingEntity->getServerScripts());
}
if (suppressDisallowedPrivateUserData) {
bumpTimestamp(properties);
properties.setPrivateUserData(existingEntity->getPrivateUserData());
}
// 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();
if (!isPhysics) {
properties.setLastEditedBy(senderNode->getUUID());
}
updateEntity(existingEntity, properties, senderNode);
existingEntity->markAsChangedOnServer();
endUpdate = usecTimestampNow();
_totalUpdates++;
} else if (isAdd) {
bool failedAdd = !allowed;
bool isCertified = !properties.getCertificateID().isEmpty();
bool isCloneable = properties.getCloneable();
int cloneLimit = properties.getCloneLimit();
if (!allowed) {
qCDebug(entities) << "Filtered entity add. ID:" << entityItemID;
} else if (!isClone && !isCertified && !senderNode->getCanRez() && !senderNode->getCanRezTmp()) {
failedAdd = true;
qCDebug(entities) << "User without 'uncertified rez rights' [" << senderNode->getUUID()
<< "] attempted to add an uncertified entity with ID:" << entityItemID;
} else if (!isClone && isCertified && !senderNode->getCanRezCertified() && !senderNode->getCanRezTmpCertified()) {
failedAdd = true;
qCDebug(entities) << "User without 'certified rez rights' [" << senderNode->getUUID()
<< "] attempted to add a certified entity with ID:" << entityItemID;
} else if (isClone && isCertified && !properties.getCertificateType().contains(DOMAIN_UNLIMITED)) {
failedAdd = true;
qCDebug(entities) << "User attempted to clone certified entity from entity ID:" << entityIDToClone;
} else if (isClone && !isCloneable) {
failedAdd = true;
qCDebug(entities) << "User attempted to clone non-cloneable entity from entity ID:" << entityIDToClone;
} else if (isClone && entityToClone && entityToClone->getCloneIDs().size() >= cloneLimit && cloneLimit != 0) {
failedAdd = true;
qCDebug(entities) << "User attempted to clone entity ID:" << entityIDToClone << " which reached it's cloneable limit.";
} else {
if (isClone) {
properties.convertToCloneProperties(entityIDToClone);
}
// this is a new entity... assign a new entityID
properties.setLastEditedBy(senderNode->getUUID());
startCreate = usecTimestampNow();
EntityItemPointer newEntity = addEntity(entityItemID, properties);
endCreate = usecTimestampNow();
_totalCreates++;
if (newEntity && isCertified && getIsServer()) {
if (!properties.verifyStaticCertificateProperties()) {
qCDebug(entities) << "User" << senderNode->getUUID()
<< "attempted to add a certified entity with ID" << entityItemID << "which failed"
<< "static certificate verification.";
// Delete the entity we just added if it doesn't pass static certificate verification
deleteEntity(entityItemID, true);
} else {
validatePop(properties.getCertificateID(), entityItemID, senderNode);
}
}
if (newEntity && isClone) {
entityToClone->addCloneID(newEntity->getEntityItemID());
newEntity->setCloneOriginID(entityIDToClone);
}
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 {
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 {
HIFI_FCDEBUG(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 (entity->isSimulated()) {
_simulation->changeEntity(entity);
}
}
void EntityTree::fixupNeedsParentFixups() {
MovingEntitiesOperator moveOperator;
QVector<EntityItemWeakPointer> entitiesToFixup;
{
QWriteLocker locker(&_needsParentFixupLock);
entitiesToFixup = _needsParentFixup;
_needsParentFixup.clear();
}
std::unordered_set<QUuid> seenEntityIds;
QMutableVectorIterator<EntityItemWeakPointer> iter(entitiesToFixup);
while (iter.hasNext()) {
const auto& entityWP = iter.next();
EntityItemPointer entity = entityWP.lock();
if (!entity) {
// entity was deleted before we found its parent
iter.remove();
continue;
}
const auto id = entity->getID();
// BUGZ-771 some entities seem to never be removed by the below logic and further seem to accumulate dupes within the _needsParentFixup list
// This block ensures that duplicates are removed from entitiesToFixup before it's re-appended to _needsParentFixup
if (0 != seenEntityIds.count(id)) {
// Entity was duplicated inside entitiesToFixup
iter.remove();
continue;
}
seenEntityIds.insert(id);
entity->requiresRecalcBoxes();
bool queryAACubeSuccess { false };
bool maxAACubeSuccess { false };
AACube newCube = entity->getQueryAACube(queryAACubeSuccess);
if (queryAACubeSuccess) {
// make sure queryAACube encompasses maxAACube
AACube maxAACube = entity->getMaximumAACube(maxAACubeSuccess);
if (maxAACubeSuccess && !newCube.contains(maxAACube)) {
newCube = maxAACube;
}
}
bool doMove = false;
if (entity->isParentIDValid() && maxAACubeSuccess) { // maxAACubeSuccess of true means all ancestors are known
iter.remove(); // this entity is all hooked up; we can remove it from the list
// this entity's parent was previously not known, and now is. Update its location in the EntityTree...
doMove = true;
// the bounds on the render-item may need to be updated, the rigid body in the physics engine may
// need to be moved.
entity->markDirtyFlags(Simulation::DIRTY_MOTION_TYPE |
Simulation::DIRTY_COLLISION_GROUP |
Simulation::DIRTY_TRANSFORM);
entityChanged(entity);
entity->locationChanged(true, false);
entity->forEachDescendant([&](SpatiallyNestablePointer object) {
if (object->getNestableType() == NestableType::Entity) {
EntityItemPointer descendantEntity = std::static_pointer_cast<EntityItem>(object);
descendantEntity->markDirtyFlags(Simulation::DIRTY_MOTION_TYPE |
Simulation::DIRTY_COLLISION_GROUP |
Simulation::DIRTY_TRANSFORM);
entityChanged(descendantEntity);
}
object->locationChanged(true, false);
});
// Update our parent's bounding box
bool success = false;
auto parent = entity->getParentPointer(success);
if (success && parent) {
parent->updateQueryAACube();
}
entity->postParentFixup();
} 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;
iter.remove(); // and pull it out of the list
}
if (queryAACubeSuccess && doMove) {
moveOperator.addEntityToMoveList(entity, newCube);
}
}
if (moveOperator.hasMovingEntities()) {
PerformanceTimer perfTimer("recurseTreeWithOperator");
recurseTreeWithOperator(&moveOperator);
}
{
QWriteLocker locker(&_needsParentFixupLock);
// add back the entities that did not get fixup
_needsParentFixup.append(entitiesToFixup);
}
}
void EntityTree::deleteDescendantsOfAvatar(QUuid avatarID) {
if (_childrenOfAvatars.contains(avatarID)) {
deleteEntities(_childrenOfAvatars[avatarID]);
_childrenOfAvatars.remove(avatarID);
}
}
void EntityTree::removeFromChildrenOfAvatars(EntityItemPointer entity) {
QUuid avatarID = entity->getParentID();
if (_childrenOfAvatars.contains(avatarID)) {
_childrenOfAvatars[avatarID].remove(entity->getID());
}
}
void EntityTree::addToNeedsParentFixupList(EntityItemPointer entity) {
QWriteLocker locker(&_needsParentFixupLock);
_needsParentFixup.append(entity);
}
void EntityTree::preUpdate() {
withWriteLock([&] {
fixupNeedsParentFixups();
if (_simulation) {
_simulation->processChangedEntities();
}
});
}
void EntityTree::update(bool simulate) {
PROFILE_RANGE(simulation_physics, "UpdateTree");
PerformanceTimer perfTimer("updateTree");
if (simulate && _simulation) {
withWriteLock([&] {
_simulation->updateEntities();
{
PROFILE_RANGE(simulation_physics, "Deletes");
SetOfEntities deadEntities;
_simulation->takeDeadEntities(deadEntities);
if (!deadEntities.empty()) {
// translate into list of ID's
QSet<EntityItemID> idsToDelete;
for (auto entity : deadEntities) {
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;
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<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);
}
}
bool EntityTree::shouldEraseEntity(EntityItemID entityID, const SharedNodePointer& sourceNode) {
EntityItemPointer existingEntity;
auto startLookup = usecTimestampNow();
existingEntity = findEntityByEntityItemID(entityID);
auto endLookup = usecTimestampNow();
_totalLookupTime += endLookup - startLookup;
auto startFilter = usecTimestampNow();
FilterType filterType = FilterType::Delete;
EntityItemProperties dummyProperties;
bool wasChanged = false;
bool allowed = (sourceNode->isAllowedEditor()) || filterProperties(existingEntity, dummyProperties, dummyProperties, wasChanged, filterType);
auto endFilter = usecTimestampNow();
_totalFilterTime += endFilter - startFilter;
if (allowed) {
if (wantEditLogging() || wantTerseEditLogging()) {
qCDebug(entities) << "User [" << sourceNode->getUUID() << "] deleting entity. ID:" << entityID;
}
} else if (wantEditLogging() || wantTerseEditLogging()) {
qCDebug(entities) << "User [" << sourceNode->getUUID() << "] attempted to deleteentity. ID:" << entityID << " Filter rejected erase.";
}
return allowed;
}
// 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<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
qCDebug(entities) << " ---- EntityTree::processEraseMessage() contained ID:" << entityID;
#endif
EntityItemID entityItemID(entityID);
if (shouldEraseEntity(entityID, sourceNode)) {
entityItemIDsToDelete << entityItemID;
cleanupCloneIDs(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<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
qCDebug(entities) << " ---- EntityTree::processEraseMessageDetails() contains id:" << entityID;
#endif
EntityItemID entityItemID(entityID);
if (shouldEraseEntity(entityID, sourceNode)) {
entityItemIDsToDelete << entityItemID;
cleanupCloneIDs(entityItemID);
}
}
deleteEntities(entityItemIDsToDelete, true, true);
}
return (int)processedBytes;
}
EntityTreeElementPointer EntityTree::getContainingElement(const EntityItemID& entityItemID) /*const*/ {
EntityItemPointer entity;
{
QReadLocker locker(&_entityMapLock);
entity = _entityMap.value(entityItemID);
}
if (entity) {
return entity->getElement();
}
return EntityTreeElementPointer(nullptr);
}
void EntityTree::addEntityMapEntry(EntityItemPointer entity) {
EntityItemID id = entity->getEntityItemID();
QWriteLocker locker(&_entityMapLock);
EntityItemPointer otherEntity = _entityMap.value(id);
if (otherEntity) {
qCWarning(entities) << "EntityTree::addEntityMapEntry() found pre-existing id " << id;
assert(false);
return;
}
_entityMap.insert(id, entity);
}
void EntityTree::clearEntityMapEntry(const EntityItemID& id) {
QWriteLocker locker(&_entityMapLock);
_entityMap.remove(id);
}
void EntityTree::debugDumpMap() {
// QHash's are implicitly shared, so we make a shared copy and use that instead.
// This way we might be able to avoid both a lock and a true copy.
QHash<EntityItemID, EntityItemPointer> localMap(_entityMap);
qCDebug(entities) << "EntityTree::debugDumpMap() --------------------------";
QHashIterator<EntityItemID, EntityItemPointer> i(localMap);
while (i.hasNext()) {
i.next();
qCDebug(entities) << i.key() << ": " << i.value()->getElement().get();
}
qCDebug(entities) << "-----------------------------------------------------";
}
class ContentsDimensionOperator : public RecurseOctreeOperator {
public:
virtual bool preRecursion(const OctreeElementPointer& element) override;
virtual bool postRecursion(const OctreeElementPointer& element) override { return true; }
glm::vec3 getDimensions() const { return _contentExtents.size(); }
float getLargestDimension() const { return _contentExtents.largestDimension(); }
private:
Extents _contentExtents;
};
bool ContentsDimensionOperator::preRecursion(const OctreeElementPointer& element) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(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(const OctreeElementPointer& element) override;
virtual bool postRecursion(const OctreeElementPointer& element) override { return true; }
};
bool DebugOperator::preRecursion(const 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(const OctreeElementPointer& element) override { return true; }
virtual bool postRecursion(const OctreeElementPointer& element) override;
};
bool PruneOperator::postRecursion(const OctreeElementPointer& element) {
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
entityTreeElement->pruneChildren();
return true;
}
void EntityTree::pruneTree() {
PruneOperator theOperator;
recurseTreeWithOperator(&theOperator);
}
QByteArray EntityTree::remapActionDataIDs(QByteArray actionData, QHash<EntityItemID, EntityItemID>& map) {
if (actionData.isEmpty()) {
return actionData;
}
QDataStream serializedActionsStream(actionData);
QVector<QByteArray> serializedActions;
serializedActionsStream >> serializedActions;
auto actionFactory = DependencyManager::get<EntityDynamicFactoryInterface>();
QHash<QUuid, EntityDynamicPointer> remappedActions;
foreach(QByteArray serializedAction, serializedActions) {
QDataStream serializedActionStream(serializedAction);
EntityDynamicType actionType;
QUuid oldActionID;
serializedActionStream >> actionType;
serializedActionStream >> oldActionID;
EntityDynamicPointer action = actionFactory->factoryBA(nullptr, serializedAction);
if (action) {
action->remapIDs(map);
remappedActions[action->getID()] = action;
}
}
QVector<QByteArray> remappedSerializedActions;
QHash<QUuid, EntityDynamicPointer>::const_iterator i = remappedActions.begin();
while (i != remappedActions.end()) {
EntityDynamicPointer action = i.value();
QByteArray bytesForAction = action->serialize();
remappedSerializedActions << bytesForAction;
i++;
}
QByteArray result;
QDataStream remappedSerializedActionsStream(&result, QIODevice::WriteOnly);
remappedSerializedActionsStream << remappedSerializedActions;
return result;
}
QVector<EntityItemID> EntityTree::sendEntities(EntityEditPacketSender* packetSender, EntityTreePointer localTree,
float x, float y, float z) {
SendEntitiesOperationArgs args;
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);
});
// The values from map are used as the list of successfully "sent" entities. If some didn't actually make it,
// pull them out. Bogus entries could happen if part of the imported data makes some reference to an entity
// that isn't in the data being imported. For those that made it, fix up their queryAACubes and send an
// add-entity packet to the server.
// fix the queryAACubes of any children that were read in before their parents, get them into the correct element
MovingEntitiesOperator moveOperator;
QHash<EntityItemID, EntityItemID>::iterator i = map.begin();
while (i != map.end()) {
EntityItemID newID = i.value();
EntityItemPointer entity = localTree->findEntityByEntityItemID(newID);
if (entity) {
if (!entity->getParentID().isNull()) {
addToNeedsParentFixupList(entity);
}
entity->forceQueryAACubeUpdate();
entity->updateQueryAACube();
moveOperator.addEntityToMoveList(entity, entity->getQueryAACube());
i++;
} else {
i = map.erase(i);
}
}
if (moveOperator.hasMovingEntities()) {
PerformanceTimer perfTimer("recurseTreeWithOperator");
localTree->recurseTreeWithOperator(&moveOperator);
}
if (!_serverlessDomain) {
// send add-entity packets to the server
i = map.begin();
while (i != map.end()) {
EntityItemID newID = i.value();
EntityItemPointer entity = localTree->findEntityByEntityItemID(newID);
if (entity) {
// queue the packet to send to the server
entity->updateQueryAACube();
EntityItemProperties properties = entity->getProperties();
properties.markAllChanged(); // so the entire property set is considered new, since we're making a new entity
packetSender->queueEditEntityMessage(PacketType::EntityAdd, localTree, newID, properties);
i++;
} else {
i = map.erase(i);
}
}
packetSender->releaseQueuedMessages();
}
return map.values().toVector();
}
bool EntityTree::sendEntitiesOperation(const OctreeElementPointer& element, void* extraData) {
SendEntitiesOperationArgs* args = static_cast<SendEntitiesOperationArgs*>(extraData);
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
auto getMapped = [&args](EntityItemID oldID) {
if (oldID.isNull()) {
return EntityItemID();
}
QHash<EntityItemID, EntityItemID>::iterator iter = args->map->find(oldID);
if (iter == args->map->end()) {
EntityItemID newID;
if (oldID == AVATAR_SELF_ID) {
auto nodeList = DependencyManager::get<NodeList>();
newID = EntityItemID(nodeList->getSessionUUID());
} else {
newID = QUuid::createUuid();
}
args->map->insert(oldID, newID);
return newID;
}
return iter.value();
};
entityTreeElement->forEachEntity([&args, &getMapped, &element](EntityItemPointer item) {
EntityItemID oldID = item->getEntityItemID();
EntityItemID newID = getMapped(oldID);
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 || oldParentID == AVATAR_SELF_ID) { // map the parent
properties.setParentID(getMapped(oldParentID));
// 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.setXNNeighborID(getMapped(properties.getXNNeighborID()));
properties.setXPNeighborID(getMapped(properties.getXPNeighborID()));
properties.setYNNeighborID(getMapped(properties.getYNNeighborID()));
properties.setYPNeighborID(getMapped(properties.getYPNeighborID()));
properties.setZNNeighborID(getMapped(properties.getZNNeighborID()));
properties.setZPNeighborID(getMapped(properties.getZPNeighborID()));
QByteArray actionData = properties.getActionData();
properties.setActionData(remapActionDataIDs(actionData, *args->map));
// set creation time to "now" for imported entities
properties.setCreated(usecTimestampNow());
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
EntityTreePointer tree = entityTreeElement->getTree();
// also update the local tree instantly (note: this is not our tree, but an alternate tree)
if (args->otherTree) {
args->otherTree->withWriteLock([&] {
EntityItemPointer entity = args->otherTree->addEntity(newID, properties);
if (entity) {
entity->deserializeActions();
}
// else: there was an error adding this entity
});
}
return newID;
});
return true;
}
bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElementPointer element, bool skipDefaultValues,
bool skipThoseWithBadParents) {
if (! entityDescription.contains("Entities")) {
entityDescription["Entities"] = QVariantList();
}
entityDescription["DataVersion"] = _persistDataVersion;
entityDescription["Id"] = _persistID;
QScriptEngine scriptEngine;
RecurseOctreeToMapOperator theOperator(entityDescription, element, &scriptEngine, skipDefaultValues,
skipThoseWithBadParents, _myAvatar);
withReadLock([&] {
recurseTreeWithOperator(&theOperator);
});
return true;
}
void convertGrabUserDataToProperties(EntityItemProperties& properties) {
GrabPropertyGroup& grabProperties = properties.getGrab();
QJsonObject userData = QJsonDocument::fromJson(properties.getUserData().toUtf8()).object();
QJsonValue grabbableKeyValue = userData["grabbableKey"];
if (grabbableKeyValue.isObject()) {
QJsonObject grabbableKey = grabbableKeyValue.toObject();
QJsonValue wantsTrigger = grabbableKey["wantsTrigger"];
if (wantsTrigger.isBool()) {
grabProperties.setTriggerable(wantsTrigger.toBool());
}
QJsonValue triggerable = grabbableKey["triggerable"];
if (triggerable.isBool()) {
grabProperties.setTriggerable(triggerable.toBool());
}
QJsonValue grabbable = grabbableKey["grabbable"];
if (grabbable.isBool()) {
grabProperties.setGrabbable(grabbable.toBool());
}
QJsonValue ignoreIK = grabbableKey["ignoreIK"];
if (ignoreIK.isBool()) {
grabProperties.setGrabFollowsController(ignoreIK.toBool());
}
QJsonValue kinematic = grabbableKey["kinematic"];
if (kinematic.isBool()) {
grabProperties.setGrabKinematic(kinematic.toBool());
}
QJsonValue equippable = grabbableKey["equippable"];
if (equippable.isBool()) {
grabProperties.setEquippable(equippable.toBool());
}
grabProperties.setGrabDelegateToParent(true);
if (grabbableKey["spatialKey"].isObject()) {
QJsonObject spatialKey = grabbableKey["spatialKey"].toObject();
grabProperties.setEquippable(true);
if (spatialKey["leftRelativePosition"].isObject()) {
grabProperties.setEquippableLeftPosition(qMapToVec3(spatialKey["leftRelativePosition"].toVariant()));
}
if (spatialKey["rightRelativePosition"].isObject()) {
grabProperties.setEquippableRightPosition(qMapToVec3(spatialKey["rightRelativePosition"].toVariant()));
}
if (spatialKey["relativeRotation"].isObject()) {
grabProperties.setEquippableLeftRotation(qMapToQuat(spatialKey["relativeRotation"].toVariant()));
grabProperties.setEquippableRightRotation(qMapToQuat(spatialKey["relativeRotation"].toVariant()));
}
}
}
QJsonValue wearableValue = userData["wearable"];
if (wearableValue.isObject()) {
QJsonObject wearable = wearableValue.toObject();
QJsonObject joints = wearable["joints"].toObject();
if (joints["LeftHand"].isArray()) {
QJsonArray leftHand = joints["LeftHand"].toArray();
if (leftHand.size() == 2) {
grabProperties.setEquippable(true);
grabProperties.setEquippableLeftPosition(qMapToVec3(leftHand[0].toVariant()));
grabProperties.setEquippableLeftRotation(qMapToQuat(leftHand[1].toVariant()));
}
}
if (joints["RightHand"].isArray()) {
QJsonArray rightHand = joints["RightHand"].toArray();
if (rightHand.size() == 2) {
grabProperties.setEquippable(true);
grabProperties.setEquippableRightPosition(qMapToVec3(rightHand[0].toVariant()));
grabProperties.setEquippableRightRotation(qMapToQuat(rightHand[1].toVariant()));
}
}
}
QJsonValue equipHotspotsValue = userData["equipHotspots"];
if (equipHotspotsValue.isArray()) {
QJsonArray equipHotspots = equipHotspotsValue.toArray();
if (equipHotspots.size() > 0) {
// just take the first one
QJsonObject firstHotSpot = equipHotspots[0].toObject();
QJsonObject joints = firstHotSpot["joints"].toObject();
if (joints["LeftHand"].isArray()) {
QJsonArray leftHand = joints["LeftHand"].toArray();
if (leftHand.size() == 2) {
grabProperties.setEquippableLeftPosition(qMapToVec3(leftHand[0].toVariant()));
grabProperties.setEquippableLeftRotation(qMapToQuat(leftHand[1].toVariant()));
}
}
if (joints["RightHand"].isArray()) {
QJsonArray rightHand = joints["RightHand"].toArray();
if (rightHand.size() == 2) {
grabProperties.setEquippable(true);
grabProperties.setEquippableRightPosition(qMapToVec3(rightHand[0].toVariant()));
grabProperties.setEquippableRightRotation(qMapToQuat(rightHand[1].toVariant()));
}
}
QJsonValue indicatorURL = firstHotSpot["modelURL"];
if (indicatorURL.isString()) {
grabProperties.setEquippableIndicatorURL(indicatorURL.toString());
}
QJsonValue indicatorScale = firstHotSpot["modelScale"];
if (indicatorScale.isDouble()) {
grabProperties.setEquippableIndicatorScale(glm::vec3((float)indicatorScale.toDouble()));
} else if (indicatorScale.isObject()) {
grabProperties.setEquippableIndicatorScale(qMapToVec3(indicatorScale.toVariant()));
}
QJsonValue indicatorOffset = firstHotSpot["position"];
if (indicatorOffset.isObject()) {
grabProperties.setEquippableIndicatorOffset(qMapToVec3(indicatorOffset.toVariant()));
}
}
}
}
bool EntityTree::readFromMap(QVariantMap& map) {
// These are needed to deal with older content (before adding inheritance modes)
int contentVersion = map["Version"].toInt();
if (map.contains("Id")) {
_persistID = map["Id"].toUuid();
}
if (map.contains("DataVersion")) {
_persistDataVersion = map["DataVersion"].toInt();
}
_namedPaths.clear();
if (map.contains("Paths")) {
QVariantMap namedPathsMap = map["Paths"].toMap();
for(QVariantMap::const_iterator iter = namedPathsMap.begin(); iter != namedPathsMap.end(); ++iter) {
QString namedPathName = iter.key();
QString namedPathViewPoint = iter.value().toString();
_namedPaths[namedPathName] = namedPathViewPoint;
}
}
// 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 EntityTree.
QVariantList entitiesQList = map["Entities"].toList();
QScriptEngine scriptEngine;
if (entitiesQList.length() == 0) {
// Empty map or invalidly formed file.
return false;
}
QMap<QUuid, QVector<QUuid>> cloneIDs;
bool success = true;
foreach (QVariant entityVariant, entitiesQList) {
// QVariantMap --> QScriptValue --> EntityItemProperties --> Entity
QVariantMap entityMap = entityVariant.toMap();
// handle parentJointName for wearables
if (_myAvatar && entityMap.contains("parentJointName") && entityMap.contains("parentID") &&
QUuid(entityMap["parentID"].toString()) == AVATAR_SELF_ID) {
entityMap["parentJointIndex"] = _myAvatar->getJointIndex(entityMap["parentJointName"].toString());
qCDebug(entities) << "Found parentJointName " << entityMap["parentJointName"].toString() <<
" mapped it to parentJointIndex " << entityMap["parentJointIndex"].toInt();
}
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());
}
// Convert old clientOnly bool to new entityHostType enum
// (must happen before setOwningAvatarID below)
if (contentVersion < (int)EntityVersion::EntityHostTypes) {
if (entityMap.contains("clientOnly")) {
properties.setEntityHostType(entityMap["clientOnly"].toBool() ? entity::HostType::AVATAR : entity::HostType::DOMAIN);
}
}
if (properties.getEntityHostType() == entity::HostType::AVATAR) {
auto nodeList = DependencyManager::get<NodeList>();
const QUuid myNodeID = nodeList->getSessionUUID();
properties.setOwningAvatarID(myNodeID);
}
// Fix for older content not containing mode fields in the zones
if (contentVersion < (int)EntityVersion::ZoneLightInheritModes && (properties.getType() == EntityTypes::EntityType::Zone)) {
// The legacy version had no keylight mode - this is set to on
properties.setKeyLightMode(COMPONENT_MODE_ENABLED);
// The ambient URL has been moved from "keyLight" to "ambientLight"
if (entityMap.contains("keyLight")) {
QVariantMap keyLightObject = entityMap["keyLight"].toMap();
properties.getAmbientLight().setAmbientURL(keyLightObject["ambientURL"].toString());
}
// Copy the skybox URL if the ambient URL is empty, as this is the legacy behaviour
// Use skybox value only if it is not empty, else set ambientMode to inherit (to use default URL)
properties.setAmbientLightMode(COMPONENT_MODE_ENABLED);
if (properties.getAmbientLight().getAmbientURL() == "") {
if (properties.getSkybox().getURL() != "") {
properties.getAmbientLight().setAmbientURL(properties.getSkybox().getURL());
} else {
properties.setAmbientLightMode(COMPONENT_MODE_INHERIT);
}
}
// The background should be enabled if the mode is skybox
// Note that if the values are default then they are not stored in the JSON file
if (entityMap.contains("backgroundMode") && (entityMap["backgroundMode"].toString() == "skybox")) {
properties.setSkyboxMode(COMPONENT_MODE_ENABLED);
} else {
properties.setSkyboxMode(COMPONENT_MODE_INHERIT);
}
}
// Convert old materials so that they use materialData instead of userData
if (contentVersion < (int)EntityVersion::MaterialData && properties.getType() == EntityTypes::EntityType::Material) {
if (properties.getMaterialURL().startsWith("userData")) {
QString materialURL = properties.getMaterialURL();
properties.setMaterialURL(materialURL.replace("userData", "materialData"));
QJsonObject userData = QJsonDocument::fromJson(properties.getUserData().toUtf8()).object();
QJsonObject materialData;
QJsonValue materialVersion = userData["materialVersion"];
if (!materialVersion.isNull()) {
materialData.insert("materialVersion", materialVersion);
userData.remove("materialVersion");
}
QJsonValue materials = userData["materials"];
if (!materials.isNull()) {
materialData.insert("materials", materials);
userData.remove("materials");
}
properties.setMaterialData(QJsonDocument(materialData).toJson());
properties.setUserData(QJsonDocument(userData).toJson());
}
}
// Convert old cloneable entities so they use cloneableData instead of userData
if (contentVersion < (int)EntityVersion::CloneableData) {
QJsonObject userData = QJsonDocument::fromJson(properties.getUserData().toUtf8()).object();
QJsonObject grabbableKey = userData["grabbableKey"].toObject();
QJsonValue cloneable = grabbableKey["cloneable"];
if (cloneable.isBool() && cloneable.toBool()) {
QJsonValue cloneLifetime = grabbableKey["cloneLifetime"];
QJsonValue cloneLimit = grabbableKey["cloneLimit"];
QJsonValue cloneDynamic = grabbableKey["cloneDynamic"];
QJsonValue cloneAvatarEntity = grabbableKey["cloneAvatarEntity"];
// This is cloneable, we need to convert the properties
properties.setCloneable(true);
properties.setCloneLifetime(cloneLifetime.toInt());
properties.setCloneLimit(cloneLimit.toInt());
properties.setCloneDynamic(cloneDynamic.toBool());
properties.setCloneAvatarEntity(cloneAvatarEntity.toBool());
}
}
// convert old grab-related userData to new grab properties
if (contentVersion < (int)EntityVersion::GrabProperties) {
convertGrabUserDataToProperties(properties);
}
// Zero out the spread values that were fixed in version ParticleEntityFix so they behave the same as before
if (contentVersion < (int)EntityVersion::ParticleEntityFix) {
properties.setRadiusSpread(0.0f);
properties.setAlphaSpread(0.0f);
properties.setColorSpread({0, 0, 0});
}
if (contentVersion < (int)EntityVersion::FixPropertiesFromCleanup) {
if (entityMap.contains("created")) {
quint64 created = QDateTime::fromString(entityMap["created"].toString().trimmed(), Qt::ISODate).toMSecsSinceEpoch() * 1000;
properties.setCreated(created);
}
}
EntityItemPointer entity = addEntity(entityItemID, properties);
if (!entity) {
qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType();
success = false;
}
if (entity) {
const QUuid& cloneOriginID = entity->getCloneOriginID();
if (!cloneOriginID.isNull()) {
cloneIDs[cloneOriginID].push_back(entity->getEntityItemID());
}
}
}
for (const auto& entityID : cloneIDs.keys()) {
auto entity = findEntityByID(entityID);
if (entity) {
entity->setCloneIDs(cloneIDs.value(entityID));
}
}
return success;
}
bool EntityTree::writeToJSON(QString& jsonString, const OctreeElementPointer& element) {
QScriptEngine scriptEngine;
RecurseOctreeToJSONOperator theOperator(element, &scriptEngine, jsonString);
withReadLock([&] {
recurseTreeWithOperator(&theOperator);
});
jsonString = theOperator.getJson();
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;
}
}
}
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();
}
std::function<QObject*(const QUuid&)> EntityTree::_getEntityObjectOperator = nullptr;
std::function<QSizeF(const QUuid&, const QString&)> EntityTree::_textSizeOperator = nullptr;
std::function<bool()> EntityTree::_areEntityClicksCapturedOperator = nullptr;
std::function<void(const QUuid&, const QVariant&)> EntityTree::_emitScriptEventOperator = nullptr;
QObject* EntityTree::getEntityObject(const QUuid& id) {
if (_getEntityObjectOperator) {
return _getEntityObjectOperator(id);
}
return nullptr;
}
QSizeF EntityTree::textSize(const QUuid& id, const QString& text) {
if (_textSizeOperator) {
return _textSizeOperator(id, text);
}
return QSizeF(0.0f, 0.0f);
}
bool EntityTree::areEntityClicksCaptured() {
if (_areEntityClicksCapturedOperator) {
return _areEntityClicksCapturedOperator();
}
return false;
}
void EntityTree::emitScriptEvent(const QUuid& id, const QVariant& message) {
if (_emitScriptEventOperator) {
_emitScriptEventOperator(id, message);
}
}
void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender,
MovingEntitiesOperator& moveOperator, bool force, bool tellServer) {
// if the queryBox has changed, tell the entity-server
EntityItemPointer entity = std::dynamic_pointer_cast<EntityItem>(object);
if (entity) {
bool queryAACubeChanged = false;
if (!entity->hasChildren()) {
// updateQueryAACube will also update all ancestors' AACubes, so we only need to call this for leaf nodes
queryAACubeChanged = entity->updateQueryAACube();
} else {
AACube oldCube = entity->getQueryAACube();
object->forEachChild([&](SpatiallyNestablePointer descendant) {
updateEntityQueryAACubeWorker(descendant, packetSender, moveOperator, force, tellServer);
});
queryAACubeChanged = oldCube != entity->getQueryAACube();
}
if (queryAACubeChanged || force) {
bool success;
AACube newCube = entity->getQueryAACube(success);
if (success) {
moveOperator.addEntityToMoveList(entity, newCube);
}
// send an edit packet to update the entity-server about the queryAABox. We do this for domain-hosted
// entities as well as for avatar-entities; the packet-sender will route the update accordingly
if (tellServer && packetSender && (entity->isDomainEntity() || entity->isAvatarEntity())) {
quint64 now = usecTimestampNow();
EntityItemProperties properties = entity->getProperties();
properties.setQueryAACubeDirty();
properties.setLocationDirty();
properties.setLastEdited(now);
packetSender->queueEditEntityMessage(PacketType::EntityEdit, getThisPointer(), entity->getID(), properties);
entity->setLastEdited(now); // so we ignore the echo from the server
entity->setLastBroadcast(now); // for debug/physics status icons
}
entity->markDirtyFlags(Simulation::DIRTY_POSITION);
entityChanged(entity);
}
}
}
void EntityTree::updateEntityQueryAACube(SpatiallyNestablePointer object, EntityEditPacketSender* packetSender,
bool force, bool tellServer) {
// This is used when something other than a script or physics moves an entity. We need to put it in the
// correct place in our local octree, update its and its children's queryAACubes, and send an edit
// packet to the entity-server.
MovingEntitiesOperator moveOperator;
updateEntityQueryAACubeWorker(object, packetSender, moveOperator, force, tellServer);
if (moveOperator.hasMovingEntities()) {
PerformanceTimer perfTimer("recurseTreeWithOperator");
recurseTreeWithOperator(&moveOperator);
}
}