2638 lines · 117.5 KB · C++
RawDownload
////  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 <glm/gtc/matrix_transform.hpp>#include <glm/gtx/transform.hpp> #include <QFutureWatcher>#include <QtConcurrent/QtConcurrentRun>#include <QJsonObject>#include <QJsonDocument>#include <QJsonArray> #include <shared/QtHelpers.h>#include <VariantMapToScriptValue.h>#include <SharedUtil.h>#include <SpatialParentFinder.h>#include <AvatarHashMap.h> #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 <EntityScriptClient.h>#include <Profile.h>#include "GrabPropertyGroup.h"#include <ScriptContext.h>#include <ScriptEngineCast.h>#include <ScriptValue.h> const QString GRABBABLE_USER_DATA = "{\"grabbableKey\":{\"grabbable\":true}}";const QString NOT_GRABBABLE_USER_DATA = "{\"grabbableKey\":{\"grabbable\":false}}"; void staticEntityScriptInitializer(ScriptManager* manager) {    auto scriptEngine = manager->engine().get();     auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();    entityScriptingInterface->init();    auto ifacePtr = entityScriptingInterface.data(); // using this when we don't want to leak a reference     registerMetaTypes(scriptEngine);     scriptRegisterMetaType(scriptEngine, EntityPropertyFlagsToScriptValue, EntityPropertyFlagsFromScriptValue);    scriptRegisterMetaType(scriptEngine, EntityItemPropertiesToScriptValue, EntityItemPropertiesFromScriptValueHonorReadOnly);    scriptRegisterMetaType(scriptEngine, EntityPropertyInfoToScriptValue, EntityPropertyInfoFromScriptValue);    scriptRegisterMetaType(scriptEngine, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue);    scriptRegisterMetaType(scriptEngine, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue);     scriptEngine->registerGlobalObject("Entities", entityScriptingInterface.data());    scriptEngine->registerFunction("Entities", "getMultipleEntityProperties", EntityScriptingInterface::getMultipleEntityProperties);     // "The return value of QObject::sender() is not valid when the slot is called via a Qt::DirectConnection from a thread    // different from this object's thread. Do not use this function in this type of scenario."    // so... yay lambdas everywhere to get the sender    manager->connect(        manager, &ScriptManager::attachDefaultEventHandlers, entityScriptingInterface.data(),        [ifacePtr, manager] { ifacePtr->attachDefaultEventHandlers(manager); },        Qt::DirectConnection);    manager->connect(manager, &ScriptManager::releaseEntityPacketSenderMessages, entityScriptingInterface.data(),                     &EntityScriptingInterface::releaseEntityPacketSenderMessages, Qt::DirectConnection);}STATIC_SCRIPT_INITIALIZER(staticEntityScriptInitializer); EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership) :    _entityTree(nullptr),    _bidOnSimulationOwnership(bidOnSimulationOwnership){    auto nodeList = DependencyManager::get<NodeList>();    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);    connect(nodeList.data(), &NodeList::canGetAndSetPrivateUserDataChanged, this, &EntityScriptingInterface::canGetAndSetPrivateUserDataChanged);    connect(nodeList.data(), &NodeList::canRezAvatarEntitiesChanged, this, &EntityScriptingInterface::canRezAvatarEntitiesChanged);     auto& packetReceiver = nodeList->getPacketReceiver();    packetReceiver.registerListener(PacketType::EntityScriptCallMethod,        PacketReceiver::makeSourcedListenerReference<EntityScriptingInterface>(this, &EntityScriptingInterface::handleEntityScriptCallMethodPacket));} void EntityScriptingInterface::releaseEntityPacketSenderMessages(bool wait) {    EntityEditPacketSender* entityPacketSender = getEntityPacketSender();    if (entityPacketSender->serversExist()) {        // release the queue of edit entity messages.        entityPacketSender->releaseQueuedMessages();         // since we're in non-threaded mode, call process so that the packets are sent        if (!entityPacketSender->isThreaded()) {            if (!wait) {                entityPacketSender->process();            } else {                // wait here till the edit packet sender is completely done sending                while (entityPacketSender->hasPacketsToSend()) {                    entityPacketSender->process();                    QCoreApplication::processEvents();                }            }        } else {            // FIXME - do we need to have a similar "wait here" loop for non-threaded packet senders?        }    }}  void EntityScriptingInterface::attachDefaultEventHandlers(ScriptManager* manager) {    // Connect up ALL the handlers to the global entities object's signals.    // (We could go signal by signal, or even handler by handler, but I don't think the efficiency is worth the complexity.)        // Bug? These handlers are deleted when entityID is deleted, which is nice.    // But if they are created by an entity script on a different entity, should they also be deleted when the entity script unloads?    // E.g., suppose a bow has an entity script that causes arrows to be created with a potential lifetime greater than the bow,    // and that the entity script adds (e.g., collision) handlers to the arrows. Should those handlers fire if the bow is unloaded?    // Also, what about when the entity script is REloaded?    // For now, we are leaving them around. Changing that would require some non-trivial digging around to find the    // handlers that were added while a given currentEntityIdentifier was in place. I don't think this is dangerous. Just perhaps unexpected. -HRS    connect(this, &EntityScriptingInterface::deletingEntity, manager,            [manager](const EntityItemID& entityID) { manager->removeAllEventHandlers(entityID); });     // Two common cases of event handler, differing only in argument signature.     /*@jsdoc         * Called when an entity event occurs on an entity as registered with {@link Script.addEventHandler}.         * @callback Script~entityEventCallback         * @param {Uuid} entityID - The ID of the entity the event has occured on.         */    using SingleEntityHandler = std::function<void(const EntityItemID&)>;    auto makeSingleEntityHandler = [manager](QString eventName) -> SingleEntityHandler {        return [manager, eventName](const EntityItemID& entityItemID) {            manager->forwardHandlerCall(entityItemID, eventName,                                        { EntityItemIDtoScriptValue(manager->engine().get(), entityItemID) });        };    };     /*@jsdoc         * Called when a pointer event occurs on an entity as registered with {@link Script.addEventHandler}.         * @callback Script~pointerEventCallback         * @param {Uuid} entityID - The ID of the entity the event has occurred on.         * @param {PointerEvent} pointerEvent - Details of the event.         */    using PointerHandler = std::function<void(const EntityItemID&, const PointerEvent&)>;    auto makePointerHandler = [manager](QString eventName) -> PointerHandler {        return [manager, eventName](const EntityItemID& entityItemID, const PointerEvent& event) {            if (!EntityTree::areEntityClicksCaptured()) {                ScriptEngine* engine = manager->engine().get();                manager->forwardHandlerCall(entityItemID, eventName,                                            { EntityItemIDtoScriptValue(engine, entityItemID), event.toScriptValue(engine) });            }        };    };     /*@jsdoc         * Called when a collision event occurs on an entity as registered with {@link Script.addEventHandler}.         * @callback Script~collisionEventCallback         * @param {Uuid} entityA - The ID of one entity in the collision.         * @param {Uuid} entityB - The ID of the other entity in the collision.         * @param {Collision} collisionEvent - Details of the collision.         */    using CollisionHandler = std::function<void(const EntityItemID&, const EntityItemID&, const Collision&)>;    auto makeCollisionHandler = [manager](QString eventName) -> CollisionHandler {        return [manager, eventName](const EntityItemID& idA, const EntityItemID& idB, const Collision& collision) {            ScriptEngine* engine = manager->engine().get();            manager->forwardHandlerCall(idA, eventName,                                        { EntityItemIDtoScriptValue(engine, idA),                                          EntityItemIDtoScriptValue(engine, idB),                                          collisionToScriptValue(engine, collision) });        };    };     /*@jsdoc         * <p>The name of an entity event. When the entity event occurs, any function that has been registered for that event          * via {@link Script.addEventHandler} is called with parameters per the entity event.</p>         * <table>         *   <thead>         *     <tr><th>Event Name</th><th>Callback Type</th><th>Entity Event</th></tr>         *   </thead>         *   <tbody>         *     <tr><td><code>"enterEntity"</code></td><td>{@link Script~entityEventCallback|entityEventCallback}</td>         *       <td>{@link Entities.enterEntity}</td></tr>         *     <tr><td><code>"leaveEntity"</code></td><td>{@link Script~entityEventCallback|entityEventCallback}</td>         *       <td>{@link Entities.leaveEntity}</td></tr>         *     <tr><td><code>"mousePressOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td>         *       <td>{@link Entities.mousePressOnEntity}</td></tr>         *     <tr><td><code>"mouseMoveOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td>         *       <td>{@link Entities.mouseMoveOnEntity}</td></tr>         *     <tr><td><code>"mouseReleaseOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td>         *       <td>{@link Entities.mouseReleaseOnEntity}</td></tr>         *     <tr><td><code>"clickDownOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td>         *       <td>{@link Entities.clickDownOnEntity}</td></tr>         *     <tr><td><code>"holdingClickOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td>         *       <td>{@link Entities.holdingClickOnEntity}</td></tr>         *     <tr><td><code>"clickReleaseOnEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td>         *       <td>{@link Entities.clickReleaseOnEntity}</td></tr>         *     <tr><td><code>"hoverEnterEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td>         *       <td>{@link Entities.hoverEnterEntity}</td></tr>         *     <tr><td><code>"hoverOverEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td>         *       <td>{@link Entities.hoverOverEntity}</td></tr>         *     <tr><td><code>"hoverLeaveEntity"</code></td><td>{@link Script~pointerEventCallback|pointerEventCallback}</td>         *       <td>{@link Entities.hoverLeaveEntity}</td></tr>         *     <tr><td><code>"collisionWithEntity"</code><td>{@link Script~collisionEventCallback|collisionEventCallback}</td>         *       </td><td>{@link Entities.collisionWithEntity}</td></tr>         *   </tbody>         * </table>         * @typedef {string} Script.EntityEvent         */    connect(this, &EntityScriptingInterface::enterEntity, manager, makeSingleEntityHandler("enterEntity"));    connect(this, &EntityScriptingInterface::leaveEntity, manager, makeSingleEntityHandler("leaveEntity"));     connect(this, &EntityScriptingInterface::mousePressOnEntity, manager, makePointerHandler("mousePressOnEntity"));    connect(this, &EntityScriptingInterface::mouseMoveOnEntity, manager, makePointerHandler("mouseMoveOnEntity"));    connect(this, &EntityScriptingInterface::mouseReleaseOnEntity, manager, makePointerHandler("mouseReleaseOnEntity"));     connect(this, &EntityScriptingInterface::clickDownOnEntity, manager, makePointerHandler("clickDownOnEntity"));    connect(this, &EntityScriptingInterface::holdingClickOnEntity, manager, makePointerHandler("holdingClickOnEntity"));    connect(this, &EntityScriptingInterface::clickReleaseOnEntity, manager, makePointerHandler("clickReleaseOnEntity"));     connect(this, &EntityScriptingInterface::hoverEnterEntity, manager, makePointerHandler("hoverEnterEntity"));    connect(this, &EntityScriptingInterface::hoverOverEntity, manager, makePointerHandler("hoverOverEntity"));    connect(this, &EntityScriptingInterface::hoverLeaveEntity, manager, makePointerHandler("hoverLeaveEntity"));     connect(this, &EntityScriptingInterface::collisionWithEntity, manager, makeCollisionHandler("collisionWithEntity"));} 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<NodeList>();    return nodeList->isAllowedEditor();} bool EntityScriptingInterface::canRez() {    auto nodeList = DependencyManager::get<NodeList>();    return nodeList->getThisNodeCanRez();} bool EntityScriptingInterface::canRezTmp() {    auto nodeList = DependencyManager::get<NodeList>();    return nodeList->getThisNodeCanRezTmp();} bool EntityScriptingInterface::canRezCertified() {    auto nodeList = DependencyManager::get<NodeList>();    return nodeList->getThisNodeCanRezCertified();} bool EntityScriptingInterface::canRezTmpCertified() {    auto nodeList = DependencyManager::get<NodeList>();    return nodeList->getThisNodeCanRezTmpCertified();} bool EntityScriptingInterface::canWriteAssets() {    auto nodeList = DependencyManager::get<NodeList>();    return nodeList->getThisNodeCanWriteAssets();} bool EntityScriptingInterface::canReplaceContent() {    auto nodeList = DependencyManager::get<NodeList>();    return nodeList->getThisNodeCanReplaceContent();} bool EntityScriptingInterface::canGetAndSetPrivateUserData() {    auto nodeList = DependencyManager::get<NodeList>();    return nodeList->getThisNodeCanGetAndSetPrivateUserData();} bool EntityScriptingInterface::canRezAvatarEntities() {    auto nodeList = DependencyManager::get<NodeList>();    return nodeList->getThisNodeCanRezAvatarEntities();} void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) {    if (_entityTree) {        disconnect(_entityTree.get(), &EntityTree::addingEntityPointer, this, &EntityScriptingInterface::onAddingEntity);        disconnect(_entityTree.get(), &EntityTree::deletingEntityPointer, this, &EntityScriptingInterface::onDeletingEntity);        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::addingEntityPointer, this, &EntityScriptingInterface::onAddingEntity, Qt::DirectConnection);        connect(_entityTree.get(), &EntityTree::deletingEntityPointer, this, &EntityScriptingInterface::onDeletingEntity, Qt::DirectConnection);        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.getVelocity());    scriptSideProperties.setLocalAngularVelocity(entitySideProperties.getAngularVelocity());    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;}  // TODO: this method looks expensive and should take properties by reference, update it, and return voidEntityItemProperties 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;     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;}  void synchronizeSpatialKey(const GrabPropertyGroup& grabProperties, QJsonObject& grabbableKey, bool& userDataChanged) {    if (grabProperties.equippableLeftPositionChanged() ||        grabProperties.equippableRightPositionChanged() ||        grabProperties.equippableRightRotationChanged() ||        grabProperties.equippableIndicatorURLChanged() ||        grabProperties.equippableIndicatorScaleChanged() ||        grabProperties.equippableIndicatorOffsetChanged()) {         QJsonObject spatialKey = grabbableKey["spatialKey"].toObject();         if (grabProperties.equippableLeftPositionChanged()) {            if (grabProperties.getEquippableLeftPosition() == INITIAL_LEFT_EQUIPPABLE_POSITION) {                spatialKey.remove("leftRelativePosition");            } else {                spatialKey["leftRelativePosition"] =                    QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableLeftPosition()));            }        }        if (grabProperties.equippableRightPositionChanged()) {            if (grabProperties.getEquippableRightPosition() == INITIAL_RIGHT_EQUIPPABLE_POSITION) {                spatialKey.remove("rightRelativePosition");            } else {                spatialKey["rightRelativePosition"] =                    QJsonValue::fromVariant(vec3ToQMap(grabProperties.getEquippableRightPosition()));            }        }        if (grabProperties.equippableLeftRotationChanged()) {            spatialKey["relativeRotation"] =                QJsonValue::fromVariant(quatToQMap(grabProperties.getEquippableLeftRotation()));        } else if (grabProperties.equippableRightRotationChanged()) {            spatialKey["relativeRotation"] =                QJsonValue::fromVariant(quatToQMap(grabProperties.getEquippableRightRotation()));        }         grabbableKey["spatialKey"] = spatialKey;        userDataChanged = true;    }}  void synchronizeGrabbableKey(const GrabPropertyGroup& grabProperties, QJsonObject& userData, bool& userDataChanged) {    if (grabProperties.triggerableChanged() ||        grabProperties.grabbableChanged() ||        grabProperties.grabFollowsControllerChanged() ||        grabProperties.grabKinematicChanged() ||        grabProperties.equippableChanged() ||        grabProperties.equippableLeftPositionChanged() ||        grabProperties.equippableRightPositionChanged() ||        grabProperties.equippableRightRotationChanged()) {         QJsonObject grabbableKey = userData["grabbableKey"].toObject();         if (grabProperties.triggerableChanged()) {            if (grabProperties.getTriggerable()) {                grabbableKey["triggerable"] = true;            } else {                grabbableKey.remove("triggerable");            }        }        if (grabProperties.grabbableChanged()) {            if (grabProperties.getGrabbable()) {                grabbableKey.remove("grabbable");            } else {                grabbableKey["grabbable"] = false;            }        }        if (grabProperties.grabFollowsControllerChanged()) {            if (grabProperties.getGrabFollowsController()) {                grabbableKey.remove("ignoreIK");            } else {

Lines 1–500 of 2638.