void SimpleEntitySimulation::updateEntitiesInternal(const quint64& now) { // If an Entity has a simulation owner and we don't get an update for some amount of time, // clear the owner. This guards against an interface failing to release the Entity when it // has finished simulating it. auto nodeList = DependencyManager::get<LimitedNodeList>(); SetOfEntities::iterator itemItr = _entitiesWithSimulator.begin(); while (itemItr != _entitiesWithSimulator.end()) { EntityItemPointer entity = *itemItr; if (entity->getSimulatorID().isNull()) { itemItr = _entitiesWithSimulator.erase(itemItr); } else if (now - entity->getLastChangedOnServer() >= AUTO_REMOVE_SIMULATION_OWNER_USEC) { SharedNodePointer ownerNode = nodeList->nodeWithUUID(entity->getSimulatorID()); if (ownerNode.isNull() || !ownerNode->isAlive()) { qCDebug(entities) << "auto-removing simulation owner" << entity->getSimulatorID(); entity->clearSimulationOwnership(); itemItr = _entitiesWithSimulator.erase(itemItr); // zero the velocity on this entity so that it doesn't drift far away entity->setVelocity(glm::vec3(0.0f)); } else { ++itemItr; } } else { ++itemItr; } } }
void SimpleEntitySimulation::clearOwnership(const QUuid& ownerID) { QMutexLocker lock(&_mutex); SetOfEntities::iterator itemItr = _entitiesWithSimulationOwner.begin(); while (itemItr != _entitiesWithSimulationOwner.end()) { EntityItemPointer entity = *itemItr; if (entity->getSimulatorID() == ownerID) { // the simulator has abandonded this object --> remove from owned list qCDebug(entities) << "auto-removing simulation owner " << entity->getSimulatorID(); itemItr = _entitiesWithSimulationOwner.erase(itemItr); if (entity->getDynamic() && entity->hasLocalVelocity()) { // it is still moving dynamically --> add to orphaned list _entitiesThatNeedSimulationOwner.insert(entity); quint64 expiry = entity->getLastChangedOnServer() + MAX_OWNERLESS_PERIOD; if (expiry < _nextOwnerlessExpiry) { _nextOwnerlessExpiry = expiry; } } // remove ownership and dirty all the tree elements that contain the it entity->clearSimulationOwnership(); entity->markAsChangedOnServer(); DirtyOctreeElementOperator op(entity->getElement()); getEntityTree()->recurseTreeWithOperator(&op); } else { ++itemItr; } } }
void SimpleEntitySimulation::changeEntityInternal(EntityItemPointer entity) { EntitySimulation::changeEntityInternal(entity); if (!entity->getSimulatorID().isNull()) { _entitiesWithSimulator.insert(entity); } entity->clearDirtyFlags(); }
void SimpleEntitySimulation::updateEntitiesInternal(const quint64& now) { if (now > _nextOwnerlessExpiry) { // search for ownerless objects that have expired QMutexLocker lock(&_mutex); _nextOwnerlessExpiry = -1; SetOfEntities::iterator itemItr = _entitiesThatNeedSimulationOwner.begin(); while (itemItr != _entitiesThatNeedSimulationOwner.end()) { EntityItemPointer entity = *itemItr; quint64 expiry = entity->getLastChangedOnServer() + MAX_OWNERLESS_PERIOD; if (expiry < now) { // no simulators have volunteered ownership --> remove from list itemItr = _entitiesThatNeedSimulationOwner.erase(itemItr); if (entity->getSimulatorID().isNull() && entity->getDynamic() && entity->hasLocalVelocity()) { // zero the derivatives entity->setVelocity(Vectors::ZERO); entity->setAngularVelocity(Vectors::ZERO); entity->setAcceleration(Vectors::ZERO); // dirty all the tree elements that contain it entity->markAsChangedOnServer(); DirtyOctreeElementOperator op(entity->getElement()); getEntityTree()->recurseTreeWithOperator(&op); } } else { ++itemItr; if (expiry < _nextOwnerlessExpiry) { _nextOwnerlessExpiry = expiry; } } } } }
QUuid EntityScriptingInterface::editEntity(QUuid id, EntityItemProperties properties) { EntityItemID entityID(id); // If we have a local entity tree set, then also update it. if (!_entityTree) { queueEntityMessage(PacketType::EntityEdit, entityID, properties); return id; } bool updatedEntity = false; _entityTree->withWriteLock([&] { updatedEntity = _entityTree->updateEntity(entityID, properties); }); if (!updatedEntity) { return QUuid(); } _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (entity) { // make sure the properties has a type, so that the encode can know which properties to include properties.setType(entity->getType()); bool hasTerseUpdateChanges = properties.hasTerseUpdateChanges(); bool hasPhysicsChanges = properties.hasMiscPhysicsChanges() || hasTerseUpdateChanges; if (hasPhysicsChanges) { auto nodeList = DependencyManager::get<NodeList>(); const QUuid myNodeID = nodeList->getSessionUUID(); if (entity->getSimulatorID() == myNodeID) { // we think we already own the simulation, so make sure to send ALL TerseUpdate properties if (hasTerseUpdateChanges) { entity->getAllTerseUpdateProperties(properties); } // TODO: if we knew that ONLY TerseUpdate properties have changed in properties AND the object // is dynamic AND it is active in the physics simulation then we could chose to NOT queue an update // and instead let the physics simulation decide when to send a terse update. This would remove // the "slide-no-rotate" glitch (and typical a double-update) that we see during the "poke rolling // balls" test. However, even if we solve this problem we still need to provide a "slerp the visible // proxy toward the true physical position" feature to hide the final glitches in the remote watcher's // simulation. if (entity->getSimulationPriority() < SCRIPT_EDIT_SIMULATION_PRIORITY) { // we re-assert our simulation ownership at a higher priority properties.setSimulationOwner(myNodeID, glm::max(entity->getSimulationPriority(), SCRIPT_EDIT_SIMULATION_PRIORITY)); } } else { // we make a bid for simulation ownership properties.setSimulationOwner(myNodeID, SCRIPT_EDIT_SIMULATION_PRIORITY); entity->flagForOwnership(); } } entity->setLastBroadcast(usecTimestampNow()); } }); queueEntityMessage(PacketType::EntityEdit, entityID, properties); return id; }
void SimpleEntitySimulation::addEntityInternal(EntityItemPointer entity) { EntitySimulation::addEntityInternal(entity); if (!entity->getSimulatorID().isNull()) { QMutexLocker lock(&_mutex); _entitiesWithSimulationOwner.insert(entity); } else if (entity->getDynamic() && entity->hasLocalVelocity()) { QMutexLocker lock(&_mutex); _entitiesThatNeedSimulationOwner.insert(entity); quint64 expiry = entity->getLastChangedOnServer() + MAX_OWNERLESS_PERIOD; if (expiry < _nextOwnerlessExpiry) { _nextOwnerlessExpiry = expiry; } } }
bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityItemProperties& origProperties, EntityTreeElementPointer containingElement, const SharedNodePointer& senderNode) { EntityItemProperties properties = origProperties; bool allowLockChange; QUuid senderID; if (senderNode.isNull()) { auto nodeList = DependencyManager::get<NodeList>(); allowLockChange = nodeList->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(getThisPointer(), 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); if (wantTerseEditLogging()) { qCDebug(entities) << senderNode->getUUID() << "physical edits suppressed"; } } } // else client accepts what the server says QString entityScriptBefore = entity->getScript(); quint64 entityScriptTimestampBefore = entity->getScriptTimestamp(); QString collisionSoundURLBefore = entity->getCollisionSoundURL(); uint32_t preFlags = entity->getDirtyFlags(); UpdateEntityOperator theOperator(getThisPointer(), containingElement, entity, properties); recurseTreeWithOperator(&theOperator); _isDirty = true; uint32_t newFlags = entity->getDirtyFlags() & ~preFlags; if (newFlags) { if (_simulation) { if (newFlags & DIRTY_SIMULATION_FLAGS) { _simulation->changeEntity(entity); } } else { // normally the _simulation clears ALL updateFlags, but since there is none we do it explicitly entity->clearDirtyFlags(); } } QString entityScriptAfter = entity->getScript(); quint64 entityScriptTimestampAfter = entity->getScriptTimestamp(); bool reload = entityScriptTimestampBefore != entityScriptTimestampAfter; if (entityScriptBefore != entityScriptAfter || reload) { emitEntityScriptChanging(entity->getEntityItemID(), reload); // the entity script has changed } maybeNotifyNewCollisionSoundURL(collisionSoundURLBefore, entity->getCollisionSoundURL()); } // TODO: this final containingElement check should eventually be removed (or wrapped in an #ifdef DEBUG). containingElement = getContainingElement(entity->getEntityItemID()); if (!containingElement) { qCDebug(entities) << "UNEXPECTED!!!! after updateEntity() we no longer have a containing element??? entityID=" << entity->getEntityItemID(); return false; } return true; }
QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& scriptSideProperties) { EntityItemProperties properties = scriptSideProperties; auto dimensions = properties.getDimensions(); float volume = dimensions.x * dimensions.y * dimensions.z; auto density = properties.getDensity(); auto newVelocity = properties.getVelocity().length(); float oldVelocity = { 0.0f }; EntityItemID entityID(id); if (!_entityTree) { queueEntityMessage(PacketType::EntityEdit, entityID, properties); //if there is no local entity entity tree, no existing velocity, use 0. float cost = calculateCost(density * volume, oldVelocity, newVelocity); cost *= costMultiplier; if (cost > _currentAvatarEnergy) { return QUuid(); } else { //debit the avatar energy and continue emit debitEnergySource(cost); } return id; } // If we have a local entity tree set, then also update it. bool updatedEntity = false; _entityTree->withWriteLock([&] { if (scriptSideProperties.parentRelatedPropertyChanged()) { // All of parentID, parentJointIndex, position, rotation are needed to make sense of any of them. // If any of these changed, pull any missing properties from the entity. EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (!entity) { return; } //existing entity, retrieve old velocity for check down below oldVelocity = entity->getVelocity().length(); if (!scriptSideProperties.parentIDChanged()) { properties.setParentID(entity->getParentID()); } if (!scriptSideProperties.parentJointIndexChanged()) { properties.setParentJointIndex(entity->getParentJointIndex()); } if (!scriptSideProperties.localPositionChanged() && !scriptSideProperties.positionChanged()) { properties.setPosition(entity->getPosition()); } if (!scriptSideProperties.localRotationChanged() && !scriptSideProperties.rotationChanged()) { properties.setRotation(entity->getOrientation()); } } properties = convertLocationFromScriptSemantics(properties); float cost = calculateCost(density * volume, oldVelocity, newVelocity); cost *= costMultiplier; if (cost > _currentAvatarEnergy) { updatedEntity = false; } else { //debit the avatar energy and continue updatedEntity = _entityTree->updateEntity(entityID, properties); if (updatedEntity) { emit debitEnergySource(cost); } } }); if (!updatedEntity) { return QUuid(); } _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (entity) { // make sure the properties has a type, so that the encode can know which properties to include properties.setType(entity->getType()); bool hasTerseUpdateChanges = properties.hasTerseUpdateChanges(); bool hasPhysicsChanges = properties.hasMiscPhysicsChanges() || hasTerseUpdateChanges; if (_bidOnSimulationOwnership && hasPhysicsChanges) { auto nodeList = DependencyManager::get<NodeList>(); const QUuid myNodeID = nodeList->getSessionUUID(); if (entity->getSimulatorID() == myNodeID) { // we think we already own the simulation, so make sure to send ALL TerseUpdate properties if (hasTerseUpdateChanges) { entity->getAllTerseUpdateProperties(properties); } // TODO: if we knew that ONLY TerseUpdate properties have changed in properties AND the object // is dynamic AND it is active in the physics simulation then we could chose to NOT queue an update // and instead let the physics simulation decide when to send a terse update. This would remove // the "slide-no-rotate" glitch (and typical double-update) that we see during the "poke rolling // balls" test. However, even if we solve this problem we still need to provide a "slerp the visible // proxy toward the true physical position" feature to hide the final glitches in the remote watcher's // simulation. if (entity->getSimulationPriority() < SCRIPT_POKE_SIMULATION_PRIORITY) { // we re-assert our simulation ownership at a higher priority properties.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); } } else { // we make a bid for simulation ownership properties.setSimulationOwner(myNodeID, SCRIPT_POKE_SIMULATION_PRIORITY); entity->pokeSimulationOwnership(); } } if (properties.parentRelatedPropertyChanged() && entity->computePuffedQueryAACube()) { properties.setQueryAACube(entity->getQueryAACube()); } entity->setLastBroadcast(usecTimestampNow()); // if we've moved an entity with children, check/update the queryAACube of all descendents and tell the server // if they've changed. entity->forEachDescendant([&](SpatiallyNestablePointer descendant) { if (descendant->getNestableType() == NestableType::Entity) { if (descendant->computePuffedQueryAACube()) { EntityItemPointer entityDescendant = std::static_pointer_cast<EntityItem>(descendant); EntityItemProperties newQueryCubeProperties; newQueryCubeProperties.setQueryAACube(descendant->getQueryAACube()); newQueryCubeProperties.setLastEdited(properties.getLastEdited()); queueEntityMessage(PacketType::EntityEdit, descendant->getID(), newQueryCubeProperties); entityDescendant->setLastBroadcast(usecTimestampNow()); } } }); } }); queueEntityMessage(PacketType::EntityEdit, entityID, properties); return id; }
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.simulatorIDChanged()) { QUuid submittedID = properties.getSimulatorID(); // 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; } // 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() || entity->getSimulatorID() == senderID) { simulationBlocked = false; } else { // the sender is trying to steal ownership from another simulator // so we apply the ownership change filter if (usecTimestampNow() - entity->getSimulatorIDChangedTime() > SIMULATOR_CHANGE_LOCKOUT_PERIOD) { simulationBlocked = false; } } } else { // the entire update is suspect --> ignore it return false; } } if (simulationBlocked) { // squash the physics-related changes. properties.setSimulatorIDChanged(false); properties.setPositionChanged(false); properties.setRotationChanged(false); } } // else client accepts what the server says QString entityScriptBefore = entity->getScript(); 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(); if (entityScriptBefore != entityScriptAfter) { emitEntityScriptChanging(entity->getEntityItemID()); // 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; }
void SimpleEntitySimulation::addEntityInternal(EntityItemPointer entity) { EntitySimulation::addEntityInternal(entity); if (!entity->getSimulatorID().isNull()) { _entitiesWithSimulator.insert(entity); } }