// // EntityScriptingInterface.cpp // libraries/entities/src // // Created by Brad Hefta-Gaub on 12/6/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 "EntityScriptingInterface.h" #include #include #include #include #include #include #include #include #include #include "EntityItemID.h" #include "EntitiesLogging.h" #include "EntityDynamicFactoryInterface.h" #include "EntityDynamicInterface.h" #include "EntitySimulation.h" #include "EntityTree.h" #include "LightEntityItem.h" #include "ModelEntityItem.h" #include "QVariantGLM.h" #include "SimulationOwner.h" #include "ZoneEntityItem.h" #include "WebEntityItem.h" #include #include EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership) : _entityTree(NULL), _bidOnSimulationOwnership(bidOnSimulationOwnership) { auto nodeList = DependencyManager::get(); connect(nodeList.data(), &NodeList::isAllowedEditorChanged, this, &EntityScriptingInterface::canAdjustLocksChanged); connect(nodeList.data(), &NodeList::canRezChanged, this, &EntityScriptingInterface::canRezChanged); connect(nodeList.data(), &NodeList::canRezTmpChanged, this, &EntityScriptingInterface::canRezTmpChanged); connect(nodeList.data(), &NodeList::canRezCertifiedChanged, this, &EntityScriptingInterface::canRezCertifiedChanged); connect(nodeList.data(), &NodeList::canRezTmpCertifiedChanged, this, &EntityScriptingInterface::canRezTmpCertifiedChanged); connect(nodeList.data(), &NodeList::canWriteAssetsChanged, this, &EntityScriptingInterface::canWriteAssetsChanged); // If the user clicks somewhere where there is no entity at all, we will release focus connect(this, &EntityScriptingInterface::mousePressOffEntity, [=]() { setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); }); auto& packetReceiver = nodeList->getPacketReceiver(); packetReceiver.registerListener(PacketType::EntityScriptCallMethod, this, "handleEntityScriptCallMethodPacket"); } void EntityScriptingInterface::queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties) { getEntityPacketSender()->queueEditEntityMessage(packetType, _entityTree, entityID, properties); } void EntityScriptingInterface::resetActivityTracking() { _activityTracking.addedEntityCount = 0; _activityTracking.deletedEntityCount = 0; _activityTracking.editedEntityCount = 0; } bool EntityScriptingInterface::canAdjustLocks() { auto nodeList = DependencyManager::get(); return nodeList->isAllowedEditor(); } bool EntityScriptingInterface::canRez() { auto nodeList = DependencyManager::get(); return nodeList->getThisNodeCanRez(); } bool EntityScriptingInterface::canRezTmp() { auto nodeList = DependencyManager::get(); return nodeList->getThisNodeCanRezTmp(); } bool EntityScriptingInterface::canRezCertified() { auto nodeList = DependencyManager::get(); return nodeList->getThisNodeCanRezCertified(); } bool EntityScriptingInterface::canRezTmpCertified() { auto nodeList = DependencyManager::get(); return nodeList->getThisNodeCanRezTmpCertified(); } bool EntityScriptingInterface::canWriteAssets() { auto nodeList = DependencyManager::get(); return nodeList->getThisNodeCanWriteAssets(); } bool EntityScriptingInterface::canReplaceContent() { auto nodeList = DependencyManager::get(); return nodeList->getThisNodeCanReplaceContent(); } void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) { if (_entityTree) { disconnect(_entityTree.get(), &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity); disconnect(_entityTree.get(), &EntityTree::deletingEntity, this, &EntityScriptingInterface::deletingEntity); disconnect(_entityTree.get(), &EntityTree::clearingEntities, this, &EntityScriptingInterface::clearingEntities); } _entityTree = elementTree; if (_entityTree) { connect(_entityTree.get(), &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity); connect(_entityTree.get(), &EntityTree::deletingEntity, this, &EntityScriptingInterface::deletingEntity); connect(_entityTree.get(), &EntityTree::clearingEntities, this, &EntityScriptingInterface::clearingEntities); } } EntityItemProperties convertPropertiesToScriptSemantics(const EntityItemProperties& entitySideProperties, bool scalesWithParent) { // In EntityTree code, properties.position and properties.rotation are relative to the parent. In javascript, // they are in world-space. The local versions are put into localPosition and localRotation and position and // rotation are converted from local to world space. EntityItemProperties scriptSideProperties = entitySideProperties; scriptSideProperties.setLocalPosition(entitySideProperties.getPosition()); scriptSideProperties.setLocalRotation(entitySideProperties.getRotation()); scriptSideProperties.setLocalVelocity(entitySideProperties.getLocalVelocity()); scriptSideProperties.setLocalAngularVelocity(entitySideProperties.getLocalAngularVelocity()); scriptSideProperties.setLocalDimensions(entitySideProperties.getDimensions()); bool success; glm::vec3 worldPosition = SpatiallyNestable::localToWorld(entitySideProperties.getPosition(), entitySideProperties.getParentID(), entitySideProperties.getParentJointIndex(), scalesWithParent, success); glm::quat worldRotation = SpatiallyNestable::localToWorld(entitySideProperties.getRotation(), entitySideProperties.getParentID(), entitySideProperties.getParentJointIndex(), scalesWithParent, success); glm::vec3 worldVelocity = SpatiallyNestable::localToWorldVelocity(entitySideProperties.getVelocity(), entitySideProperties.getParentID(), entitySideProperties.getParentJointIndex(), scalesWithParent, success); glm::vec3 worldAngularVelocity = SpatiallyNestable::localToWorldAngularVelocity(entitySideProperties.getAngularVelocity(), entitySideProperties.getParentID(), entitySideProperties.getParentJointIndex(), scalesWithParent, success); glm::vec3 worldDimensions = SpatiallyNestable::localToWorldDimensions(entitySideProperties.getDimensions(), entitySideProperties.getParentID(), entitySideProperties.getParentJointIndex(), scalesWithParent, success); scriptSideProperties.setPosition(worldPosition); scriptSideProperties.setRotation(worldRotation); scriptSideProperties.setVelocity(worldVelocity); scriptSideProperties.setAngularVelocity(worldAngularVelocity); scriptSideProperties.setDimensions(worldDimensions); return scriptSideProperties; } EntityItemProperties convertPropertiesFromScriptSemantics(const EntityItemProperties& scriptSideProperties, bool scalesWithParent) { // convert position and rotation properties from world-space to local, unless localPosition and localRotation // are set. If they are set, they overwrite position and rotation. EntityItemProperties entitySideProperties = scriptSideProperties; bool success; // TODO -- handle velocity and angularVelocity if (scriptSideProperties.localPositionChanged()) { entitySideProperties.setPosition(scriptSideProperties.getLocalPosition()); } else if (scriptSideProperties.positionChanged()) { glm::vec3 localPosition = SpatiallyNestable::worldToLocal(entitySideProperties.getPosition(), entitySideProperties.getParentID(), entitySideProperties.getParentJointIndex(), scalesWithParent, success); entitySideProperties.setPosition(localPosition); } if (scriptSideProperties.localRotationChanged()) { entitySideProperties.setRotation(scriptSideProperties.getLocalRotation()); } else if (scriptSideProperties.rotationChanged()) { glm::quat localRotation = SpatiallyNestable::worldToLocal(entitySideProperties.getRotation(), entitySideProperties.getParentID(), entitySideProperties.getParentJointIndex(), scalesWithParent, success); entitySideProperties.setRotation(localRotation); } if (scriptSideProperties.localVelocityChanged()) { entitySideProperties.setVelocity(scriptSideProperties.getLocalVelocity()); } else if (scriptSideProperties.velocityChanged()) { glm::vec3 localVelocity = SpatiallyNestable::worldToLocalVelocity(entitySideProperties.getVelocity(), entitySideProperties.getParentID(), entitySideProperties.getParentJointIndex(), scalesWithParent, success); entitySideProperties.setVelocity(localVelocity); } if (scriptSideProperties.localAngularVelocityChanged()) { entitySideProperties.setAngularVelocity(scriptSideProperties.getLocalAngularVelocity()); } else if (scriptSideProperties.angularVelocityChanged()) { glm::vec3 localAngularVelocity = SpatiallyNestable::worldToLocalAngularVelocity(entitySideProperties.getAngularVelocity(), entitySideProperties.getParentID(), entitySideProperties.getParentJointIndex(), scalesWithParent, success); entitySideProperties.setAngularVelocity(localAngularVelocity); } if (scriptSideProperties.localDimensionsChanged()) { entitySideProperties.setDimensions(scriptSideProperties.getLocalDimensions()); } else if (scriptSideProperties.dimensionsChanged()) { glm::vec3 localDimensions = SpatiallyNestable::worldToLocalDimensions(entitySideProperties.getDimensions(), entitySideProperties.getParentID(), entitySideProperties.getParentJointIndex(), scalesWithParent, success); entitySideProperties.setDimensions(localDimensions); } return entitySideProperties; } QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties, bool clientOnly) { PROFILE_RANGE(script_entities, __FUNCTION__); _activityTracking.addedEntityCount++; auto nodeList = DependencyManager::get(); auto sessionID = nodeList->getSessionUUID(); EntityItemProperties propertiesWithSimID = properties; if (clientOnly) { const QUuid myNodeID = sessionID; propertiesWithSimID.setClientOnly(clientOnly); propertiesWithSimID.setOwningAvatarID(myNodeID); } propertiesWithSimID.setLastEditedBy(sessionID); bool scalesWithParent = propertiesWithSimID.getScalesWithParent(); propertiesWithSimID = convertPropertiesFromScriptSemantics(propertiesWithSimID, scalesWithParent); propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged()); EntityItemID id; // If we have a local entity tree set, then also update it. bool success = addLocalEntityCopy(propertiesWithSimID, id); // queue the packet if (success) { queueEntityMessage(PacketType::EntityAdd, id, propertiesWithSimID); return id; } else { return QUuid(); } } bool EntityScriptingInterface::addLocalEntityCopy(EntityItemProperties& properties, EntityItemID& id, bool isClone) { bool success = true; id = EntityItemID(QUuid::createUuid()); if (_entityTree) { _entityTree->withWriteLock([&] { EntityItemPointer entity = _entityTree->addEntity(id, properties, isClone); if (entity) { if (properties.queryAACubeRelatedPropertyChanged()) { // due to parenting, the server may not know where something is in world-space, so include the bounding cube. bool success; AACube queryAACube = entity->getQueryAACube(success); if (success) { properties.setQueryAACube(queryAACube); } } entity->setLastBroadcast(usecTimestampNow()); // since we're creating this object we will immediately volunteer to own its simulation entity->flagForOwnershipBid(VOLUNTEER_SIMULATION_PRIORITY); properties.setLastEdited(entity->getLastEdited()); } else { qCDebug(entities) << "script failed to add new Entity to local Octree"; success = false; } }); } return success; } QUuid EntityScriptingInterface::addModelEntity(const QString& name, const QString& modelUrl, const QString& textures, const QString& shapeType, bool dynamic, bool collisionless, const glm::vec3& position, const glm::vec3& gravity) { _activityTracking.addedEntityCount++; EntityItemProperties properties; properties.setType(EntityTypes::Model); properties.setName(name); properties.setModelURL(modelUrl); properties.setShapeTypeFromString(shapeType); properties.setDynamic(dynamic); properties.setCollisionless(collisionless); properties.setPosition(position); properties.setGravity(gravity); if (!textures.isEmpty()) { properties.setTextures(textures); } auto nodeList = DependencyManager::get(); auto sessionID = nodeList->getSessionUUID(); properties.setLastEditedBy(sessionID); return addEntity(properties); } QUuid EntityScriptingInterface::cloneEntity(QUuid entityIDToClone) { EntityItemID newEntityID; EntityItemProperties properties = getEntityProperties(entityIDToClone); bool cloneAvatarEntity = properties.getCloneAvatarEntity(); properties.convertToCloneProperties(entityIDToClone); if (cloneAvatarEntity) { return addEntity(properties, true); } else { // setLastEdited timestamp to 0 to ensure this entity gets updated with the properties // from the server-created entity, don't change this unless you know what you are doing properties.setLastEdited(0); bool success = addLocalEntityCopy(properties, newEntityID, true); if (success) { getEntityPacketSender()->queueCloneEntityMessage(entityIDToClone, newEntityID); return newEntityID; } else { return QUuid(); } } } EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity) { EntityPropertyFlags noSpecificProperties; return getEntityProperties(identity, noSpecificProperties); } EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity, EntityPropertyFlags desiredProperties) { PROFILE_RANGE(script_entities, __FUNCTION__); bool scalesWithParent { false }; EntityItemProperties results; if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(identity)); if (entity) { scalesWithParent = entity->getScalesWithParent(); if (desiredProperties.getHasProperty(PROP_POSITION) || desiredProperties.getHasProperty(PROP_ROTATION) || desiredProperties.getHasProperty(PROP_LOCAL_POSITION) || desiredProperties.getHasProperty(PROP_LOCAL_ROTATION) || desiredProperties.getHasProperty(PROP_LOCAL_VELOCITY) || desiredProperties.getHasProperty(PROP_LOCAL_ANGULAR_VELOCITY) || desiredProperties.getHasProperty(PROP_LOCAL_DIMENSIONS)) { // if we are explicitly getting position or rotation, we need parent information to make sense of them. desiredProperties.setHasProperty(PROP_PARENT_ID); desiredProperties.setHasProperty(PROP_PARENT_JOINT_INDEX); } if (desiredProperties.isEmpty()) { // these are left out of EntityItem::getEntityProperties so that localPosition and localRotation // don't end up in json saves, etc. We still want them here, though. EncodeBitstreamParams params; // unknown desiredProperties = entity->getEntityProperties(params); desiredProperties.setHasProperty(PROP_LOCAL_POSITION); desiredProperties.setHasProperty(PROP_LOCAL_ROTATION); desiredProperties.setHasProperty(PROP_LOCAL_VELOCITY); desiredProperties.setHasProperty(PROP_LOCAL_ANGULAR_VELOCITY); desiredProperties.setHasProperty(PROP_LOCAL_DIMENSIONS); } results = entity->getProperties(desiredProperties); } }); } return convertPropertiesToScriptSemantics(results, scalesWithParent); } QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& scriptSideProperties) { PROFILE_RANGE(script_entities, __FUNCTION__); _activityTracking.editedEntityCount++; auto nodeList = DependencyManager::get(); auto sessionID = nodeList->getSessionUUID(); EntityItemProperties properties = scriptSideProperties; properties.setLastEditedBy(sessionID); EntityItemID entityID(id); if (!_entityTree) { queueEntityMessage(PacketType::EntityEdit, entityID, properties); return id; } // If we have a local entity tree set, then also update it. bool updatedEntity = false; _entityTree->withWriteLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (!entity) { return; } if (entity->getClientOnly() && entity->getOwningAvatarID() != nodeList->getSessionUUID()) { // don't edit other avatar's avatarEntities return; } 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. if (!scriptSideProperties.parentIDChanged()) { properties.setParentID(entity->getParentID()); } if (!scriptSideProperties.parentJointIndexChanged()) { properties.setParentJointIndex(entity->getParentJointIndex()); } if (!scriptSideProperties.localPositionChanged() && !scriptSideProperties.positionChanged()) { properties.setPosition(entity->getWorldPosition()); } if (!scriptSideProperties.localRotationChanged() && !scriptSideProperties.rotationChanged()) { properties.setRotation(entity->getWorldOrientation()); } if (!scriptSideProperties.localDimensionsChanged() && !scriptSideProperties.dimensionsChanged()) { properties.setDimensions(entity->getScaledDimensions()); } } properties.setClientOnly(entity->getClientOnly()); properties.setOwningAvatarID(entity->getOwningAvatarID()); properties = convertPropertiesFromScriptSemantics(properties, properties.getScalesWithParent()); updatedEntity = _entityTree->updateEntity(entityID, properties); }); // FIXME: We need to figure out a better way to handle this. Allowing these edits to go through potentially // breaks entities that are parented. // // To handle cases where a script needs to edit an entity with a _known_ entity id but doesn't exist // in the local entity tree, we need to allow those edits to go through to the server. // if (!updatedEntity) { // return QUuid(); // } bool entityFound { false }; _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (entity) { entityFound = true; // 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(); 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->flagForOwnershipBid(SCRIPT_POKE_SIMULATION_PRIORITY); } } if (properties.queryAACubeRelatedPropertyChanged()) { properties.setQueryAACube(entity->getQueryAACube()); } entity->setLastBroadcast(usecTimestampNow()); properties.setLastEdited(entity->getLastEdited()); // 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->updateQueryAACube()) { EntityItemPointer entityDescendant = std::static_pointer_cast(descendant); EntityItemProperties newQueryCubeProperties; newQueryCubeProperties.setQueryAACube(descendant->getQueryAACube()); newQueryCubeProperties.setLastEdited(properties.getLastEdited()); queueEntityMessage(PacketType::EntityEdit, descendant->getID(), newQueryCubeProperties); entityDescendant->setLastBroadcast(usecTimestampNow()); } } }); } else { // Sometimes ESS don't have the entity they are trying to edit in their local tree. In this case, // convertPropertiesFromScriptSemantics doesn't get called and local* edits will get dropped. // This is because, on the script side, "position" is in world frame, but in the network // protocol and in the internal data-structures, "position" is "relative to parent". // Compensate here. The local* versions will get ignored during the edit-packet encoding. if (properties.localPositionChanged()) { properties.setPosition(properties.getLocalPosition()); } if (properties.localRotationChanged()) { properties.setRotation(properties.getLocalRotation()); } if (properties.localVelocityChanged()) { properties.setVelocity(properties.getLocalVelocity()); } if (properties.localAngularVelocityChanged()) { properties.setAngularVelocity(properties.getLocalAngularVelocity()); } if (properties.localDimensionsChanged()) { properties.setDimensions(properties.getLocalDimensions()); } } }); if (!entityFound) { // we've made an edit to an entity we don't know about, or to a non-entity. If it's a known non-entity, // print a warning and don't send an edit packet to the entity-server. QSharedPointer parentFinder = DependencyManager::get(); if (parentFinder) { bool success; auto nestableWP = parentFinder->find(id, success, static_cast(_entityTree.get())); if (success) { auto nestable = nestableWP.lock(); if (nestable) { NestableType nestableType = nestable->getNestableType(); if (nestableType == NestableType::Overlay || nestableType == NestableType::Avatar) { qCWarning(entities) << "attempted edit on non-entity: " << id << nestable->getName(); return QUuid(); // null script value to indicate failure } } } } } // we queue edit packets even if we don't know about the entity. This is to allow AC agents // to edit entities they know only by ID. queueEntityMessage(PacketType::EntityEdit, entityID, properties); return id; } void EntityScriptingInterface::deleteEntity(QUuid id) { PROFILE_RANGE(script_entities, __FUNCTION__); _activityTracking.deletedEntityCount++; EntityItemID entityID(id); bool shouldDelete = true; // If we have a local entity tree set, then also update it. if (_entityTree) { _entityTree->withWriteLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (entity) { auto nodeList = DependencyManager::get(); const QUuid myNodeID = nodeList->getSessionUUID(); if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) { // don't delete other avatar's avatarEntities // If you actually own the entity but the onwership property is not set because of a domain switch // The lines below makes sure the entity is deleted once its properties are set. auto avatarHashMap = DependencyManager::get(); AvatarSharedPointer myAvatar = avatarHashMap->getAvatarBySessionID(myNodeID); myAvatar->insertDetachedEntityID(id); shouldDelete = false; return; } if (entity->getLocked()) { shouldDelete = false; } else { // only delete local entities, server entities will round trip through the server filters if (entity->getClientOnly() || _entityTree->isServerlessMode()) { _entityTree->deleteEntity(entityID); } } } }); } // if at this point, we know the id, and we should still delete the entity, send the update to the entity server if (shouldDelete) { getEntityPacketSender()->queueEraseEntityMessage(entityID); } } void EntityScriptingInterface::setEntitiesScriptEngine(QSharedPointer engine) { std::lock_guard lock(_entitiesScriptEngineLock); _entitiesScriptEngine = engine; } void EntityScriptingInterface::callEntityMethod(QUuid id, const QString& method, const QStringList& params) { PROFILE_RANGE(script_entities, __FUNCTION__); std::lock_guard lock(_entitiesScriptEngineLock); if (_entitiesScriptEngine) { EntityItemID entityID{ id }; _entitiesScriptEngine->callEntityScriptMethod(entityID, method, params); } } void EntityScriptingInterface::callEntityServerMethod(QUuid id, const QString& method, const QStringList& params) { PROFILE_RANGE(script_entities, __FUNCTION__); DependencyManager::get()->callEntityServerMethod(id, method, params); } void EntityScriptingInterface::callEntityClientMethod(QUuid clientSessionID, QUuid entityID, const QString& method, const QStringList& params) { PROFILE_RANGE(script_entities, __FUNCTION__); auto scriptServerServices = DependencyManager::get(); // this won't be available on clients if (scriptServerServices) { scriptServerServices->callEntityClientMethod(clientSessionID, entityID, method, params); } else { qWarning() << "Entities.callEntityClientMethod() not allowed in client"; } } void EntityScriptingInterface::handleEntityScriptCallMethodPacket(QSharedPointer receivedMessage, SharedNodePointer senderNode) { PROFILE_RANGE(script_entities, __FUNCTION__); auto nodeList = DependencyManager::get(); SharedNodePointer entityScriptServer = nodeList->soloNodeOfType(NodeType::EntityScriptServer); if (entityScriptServer == senderNode) { auto entityID = QUuid::fromRfc4122(receivedMessage->read(NUM_BYTES_RFC4122_UUID)); auto method = receivedMessage->readString(); quint16 paramCount; receivedMessage->readPrimitive(¶mCount); QStringList params; for (int param = 0; param < paramCount; param++) { auto paramString = receivedMessage->readString(); params << paramString; } std::lock_guard lock(_entitiesScriptEngineLock); if (_entitiesScriptEngine) { _entitiesScriptEngine->callEntityScriptMethod(entityID, method, params, senderNode->getUUID()); } } } QUuid EntityScriptingInterface::findClosestEntity(const glm::vec3& center, float radius) const { PROFILE_RANGE(script_entities, __FUNCTION__); EntityItemID result; if (_entityTree) { EntityItemPointer closestEntity; _entityTree->withReadLock([&] { closestEntity = _entityTree->findClosestEntity(center, radius); }); if (closestEntity) { result = closestEntity->getEntityItemID(); } } return result; } void EntityScriptingInterface::dumpTree() const { if (_entityTree) { _entityTree->withReadLock([&] { _entityTree->dumpTree(); }); } } QVector EntityScriptingInterface::findEntities(const glm::vec3& center, float radius) const { PROFILE_RANGE(script_entities, __FUNCTION__); QVector result; if (_entityTree) { QVector entities; _entityTree->withReadLock([&] { _entityTree->findEntities(center, radius, entities); }); foreach (EntityItemPointer entity, entities) { result << entity->getEntityItemID(); } } return result; } QVector EntityScriptingInterface::findEntitiesInBox(const glm::vec3& corner, const glm::vec3& dimensions) const { PROFILE_RANGE(script_entities, __FUNCTION__); QVector result; if (_entityTree) { QVector entities; _entityTree->withReadLock([&] { AABox box(corner, dimensions); _entityTree->findEntities(box, entities); }); foreach (EntityItemPointer entity, entities) { result << entity->getEntityItemID(); } } return result; } QVector EntityScriptingInterface::findEntitiesInFrustum(QVariantMap frustum) const { PROFILE_RANGE(script_entities, __FUNCTION__); QVector result; const QString POSITION_PROPERTY = "position"; bool positionOK = frustum.contains(POSITION_PROPERTY); glm::vec3 position = positionOK ? qMapToGlmVec3(frustum[POSITION_PROPERTY]) : glm::vec3(); const QString ORIENTATION_PROPERTY = "orientation"; bool orientationOK = frustum.contains(ORIENTATION_PROPERTY); glm::quat orientation = orientationOK ? qMapToGlmQuat(frustum[ORIENTATION_PROPERTY]) : glm::quat(); const QString PROJECTION_PROPERTY = "projection"; bool projectionOK = frustum.contains(PROJECTION_PROPERTY); glm::mat4 projection = projectionOK ? qMapToGlmMat4(frustum[PROJECTION_PROPERTY]) : glm::mat4(); const QString CENTER_RADIUS_PROPERTY = "centerRadius"; bool centerRadiusOK = frustum.contains(CENTER_RADIUS_PROPERTY); float centerRadius = centerRadiusOK ? frustum[CENTER_RADIUS_PROPERTY].toFloat() : 0.0f; if (positionOK && orientationOK && projectionOK && centerRadiusOK) { ViewFrustum viewFrustum; viewFrustum.setPosition(position); viewFrustum.setOrientation(orientation); viewFrustum.setProjection(projection); viewFrustum.setCenterRadius(centerRadius); viewFrustum.calculate(); if (_entityTree) { QVector entities; _entityTree->withReadLock([&] { _entityTree->findEntities(viewFrustum, entities); }); foreach(EntityItemPointer entity, entities) { result << entity->getEntityItemID(); } } } return result; } QVector EntityScriptingInterface::findEntitiesByType(const QString entityType, const glm::vec3& center, float radius) const { EntityTypes::EntityType type = EntityTypes::getEntityTypeFromName(entityType); QVector result; if (_entityTree) { QVector entities; _entityTree->withReadLock([&] { _entityTree->findEntities(center, radius, entities); }); foreach(EntityItemPointer entity, entities) { if (entity->getType() == type) { result << entity->getEntityItemID().toString(); } } } return result; } QVector EntityScriptingInterface::findEntitiesByName(const QString entityName, const glm::vec3& center, float radius, bool caseSensitiveSearch) const { QVector result; if (_entityTree) { QVector entities; _entityTree->withReadLock([&] { _entityTree->findEntities(center, radius, entities); }); if (caseSensitiveSearch) { foreach(EntityItemPointer entity, entities) { if (entity->getName() == entityName) { result << entity->getEntityItemID(); } } } else { QString entityNameLowerCase = entityName.toLower(); foreach(EntityItemPointer entity, entities) { QString entityItemLowerCase = entity->getName().toLower(); if (entityItemLowerCase == entityNameLowerCase) { result << entity->getEntityItemID(); } } } } return result; } RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersection(const PickRay& ray, bool precisionPicking, const QScriptValue& entityIdsToInclude, const QScriptValue& entityIdsToDiscard, bool visibleOnly, bool collidableOnly) { QVector entitiesToInclude = qVectorEntityItemIDFromScriptValue(entityIdsToInclude); QVector entitiesToDiscard = qVectorEntityItemIDFromScriptValue(entityIdsToDiscard); return findRayIntersectionVector(ray, precisionPicking, entitiesToInclude, entitiesToDiscard, visibleOnly, collidableOnly); } RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersectionVector(const PickRay& ray, bool precisionPicking, const QVector& entityIdsToInclude, const QVector& entityIdsToDiscard, bool visibleOnly, bool collidableOnly) { PROFILE_RANGE(script_entities, __FUNCTION__); return findRayIntersectionWorker(ray, Octree::Lock, precisionPicking, entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly); } // FIXME - we should remove this API and encourage all users to use findRayIntersection() instead. We've changed // findRayIntersection() to be blocking because it never makes sense for a script to get back a non-answer RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersectionBlocking(const PickRay& ray, bool precisionPicking, const QScriptValue& entityIdsToInclude, const QScriptValue& entityIdsToDiscard) { qWarning() << "Entities.findRayIntersectionBlocking() is obsolete, use Entities.findRayIntersection() instead."; const QVector& entitiesToInclude = qVectorEntityItemIDFromScriptValue(entityIdsToInclude); const QVector entitiesToDiscard = qVectorEntityItemIDFromScriptValue(entityIdsToDiscard); return findRayIntersectionWorker(ray, Octree::Lock, precisionPicking, entitiesToInclude, entitiesToDiscard); } RayToEntityIntersectionResult EntityScriptingInterface::findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType, bool precisionPicking, const QVector& entityIdsToInclude, const QVector& entityIdsToDiscard, bool visibleOnly, bool collidableOnly) { RayToEntityIntersectionResult result; if (_entityTree) { OctreeElementPointer element; result.entityID = _entityTree->findRayIntersection(ray.origin, ray.direction, entityIdsToInclude, entityIdsToDiscard, visibleOnly, collidableOnly, precisionPicking, element, result.distance, result.face, result.surfaceNormal, result.extraInfo, lockType, &result.accurate); result.intersects = !result.entityID.isNull(); if (result.intersects) { result.intersection = ray.origin + (ray.direction * result.distance); } } return result; } bool EntityScriptingInterface::reloadServerScripts(QUuid entityID) { auto client = DependencyManager::get(); return client->reloadServerScript(entityID); } bool EntityPropertyMetadataRequest::script(EntityItemID entityID, QScriptValue handler) { using LocalScriptStatusRequest = QFutureWatcher; LocalScriptStatusRequest* request = new LocalScriptStatusRequest; QObject::connect(request, &LocalScriptStatusRequest::finished, _engine, [=]() mutable { auto details = request->result().toMap(); QScriptValue err, result; if (details.contains("isError")) { if (!details.contains("message")) { details["message"] = details["errorInfo"]; } err = _engine->makeError(_engine->toScriptValue(details)); } else { details["success"] = true; result = _engine->toScriptValue(details); } callScopedHandlerObject(handler, err, result); request->deleteLater(); }); auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->withEntitiesScriptEngine([&](QSharedPointer entitiesScriptEngine) { if (entitiesScriptEngine) { request->setFuture(entitiesScriptEngine->getLocalEntityScriptDetails(entityID)); } }); if (!request->isStarted()) { request->deleteLater(); callScopedHandlerObject(handler, _engine->makeError("Entities Scripting Provider unavailable", "InternalError"), QScriptValue()); return false; } return true; } bool EntityPropertyMetadataRequest::serverScripts(EntityItemID entityID, QScriptValue handler) { auto client = DependencyManager::get(); auto request = client->createScriptStatusRequest(entityID); QPointer engine = _engine; QObject::connect(request, &GetScriptStatusRequest::finished, _engine, [=](GetScriptStatusRequest* request) mutable { auto engine = _engine; if (!engine) { qCDebug(entities) << __FUNCTION__ << " -- engine destroyed while inflight" << entityID; return; } QVariantMap details; details["success"] = request->getResponseReceived(); details["isRunning"] = request->getIsRunning(); details["status"] = EntityScriptStatus_::valueToKey(request->getStatus()).toLower(); details["errorInfo"] = request->getErrorInfo(); QScriptValue err, result; if (!details["success"].toBool()) { if (!details.contains("message") && details.contains("errorInfo")) { details["message"] = details["errorInfo"]; } if (details["message"].toString().isEmpty()) { details["message"] = "entity server script details not found"; } err = engine->makeError(engine->toScriptValue(details)); } else { result = engine->toScriptValue(details); } callScopedHandlerObject(handler, err, result); request->deleteLater(); }); request->start(); return true; } bool EntityScriptingInterface::queryPropertyMetadata(QUuid entityID, QScriptValue property, QScriptValue scopeOrCallback, QScriptValue methodOrName) { auto name = property.toString(); auto handler = makeScopedHandlerObject(scopeOrCallback, methodOrName); QPointer engine = dynamic_cast(handler.engine()); if (!engine) { qCDebug(entities) << "queryPropertyMetadata without detectable engine" << entityID << name; return false; } #ifdef DEBUG_ENGINE_STATE connect(engine, &QObject::destroyed, this, [=]() { qDebug() << "queryPropertyMetadata -- engine destroyed!" << (!engine ? "nullptr" : "engine"); }); #endif if (!handler.property("callback").isFunction()) { qDebug() << "!handler.callback.isFunction" << engine; engine->raiseException(engine->makeError("callback is not a function", "TypeError")); return false; } // NOTE: this approach is a work-in-progress and for now just meant to work 100% correctly and provide // some initial structure for organizing metadata adapters around. // The extra layer of indirection is *essential* because in real world conditions errors are often introduced // by accident and sometimes without exact memory of "what just changed." // Here the scripter only needs to know an entityID and a property name -- which means all scripters can // level this method when stuck in dead-end scenarios or to learn more about "magic" Entity properties // like .script that work in terms of side-effects. // This is an async callback pattern -- so if needed C++ can easily throttle or restrict queries later. EntityPropertyMetadataRequest request(engine); if (name == "script") { return request.script(entityID, handler); } else if (name == "serverScripts") { return request.serverScripts(entityID, handler); } else { engine->raiseException(engine->makeError("metadata for property " + name + " is not yet queryable")); engine->maybeEmitUncaughtException(__FUNCTION__); return false; } } bool EntityScriptingInterface::getServerScriptStatus(QUuid entityID, QScriptValue callback) { auto client = DependencyManager::get(); auto request = client->createScriptStatusRequest(entityID); connect(request, &GetScriptStatusRequest::finished, callback.engine(), [callback](GetScriptStatusRequest* request) mutable { QString statusString = EntityScriptStatus_::valueToKey(request->getStatus());; QScriptValueList args { request->getResponseReceived(), request->getIsRunning(), statusString.toLower(), request->getErrorInfo() }; callback.call(QScriptValue(), args); request->deleteLater(); }); request->start(); return true; } void EntityScriptingInterface::setLightsArePickable(bool value) { LightEntityItem::setLightsArePickable(value); } bool EntityScriptingInterface::getLightsArePickable() const { return LightEntityItem::getLightsArePickable(); } void EntityScriptingInterface::setZonesArePickable(bool value) { ZoneEntityItem::setZonesArePickable(value); } bool EntityScriptingInterface::getZonesArePickable() const { return ZoneEntityItem::getZonesArePickable(); } void EntityScriptingInterface::setDrawZoneBoundaries(bool value) { ZoneEntityItem::setDrawZoneBoundaries(value); } bool EntityScriptingInterface::getDrawZoneBoundaries() const { return ZoneEntityItem::getDrawZoneBoundaries(); } RayToEntityIntersectionResult::RayToEntityIntersectionResult() : intersects(false), accurate(true), // assume it's accurate entityID(), distance(0), face() { } QScriptValue RayToEntityIntersectionResultToScriptValue(QScriptEngine* engine, const RayToEntityIntersectionResult& value) { PROFILE_RANGE(script_entities, __FUNCTION__); QScriptValue obj = engine->newObject(); obj.setProperty("intersects", value.intersects); obj.setProperty("accurate", value.accurate); QScriptValue entityItemValue = EntityItemIDtoScriptValue(engine, value.entityID); obj.setProperty("entityID", entityItemValue); obj.setProperty("distance", value.distance); QString faceName = ""; // handle BoxFace /**jsdoc *

A BoxFace specifies the face of an axis-aligned (AA) box. * * * * * * * * * * * * * *
ValueDescription
"MIN_X_FACE"The minimum x-axis face.
"MAX_X_FACE"The maximum x-axis face.
"MIN_Y_FACE"The minimum y-axis face.
"MAX_Y_FACE"The maximum y-axis face.
"MIN_Z_FACE"The minimum z-axis face.
"MAX_Z_FACE"The maximum z-axis face.
"UNKNOWN_FACE"Unknown value.
* @typedef {string} BoxFace */ // FIXME: Move enum to string function to BoxBase.cpp. switch (value.face) { case MIN_X_FACE: faceName = "MIN_X_FACE"; break; case MAX_X_FACE: faceName = "MAX_X_FACE"; break; case MIN_Y_FACE: faceName = "MIN_Y_FACE"; break; case MAX_Y_FACE: faceName = "MAX_Y_FACE"; break; case MIN_Z_FACE: faceName = "MIN_Z_FACE"; break; case MAX_Z_FACE: faceName = "MAX_Z_FACE"; break; case UNKNOWN_FACE: faceName = "UNKNOWN_FACE"; break; } obj.setProperty("face", faceName); QScriptValue intersection = vec3toScriptValue(engine, value.intersection); obj.setProperty("intersection", intersection); QScriptValue surfaceNormal = vec3toScriptValue(engine, value.surfaceNormal); obj.setProperty("surfaceNormal", surfaceNormal); obj.setProperty("extraInfo", engine->toScriptValue(value.extraInfo)); return obj; } void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, RayToEntityIntersectionResult& value) { PROFILE_RANGE(script_entities, __FUNCTION__); value.intersects = object.property("intersects").toVariant().toBool(); value.accurate = object.property("accurate").toVariant().toBool(); QScriptValue entityIDValue = object.property("entityID"); // EntityItemIDfromScriptValue(entityIDValue, value.entityID); quuidFromScriptValue(entityIDValue, value.entityID); value.distance = object.property("distance").toVariant().toFloat(); QString faceName = object.property("face").toVariant().toString(); if (faceName == "MIN_X_FACE") { value.face = MIN_X_FACE; } else if (faceName == "MAX_X_FACE") { value.face = MAX_X_FACE; } else if (faceName == "MIN_Y_FACE") { value.face = MIN_Y_FACE; } else if (faceName == "MAX_Y_FACE") { value.face = MAX_Y_FACE; } else if (faceName == "MIN_Z_FACE") { value.face = MIN_Z_FACE; } else { value.face = MAX_Z_FACE; }; QScriptValue intersection = object.property("intersection"); if (intersection.isValid()) { vec3FromScriptValue(intersection, value.intersection); } QScriptValue surfaceNormal = object.property("surfaceNormal"); if (surfaceNormal.isValid()) { vec3FromScriptValue(surfaceNormal, value.surfaceNormal); } value.extraInfo = object.property("extraInfo").toVariant().toMap(); } bool EntityScriptingInterface::polyVoxWorker(QUuid entityID, std::function actor) { PROFILE_RANGE(script_entities, __FUNCTION__); if (!_entityTree) { return false; } EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (!entity) { qCDebug(entities) << "EntityScriptingInterface::setVoxels no entity with ID" << entityID; return false; } EntityTypes::EntityType entityType = entity->getType(); if (entityType != EntityTypes::PolyVox) { return false; } auto polyVoxEntity = std::dynamic_pointer_cast(entity); bool result; _entityTree->withWriteLock([&] { result = actor(*polyVoxEntity); }); return result; } bool EntityScriptingInterface::setPoints(QUuid entityID, std::function actor) { PROFILE_RANGE(script_entities, __FUNCTION__); if (!_entityTree) { return false; } EntityItemPointer entity = static_cast(_entityTree->findEntityByEntityItemID(entityID)); if (!entity) { qCDebug(entities) << "EntityScriptingInterface::setPoints no entity with ID" << entityID; } EntityTypes::EntityType entityType = entity->getType(); if (entityType != EntityTypes::Line) { return false; } auto now = usecTimestampNow(); auto lineEntity = std::static_pointer_cast(entity); bool success; _entityTree->withWriteLock([&] { success = actor(*lineEntity); entity->setLastEdited(now); entity->setLastBroadcast(now); }); EntityItemProperties properties; _entityTree->withReadLock([&] { properties = entity->getProperties(); }); properties.setLinePointsDirty(); properties.setLastEdited(now); queueEntityMessage(PacketType::EntityEdit, entityID, properties); return success; } bool EntityScriptingInterface::setVoxelSphere(QUuid entityID, const glm::vec3& center, float radius, int value) { PROFILE_RANGE(script_entities, __FUNCTION__); return polyVoxWorker(entityID, [center, radius, value](PolyVoxEntityItem& polyVoxEntity) { return polyVoxEntity.setSphere(center, radius, value); }); } bool EntityScriptingInterface::setVoxelCapsule(QUuid entityID, const glm::vec3& start, const glm::vec3& end, float radius, int value) { PROFILE_RANGE(script_entities, __FUNCTION__); return polyVoxWorker(entityID, [start, end, radius, value](PolyVoxEntityItem& polyVoxEntity) { return polyVoxEntity.setCapsule(start, end, radius, value); }); } bool EntityScriptingInterface::setVoxel(QUuid entityID, const glm::vec3& position, int value) { PROFILE_RANGE(script_entities, __FUNCTION__); return polyVoxWorker(entityID, [position, value](PolyVoxEntityItem& polyVoxEntity) { return polyVoxEntity.setVoxelInVolume(position, value); }); } bool EntityScriptingInterface::setAllVoxels(QUuid entityID, int value) { PROFILE_RANGE(script_entities, __FUNCTION__); return polyVoxWorker(entityID, [value](PolyVoxEntityItem& polyVoxEntity) { return polyVoxEntity.setAll(value); }); } bool EntityScriptingInterface::setVoxelsInCuboid(QUuid entityID, const glm::vec3& lowPosition, const glm::vec3& cuboidSize, int value) { PROFILE_RANGE(script_entities, __FUNCTION__); return polyVoxWorker(entityID, [lowPosition, cuboidSize, value](PolyVoxEntityItem& polyVoxEntity) { return polyVoxEntity.setCuboid(lowPosition, cuboidSize, value); }); } bool EntityScriptingInterface::setAllPoints(QUuid entityID, const QVector& points) { PROFILE_RANGE(script_entities, __FUNCTION__); EntityItemPointer entity = static_cast(_entityTree->findEntityByEntityItemID(entityID)); if (!entity) { qCDebug(entities) << "EntityScriptingInterface::setPoints no entity with ID" << entityID; } EntityTypes::EntityType entityType = entity->getType(); if (entityType == EntityTypes::Line) { return setPoints(entityID, [points](LineEntityItem& lineEntity) -> bool { return (LineEntityItem*)lineEntity.setLinePoints(points); }); } return false; } bool EntityScriptingInterface::appendPoint(QUuid entityID, const glm::vec3& point) { PROFILE_RANGE(script_entities, __FUNCTION__); EntityItemPointer entity = static_cast(_entityTree->findEntityByEntityItemID(entityID)); if (!entity) { qCDebug(entities) << "EntityScriptingInterface::setPoints no entity with ID" << entityID; } EntityTypes::EntityType entityType = entity->getType(); if (entityType == EntityTypes::Line) { return setPoints(entityID, [point](LineEntityItem& lineEntity) -> bool { return (LineEntityItem*)lineEntity.appendPoint(point); }); } return false; } bool EntityScriptingInterface::actionWorker(const QUuid& entityID, std::function actor) { if (!_entityTree) { return false; } auto nodeList = DependencyManager::get(); const QUuid myNodeID = nodeList->getSessionUUID(); EntityItemProperties properties; EntityItemPointer entity; bool doTransmit = false; _entityTree->withWriteLock([&] { EntitySimulationPointer simulation = _entityTree->getSimulation(); entity = _entityTree->findEntityByEntityItemID(entityID); if (!entity) { qCDebug(entities) << "actionWorker -- unknown entity" << entityID; return; } if (!simulation) { qCDebug(entities) << "actionWorker -- no simulation" << entityID; return; } if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) { return; } doTransmit = actor(simulation, entity); _entityTree->entityChanged(entity); if (doTransmit) { properties.setClientOnly(entity->getClientOnly()); properties.setOwningAvatarID(entity->getOwningAvatarID()); } }); // transmit the change if (doTransmit) { _entityTree->withReadLock([&] { properties = entity->getProperties(); }); properties.setActionDataDirty(); auto now = usecTimestampNow(); properties.setLastEdited(now); queueEntityMessage(PacketType::EntityEdit, entityID, properties); } return doTransmit; } QUuid EntityScriptingInterface::addAction(const QString& actionTypeString, const QUuid& entityID, const QVariantMap& arguments) { PROFILE_RANGE(script_entities, __FUNCTION__); QUuid actionID = QUuid::createUuid(); auto actionFactory = DependencyManager::get(); bool success = false; actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { // create this action even if the entity doesn't have physics info. it will often be the // case that a script adds an action immediately after an object is created, and the physicsInfo // is computed asynchronously. // if (!entity->getPhysicsInfo()) { // return false; // } EntityDynamicType dynamicType = EntityDynamicInterface::dynamicTypeFromString(actionTypeString); if (dynamicType == DYNAMIC_TYPE_NONE) { return false; } EntityDynamicPointer action = actionFactory->factory(dynamicType, actionID, entity, arguments); if (!action) { return false; } action->setIsMine(true); success = entity->addAction(simulation, action); entity->flagForOwnershipBid(SCRIPT_GRAB_SIMULATION_PRIORITY); return false; // Physics will cause a packet to be sent, so don't send from here. }); if (success) { return actionID; } return QUuid(); } bool EntityScriptingInterface::updateAction(const QUuid& entityID, const QUuid& actionID, const QVariantMap& arguments) { PROFILE_RANGE(script_entities, __FUNCTION__); return actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { bool success = entity->updateAction(simulation, actionID, arguments); if (success) { entity->flagForOwnershipBid(SCRIPT_GRAB_SIMULATION_PRIORITY); } return success; }); } bool EntityScriptingInterface::deleteAction(const QUuid& entityID, const QUuid& actionID) { PROFILE_RANGE(script_entities, __FUNCTION__); bool success = false; actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { success = entity->removeAction(simulation, actionID); if (success) { // reduce from grab to poke entity->flagForOwnershipBid(SCRIPT_POKE_SIMULATION_PRIORITY); } return false; // Physics will cause a packet to be sent, so don't send from here. }); return success; } QVector EntityScriptingInterface::getActionIDs(const QUuid& entityID) { PROFILE_RANGE(script_entities, __FUNCTION__); QVector result; actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { QList actionIDs = entity->getActionIDs(); result = QVector::fromList(actionIDs); return false; // don't send an edit packet }); return result; } QVariantMap EntityScriptingInterface::getActionArguments(const QUuid& entityID, const QUuid& actionID) { PROFILE_RANGE(script_entities, __FUNCTION__); QVariantMap result; actionWorker(entityID, [&](EntitySimulationPointer simulation, EntityItemPointer entity) { result = entity->getActionArguments(actionID); return false; // don't send an edit packet }); return result; } EntityItemPointer EntityScriptingInterface::checkForTreeEntityAndTypeMatch(const QUuid& entityID, EntityTypes::EntityType entityType) { if (!_entityTree) { return EntityItemPointer(); } EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID); if (!entity) { qCDebug(entities) << "EntityScriptingInterface::checkForTreeEntityAndTypeMatch - no entity with ID" << entityID; return entity; } if (entityType != EntityTypes::Unknown && entity->getType() != entityType) { return EntityItemPointer(); } return entity; } glm::vec3 EntityScriptingInterface::voxelCoordsToWorldCoords(const QUuid& entityID, glm::vec3 voxelCoords) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::PolyVox)) { auto polyVoxEntity = std::dynamic_pointer_cast(entity); return polyVoxEntity->voxelCoordsToWorldCoords(voxelCoords); } else { return glm::vec3(0.0f); } } glm::vec3 EntityScriptingInterface::worldCoordsToVoxelCoords(const QUuid& entityID, glm::vec3 worldCoords) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::PolyVox)) { auto polyVoxEntity = std::dynamic_pointer_cast(entity); return polyVoxEntity->worldCoordsToVoxelCoords(worldCoords); } else { return glm::vec3(0.0f); } } glm::vec3 EntityScriptingInterface::voxelCoordsToLocalCoords(const QUuid& entityID, glm::vec3 voxelCoords) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::PolyVox)) { auto polyVoxEntity = std::dynamic_pointer_cast(entity); return polyVoxEntity->voxelCoordsToLocalCoords(voxelCoords); } else { return glm::vec3(0.0f); } } glm::vec3 EntityScriptingInterface::localCoordsToVoxelCoords(const QUuid& entityID, glm::vec3 localCoords) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::PolyVox)) { auto polyVoxEntity = std::dynamic_pointer_cast(entity); return polyVoxEntity->localCoordsToVoxelCoords(localCoords); } else { return glm::vec3(0.0f); } } glm::vec3 EntityScriptingInterface::getAbsoluteJointTranslationInObjectFrame(const QUuid& entityID, int jointIndex) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) { auto modelEntity = std::dynamic_pointer_cast(entity); return modelEntity->getAbsoluteJointTranslationInObjectFrame(jointIndex); } else { return glm::vec3(0.0f); } } glm::quat EntityScriptingInterface::getAbsoluteJointRotationInObjectFrame(const QUuid& entityID, int jointIndex) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) { auto modelEntity = std::dynamic_pointer_cast(entity); return modelEntity->getAbsoluteJointRotationInObjectFrame(jointIndex); } else { return glm::quat(); } } bool EntityScriptingInterface::setAbsoluteJointTranslationInObjectFrame(const QUuid& entityID, int jointIndex, glm::vec3 translation) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) { auto now = usecTimestampNow(); auto modelEntity = std::dynamic_pointer_cast(entity); bool result = modelEntity->setAbsoluteJointTranslationInObjectFrame(jointIndex, translation); if (result) { EntityItemProperties properties; _entityTree->withWriteLock([&] { properties = entity->getProperties(); entity->setLastBroadcast(now); }); properties.setJointTranslationsDirty(); properties.setLastEdited(now); queueEntityMessage(PacketType::EntityEdit, entityID, properties); return true; } } return false; } bool EntityScriptingInterface::setAbsoluteJointRotationInObjectFrame(const QUuid& entityID, int jointIndex, glm::quat rotation) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) { auto now = usecTimestampNow(); auto modelEntity = std::dynamic_pointer_cast(entity); bool result = modelEntity->setAbsoluteJointRotationInObjectFrame(jointIndex, rotation); if (result) { EntityItemProperties properties; _entityTree->withWriteLock([&] { properties = entity->getProperties(); entity->setLastBroadcast(now); }); properties.setJointRotationsDirty(); properties.setLastEdited(now); queueEntityMessage(PacketType::EntityEdit, entityID, properties); return true; } } return false; } glm::vec3 EntityScriptingInterface::getLocalJointTranslation(const QUuid& entityID, int jointIndex) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) { auto modelEntity = std::dynamic_pointer_cast(entity); return modelEntity->getLocalJointTranslation(jointIndex); } else { return glm::vec3(0.0f); } } glm::quat EntityScriptingInterface::getLocalJointRotation(const QUuid& entityID, int jointIndex) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) { auto modelEntity = std::dynamic_pointer_cast(entity); return modelEntity->getLocalJointRotation(jointIndex); } else { return glm::quat(); } } bool EntityScriptingInterface::setLocalJointTranslation(const QUuid& entityID, int jointIndex, glm::vec3 translation) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) { auto now = usecTimestampNow(); auto modelEntity = std::dynamic_pointer_cast(entity); bool result = modelEntity->setLocalJointTranslation(jointIndex, translation); if (result) { EntityItemProperties properties; _entityTree->withWriteLock([&] { properties = entity->getProperties(); entity->setLastBroadcast(now); }); properties.setJointTranslationsDirty(); properties.setLastEdited(now); queueEntityMessage(PacketType::EntityEdit, entityID, properties); return true; } } return false; } bool EntityScriptingInterface::setLocalJointRotation(const QUuid& entityID, int jointIndex, glm::quat rotation) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) { auto now = usecTimestampNow(); auto modelEntity = std::dynamic_pointer_cast(entity); bool result = modelEntity->setLocalJointRotation(jointIndex, rotation); if (result) { EntityItemProperties properties; _entityTree->withWriteLock([&] { properties = entity->getProperties(); entity->setLastBroadcast(now); }); properties.setJointRotationsDirty(); properties.setLastEdited(now); queueEntityMessage(PacketType::EntityEdit, entityID, properties); return true; } } return false; } bool EntityScriptingInterface::setLocalJointRotations(const QUuid& entityID, const QVector& rotations) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) { auto now = usecTimestampNow(); auto modelEntity = std::dynamic_pointer_cast(entity); bool result = false; for (int index = 0; index < rotations.size(); index++) { result |= modelEntity->setLocalJointRotation(index, rotations[index]); } if (result) { EntityItemProperties properties; _entityTree->withWriteLock([&] { entity->setLastEdited(now); entity->setLastBroadcast(now); properties = entity->getProperties(); }); properties.setJointRotationsDirty(); properties.setLastEdited(now); queueEntityMessage(PacketType::EntityEdit, entityID, properties); return true; } } return false; } bool EntityScriptingInterface::setLocalJointTranslations(const QUuid& entityID, const QVector& translations) { if (auto entity = checkForTreeEntityAndTypeMatch(entityID, EntityTypes::Model)) { auto now = usecTimestampNow(); auto modelEntity = std::dynamic_pointer_cast(entity); bool result = false; for (int index = 0; index < translations.size(); index++) { result |= modelEntity->setLocalJointTranslation(index, translations[index]); } if (result) { EntityItemProperties properties; _entityTree->withWriteLock([&] { entity->setLastEdited(now); entity->setLastBroadcast(now); properties = entity->getProperties(); }); properties.setJointTranslationsDirty(); properties.setLastEdited(now); queueEntityMessage(PacketType::EntityEdit, entityID, properties); return true; } } return false; } bool EntityScriptingInterface::setLocalJointsData(const QUuid& entityID, const QVector& rotations, const QVector& translations) { // for a model with 80 joints, sending both these in one edit packet causes the packet to be too large. return setLocalJointRotations(entityID, rotations) || setLocalJointTranslations(entityID, translations); } int EntityScriptingInterface::getJointIndex(const QUuid& entityID, const QString& name) { if (!_entityTree) { return -1; } int result; BLOCKING_INVOKE_METHOD(_entityTree.get(), "getJointIndex", Q_RETURN_ARG(int, result), Q_ARG(QUuid, entityID), Q_ARG(QString, name)); return result; } QStringList EntityScriptingInterface::getJointNames(const QUuid& entityID) { if (!_entityTree) { return QStringList(); } QStringList result; BLOCKING_INVOKE_METHOD(_entityTree.get(), "getJointNames", Q_RETURN_ARG(QStringList, result), Q_ARG(QUuid, entityID)); return result; } QVector EntityScriptingInterface::getChildrenIDs(const QUuid& parentID) { QVector result; if (!_entityTree) { return result; } _entityTree->withReadLock([&] { QSharedPointer parentFinder = DependencyManager::get(); if (!parentFinder) { return; } bool success; SpatiallyNestableWeakPointer parentWP = parentFinder->find(parentID, success); if (!success) { return; } SpatiallyNestablePointer parent = parentWP.lock(); if (!parent) { return; } parent->forEachChild([&](SpatiallyNestablePointer child) { result.push_back(child->getID()); }); }); return result; } bool EntityScriptingInterface::isChildOfParent(QUuid childID, QUuid parentID) { bool isChild = false; if (!_entityTree) { return isChild; } _entityTree->withReadLock([&] { EntityItemPointer parent = _entityTree->findEntityByEntityItemID(parentID); if (parent) { parent->forEachDescendant([&](SpatiallyNestablePointer descendant) { if (descendant->getID() == childID) { isChild = true; return; } }); } }); return isChild; } QString EntityScriptingInterface::getNestableType(QUuid id) { QSharedPointer parentFinder = DependencyManager::get(); if (!parentFinder) { return "unknown"; } bool success; SpatiallyNestableWeakPointer objectWP = parentFinder->find(id, success); if (!success) { return "unknown"; } SpatiallyNestablePointer object = objectWP.lock(); if (!object) { return "unknown"; } NestableType nestableType = object->getNestableType(); return SpatiallyNestable::nestableTypeToString(nestableType); } QVector EntityScriptingInterface::getChildrenIDsOfJoint(const QUuid& parentID, int jointIndex) { QVector result; if (!_entityTree) { return result; } _entityTree->withReadLock([&] { QSharedPointer parentFinder = DependencyManager::get(); if (!parentFinder) { return; } bool success; SpatiallyNestableWeakPointer parentWP = parentFinder->find(parentID, success); if (!success) { return; } SpatiallyNestablePointer parent = parentWP.lock(); if (!parent) { return; } parent->forEachChild([&](SpatiallyNestablePointer child) { if (child->getParentJointIndex() == jointIndex) { result.push_back(child->getID()); } }); }); return result; } QUuid EntityScriptingInterface::getKeyboardFocusEntity() const { QUuid result; QMetaObject::invokeMethod(qApp, "getKeyboardFocusEntity", Qt::DirectConnection, Q_RETURN_ARG(QUuid, result)); return result; } void EntityScriptingInterface::setKeyboardFocusEntity(const EntityItemID& id) { QMetaObject::invokeMethod(qApp, "setKeyboardFocusEntity", Qt::DirectConnection, Q_ARG(EntityItemID, id)); } void EntityScriptingInterface::sendMousePressOnEntity(const EntityItemID& id, const PointerEvent& event) { emit mousePressOnEntity(id, event); } void EntityScriptingInterface::sendMouseMoveOnEntity(const EntityItemID& id, const PointerEvent& event) { emit mouseMoveOnEntity(id, event); } void EntityScriptingInterface::sendMouseReleaseOnEntity(const EntityItemID& id, const PointerEvent& event) { emit mouseReleaseOnEntity(id, event); } void EntityScriptingInterface::sendClickDownOnEntity(const EntityItemID& id, const PointerEvent& event) { emit clickDownOnEntity(id, event); } void EntityScriptingInterface::sendHoldingClickOnEntity(const EntityItemID& id, const PointerEvent& event) { emit holdingClickOnEntity(id, event); } void EntityScriptingInterface::sendClickReleaseOnEntity(const EntityItemID& id, const PointerEvent& event) { emit clickReleaseOnEntity(id, event); } void EntityScriptingInterface::sendHoverEnterEntity(const EntityItemID& id, const PointerEvent& event) { emit hoverEnterEntity(id, event); } void EntityScriptingInterface::sendHoverOverEntity(const EntityItemID& id, const PointerEvent& event) { emit hoverOverEntity(id, event); } void EntityScriptingInterface::sendHoverLeaveEntity(const EntityItemID& id, const PointerEvent& event) { emit hoverLeaveEntity(id, event); } bool EntityScriptingInterface::wantsHandControllerPointerEvents(QUuid id) { bool result = false; if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(id)); if (entity) { result = entity->wantsHandControllerPointerEvents(); } }); } return result; } void EntityScriptingInterface::emitScriptEvent(const EntityItemID& entityID, const QVariant& message) { if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (entity) { entity->emitScriptEvent(message); } }); } } // TODO move this someplace that makes more sense... bool EntityScriptingInterface::AABoxIntersectsCapsule(const glm::vec3& low, const glm::vec3& dimensions, const glm::vec3& start, const glm::vec3& end, float radius) { glm::vec3 penetration; AABox aaBox(low, dimensions); return aaBox.findCapsulePenetration(start, end, radius, penetration); } void EntityScriptingInterface::getMeshes(QUuid entityID, QScriptValue callback) { PROFILE_RANGE(script_entities, __FUNCTION__); EntityItemPointer entity = static_cast(_entityTree->findEntityByEntityItemID(entityID)); if (!entity) { qCDebug(entities) << "EntityScriptingInterface::getMeshes no entity with ID" << entityID; QScriptValueList args { callback.engine()->undefinedValue(), false }; callback.call(QScriptValue(), args); return; } MeshProxyList result; bool success = entity->getMeshes(result); if (success) { QScriptValue resultAsScriptValue = meshesToScriptValue(callback.engine(), result); QScriptValueList args { resultAsScriptValue, true }; callback.call(QScriptValue(), args); } else { QScriptValueList args { callback.engine()->undefinedValue(), false }; callback.call(QScriptValue(), args); } } glm::mat4 EntityScriptingInterface::getEntityTransform(const QUuid& entityID) { glm::mat4 result; if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (entity) { glm::mat4 translation = glm::translate(entity->getWorldPosition()); glm::mat4 rotation = glm::mat4_cast(entity->getWorldOrientation()); result = translation * rotation; } }); } return result; } glm::mat4 EntityScriptingInterface::getEntityLocalTransform(const QUuid& entityID) { glm::mat4 result; if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (entity) { glm::mat4 translation = glm::translate(entity->getLocalPosition()); glm::mat4 rotation = glm::mat4_cast(entity->getLocalOrientation()); result = translation * rotation; } }); } return result; } QString EntityScriptingInterface::getStaticCertificateJSON(const QUuid& entityID) { QByteArray result; if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (entity) { result = entity->getProperties().getStaticCertificateJSON(); } }); } return result; } bool EntityScriptingInterface::verifyStaticCertificateProperties(const QUuid& entityID) { bool result = false; if (_entityTree) { _entityTree->withReadLock([&] { EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); if (entity) { result = entity->getProperties().verifyStaticCertificateProperties(); } }); } return result; }