mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-29 15:42:57 +02:00
1118 lines
42 KiB
C++
1118 lines
42 KiB
C++
//
|
|
// EntityTree.cpp
|
|
// libraries/entities/src
|
|
//
|
|
// Created by Brad Hefta-Gaub on 12/4/13.
|
|
// Copyright 2013 High Fidelity, Inc.
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
//
|
|
|
|
#include <PerfStat.h>
|
|
#include <QDateTime>
|
|
#include <QtScript/QScriptEngine>
|
|
|
|
#include "EntityTree.h"
|
|
#include "EntitySimulation.h"
|
|
#include "VariantMapToScriptValue.h"
|
|
|
|
#include "AddEntityOperator.h"
|
|
#include "MovingEntitiesOperator.h"
|
|
#include "UpdateEntityOperator.h"
|
|
#include "QVariantGLM.h"
|
|
#include "EntitiesLogging.h"
|
|
#include "RecurseOctreeToMapOperator.h"
|
|
#include "LogHandler.h"
|
|
|
|
|
|
EntityTree::EntityTree(bool shouldReaverage) :
|
|
Octree(shouldReaverage),
|
|
_fbxService(NULL),
|
|
_simulation(NULL)
|
|
{
|
|
_rootElement = createNewElement();
|
|
resetClientEditStats();
|
|
}
|
|
|
|
EntityTree::~EntityTree() {
|
|
eraseAllOctreeElements(false);
|
|
}
|
|
|
|
EntityTreeElement* EntityTree::createNewElement(unsigned char * octalCode) {
|
|
EntityTreeElement* newElement = new EntityTreeElement(octalCode);
|
|
newElement->setTree(this);
|
|
return newElement;
|
|
}
|
|
|
|
void EntityTree::eraseAllOctreeElements(bool createNewRoot) {
|
|
emit clearingEntities();
|
|
|
|
// this would be a good place to clean up our entities...
|
|
if (_simulation) {
|
|
_simulation->lock();
|
|
_simulation->clearEntities();
|
|
_simulation->unlock();
|
|
}
|
|
foreach (EntityTreeElement* element, _entityToElementMap) {
|
|
element->cleanupEntities();
|
|
}
|
|
_entityToElementMap.clear();
|
|
Octree::eraseAllOctreeElements(createNewRoot);
|
|
|
|
resetClientEditStats();
|
|
}
|
|
|
|
bool EntityTree::handlesEditPacketType(PacketType::Value packetType) const {
|
|
// we handle these types of "edit" packets
|
|
switch (packetType) {
|
|
case PacketType::EntityAdd:
|
|
case PacketType::EntityEdit:
|
|
case PacketType::EntityErase:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Adds a new entity item to the tree
|
|
void EntityTree::postAddEntity(EntityItemPointer entity) {
|
|
assert(entity);
|
|
// check to see if we need to simulate this entity..
|
|
if (_simulation) {
|
|
_simulation->lock();
|
|
_simulation->addEntity(entity);
|
|
_simulation->unlock();
|
|
}
|
|
_isDirty = true;
|
|
maybeNotifyNewCollisionSoundURL("", entity->getCollisionSoundURL());
|
|
emit addingEntity(entity->getEntityItemID());
|
|
}
|
|
|
|
bool EntityTree::updateEntity(const EntityItemID& entityID, const EntityItemProperties& properties, const SharedNodePointer& senderNode) {
|
|
EntityTreeElement* containingElement = getContainingElement(entityID);
|
|
if (!containingElement) {
|
|
return false;
|
|
}
|
|
|
|
EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID);
|
|
if (!existingEntity) {
|
|
return false;
|
|
}
|
|
|
|
return updateEntityWithElement(existingEntity, properties, containingElement, senderNode);
|
|
}
|
|
|
|
bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperties& properties, const SharedNodePointer& senderNode) {
|
|
EntityTreeElement* containingElement = getContainingElement(entity->getEntityItemID());
|
|
if (!containingElement) {
|
|
return false;
|
|
}
|
|
return updateEntityWithElement(entity, properties, containingElement, senderNode);
|
|
}
|
|
|
|
bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityItemProperties& origProperties,
|
|
EntityTreeElement* containingElement, const SharedNodePointer& senderNode) {
|
|
EntityItemProperties properties = origProperties;
|
|
|
|
bool allowLockChange;
|
|
QUuid senderID;
|
|
if (senderNode.isNull()) {
|
|
auto nodeList = DependencyManager::get<NodeList>();
|
|
allowLockChange = nodeList->getThisNodeCanAdjustLocks();
|
|
senderID = nodeList->getSessionUUID();
|
|
} else {
|
|
allowLockChange = senderNode->getCanAdjustLocks();
|
|
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);
|
|
UpdateEntityOperator theOperator(this, containingElement, entity, tempProperties);
|
|
recurseTreeWithOperator(&theOperator);
|
|
_isDirty = true;
|
|
}
|
|
}
|
|
} else {
|
|
if (getIsServer()) {
|
|
bool simulationBlocked = !entity->getSimulatorID().isNull();
|
|
if (properties.simulationOwnerChanged()) {
|
|
QUuid submittedID = properties.getSimulationOwner().getID();
|
|
// a legit interface will only submit their own ID or NULL:
|
|
if (submittedID.isNull()) {
|
|
if (entity->getSimulatorID() == senderID) {
|
|
// We only allow the simulation owner to clear their own simulationID's.
|
|
simulationBlocked = false;
|
|
properties.clearSimulationOwner(); // clear everything
|
|
}
|
|
// else: We assume the sender really did believe it was the simulation owner when it sent
|
|
} else if (submittedID == senderID) {
|
|
// the sender is trying to take or continue ownership
|
|
if (entity->getSimulatorID().isNull()) {
|
|
// the sender it taking ownership
|
|
properties.promoteSimulationPriority(RECRUIT_SIMULATION_PRIORITY);
|
|
simulationBlocked = false;
|
|
} else if (entity->getSimulatorID() == senderID) {
|
|
// the sender is asserting ownership
|
|
simulationBlocked = false;
|
|
} else {
|
|
// the sender is trying to steal ownership from another simulator
|
|
// so we apply the rules for ownership change:
|
|
// (1) higher priority wins
|
|
// (2) equal priority wins if ownership filter has expired except...
|
|
uint8_t oldPriority = entity->getSimulationPriority();
|
|
uint8_t newPriority = properties.getSimulationOwner().getPriority();
|
|
if (newPriority > oldPriority ||
|
|
(newPriority == oldPriority && properties.getSimulationOwner().hasExpired())) {
|
|
simulationBlocked = false;
|
|
}
|
|
}
|
|
} else {
|
|
// the entire update is suspect --> ignore it
|
|
return false;
|
|
}
|
|
} else {
|
|
simulationBlocked = senderID != entity->getSimulatorID();
|
|
}
|
|
if (simulationBlocked) {
|
|
// squash ownership and physics-related changes.
|
|
properties.setSimulationOwnerChanged(false);
|
|
properties.setPositionChanged(false);
|
|
properties.setRotationChanged(false);
|
|
properties.setVelocityChanged(false);
|
|
properties.setAngularVelocityChanged(false);
|
|
properties.setAccelerationChanged(false);
|
|
}
|
|
}
|
|
// else client accepts what the server says
|
|
|
|
QString entityScriptBefore = entity->getScript();
|
|
quint64 entityScriptTimestampBefore = entity->getScriptTimestamp();
|
|
QString collisionSoundURLBefore = entity->getCollisionSoundURL();
|
|
uint32_t preFlags = entity->getDirtyFlags();
|
|
UpdateEntityOperator theOperator(this, containingElement, entity, properties);
|
|
recurseTreeWithOperator(&theOperator);
|
|
_isDirty = true;
|
|
|
|
uint32_t newFlags = entity->getDirtyFlags() & ~preFlags;
|
|
if (newFlags) {
|
|
if (_simulation) {
|
|
if (newFlags & DIRTY_SIMULATION_FLAGS) {
|
|
_simulation->lock();
|
|
_simulation->changeEntity(entity);
|
|
_simulation->unlock();
|
|
}
|
|
} else {
|
|
// normally the _simulation clears ALL updateFlags, but since there is none we do it explicitly
|
|
entity->clearDirtyFlags();
|
|
}
|
|
}
|
|
|
|
QString entityScriptAfter = entity->getScript();
|
|
quint64 entityScriptTimestampAfter = entity->getScriptTimestamp();
|
|
bool reload = entityScriptTimestampBefore != entityScriptTimestampAfter;
|
|
if (entityScriptBefore != entityScriptAfter || reload) {
|
|
emitEntityScriptChanging(entity->getEntityItemID(), reload); // the entity script has changed
|
|
}
|
|
maybeNotifyNewCollisionSoundURL(collisionSoundURLBefore, entity->getCollisionSoundURL());
|
|
}
|
|
|
|
// TODO: this final containingElement check should eventually be removed (or wrapped in an #ifdef DEBUG).
|
|
containingElement = getContainingElement(entity->getEntityItemID());
|
|
if (!containingElement) {
|
|
qCDebug(entities) << "UNEXPECTED!!!! after updateEntity() we no longer have a containing element??? entityID="
|
|
<< entity->getEntityItemID();
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) {
|
|
EntityItemPointer result = NULL;
|
|
|
|
if (getIsClient()) {
|
|
// if our Node isn't allowed to create entities in this domain, don't try.
|
|
auto nodeList = DependencyManager::get<NodeList>();
|
|
if (nodeList && !nodeList->getThisNodeCanRez()) {
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
bool recordCreationTime = false;
|
|
if (properties.getCreated() == UNKNOWN_CREATED_TIME) {
|
|
// the entity's creation time was not specified in properties, which means this is a NEW entity
|
|
// and we must record its creation time
|
|
recordCreationTime = true;
|
|
}
|
|
|
|
// You should not call this on existing entities that are already part of the tree! Call updateEntity()
|
|
EntityTreeElement* containingElement = getContainingElement(entityID);
|
|
if (containingElement) {
|
|
qCDebug(entities) << "UNEXPECTED!!! ----- don't call addEntity() on existing entity items. entityID=" << entityID
|
|
<< "containingElement=" << containingElement;
|
|
return result;
|
|
}
|
|
|
|
// construct the instance of the entity
|
|
EntityTypes::EntityType type = properties.getType();
|
|
result = EntityTypes::constructEntityItem(type, entityID, properties);
|
|
|
|
if (result) {
|
|
if (recordCreationTime) {
|
|
result->recordCreationTime();
|
|
}
|
|
// Recurse the tree and store the entity in the correct tree element
|
|
AddEntityOperator theOperator(this, result);
|
|
recurseTreeWithOperator(&theOperator);
|
|
|
|
postAddEntity(result);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void EntityTree::emitEntityScriptChanging(const EntityItemID& entityItemID, const bool reload) {
|
|
emit entityScriptChanging(entityItemID, reload);
|
|
}
|
|
|
|
void EntityTree::maybeNotifyNewCollisionSoundURL(const QString& previousCollisionSoundURL, const QString& nextCollisionSoundURL) {
|
|
if (!nextCollisionSoundURL.isEmpty() && (nextCollisionSoundURL != previousCollisionSoundURL)) {
|
|
emit newCollisionSoundURL(QUrl(nextCollisionSoundURL));
|
|
}
|
|
}
|
|
|
|
void EntityTree::setSimulation(EntitySimulation* simulation) {
|
|
if (simulation) {
|
|
// assert that the simulation's backpointer has already been properly connected
|
|
assert(simulation->getEntityTree() == this);
|
|
}
|
|
if (_simulation && _simulation != simulation) {
|
|
// It's important to clearEntities() on the simulation since taht will update each
|
|
// EntityItem::_simulationState correctly so as to not confuse the next _simulation.
|
|
_simulation->lock();
|
|
_simulation->clearEntities();
|
|
_simulation->unlock();
|
|
}
|
|
_simulation = simulation;
|
|
}
|
|
|
|
void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ignoreWarnings) {
|
|
EntityTreeElement* containingElement = getContainingElement(entityID);
|
|
if (!containingElement) {
|
|
if (!ignoreWarnings) {
|
|
qCDebug(entities) << "UNEXPECTED!!!! EntityTree::deleteEntity() entityID doesn't exist!!! entityID=" << entityID;
|
|
}
|
|
return;
|
|
}
|
|
|
|
EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID);
|
|
if (!existingEntity) {
|
|
if (!ignoreWarnings) {
|
|
qCDebug(entities) << "UNEXPECTED!!!! don't call EntityTree::deleteEntity() on entity items that don't exist. "
|
|
"entityID=" << entityID;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (existingEntity->getLocked() && !force) {
|
|
if (!ignoreWarnings) {
|
|
qCDebug(entities) << "ERROR! EntityTree::deleteEntity() trying to delete locked entity. entityID=" << entityID;
|
|
}
|
|
return;
|
|
}
|
|
|
|
emit deletingEntity(entityID);
|
|
|
|
// NOTE: callers must lock the tree before using this method
|
|
DeleteEntityOperator theOperator(this, entityID);
|
|
recurseTreeWithOperator(&theOperator);
|
|
processRemovedEntities(theOperator);
|
|
_isDirty = true;
|
|
}
|
|
|
|
void EntityTree::deleteEntities(QSet<EntityItemID> entityIDs, bool force, bool ignoreWarnings) {
|
|
// NOTE: callers must lock the tree before using this method
|
|
DeleteEntityOperator theOperator(this);
|
|
foreach(const EntityItemID& entityID, entityIDs) {
|
|
EntityTreeElement* containingElement = getContainingElement(entityID);
|
|
if (!containingElement) {
|
|
if (!ignoreWarnings) {
|
|
qCDebug(entities) << "UNEXPECTED!!!! EntityTree::deleteEntities() entityID doesn't exist!!! entityID=" << entityID;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
EntityItemPointer existingEntity = containingElement->getEntityWithEntityItemID(entityID);
|
|
if (!existingEntity) {
|
|
if (!ignoreWarnings) {
|
|
qCDebug(entities) << "UNEXPECTED!!!! don't call EntityTree::deleteEntities() on entity items that don't exist. "
|
|
"entityID=" << entityID;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (existingEntity->getLocked() && !force) {
|
|
if (!ignoreWarnings) {
|
|
qCDebug(entities) << "ERROR! EntityTree::deleteEntities() trying to delete locked entity. entityID=" << entityID;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// tell our delete operator about this entityID
|
|
theOperator.addEntityIDToDeleteList(entityID);
|
|
emit deletingEntity(entityID);
|
|
}
|
|
|
|
if (theOperator.getEntities().size() > 0) {
|
|
recurseTreeWithOperator(&theOperator);
|
|
processRemovedEntities(theOperator);
|
|
_isDirty = true;
|
|
}
|
|
}
|
|
|
|
void EntityTree::processRemovedEntities(const DeleteEntityOperator& theOperator) {
|
|
const RemovedEntities& entities = theOperator.getEntities();
|
|
if (_simulation) {
|
|
_simulation->lock();
|
|
}
|
|
foreach(const EntityToDeleteDetails& details, entities) {
|
|
EntityItemPointer theEntity = details.entity;
|
|
|
|
if (getIsServer()) {
|
|
// set up the deleted entities ID
|
|
quint64 deletedAt = usecTimestampNow();
|
|
_recentlyDeletedEntitiesLock.lockForWrite();
|
|
_recentlyDeletedEntityItemIDs.insert(deletedAt, theEntity->getEntityItemID());
|
|
_recentlyDeletedEntitiesLock.unlock();
|
|
}
|
|
|
|
if (_simulation) {
|
|
theEntity->clearActions(_simulation);
|
|
_simulation->removeEntity(theEntity);
|
|
}
|
|
}
|
|
if (_simulation) {
|
|
_simulation->unlock();
|
|
}
|
|
}
|
|
|
|
|
|
class FindNearPointArgs {
|
|
public:
|
|
glm::vec3 position;
|
|
float targetRadius;
|
|
bool found;
|
|
EntityItemPointer closestEntity;
|
|
float closestEntityDistance;
|
|
};
|
|
|
|
|
|
bool EntityTree::findNearPointOperation(OctreeElement* element, void* extraData) {
|
|
FindNearPointArgs* args = static_cast<FindNearPointArgs*>(extraData);
|
|
EntityTreeElement* entityTreeElement = static_cast<EntityTreeElement*>(element);
|
|
|
|
glm::vec3 penetration;
|
|
bool sphereIntersection = entityTreeElement->getAACube().findSpherePenetration(args->position, args->targetRadius, penetration);
|
|
|
|
// If this entityTreeElement contains the point, then search it...
|
|
if (sphereIntersection) {
|
|
EntityItemPointer thisClosestEntity = entityTreeElement->getClosestEntity(args->position);
|
|
|
|
// we may have gotten NULL back, meaning no entity was available
|
|
if (thisClosestEntity) {
|
|
glm::vec3 entityPosition = thisClosestEntity->getPosition();
|
|
float distanceFromPointToEntity = glm::distance(entityPosition, args->position);
|
|
|
|
// If we're within our target radius
|
|
if (distanceFromPointToEntity <= args->targetRadius) {
|
|
// we are closer than anything else we've found
|
|
if (distanceFromPointToEntity < args->closestEntityDistance) {
|
|
args->closestEntity = thisClosestEntity;
|
|
args->closestEntityDistance = distanceFromPointToEntity;
|
|
args->found = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// we should be able to optimize this...
|
|
return true; // keep searching in case children have closer entities
|
|
}
|
|
|
|
// if this element doesn't contain the point, then none of its children can contain the point, so stop searching
|
|
return false;
|
|
}
|
|
|
|
EntityItemPointer EntityTree::findClosestEntity(glm::vec3 position, float targetRadius) {
|
|
FindNearPointArgs args = { position, targetRadius, false, NULL, FLT_MAX };
|
|
lockForRead();
|
|
// NOTE: This should use recursion, since this is a spatial operation
|
|
recurseTreeWithOperation(findNearPointOperation, &args);
|
|
unlock();
|
|
return args.closestEntity;
|
|
}
|
|
|
|
class FindAllNearPointArgs {
|
|
public:
|
|
glm::vec3 position;
|
|
float targetRadius;
|
|
QVector<EntityItemPointer> entities;
|
|
};
|
|
|
|
|
|
bool EntityTree::findInSphereOperation(OctreeElement* element, void* extraData) {
|
|
FindAllNearPointArgs* args = static_cast<FindAllNearPointArgs*>(extraData);
|
|
glm::vec3 penetration;
|
|
bool sphereIntersection = element->getAACube().findSpherePenetration(args->position, args->targetRadius, penetration);
|
|
|
|
// If this element contains the point, then search it...
|
|
if (sphereIntersection) {
|
|
EntityTreeElement* entityTreeElement = static_cast<EntityTreeElement*>(element);
|
|
entityTreeElement->getEntities(args->position, args->targetRadius, args->entities);
|
|
return true; // keep searching in case children have closer entities
|
|
}
|
|
|
|
// if this element doesn't contain the point, then none of it's children can contain the point, so stop searching
|
|
return false;
|
|
}
|
|
|
|
// NOTE: assumes caller has handled locking
|
|
void EntityTree::findEntities(const glm::vec3& center, float radius, QVector<EntityItemPointer>& foundEntities) {
|
|
FindAllNearPointArgs args = { center, radius, QVector<EntityItemPointer>() };
|
|
// NOTE: This should use recursion, since this is a spatial operation
|
|
recurseTreeWithOperation(findInSphereOperation, &args);
|
|
|
|
// swap the two lists of entity pointers instead of copy
|
|
foundEntities.swap(args.entities);
|
|
}
|
|
|
|
class FindEntitiesInCubeArgs {
|
|
public:
|
|
FindEntitiesInCubeArgs(const AACube& cube)
|
|
: _cube(cube), _foundEntities() {
|
|
}
|
|
|
|
AACube _cube;
|
|
QVector<EntityItemPointer> _foundEntities;
|
|
};
|
|
|
|
bool EntityTree::findInCubeOperation(OctreeElement* element, void* extraData) {
|
|
FindEntitiesInCubeArgs* args = static_cast<FindEntitiesInCubeArgs*>(extraData);
|
|
if (element->getAACube().touches(args->_cube)) {
|
|
EntityTreeElement* entityTreeElement = static_cast<EntityTreeElement*>(element);
|
|
entityTreeElement->getEntities(args->_cube, args->_foundEntities);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// NOTE: assumes caller has handled locking
|
|
void EntityTree::findEntities(const AACube& cube, QVector<EntityItemPointer>& foundEntities) {
|
|
FindEntitiesInCubeArgs args(cube);
|
|
// NOTE: This should use recursion, since this is a spatial operation
|
|
recurseTreeWithOperation(findInCubeOperation, &args);
|
|
// swap the two lists of entity pointers instead of copy
|
|
foundEntities.swap(args._foundEntities);
|
|
}
|
|
|
|
class FindEntitiesInBoxArgs {
|
|
public:
|
|
FindEntitiesInBoxArgs(const AABox& box)
|
|
: _box(box), _foundEntities() {
|
|
}
|
|
|
|
AABox _box;
|
|
QVector<EntityItemPointer> _foundEntities;
|
|
};
|
|
|
|
bool EntityTree::findInBoxOperation(OctreeElement* element, void* extraData) {
|
|
FindEntitiesInBoxArgs* args = static_cast<FindEntitiesInBoxArgs*>(extraData);
|
|
if (element->getAACube().touches(args->_box)) {
|
|
EntityTreeElement* entityTreeElement = static_cast<EntityTreeElement*>(element);
|
|
entityTreeElement->getEntities(args->_box, args->_foundEntities);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// NOTE: assumes caller has handled locking
|
|
void EntityTree::findEntities(const AABox& box, QVector<EntityItemPointer>& foundEntities) {
|
|
FindEntitiesInBoxArgs args(box);
|
|
// NOTE: This should use recursion, since this is a spatial operation
|
|
recurseTreeWithOperation(findInBoxOperation, &args);
|
|
// swap the two lists of entity pointers instead of copy
|
|
foundEntities.swap(args._foundEntities);
|
|
}
|
|
|
|
EntityItemPointer EntityTree::findEntityByID(const QUuid& id) {
|
|
EntityItemID entityID(id);
|
|
return findEntityByEntityItemID(entityID);
|
|
}
|
|
|
|
EntityItemPointer EntityTree::findEntityByEntityItemID(const EntityItemID& entityID) /*const*/ {
|
|
EntityItemPointer foundEntity = NULL;
|
|
EntityTreeElement* containingElement = getContainingElement(entityID);
|
|
if (containingElement) {
|
|
foundEntity = containingElement->getEntityWithEntityItemID(entityID);
|
|
}
|
|
return foundEntity;
|
|
}
|
|
|
|
int EntityTree::processEditPacketData(NLPacket& packet, const unsigned char* editData, int maxLength,
|
|
const SharedNodePointer& senderNode) {
|
|
|
|
if (!getIsServer()) {
|
|
qCDebug(entities) << "UNEXPECTED!!! processEditPacketData() should only be called on a server tree.";
|
|
return 0;
|
|
}
|
|
|
|
int processedBytes = 0;
|
|
// we handle these types of "edit" packets
|
|
switch (packet.getType()) {
|
|
case PacketType::EntityErase: {
|
|
QByteArray dataByteArray = QByteArray::fromRawData(reinterpret_cast<const char*>(editData), maxLength);
|
|
processedBytes = processEraseMessageDetails(dataByteArray, senderNode);
|
|
break;
|
|
}
|
|
|
|
case PacketType::EntityAdd:
|
|
case PacketType::EntityEdit: {
|
|
quint64 startDecode = 0, endDecode = 0;
|
|
quint64 startLookup = 0, endLookup = 0;
|
|
quint64 startUpdate = 0, endUpdate = 0;
|
|
quint64 startCreate = 0, endCreate = 0;
|
|
quint64 startLogging = 0, endLogging = 0;
|
|
|
|
_totalEditMessages++;
|
|
|
|
EntityItemID entityItemID;
|
|
EntityItemProperties properties;
|
|
startDecode = usecTimestampNow();
|
|
|
|
bool validEditPacket = EntityItemProperties::decodeEntityEditPacket(editData, maxLength, processedBytes,
|
|
entityItemID, properties);
|
|
endDecode = usecTimestampNow();
|
|
|
|
// If we got a valid edit packet, then it could be a new entity or it could be an update to
|
|
// an existing entity... handle appropriately
|
|
if (validEditPacket) {
|
|
// search for the entity by EntityItemID
|
|
startLookup = usecTimestampNow();
|
|
EntityItemPointer existingEntity = findEntityByEntityItemID(entityItemID);
|
|
endLookup = usecTimestampNow();
|
|
if (existingEntity && packet.getType() == PacketType::EntityEdit) {
|
|
// if the EntityItem exists, then update it
|
|
startLogging = usecTimestampNow();
|
|
if (wantEditLogging()) {
|
|
qCDebug(entities) << "User [" << senderNode->getUUID() << "] editing entity. ID:" << entityItemID;
|
|
qCDebug(entities) << " properties:" << properties;
|
|
}
|
|
endLogging = usecTimestampNow();
|
|
|
|
startUpdate = usecTimestampNow();
|
|
updateEntity(entityItemID, properties, senderNode);
|
|
existingEntity->markAsChangedOnServer();
|
|
endUpdate = usecTimestampNow();
|
|
_totalUpdates++;
|
|
} else if (packet.getType() == PacketType::EntityAdd) {
|
|
if (senderNode->getCanRez()) {
|
|
// this is a new entity... assign a new entityID
|
|
properties.setCreated(properties.getLastEdited());
|
|
startCreate = usecTimestampNow();
|
|
EntityItemPointer newEntity = addEntity(entityItemID, properties);
|
|
endCreate = usecTimestampNow();
|
|
_totalCreates++;
|
|
if (newEntity) {
|
|
newEntity->markAsChangedOnServer();
|
|
notifyNewlyCreatedEntity(*newEntity, senderNode);
|
|
|
|
startLogging = usecTimestampNow();
|
|
if (wantEditLogging()) {
|
|
qCDebug(entities) << "User [" << senderNode->getUUID() << "] added entity. ID:"
|
|
<< newEntity->getEntityItemID();
|
|
qCDebug(entities) << " properties:" << properties;
|
|
}
|
|
endLogging = usecTimestampNow();
|
|
|
|
}
|
|
} else {
|
|
qCDebug(entities) << "User without 'rez rights' [" << senderNode->getUUID()
|
|
<< "] attempted to add an entity.";
|
|
}
|
|
} else {
|
|
static QString repeatedMessage =
|
|
LogHandler::getInstance().addRepeatedMessageRegex("^Add or Edit failed.*");
|
|
qCDebug(entities) << "Add or Edit failed." << packet.getType() << existingEntity.get();
|
|
}
|
|
}
|
|
|
|
|
|
_totalDecodeTime += endDecode - startDecode;
|
|
_totalLookupTime += endLookup - startLookup;
|
|
_totalUpdateTime += endUpdate - startUpdate;
|
|
_totalCreateTime += endCreate - startCreate;
|
|
_totalLoggingTime += endLogging - startLogging;
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
processedBytes = 0;
|
|
break;
|
|
}
|
|
return processedBytes;
|
|
}
|
|
|
|
|
|
void EntityTree::notifyNewlyCreatedEntity(const EntityItem& newEntity, const SharedNodePointer& senderNode) {
|
|
_newlyCreatedHooksLock.lockForRead();
|
|
for (int i = 0; i < _newlyCreatedHooks.size(); i++) {
|
|
_newlyCreatedHooks[i]->entityCreated(newEntity, senderNode);
|
|
}
|
|
_newlyCreatedHooksLock.unlock();
|
|
}
|
|
|
|
void EntityTree::addNewlyCreatedHook(NewlyCreatedEntityHook* hook) {
|
|
_newlyCreatedHooksLock.lockForWrite();
|
|
_newlyCreatedHooks.push_back(hook);
|
|
_newlyCreatedHooksLock.unlock();
|
|
}
|
|
|
|
void EntityTree::removeNewlyCreatedHook(NewlyCreatedEntityHook* hook) {
|
|
_newlyCreatedHooksLock.lockForWrite();
|
|
for (int i = 0; i < _newlyCreatedHooks.size(); i++) {
|
|
if (_newlyCreatedHooks[i] == hook) {
|
|
_newlyCreatedHooks.erase(_newlyCreatedHooks.begin() + i);
|
|
break;
|
|
}
|
|
}
|
|
_newlyCreatedHooksLock.unlock();
|
|
}
|
|
|
|
|
|
void EntityTree::releaseSceneEncodeData(OctreeElementExtraEncodeData* extraEncodeData) const {
|
|
foreach(void* extraData, *extraEncodeData) {
|
|
EntityTreeElementExtraEncodeData* thisExtraEncodeData = static_cast<EntityTreeElementExtraEncodeData*>(extraData);
|
|
delete thisExtraEncodeData;
|
|
}
|
|
extraEncodeData->clear();
|
|
}
|
|
|
|
void EntityTree::entityChanged(EntityItemPointer entity) {
|
|
if (_simulation) {
|
|
_simulation->lock();
|
|
_simulation->changeEntity(entity);
|
|
_simulation->unlock();
|
|
}
|
|
}
|
|
|
|
void EntityTree::update() {
|
|
if (_simulation) {
|
|
lockForWrite();
|
|
_simulation->lock();
|
|
_simulation->updateEntities();
|
|
VectorOfEntities pendingDeletes;
|
|
_simulation->getEntitiesToDelete(pendingDeletes);
|
|
_simulation->unlock();
|
|
|
|
if (pendingDeletes.size() > 0) {
|
|
// translate into list of ID's
|
|
QSet<EntityItemID> idsToDelete;
|
|
|
|
for (auto entity : pendingDeletes) {
|
|
idsToDelete.insert(entity->getEntityItemID());
|
|
}
|
|
|
|
// delete these things the roundabout way
|
|
deleteEntities(idsToDelete, true);
|
|
}
|
|
unlock();
|
|
}
|
|
}
|
|
|
|
bool EntityTree::hasEntitiesDeletedSince(quint64 sinceTime) {
|
|
// we can probably leverage the ordered nature of QMultiMap to do this quickly...
|
|
bool hasSomethingNewer = false;
|
|
|
|
_recentlyDeletedEntitiesLock.lockForRead();
|
|
QMultiMap<quint64, QUuid>::const_iterator iterator = _recentlyDeletedEntityItemIDs.constBegin();
|
|
while (iterator != _recentlyDeletedEntityItemIDs.constEnd()) {
|
|
if (iterator.key() > sinceTime) {
|
|
hasSomethingNewer = true;
|
|
}
|
|
++iterator;
|
|
}
|
|
_recentlyDeletedEntitiesLock.unlock();
|
|
return hasSomethingNewer;
|
|
}
|
|
|
|
// sinceTime is an in/out parameter - it will be side effected with the last time sent out
|
|
std::unique_ptr<NLPacket> EntityTree::encodeEntitiesDeletedSince(OCTREE_PACKET_SEQUENCE sequenceNumber, quint64& sinceTime,
|
|
bool& hasMore) {
|
|
|
|
auto deletesPacket = NLPacket::create(PacketType::EntityErase);
|
|
|
|
// pack in flags
|
|
OCTREE_PACKET_FLAGS flags = 0;
|
|
deletesPacket->writePrimitive(flags);
|
|
|
|
// pack in sequence number
|
|
deletesPacket->writePrimitive(sequenceNumber);
|
|
|
|
// pack in timestamp
|
|
OCTREE_PACKET_SENT_TIME now = usecTimestampNow();
|
|
deletesPacket->writePrimitive(now);
|
|
|
|
// figure out where we are now and pack a temporary number of IDs
|
|
uint16_t numberOfIDs = 0;
|
|
qint64 numberOfIDsPos = deletesPacket->pos();
|
|
deletesPacket->writePrimitive(numberOfIDs);
|
|
|
|
// we keep a multi map of entity IDs to timestamps, we only want to include the entity IDs that have been
|
|
// deleted since we last sent to this node
|
|
_recentlyDeletedEntitiesLock.lockForRead();
|
|
|
|
bool hasFilledPacket = false;
|
|
|
|
auto it = _recentlyDeletedEntityItemIDs.constBegin();
|
|
while (it != _recentlyDeletedEntityItemIDs.constEnd()) {
|
|
QList<QUuid> values = _recentlyDeletedEntityItemIDs.values(it.key());
|
|
for (int valueItem = 0; valueItem < values.size(); ++valueItem) {
|
|
|
|
// if the timestamp is more recent then out last sent time, include it
|
|
if (it.key() > sinceTime) {
|
|
QUuid entityID = values.at(valueItem);
|
|
deletesPacket->write(entityID.toRfc4122());
|
|
|
|
++numberOfIDs;
|
|
|
|
// check to make sure we have room for one more ID
|
|
if (NUM_BYTES_RFC4122_UUID > deletesPacket->bytesAvailableForWrite()) {
|
|
hasFilledPacket = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// check to see if we're about to return
|
|
if (hasFilledPacket) {
|
|
// let our caller know how far we got
|
|
sinceTime = it.key();
|
|
|
|
break;
|
|
}
|
|
|
|
++it;
|
|
}
|
|
|
|
// if we got to the end, then we're done sending
|
|
if (it == _recentlyDeletedEntityItemIDs.constEnd()) {
|
|
hasMore = false;
|
|
}
|
|
|
|
_recentlyDeletedEntitiesLock.unlock();
|
|
|
|
// replace the count for the number of included IDs
|
|
deletesPacket->seek(numberOfIDsPos);
|
|
deletesPacket->writePrimitive(numberOfIDs);
|
|
|
|
return deletesPacket;
|
|
}
|
|
|
|
|
|
// called by the server when it knows all nodes have been sent deleted packets
|
|
void EntityTree::forgetEntitiesDeletedBefore(quint64 sinceTime) {
|
|
QSet<quint64> keysToRemove;
|
|
|
|
_recentlyDeletedEntitiesLock.lockForWrite();
|
|
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() <= sinceTime) {
|
|
keysToRemove << iterator.key();
|
|
}
|
|
++iterator;
|
|
}
|
|
|
|
// Now run through the keysToRemove and remove them
|
|
foreach (quint64 value, keysToRemove) {
|
|
_recentlyDeletedEntityItemIDs.remove(value);
|
|
}
|
|
|
|
_recentlyDeletedEntitiesLock.unlock();
|
|
}
|
|
|
|
|
|
// TODO: consider consolidating processEraseMessageDetails() and processEraseMessage()
|
|
int EntityTree::processEraseMessage(NLPacket& packet, const SharedNodePointer& sourceNode) {
|
|
lockForWrite();
|
|
|
|
packet.seek(sizeof(OCTREE_PACKET_FLAGS) + sizeof(OCTREE_PACKET_SEQUENCE) + sizeof(OCTREE_PACKET_SENT_TIME));
|
|
|
|
uint16_t numberOfIDs = 0; // placeholder for now
|
|
packet.readPrimitive(&numberOfIDs);
|
|
|
|
if (numberOfIDs > 0) {
|
|
QSet<EntityItemID> entityItemIDsToDelete;
|
|
|
|
for (size_t i = 0; i < numberOfIDs; i++) {
|
|
|
|
if (NUM_BYTES_RFC4122_UUID > packet.bytesLeftToRead()) {
|
|
qCDebug(entities) << "EntityTree::processEraseMessage().... bailing because not enough bytes in buffer";
|
|
break; // bail to prevent buffer overflow
|
|
}
|
|
|
|
QUuid entityID = QUuid::fromRfc4122(packet.read(NUM_BYTES_RFC4122_UUID));
|
|
|
|
EntityItemID entityItemID(entityID);
|
|
entityItemIDsToDelete << entityItemID;
|
|
|
|
if (wantEditLogging()) {
|
|
qCDebug(entities) << "User [" << sourceNode->getUUID() << "] deleting entity. ID:" << entityItemID;
|
|
}
|
|
|
|
}
|
|
deleteEntities(entityItemIDsToDelete, true, true);
|
|
}
|
|
unlock();
|
|
return packet.pos();
|
|
}
|
|
|
|
// 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) {
|
|
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(processedBytes, NUM_BYTES_RFC4122_UUID);
|
|
QUuid entityID = QUuid::fromRfc4122(encodedID);
|
|
dataAt += encodedID.size();
|
|
processedBytes += encodedID.size();
|
|
|
|
EntityItemID entityItemID(entityID);
|
|
entityItemIDsToDelete << entityItemID;
|
|
|
|
if (wantEditLogging()) {
|
|
qCDebug(entities) << "User [" << sourceNode->getUUID() << "] deleting entity. ID:" << entityItemID;
|
|
}
|
|
|
|
}
|
|
deleteEntities(entityItemIDsToDelete, true, true);
|
|
}
|
|
return processedBytes;
|
|
}
|
|
|
|
EntityTreeElement* EntityTree::getContainingElement(const EntityItemID& entityItemID) /*const*/ {
|
|
// TODO: do we need to make this thread safe? Or is it acceptable as is
|
|
EntityTreeElement* element = _entityToElementMap.value(entityItemID);
|
|
return element;
|
|
}
|
|
|
|
void EntityTree::setContainingElement(const EntityItemID& entityItemID, EntityTreeElement* element) {
|
|
// TODO: do we need to make this thread safe? Or is it acceptable as is
|
|
if (element) {
|
|
_entityToElementMap[entityItemID] = element;
|
|
} else {
|
|
_entityToElementMap.remove(entityItemID);
|
|
}
|
|
}
|
|
|
|
void EntityTree::debugDumpMap() {
|
|
qCDebug(entities) << "EntityTree::debugDumpMap() --------------------------";
|
|
QHashIterator<EntityItemID, EntityTreeElement*> i(_entityToElementMap);
|
|
while (i.hasNext()) {
|
|
i.next();
|
|
qCDebug(entities) << i.key() << ": " << i.value();
|
|
}
|
|
qCDebug(entities) << "-----------------------------------------------------";
|
|
}
|
|
|
|
class ContentsDimensionOperator : public RecurseOctreeOperator {
|
|
public:
|
|
virtual bool preRecursion(OctreeElement* element);
|
|
virtual bool postRecursion(OctreeElement* element) { return true; }
|
|
float getLargestDimension() const { return _contentExtents.largestDimension(); }
|
|
private:
|
|
Extents _contentExtents;
|
|
};
|
|
|
|
bool ContentsDimensionOperator::preRecursion(OctreeElement* element) {
|
|
EntityTreeElement* entityTreeElement = static_cast<EntityTreeElement*>(element);
|
|
entityTreeElement->expandExtentsToContents(_contentExtents);
|
|
return true;
|
|
}
|
|
|
|
float EntityTree::getContentsLargestDimension() {
|
|
ContentsDimensionOperator theOperator;
|
|
recurseTreeWithOperator(&theOperator);
|
|
return theOperator.getLargestDimension();
|
|
}
|
|
|
|
class DebugOperator : public RecurseOctreeOperator {
|
|
public:
|
|
virtual bool preRecursion(OctreeElement* element);
|
|
virtual bool postRecursion(OctreeElement* element) { return true; }
|
|
};
|
|
|
|
bool DebugOperator::preRecursion(OctreeElement* element) {
|
|
EntityTreeElement* entityTreeElement = static_cast<EntityTreeElement*>(element);
|
|
qCDebug(entities) << "EntityTreeElement [" << entityTreeElement << "]";
|
|
entityTreeElement->debugDump();
|
|
return true;
|
|
}
|
|
|
|
void EntityTree::dumpTree() {
|
|
DebugOperator theOperator;
|
|
recurseTreeWithOperator(&theOperator);
|
|
}
|
|
|
|
class PruneOperator : public RecurseOctreeOperator {
|
|
public:
|
|
virtual bool preRecursion(OctreeElement* element) { return true; }
|
|
virtual bool postRecursion(OctreeElement* element);
|
|
};
|
|
|
|
bool PruneOperator::postRecursion(OctreeElement* element) {
|
|
EntityTreeElement* entityTreeElement = static_cast<EntityTreeElement*>(element);
|
|
entityTreeElement->pruneChildren();
|
|
return true;
|
|
}
|
|
|
|
void EntityTree::pruneTree() {
|
|
PruneOperator theOperator;
|
|
recurseTreeWithOperator(&theOperator);
|
|
}
|
|
|
|
QVector<EntityItemID> EntityTree::sendEntities(EntityEditPacketSender* packetSender, EntityTree* localTree, float x, float y, float z) {
|
|
SendEntitiesOperationArgs args;
|
|
args.packetSender = packetSender;
|
|
args.localTree = localTree;
|
|
args.root = glm::vec3(x, y, z);
|
|
QVector<EntityItemID> newEntityIDs;
|
|
args.newEntityIDs = &newEntityIDs;
|
|
recurseTreeWithOperation(sendEntitiesOperation, &args);
|
|
packetSender->releaseQueuedMessages();
|
|
|
|
return newEntityIDs;
|
|
}
|
|
|
|
bool EntityTree::sendEntitiesOperation(OctreeElement* element, void* extraData) {
|
|
SendEntitiesOperationArgs* args = static_cast<SendEntitiesOperationArgs*>(extraData);
|
|
EntityTreeElement* entityTreeElement = static_cast<EntityTreeElement*>(element);
|
|
|
|
const EntityItems& entities = entityTreeElement->getEntities();
|
|
for (int i = 0; i < entities.size(); i++) {
|
|
EntityItemID newID(QUuid::createUuid());
|
|
args->newEntityIDs->append(newID);
|
|
EntityItemProperties properties = entities[i]->getProperties();
|
|
properties.setPosition(properties.getPosition() + args->root);
|
|
properties.markAllChanged(); // so the entire property set is considered new, since we're making a new entity
|
|
|
|
// queue the packet to send to the server
|
|
args->packetSender->queueEditEntityMessage(PacketType::EntityAdd, newID, properties);
|
|
|
|
// also update the local tree instantly (note: this is not our tree, but an alternate tree)
|
|
if (args->localTree) {
|
|
args->localTree->lockForWrite();
|
|
args->localTree->addEntity(newID, properties);
|
|
args->localTree->unlock();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool EntityTree::writeToMap(QVariantMap& entityDescription, OctreeElement* element, bool skipDefaultValues) {
|
|
entityDescription["Entities"] = QVariantList();
|
|
QScriptEngine scriptEngine;
|
|
RecurseOctreeToMapOperator theOperator(entityDescription, element, &scriptEngine, skipDefaultValues);
|
|
recurseTreeWithOperator(&theOperator);
|
|
return true;
|
|
}
|
|
|
|
bool EntityTree::readFromMap(QVariantMap& map) {
|
|
// map will have a top-level list keyed as "Entities". This will be extracted
|
|
// and iterated over. Each member of this list is converted to a QVariantMap, then
|
|
// to a QScriptValue, and then to EntityItemProperties. These properties are used
|
|
// to add the new entity to the EnitytTree.
|
|
QVariantList entitiesQList = map["Entities"].toList();
|
|
QScriptEngine scriptEngine;
|
|
|
|
foreach (QVariant entityVariant, entitiesQList) {
|
|
// QVariantMap --> QScriptValue --> EntityItemProperties --> Entity
|
|
QVariantMap entityMap = entityVariant.toMap();
|
|
QScriptValue entityScriptValue = variantMapToScriptValue(entityMap, scriptEngine);
|
|
EntityItemProperties properties;
|
|
EntityItemPropertiesFromScriptValueIgnoreReadOnly(entityScriptValue, properties);
|
|
|
|
EntityItemID entityItemID;
|
|
if (entityMap.contains("id")) {
|
|
entityItemID = EntityItemID(QUuid(entityMap["id"].toString()));
|
|
} else {
|
|
entityItemID = EntityItemID(QUuid::createUuid());
|
|
}
|
|
|
|
EntityItemPointer entity = addEntity(entityItemID, properties);
|
|
if (!entity) {
|
|
qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void EntityTree::resetClientEditStats() {
|
|
_treeResetTime = usecTimestampNow();
|
|
_maxEditDelta = 0;
|
|
_totalEditDeltas = 0;
|
|
_totalTrackedEdits = 0;
|
|
}
|
|
|
|
|
|
|
|
void EntityTree::trackIncomingEntityLastEdited(quint64 lastEditedTime, int bytesRead) {
|
|
// we don't want to track all edit deltas, just those edits that have happend
|
|
// since we connected to this domain. This will filter out all previously created
|
|
// content and only track new edits
|
|
if (lastEditedTime > _treeResetTime) {
|
|
quint64 now = usecTimestampNow();
|
|
quint64 sinceEdit = now - lastEditedTime;
|
|
|
|
_totalEditDeltas += sinceEdit;
|
|
_totalEditBytes += bytesRead;
|
|
_totalTrackedEdits++;
|
|
if (sinceEdit > _maxEditDelta) {
|
|
_maxEditDelta = sinceEdit;
|
|
}
|
|
}
|
|
}
|