diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp
index f7f8e8a9c1..cb90df58e5 100644
--- a/assignment-client/src/audio/AudioMixerSlave.cpp
+++ b/assignment-client/src/audio/AudioMixerSlave.cpp
@@ -51,7 +51,7 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData&
 // mix helpers
 inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd);
 inline float computeGain(float masterAvatarGain, float masterInjectorGain, const AvatarAudioStream& listeningNodeStream,
-        const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho);
+        const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance);
 inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
         const glm::vec3& relativePosition);
 
@@ -504,14 +504,12 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre
     glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition();
 
     float distance = glm::max(glm::length(relativePosition), EPSILON);
+    float gain = isEcho ? 1.0f
+                        : (isSoloing ? masterAvatarGain
+                                     : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd,
+                                                   relativePosition, distance));
     float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition);
 
-    float gain = masterAvatarGain;
-    if (!isSoloing) {
-        gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition,
-                           distance, isEcho);
-    }
-
     const int HRTF_DATASET_INDEX = 1;
 
     if (!streamToAdd->lastPopSucceeded()) {
@@ -599,8 +597,8 @@ void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream&
     glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition();
 
     float distance = glm::max(glm::length(relativePosition), EPSILON);
-    float gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition,
-                             distance, isEcho);
+    float gain = isEcho ? 1.0f : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, 
+                                             relativePosition, distance);
     float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition);
 
     mixableStream.hrtf->setParameterHistory(azimuth, distance, gain);
@@ -743,8 +741,7 @@ float computeGain(float masterAvatarGain,
                   const AvatarAudioStream& listeningNodeStream,
                   const PositionalAudioStream& streamToAdd,
                   const glm::vec3& relativePosition,
-                  float distance,
-                  bool isEcho) {
+                  float distance) {
     float gain = 1.0f;
 
     // injector: apply attenuation
@@ -754,7 +751,7 @@ float computeGain(float masterAvatarGain,
         gain *= masterInjectorGain;
 
     // avatar: apply fixed off-axis attenuation to make them quieter as they turn away
-    } else if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) {
+    } else if (streamToAdd.getType() == PositionalAudioStream::Microphone) {
         glm::vec3 rotatedListenerPosition = glm::inverse(streamToAdd.getOrientation()) * relativePosition;
 
         // source directivity is based on angle of emission, in local coordinates
diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp
index 33e1034128..9816cebf43 100644
--- a/assignment-client/src/avatars/AvatarMixer.cpp
+++ b/assignment-client/src/avatars/AvatarMixer.cpp
@@ -253,10 +253,29 @@ void AvatarMixer::start() {
 
         int lockWait, nodeTransform, functor;
 
+        // Set our query each frame
         {
             _entityViewer.queryOctree();
         }
 
+        // Dirty the hero status if there's been an entity change.
+        {
+            if (_dirtyHeroStatus) {
+                _dirtyHeroStatus = false;
+                nodeList->nestedEach([](NodeList::const_iterator cbegin, NodeList::const_iterator cend) {
+                    std::for_each(cbegin, cend, [](const SharedNodePointer& node) {
+                        if (node->getType() == NodeType::Agent) {
+                            NodeData* nodeData = node->getLinkedData();
+                            if (nodeData) {
+                                auto& avatar = static_cast<AvatarMixerClientData*>(nodeData)->getAvatar();
+                                avatar.setNeedsHeroCheck();
+                            }
+                        }
+                    });
+                });
+            }
+        }
+
         // Allow nodes to process any pending/queued packets across our worker threads
         {
             auto start = usecTimestampNow();
@@ -827,7 +846,7 @@ void AvatarMixer::sendStatsPacket() {
 
     QJsonObject avatarsObject;
     auto nodeList = DependencyManager::get<NodeList>();
-    // add stats for each listerner
+    // add stats for each listener
     nodeList->eachNode([&](const SharedNodePointer& node) {
         QJsonObject avatarStats;
 
@@ -851,6 +870,12 @@ void AvatarMixer::sendStatsPacket() {
                 avatarStats["delta_full_vs_avatar_data_kbps"] =
                     (double)outboundAvatarDataKbps - avatarStats[OUTBOUND_AVATAR_DATA_STATS_KEY].toDouble();
             }
+
+            if (node->getType() != NodeType::Agent) {  // Nodes that aren't avatars
+                const QString displayName
+                    { node->getType() == NodeType::EntityScriptServer ? "ENTITY SCRIPT SERVER" : "ENTITY SERVER" };
+                avatarStats["display_name"] = displayName;
+            }
         }
 
         avatarsObject[uuidStringWithoutCurlyBraces(node->getUUID())] = avatarStats;
@@ -973,19 +998,30 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
     {
         const QString CONNECTION_RATE = "connection_rate";
         auto nodeList = DependencyManager::get<NodeList>();
-        auto defaultConnectionRate = nodeList->getMaxConnectionRate();
-        int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toInt((int)defaultConnectionRate);
-        nodeList->setMaxConnectionRate(connectionRate);
+        bool success;
+        int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toString().toInt(&success);
+        if (success) {
+            nodeList->setMaxConnectionRate(connectionRate);
+        }
+    }
+
+    {   // Fraction of downstream bandwidth reserved for 'hero' avatars:
+        static const QString PRIORITY_FRACTION_KEY = "priority_fraction";
+        if (avatarMixerGroupObject.contains(PRIORITY_FRACTION_KEY)) {
+            float priorityFraction = float(avatarMixerGroupObject[PRIORITY_FRACTION_KEY].toDouble());
+            _slavePool.setPriorityReservedFraction(std::min(std::max(0.0f, priorityFraction), 1.0f));
+            qCDebug(avatars) << "Avatar mixer reserving" << priorityFraction << "of bandwidth for priority avatars";
+        }
     }
 
     const QString AVATARS_SETTINGS_KEY = "avatars";
 
     static const QString MIN_HEIGHT_OPTION = "min_avatar_height";
-    float settingMinHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT);
+    float settingMinHeight = avatarMixerGroupObject[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT);
     _domainMinimumHeight = glm::clamp(settingMinHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT);
 
     static const QString MAX_HEIGHT_OPTION = "max_avatar_height";
-    float settingMaxHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT);
+    float settingMaxHeight = avatarMixerGroupObject[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT);
     _domainMaximumHeight = glm::clamp(settingMaxHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT);
 
     // make sure that the domain owner didn't flip min and max
@@ -997,11 +1033,11 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
                      << "and a maximum avatar height of" << _domainMaximumHeight;
 
     static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist";
-    _slaveSharedData.skeletonURLWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION]
+    _slaveSharedData.skeletonURLWhitelist = avatarMixerGroupObject[AVATAR_WHITELIST_OPTION]
         .toString().split(',', QString::KeepEmptyParts);
 
     static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar";
-    _slaveSharedData.skeletonReplacementURL = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION]
+    _slaveSharedData.skeletonReplacementURL = avatarMixerGroupObject[REPLACEMENT_AVATAR_OPTION]
         .toString();
 
     if (_slaveSharedData.skeletonURLWhitelist.count() == 1 && _slaveSharedData.skeletonURLWhitelist[0].isEmpty()) {
@@ -1018,9 +1054,12 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
 
 void AvatarMixer::setupEntityQuery() {
     _entityViewer.init();
+    EntityTreePointer entityTree = _entityViewer.getTree();
     DependencyManager::registerInheritance<SpatialParentFinder, AssignmentParentFinder>();
-    DependencyManager::set<AssignmentParentFinder>(_entityViewer.getTree());
-    _slaveSharedData.entityTree = _entityViewer.getTree();
+    DependencyManager::set<AssignmentParentFinder>(entityTree);
+
+    connect(entityTree.get(), &EntityTree::addingEntityPointer, this, &AvatarMixer::entityAdded);
+    connect(entityTree.get(), &EntityTree::deletingEntityPointer, this, &AvatarMixer::entityChange);
 
     // ES query: {"avatarPriority": true, "type": "Zone"}
     QJsonObject priorityZoneQuery;
@@ -1028,6 +1067,7 @@ void AvatarMixer::setupEntityQuery() {
     priorityZoneQuery["type"] = "Zone";
 
     _entityViewer.getOctreeQuery().setJSONParameters(priorityZoneQuery);
+    _slaveSharedData.entityTree = entityTree;
 }
 
 void AvatarMixer::handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
@@ -1064,6 +1104,25 @@ void AvatarMixer::handleOctreePacket(QSharedPointer<ReceivedMessage> message, Sh
     }
 }
 
+void AvatarMixer::entityAdded(EntityItem* entity) {
+    if (entity->getType() == EntityTypes::Zone) {
+        _dirtyHeroStatus = true;
+        entity->registerChangeHandler([this](const EntityItemID& entityItemID) {
+            entityChange();
+        });
+    }
+}
+
+void AvatarMixer::entityRemoved(EntityItem * entity) {
+    if (entity->getType() == EntityTypes::Zone) {
+        _dirtyHeroStatus = true;
+    }
+}
+
+void AvatarMixer::entityChange() {
+    _dirtyHeroStatus = true;
+}
+
 void AvatarMixer::aboutToFinish() {
     DependencyManager::destroy<ResourceManager>();
     DependencyManager::destroy<ResourceCacheSharedItems>();
diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h
index 9393ea6c56..10dff5e8a4 100644
--- a/assignment-client/src/avatars/AvatarMixer.h
+++ b/assignment-client/src/avatars/AvatarMixer.h
@@ -34,8 +34,8 @@ public:
 
     static bool shouldReplicateTo(const Node& from, const Node& to) {
         return to.getType() == NodeType::DownstreamAvatarMixer &&
-               to.getPublicSocket() != from.getPublicSocket() &&
-               to.getLocalSocket() != from.getLocalSocket();
+            to.getPublicSocket() != from.getPublicSocket() &&
+            to.getLocalSocket() != from.getLocalSocket();
     }
 
 public slots:
@@ -46,6 +46,11 @@ public slots:
 
     void sendStatsPacket() override;
 
+    // Avatar zone possibly changed
+    void entityAdded(EntityItem* entity);
+    void entityRemoved(EntityItem* entity);
+    void entityChange();
+
 private slots:
     void queueIncomingPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node);
     void handleAdjustAvatarSorting(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
@@ -80,6 +85,7 @@ private:
 
     // Attach to entity tree for avatar-priority zone info.
     EntityTreeHeadlessViewer _entityViewer;
+    bool _dirtyHeroStatus { true };  // Dirty the needs-hero-update
 
     // FIXME - new throttling - use these values somehow
     float _trailingMixRatio { 0.0f };
diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp
index 557c5c9fe3..1b86e0dff2 100644
--- a/assignment-client/src/avatars/AvatarMixerClientData.cpp
+++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp
@@ -129,7 +129,7 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared
         incrementNumOutOfOrderSends();
     }
     _lastReceivedSequenceNumber = sequenceNumber;
-    glm::vec3 oldPosition = getPosition();
+    glm::vec3 oldPosition = _avatar->getClientGlobalPosition();
     bool oldHasPriority = _avatar->getHasPriority();
 
     // compute the offset to the data payload
@@ -140,23 +140,13 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared
     // Regardless of what the client says, restore the priority as we know it without triggering any update.
     _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority);
 
-    auto newPosition = getPosition();
-    if (newPosition != oldPosition) {
-//#define AVATAR_HERO_TEST_HACK
-#ifdef AVATAR_HERO_TEST_HACK
-        {
-            const static QString heroKey { "HERO" };
-            _avatar->setPriorityAvatar(_avatar->getDisplayName().contains(heroKey));
-        }
-#else
+    auto newPosition = _avatar->getClientGlobalPosition();
+    if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) {
         EntityTree& entityTree = *slaveSharedData.entityTree;
-        FindPriorityZone findPriorityZone { newPosition, false } ;
+        FindPriorityZone findPriorityZone { newPosition } ;
         entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone);
         _avatar->setHasPriority(findPriorityZone.isInPriorityZone);
-        //if (findPriorityZone.isInPriorityZone) {
-        //    qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone";
-        //}
-#endif
+        _avatar->setNeedsHeroCheck(false);
     }
 
     return true;
@@ -341,7 +331,7 @@ void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedDa
 
                 // the returned set traits packet uses the trait version from the incoming packet
                 // so the client knows they should not overwrite if they have since changed the trait
-                _avatar->packTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion);
+                AvatarTraits::packVersionedTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion, *_avatar);
 
                 auto nodeList = DependencyManager::get<NodeList>();
                 nodeList->sendPacket(std::move(packet), sendingNode);
diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp
index e59c81f4b7..32c944f5b8 100644
--- a/assignment-client/src/avatars/AvatarMixerSlave.cpp
+++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp
@@ -43,12 +43,14 @@ void AvatarMixerSlave::configure(ConstIter begin, ConstIter end) {
 
 void AvatarMixerSlave::configureBroadcast(ConstIter begin, ConstIter end, 
                                 p_high_resolution_clock::time_point lastFrameTimestamp,
-                                float maxKbpsPerNode, float throttlingRatio) {
+                                float maxKbpsPerNode, float throttlingRatio,
+                                float priorityReservedFraction) {
     _begin = begin;
     _end = end;
     _lastFrameTimestamp = lastFrameTimestamp;
     _maxKbpsPerNode = maxKbpsPerNode;
     _throttlingRatio = throttlingRatio;
+    _avatarHeroFraction = priorityReservedFraction;
 }
 
 void AvatarMixerSlave::harvestStats(AvatarMixerSlaveStats& stats) {
@@ -139,7 +141,8 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
                 if (lastReceivedVersion > lastSentVersionRef) {
                     bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten);
                     // there is an update to this trait, add it to the traits packet
-                    bytesWritten += sendingAvatar->packTrait(traitType, traitsPacketList, lastReceivedVersion);
+                    bytesWritten += AvatarTraits::packVersionedTrait(traitType, traitsPacketList,
+                                                                     lastReceivedVersion, *sendingAvatar);
                     // update the last sent version
                     lastSentVersionRef = lastReceivedVersion;
                     // Remember which versions we sent in this particular packet
@@ -194,7 +197,8 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
                     bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten);
 
                     // this instance version exists and has never been sent or is newer so we need to send it
-                    bytesWritten += sendingAvatar->packTraitInstance(traitType, instanceID, traitsPacketList, receivedVersion);
+                    bytesWritten += AvatarTraits::packVersionedTraitInstance(traitType, instanceID, traitsPacketList,
+                                                                             receivedVersion, *sendingAvatar);
 
                     if (sentInstanceIt != sentIDValuePairs.end()) {
                         sentInstanceIt->value = receivedVersion;
@@ -308,7 +312,6 @@ namespace {
 }  // Close anonymous namespace.
 
 void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) {
-    const float AVATAR_HERO_FRACTION { 0.4f };
     const Node* destinationNode = node.data();
 
     auto nodeList = DependencyManager::get<NodeList>();
@@ -343,7 +346,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
 
     // max number of avatarBytes per frame (13 900, typical)
     const int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND);
-    const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * AVATAR_HERO_FRACTION);  // 5555, typical
+    const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * _avatarHeroFraction);  // 5555, typical
 
     // keep track of the number of other avatars held back in this frame
     int numAvatarsHeldBack = 0;
@@ -469,8 +472,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
                 SortableAvatar(avatarNodeData, sourceAvatarNode, lastEncodeTime));
         }
         
-        // If Avatar A's PAL WAS open but is no longer open, AND
-        // Avatar A is ignoring Avatar B OR Avatar B is ignoring Avatar A...
+        // If Node A's PAL WAS open but is no longer open, AND
+        // Node A is ignoring Avatar B OR Node B is ignoring Avatar A...
         //
         // This is a bit heavy-handed still - there are cases where a kill packet
         // will be sent when it doesn't need to be (but where it _should_ be OK to send).
@@ -539,7 +542,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
             const MixerAvatar* sourceAvatar = sourceNodeData->getConstAvatarData();
 
             // Typically all out-of-view avatars but such avatars' priorities will rise with time:
-            bool isLowerPriority = currentVariant != kHero && sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD;  // XXX: hero handling?
+            bool isLowerPriority = sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD;
 
             if (isLowerPriority) {
                 detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData;
@@ -548,8 +551,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
                 detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData;
                 destinationNodeData->incrementAvatarInView();
 
-                // If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO
-                // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A.
+                // If the time that the mixer sent AVATAR DATA about Avatar B to Node A is BEFORE OR EQUAL TO
+                // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Node A.
                 if (sourceAvatar->hasProcessedFirstIdentity()
                     && destinationNodeData->getLastBroadcastTime(sourceNode->getLocalID()) <= sourceNodeData->getIdentityChangeTimestamp()) {
                     identityBytesSent += sendIdentityPacket(*identityPacketList, sourceNodeData, *destinationNode);
diff --git a/assignment-client/src/avatars/AvatarMixerSlave.h b/assignment-client/src/avatars/AvatarMixerSlave.h
index 8c5ad6b181..f14e50e11f 100644
--- a/assignment-client/src/avatars/AvatarMixerSlave.h
+++ b/assignment-client/src/avatars/AvatarMixerSlave.h
@@ -110,7 +110,8 @@ public:
     void configure(ConstIter begin, ConstIter end);
     void configureBroadcast(ConstIter begin, ConstIter end, 
                     p_high_resolution_clock::time_point lastFrameTimestamp, 
-                    float maxKbpsPerNode, float throttlingRatio);
+                    float maxKbpsPerNode, float throttlingRatio,
+                    float priorityReservedFraction);
 
     void processIncomingPackets(const SharedNodePointer& node);
     void broadcastAvatarData(const SharedNodePointer& node);
@@ -140,6 +141,7 @@ private:
     p_high_resolution_clock::time_point _lastFrameTimestamp;
     float _maxKbpsPerNode { 0.0f };
     float _throttlingRatio { 0.0f };
+    float _avatarHeroFraction { 0.4f };
 
     AvatarMixerSlaveStats _stats;
     SlaveSharedData* _sharedData;
diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp
index 013d914cbe..357b347a94 100644
--- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp
+++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp
@@ -76,7 +76,8 @@ void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end,
                                                float maxKbpsPerNode, float throttlingRatio) {
     _function = &AvatarMixerSlave::broadcastAvatarData;
     _configure = [=](AvatarMixerSlave& slave) { 
-        slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio);
+        slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio,
+            _priorityReservedFraction);
    };
     run(begin, end);
 }
diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.h b/assignment-client/src/avatars/AvatarMixerSlavePool.h
index 71a9ace0d3..b05abde2a3 100644
--- a/assignment-client/src/avatars/AvatarMixerSlavePool.h
+++ b/assignment-client/src/avatars/AvatarMixerSlavePool.h
@@ -73,7 +73,10 @@ public:
     void each(std::function<void(AvatarMixerSlave& slave)> functor);
 
     void setNumThreads(int numThreads);
-    int numThreads() { return _numThreads; }
+    int numThreads() const { return _numThreads; }
+
+    void setPriorityReservedFraction(float fraction) { _priorityReservedFraction = fraction; }
+    float getPriorityReservedFraction() const { return  _priorityReservedFraction; }
 
 private:
     void run(ConstIter begin, ConstIter end);
@@ -91,7 +94,11 @@ private:
     ConditionVariable _poolCondition;
     void (AvatarMixerSlave::*_function)(const SharedNodePointer& node);
     std::function<void(AvatarMixerSlave&)> _configure;
+
+    // Set from Domain Settings:
+    float _priorityReservedFraction { 0.4f };
     int _numThreads { 0 };
+
     int _numStarted { 0 }; // guarded by _mutex
     int _numFinished { 0 }; // guarded by _mutex
     int _numStopped { 0 }; // guarded by _mutex
diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h
index 3e80704495..01e5e91b44 100644
--- a/assignment-client/src/avatars/MixerAvatar.h
+++ b/assignment-client/src/avatars/MixerAvatar.h
@@ -19,8 +19,12 @@
 
 class MixerAvatar : public AvatarData {
 public:
+    bool getNeedsHeroCheck() const { return _needsHeroCheck; }
+    void setNeedsHeroCheck(bool needsHeroCheck = true)
+        { _needsHeroCheck = needsHeroCheck; }
 
 private:
+    bool _needsHeroCheck { false };
 };
 
 using MixerAvatarSharedPointer = std::shared_ptr<MixerAvatar>;
diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h
index fbe5675bd8..e5df411099 100644
--- a/assignment-client/src/avatars/ScriptableAvatar.h
+++ b/assignment-client/src/avatars/ScriptableAvatar.h
@@ -74,6 +74,7 @@
  *     avatar. <em>Read-only.</em>
  * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's
  *     size in the virtual world. <em>Read-only.</em>
+ * @property {boolean} hasPriority - is the avatar in a Hero zone? <em>Read-only.</em>
  *
  * @example <caption>Create a scriptable avatar.</caption>
  * (function () {
diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json
index 140c7d6c17..352106dcf7 100644
--- a/domain-server/resources/describe-settings.json
+++ b/domain-server/resources/describe-settings.json
@@ -1310,6 +1310,15 @@
           "placeholder": "50",
           "default": "50",
           "advanced": true
+        },
+        {
+            "name": "priority_fraction",
+            "type": "double",
+            "label": "Hero Bandwidth",
+            "help": "Fraction of downstream bandwidth reserved for avatars in 'Hero' zones",
+            "placeholder": "0.40",
+            "default": "0.40",
+            "advanced": true
         }
       ]
     },
diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp
index 8d5cb165cb..5f82700e9c 100644
--- a/domain-server/src/DomainServer.cpp
+++ b/domain-server/src/DomainServer.cpp
@@ -1766,14 +1766,14 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointer<ReceivedMessag
 
     bool remoteHasExistingData { false };
     QUuid id;
-    int version;
+    int dataVersion;
     message->readPrimitive(&remoteHasExistingData);
     if (remoteHasExistingData) {
         constexpr size_t UUID_SIZE_BYTES = 16;
         auto idData = message->read(UUID_SIZE_BYTES);
         id = QUuid::fromRfc4122(idData);
-        message->readPrimitive(&version);
-        qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << version << ")";
+        message->readPrimitive(&dataVersion);
+        qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << dataVersion << ")";
     } else {
         qCDebug(domain_server) << "Entity server does not have existing data";
     }
@@ -1782,11 +1782,11 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointer<ReceivedMessag
     auto reply = NLPacketList::create(PacketType::OctreeDataFileReply, QByteArray(), true, true);
     OctreeUtils::RawEntityData data;
     if (data.readOctreeDataInfoFromFile(entityFilePath)) {
-        if (data.id == id && data.version <= version) {
+        if (data.id == id && data.dataVersion <= dataVersion) {
             qCDebug(domain_server) << "ES has sufficient octree data, not sending data";
             reply->writePrimitive(false);
         } else {
-            qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.version << ")";
+            qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.dataVersion << ")";
             QFile file(entityFilePath);
             if (file.open(QIODevice::ReadOnly)) {
                 reply->writePrimitive(true);
diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml
index ba5e162391..4eea3566b8 100644
--- a/interface/resources/qml/dialogs/FileDialog.qml
+++ b/interface/resources/qml/dialogs/FileDialog.qml
@@ -9,7 +9,7 @@
 //
 
 import QtQuick 2.7
-import Qt.labs.folderlistmodel 2.1
+import Qt.labs.folderlistmodel 2.2
 import Qt.labs.settings 1.0
 import QtQuick.Dialogs 1.2 as OriginalDialogs
 import QtQuick.Controls 1.4 as QQC1
@@ -320,6 +320,7 @@ ModalWindow {
         FolderListModel {
             id: folderListModel
             nameFilters: selectionType.currentFilter
+            caseSensitive: false
             showDirsFirst: true
             showDotAndDotDot: false
             showFiles: !root.selectDirectory
diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml
index 6c4e32dc5a..5bcc42f101 100644
--- a/interface/resources/qml/dialogs/TabletFileDialog.qml
+++ b/interface/resources/qml/dialogs/TabletFileDialog.qml
@@ -9,7 +9,7 @@
 //
 
 import QtQuick 2.7
-import Qt.labs.folderlistmodel 2.1
+import Qt.labs.folderlistmodel 2.2
 import Qt.labs.settings 1.0
 import QtQuick.Dialogs 1.2 as OriginalDialogs
 import QtQuick.Controls 1.4 as QQC1
@@ -285,6 +285,7 @@ TabletModalWindow {
         FolderListModel {
             id: folderListModel
             nameFilters: selectionType.currentFilter
+            caseSensitive: false
             showDirsFirst: true
             showDotAndDotDot: false
             showFiles: !root.selectDirectory
diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml
index 753b9c5a81..997407885b 100644
--- a/interface/resources/qml/hifi/AvatarApp.qml
+++ b/interface/resources/qml/hifi/AvatarApp.qml
@@ -16,6 +16,8 @@ Rectangle {
     property bool keyboardRaised: false
     property bool punctuationMode: false
 
+    HifiConstants { id: hifi }
+
     HifiControls.Keyboard {
         id: keyboard
         z: 1000
@@ -48,6 +50,7 @@ Rectangle {
 
     property var jointNames: []
     property var currentAvatarSettings;
+    property bool wearablesFrozen;
 
     function fetchAvatarModelName(marketId, avatar) {
         var xmlhttp = new XMLHttpRequest();
@@ -187,6 +190,8 @@ Rectangle {
             updateCurrentAvatarInBookmarks(currentAvatar);
         } else if (message.method === 'selectAvatarEntity') {
             adjustWearables.selectWearableByID(message.entityID);
+        } else if (message.method === 'wearablesFrozenChanged') {
+            wearablesFrozen = message.wearablesFrozen;
         }
     }
 
@@ -507,6 +512,7 @@ Rectangle {
         }
 
         SquareLabel {
+            id: adjustLabel
             anchors.right: parent.right
             anchors.verticalCenter: wearablesLabel.verticalCenter
             glyphText: "\ue02e"
@@ -515,6 +521,17 @@ Rectangle {
                 adjustWearables.open(currentAvatar);
             }
         }
+
+        SquareLabel {
+            anchors.right: adjustLabel.left
+            anchors.verticalCenter: wearablesLabel.verticalCenter
+            anchors.rightMargin: 15
+            glyphText: wearablesFrozen ? hifi.glyphs.lock : hifi.glyphs.unlock;
+
+            onClicked: {
+                emitSendToScript({'method' : 'toggleWearablesFrozen'});
+            }
+        }
     }
 
     Rectangle {
diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml
index 89a30b4b91..55378589ec 100644
--- a/interface/resources/qml/hifi/audio/MicBar.qml
+++ b/interface/resources/qml/hifi/audio/MicBar.qml
@@ -40,6 +40,9 @@ Rectangle {
         AudioScriptingInterface.pushToTalkChanged.connect(function() {
             pushToTalk = AudioScriptingInterface.pushToTalk;
         });
+        AudioScriptingInterface.pushingToTalkChanged.connect(function() {
+            pushingToTalk = AudioScriptingInterface.pushingToTalk;
+        });
     }
 
     property bool standalone: false;
@@ -100,16 +103,16 @@ Rectangle {
     QtObject {
         id: colors;
 
-        readonly property string unmuted: "#FFF";
-        readonly property string muted: "#E2334D";
+        readonly property string unmutedColor: "#FFF";
+        readonly property string mutedColor: "#E2334D";
         readonly property string gutter: "#575757";
         readonly property string greenStart: "#39A38F";
         readonly property string greenEnd: "#1FC6A6";
         readonly property string yellow: "#C0C000";
-        readonly property string red: colors.muted;
+        readonly property string red: colors.mutedColor;
         readonly property string fill: "#55000000";
         readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF";
-        readonly property string icon: micBar.muted ? muted : unmuted;
+        readonly property string icon: muted ? colors.mutedColor : unmutedColor;
     }
 
     Item {
@@ -157,7 +160,6 @@ Rectangle {
     Item {
         id: status;
 
-
         visible: (pushToTalk && !pushingToTalk) || muted;
 
         anchors {
diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml
index 136d535b3f..391e4fab37 100644
--- a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml
+++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml
@@ -113,6 +113,7 @@ Rectangle {
                 } else if (prop === 'dimensions') {
                     scalespinner.set(wearable[prop].x / wearable.naturalDimensions.x);
                 }
+                modified = true;
             }
         }
 
diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml
index 7dcdf9b434..619547ef43 100644
--- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml
+++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml
@@ -664,7 +664,7 @@ Rectangle {
                         text: "LOG IN"
 
                         onClicked: {
-                            sendToScript({method: 'needsLogIn_loginClicked'});
+                            sendToScript({method: 'marketplace_loginClicked'});
                         }
                     }
 
diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml
index a27c7b59dc..36a37134bf 100644
--- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml
+++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml
@@ -9,7 +9,7 @@
 //
 
 import QtQuick 2.7
-import Qt.labs.folderlistmodel 2.1
+import Qt.labs.folderlistmodel 2.2
 import Qt.labs.settings 1.0
 import QtQuick.Dialogs 1.2 as OriginalDialogs
 import QtQuick.Controls 1.4 as QQC1
@@ -279,6 +279,7 @@ Rectangle {
         FolderListModel {
             id: folderListModel
             nameFilters: selectionType.currentFilter
+            caseSensitive: false
             showDirsFirst: true
             showDotAndDotDot: false
             showFiles: !root.selectDirectory
diff --git a/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml b/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml
index d5fab57501..995af90f0b 100644
--- a/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml
+++ b/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml
@@ -344,6 +344,7 @@ Item {
         readonly property string stop_square: "\ue01e"
         readonly property string avatarTPose: "\ue01f"
         readonly property string lock: "\ue006"
+        readonly property string unlock: "\ue039"
         readonly property string checkmark: "\ue020"
         readonly property string leftRightArrows: "\ue021"
         readonly property string hfc: "\ue022"
diff --git a/interface/resources/qml/stylesUit/HifiConstants.qml b/interface/resources/qml/stylesUit/HifiConstants.qml
index 75f028cd4f..2394b36106 100644
--- a/interface/resources/qml/stylesUit/HifiConstants.qml
+++ b/interface/resources/qml/stylesUit/HifiConstants.qml
@@ -330,6 +330,7 @@ QtObject {
         readonly property string stop_square: "\ue01e"
         readonly property string avatarTPose: "\ue01f"
         readonly property string lock: "\ue006"
+        readonly property string unlock: "\ue039"
         readonly property string checkmark: "\ue020"
         readonly property string leftRightArrows: "\ue021"
         readonly property string hfc: "\ue022"
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index ecafbfdb2c..efb7bfa62f 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -1210,10 +1210,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
     connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle()));
     connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle()));
     connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() {
-        auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>();
-        if (tabletScriptingInterface) {
-            tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr);
-        }
         auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
         entityScriptingInterface->deleteEntity(getTabletScreenID());
         entityScriptingInterface->deleteEntity(getTabletHomeButtonID());
diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp
index 43e50ea049..01a40e89fd 100644
--- a/interface/src/avatar/AvatarDoctor.cpp
+++ b/interface/src/avatar/AvatarDoctor.cpp
@@ -92,7 +92,7 @@ void AvatarDoctor::startDiagnosing() {
         _model = resource;
         const auto model = resource.data();
         const auto avatarModel = resource.data()->getHFMModel();
-        if (!avatarModel.originalURL.endsWith(".fbx")) {
+        if (!avatarModel.originalURL.toLower().endsWith(".fbx")) {
             addError("Unsupported avatar model format.", "unsupported-format");
             emit complete(getErrors());
             return;
diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index f2d8730ebb..27151d0615 100644
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -5463,7 +5463,7 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys
         }
         auto collisionJoints = collisionsConfig.keys();
         if (collisionJoints.size() > 0) {
-            collisionSystem.resetCollisions();
+            collisionSystem.clearSelfCollisions();
             for (auto &jointName : collisionJoints) {
                 int jointIndex = getJointIndex(jointName);
                 FlowCollisionSettings collisionsSettings;
@@ -5478,6 +5478,7 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys
                 collisionSystem.addCollisionSphere(jointIndex, collisionsSettings);
             }
         }
+        flow.updateScale();
     }
 }
 
diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h
index edb686a6a6..0859c20153 100755
--- a/interface/src/avatar/MyAvatar.h
+++ b/interface/src/avatar/MyAvatar.h
@@ -295,6 +295,7 @@ class MyAvatar : public Avatar {
      * @comment Avatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame - Don't borrow because implementation is different.
      * @borrows Avatar.getTargetScale as getTargetScale
      * @borrows Avatar.resetLastSent as resetLastSent
+     * @borrows Avatar.hasPriority as hasPriority
      */
     // FIXME: `glm::vec3 position` is not accessible from QML, so this exposes position in a QML-native type
     Q_PROPERTY(QVector3D qmlPosition READ getQmlPosition)
@@ -2170,7 +2171,7 @@ private:
     bool getEnableStepResetRotation() const { return _stepResetRotationEnabled; }
     void setEnableDrawAverageFacing(bool drawAverage) { _drawAverageFacingEnabled = drawAverage; }
     bool getEnableDrawAverageFacing() const { return _drawAverageFacingEnabled; }
-    bool isMyAvatar() const override { return true; }
+    virtual bool isMyAvatar() const override { return true; }
     virtual int parseDataFromBuffer(const QByteArray& buffer) override;
     virtual glm::vec3 getSkeletonPosition() const override;
     int _skeletonModelChangeCount { 0 };
diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp
index 11eb6542c4..b100b33dc8 100755
--- a/interface/src/avatar/OtherAvatar.cpp
+++ b/interface/src/avatar/OtherAvatar.cpp
@@ -365,7 +365,7 @@ void OtherAvatar::handleChangedAvatarEntityData() {
     // AVATAR ENTITY UPDATE FLOW
     // - if queueEditEntityMessage() sees "AvatarEntity" HostType it calls _myAvatar->storeAvatarEntityDataPayload()
     // - storeAvatarEntityDataPayload() saves the payload and flags the trait instance for the entity as updated,
-    // - ClientTraitsHandler::sendChangedTraitsToMixea() sends the entity bytes to the mixer which relays them to other interfaces
+    // - ClientTraitsHandler::sendChangedTraitsToMixer() sends the entity bytes to the mixer which relays them to other interfaces
     // - AvatarHashMap::processBulkAvatarTraits() on other interfaces calls avatar->processTraitInstance()
     // - AvatarData::processTraitInstance() calls storeAvatarEntityDataPayload(), which sets _avatarEntityDataChanged = true
     // - (My)Avatar::simulate() calls handleChangedAvatarEntityData() every frame which checks _avatarEntityDataChanged
@@ -495,6 +495,18 @@ void OtherAvatar::handleChangedAvatarEntityData() {
                 const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}");
                 entity->setParentID(NULL_ID);
                 entity->setParentID(oldParentID);
+
+                if (entity->stillHasMyGrabAction()) {
+                    // For this case: we want to ignore transform+velocities coming from authoritative OtherAvatar
+                    // because the MyAvatar is grabbing and we expect the local grab state
+                    // to have enough information to prevent simulation drift.
+                    //
+                    // Clever readers might realize this could cause problems.  For example,
+                    // if an ignored OtherAvagtar were to simultanously grab the object then there would be
+                    // a noticeable discrepancy between participants in the distributed physics simulation,
+                    // however the difference would be stable and would not drift.
+                    properties.clearTransformOrVelocityChanges();
+                }
                 if (entityTree->updateEntity(entityID, properties)) {
                     entity->updateLastEditedFromRemote();
                 } else {
diff --git a/libraries/animation/src/Flow.cpp b/libraries/animation/src/Flow.cpp
index 5bc2021e5e..1f9e72bf28 100644
--- a/libraries/animation/src/Flow.cpp
+++ b/libraries/animation/src/Flow.cpp
@@ -67,17 +67,23 @@ void FlowCollisionSystem::addCollisionSphere(int jointIndex, const FlowCollision
     auto collision = FlowCollisionSphere(jointIndex, settings, isTouch);
     collision.setPosition(position);
     if (isSelfCollision) {
-        _selfCollisions.push_back(collision);
+        if (!isTouch) {
+            _selfCollisions.push_back(collision);
+        } else {
+            _selfTouchCollisions.push_back(collision);
+        }
     } else {
         _othersCollisions.push_back(collision);
     }
-
 };
+
 void FlowCollisionSystem::resetCollisions() {
     _allCollisions.clear();
     _othersCollisions.clear();
+    _selfTouchCollisions.clear();
     _selfCollisions.clear();
 }
+
 FlowCollisionResult FlowCollisionSystem::computeCollision(const std::vector<FlowCollisionResult> collisions) {
     FlowCollisionResult result;
     if (collisions.size() > 1) {
@@ -106,6 +112,10 @@ void FlowCollisionSystem::setScale(float scale) {
         _selfCollisions[j]._radius = _selfCollisions[j]._initialRadius * scale;
         _selfCollisions[j]._offset = _selfCollisions[j]._initialOffset * scale;
     }
+    for (size_t j = 0; j < _selfTouchCollisions.size(); j++) {
+        _selfTouchCollisions[j]._radius = _selfTouchCollisions[j]._initialRadius * scale;
+        _selfTouchCollisions[j]._offset = _selfTouchCollisions[j]._initialOffset * scale;
+    }
 };
 
 std::vector<FlowCollisionResult> FlowCollisionSystem::checkFlowThreadCollisions(FlowThread* flowThread) {
@@ -178,9 +188,9 @@ void FlowCollisionSystem::setCollisionSettingsByJoint(int jointIndex, const Flow
 }
 void FlowCollisionSystem::prepareCollisions() {
     _allCollisions.clear();
-    _allCollisions.resize(_selfCollisions.size() + _othersCollisions.size());
-    std::copy(_selfCollisions.begin(), _selfCollisions.begin() + _selfCollisions.size(), _allCollisions.begin());
-    std::copy(_othersCollisions.begin(), _othersCollisions.begin() + _othersCollisions.size(), _allCollisions.begin() + _selfCollisions.size());
+    _allCollisions.insert(_allCollisions.end(), _selfCollisions.begin(), _selfCollisions.end());
+    _allCollisions.insert(_allCollisions.end(), _othersCollisions.begin(), _othersCollisions.end());
+    _allCollisions.insert(_allCollisions.end(), _selfTouchCollisions.begin(), _selfTouchCollisions.end());
     _othersCollisions.clear();
 }
 
@@ -273,18 +283,20 @@ void FlowJoint::setRecoveryPosition(const glm::vec3& recoveryPosition) {
 }
 
 void FlowJoint::update(float deltaTime) {
-    glm::vec3 accelerationOffset = glm::vec3(0.0f);
-    if (_settings._stiffness > 0.0f) {
-        glm::vec3 recoveryVector = _recoveryPosition - _currentPosition;
-        float recoveryFactor = powf(_settings._stiffness, 3.0f);
-        accelerationOffset = recoveryVector * recoveryFactor;
-    }
-    FlowNode::update(deltaTime, accelerationOffset);
-    if (_anchored) {
-        if (!_isHelper) {
-            _currentPosition = _updatedPosition;
-        } else {
-            _currentPosition = _parentPosition;
+    if (_settings._active) {
+        glm::vec3 accelerationOffset = glm::vec3(0.0f);
+        if (_settings._stiffness > 0.0f) {
+            glm::vec3 recoveryVector = _recoveryPosition - _currentPosition;
+            float recoveryFactor = powf(_settings._stiffness, 3.0f);
+            accelerationOffset = recoveryVector * recoveryFactor;
+        }
+        FlowNode::update(deltaTime, accelerationOffset);
+        if (_anchored) {
+            if (!_isHelper) {
+                _currentPosition = _updatedPosition;
+            } else {
+                _currentPosition = _parentPosition;
+            }
         }
     }
 };
@@ -674,6 +686,14 @@ bool Flow::updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t thr
     return true;
 }
 
+void Flow::updateCollisionJoint(FlowCollisionSphere& collision, AnimPoseVec& absolutePoses) {
+    glm::quat jointRotation;
+    getJointPositionInWorldFrame(absolutePoses, collision._jointIndex, collision._position, _entityPosition, _entityRotation);
+    getJointRotationInWorldFrame(absolutePoses, collision._jointIndex, jointRotation, _entityRotation);
+    glm::vec3 worldOffset = jointRotation * collision._offset;
+    collision._position = collision._position + worldOffset;
+}
+
 void Flow::updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) {
     updateAbsolutePoses(relativePoses, absolutePoses);
     for (auto &jointData : _flowJointData) {
@@ -695,11 +715,11 @@ void Flow::updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses)
     }
     auto &selfCollisions = _collisionSystem.getSelfCollisions();
     for (auto &collision : selfCollisions) {
-        glm::quat jointRotation;
-        getJointPositionInWorldFrame(absolutePoses, collision._jointIndex, collision._position, _entityPosition, _entityRotation);
-        getJointRotationInWorldFrame(absolutePoses, collision._jointIndex, jointRotation, _entityRotation);
-        glm::vec3 worldOffset = jointRotation * collision._offset;
-        collision._position = collision._position + worldOffset;
+        updateCollisionJoint(collision, absolutePoses);
+    }
+    auto &selfTouchCollisions = _collisionSystem.getSelfTouchCollisions();
+    for (auto &collision : selfTouchCollisions) {
+        updateCollisionJoint(collision, absolutePoses);
     }
     _collisionSystem.prepareCollisions();
 }
@@ -710,7 +730,7 @@ void Flow::setJoints(AnimPoseVec& relativePoses, const std::vector<bool>& overri
         for (int jointIndex : joints) {
             auto &joint = _flowJointData[jointIndex];
             if (jointIndex >= 0 && jointIndex < (int)relativePoses.size() && !overrideFlags[jointIndex]) {
-                relativePoses[jointIndex].rot() = joint.getCurrentRotation();
+                relativePoses[jointIndex].rot() = joint.getSettings()._active ? joint.getCurrentRotation() : joint.getInitialRotation();
             }            
         }
     }
diff --git a/libraries/animation/src/Flow.h b/libraries/animation/src/Flow.h
index ad81c2be77..5dc1a3ba3e 100644
--- a/libraries/animation/src/Flow.h
+++ b/libraries/animation/src/Flow.h
@@ -140,6 +140,7 @@ public:
     std::vector<FlowCollisionResult> checkFlowThreadCollisions(FlowThread* flowThread);
 
     std::vector<FlowCollisionSphere>& getSelfCollisions() { return _selfCollisions; };
+    std::vector<FlowCollisionSphere>& getSelfTouchCollisions() { return _selfTouchCollisions; };
     void setOthersCollisions(const std::vector<FlowCollisionSphere>& othersCollisions) { _othersCollisions = othersCollisions; }
     void prepareCollisions();
     void resetCollisions();
@@ -150,9 +151,11 @@ public:
     void setActive(bool active) { _active = active; }
     bool getActive() const { return _active; }
     const std::vector<FlowCollisionSphere>& getCollisions() const { return _selfCollisions; }
+    void clearSelfCollisions() { _selfCollisions.clear(); }
 protected:
     std::vector<FlowCollisionSphere> _selfCollisions;
     std::vector<FlowCollisionSphere> _othersCollisions;
+    std::vector<FlowCollisionSphere> _selfTouchCollisions;
     std::vector<FlowCollisionSphere> _allCollisions;
     float _scale { 1.0f };
     bool _active { false };
@@ -210,7 +213,7 @@ public:
     bool isHelper() const { return _isHelper; }
 
     const FlowPhysicsSettings& getSettings() { return _settings; }
-    void setSettings(const FlowPhysicsSettings& settings) { _settings = settings; }
+    void setSettings(const FlowPhysicsSettings& settings) { _settings = settings; _initialRadius = _settings._radius; }
 
     const glm::vec3& getCurrentPosition() const { return _currentPosition; }
     int getIndex() const { return _index; }
@@ -222,6 +225,7 @@ public:
     const glm::quat& getCurrentRotation() const { return _currentRotation; }
     const glm::vec3& getCurrentTranslation() const { return _initialTranslation; }
     const glm::vec3& getInitialPosition() const { return _initialPosition; }
+    const glm::quat& getInitialRotation() const { return _initialRotation; }
     bool isColliding() const { return _colliding; }
 
 protected:
@@ -297,6 +301,7 @@ public:
     void setPhysicsSettingsForGroup(const QString& group, const FlowPhysicsSettings& settings);
     const std::map<QString, FlowPhysicsSettings>& getGroupSettings() const { return _groupSettings; }
     void cleanUp();
+    void updateScale() { setScale(_scale); }
 
 signals:
     void onCleanup();
@@ -311,6 +316,7 @@ private:
 
     void setJoints(AnimPoseVec& relativePoses, const std::vector<bool>& overrideFlags);
     void updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses);
+    void updateCollisionJoint(FlowCollisionSphere& collision, AnimPoseVec& absolutePoses);
     bool updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t threadIndex);
     void updateGroupSettings(const QString& group, const FlowPhysicsSettings& settings);
     void setScale(float scale);
diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp
index 82ab067472..43e94d23e8 100644
--- a/libraries/animation/src/Rig.cpp
+++ b/libraries/animation/src/Rig.cpp
@@ -1422,7 +1422,8 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons
                 _networkAnimState.blendTime += deltaTime;
                 alpha = _computeNetworkAnimation ? (_networkAnimState.blendTime / TOTAL_BLEND_TIME) : (1.0f - (_networkAnimState.blendTime / TOTAL_BLEND_TIME));
                 alpha = glm::clamp(alpha, 0.0f, 1.0f);
-                for (size_t i = 0; i < _networkPoseSet._relativePoses.size(); i++) {
+                size_t numJoints = std::min(_networkPoseSet._relativePoses.size(), _internalPoseSet._relativePoses.size());
+                for (size_t i = 0; i < numJoints; i++) {
                     _networkPoseSet._relativePoses[i].blend(_internalPoseSet._relativePoses[i], alpha);
                 }
             }
diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
index 992ee5db96..839c4ed1d9 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp
@@ -372,13 +372,6 @@ bool Avatar::applyGrabChanges() {
                 target->removeGrab(grab);
                 _avatarGrabs.erase(itr);
                 grabAddedOrRemoved = true;
-                if (isMyAvatar()) {
-                    const EntityItemPointer& entity = std::dynamic_pointer_cast<EntityItem>(target);
-                    if (entity && entity->getEntityHostType() == entity::HostType::AVATAR && entity->getSimulationOwner().getID() == getID()) {
-                        EntityItemProperties properties = entity->getProperties();
-                        sendPacket(entity->getID());
-                    }
-                }
             } else {
                 undeleted.push_back(id);
             }
diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
index b4683d6032..974fae2034 100644
--- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
+++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h
@@ -180,7 +180,6 @@ public:
     /// Returns the distance to use as a LOD parameter.
     float getLODDistance() const;
 
-    virtual bool isMyAvatar() const override { return false; }
     virtual void createOrb() { }
 
     enum class LoadingStatus {
diff --git a/libraries/avatars/src/AssociatedTraitValues.h b/libraries/avatars/src/AssociatedTraitValues.h
index e3060a8097..0df8cd9bb5 100644
--- a/libraries/avatars/src/AssociatedTraitValues.h
+++ b/libraries/avatars/src/AssociatedTraitValues.h
@@ -28,9 +28,10 @@
 namespace AvatarTraits {
     template<typename T, T defaultValue>
     class AssociatedTraitValues {
+        using SimpleTypesArray = std::array<T, NUM_SIMPLE_TRAITS>;
     public:
         // constructor that pre-fills _simpleTypes with the default value specified by the template
-        AssociatedTraitValues() : _simpleTypes(FirstInstancedTrait, defaultValue) {}
+        AssociatedTraitValues() { std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue); }
 
         /// inserts the given value for the given simple trait type
         void insert(TraitType type, T value) { _simpleTypes[type] = value; }
@@ -71,12 +72,12 @@ namespace AvatarTraits {
         }
 
         /// const iterators for the vector of simple type values
-        typename std::vector<T>::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); }
-        typename std::vector<T>::const_iterator simpleCEnd() const { return _simpleTypes.cend(); }
+        typename SimpleTypesArray::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); }
+        typename SimpleTypesArray::const_iterator simpleCEnd() const { return _simpleTypes.cend(); }
 
         /// non-const iterators for the vector of simple type values
-        typename std::vector<T>::iterator simpleBegin() { return _simpleTypes.begin(); }
-        typename std::vector<T>::iterator simpleEnd() { return _simpleTypes.end(); }
+        typename SimpleTypesArray::iterator simpleBegin() { return _simpleTypes.begin(); }
+        typename SimpleTypesArray::iterator simpleEnd() { return _simpleTypes.end(); }
 
         struct TraitWithInstances {
             TraitType traitType;
@@ -96,7 +97,7 @@ namespace AvatarTraits {
         typename std::vector<TraitWithInstances>::iterator instancedEnd() { return _instancedTypes.end(); }
 
     private:
-        std::vector<T> _simpleTypes;
+        SimpleTypesArray _simpleTypes;
 
         /// return the iterator to the matching TraitWithInstances object for a given instanced trait type
         typename std::vector<TraitWithInstances>::iterator instancesForTrait(TraitType traitType) {
diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp
index 39dfaa8a1a..a2b0b808ba 100755
--- a/libraries/avatars/src/AvatarData.cpp
+++ b/libraries/avatars/src/AvatarData.cpp
@@ -1143,10 +1143,11 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
         // we store the hand state as well as other items in a shared bitset. The hand state is an octal, but is split
         // into two sections to maintain backward compatibility. The bits are ordered as such (0-7 left to right).
         // AA 6/1/18 added three more flags bits 8,9, and 10 for procedural audio, blink, and eye saccade enabled
-        //     +---+-----+-----+--+--+--+--+-----+
-        //     |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|xxxxx|
-        //     +---+-----+-----+--+--+--+--+-----+
+        //     +---+-----+-----+--+--+--+--+--+----+
+        //     |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|He|xxxx|
+        //     +---+-----+-----+--+--+--+--+--+----+
         // Hand state - H0,H1,H2 is found in the 3rd, 4th, and 8th bits
+        // Hero-avatar status (He) - 12th bit
         auto newHandState = getSemiNibbleAt(bitItems, HAND_STATE_START_BIT)
             + (oneAtBit16(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0);
 
@@ -1990,42 +1991,16 @@ QUrl AvatarData::getWireSafeSkeletonModelURL() const {
     }
 }
 
-qint64 AvatarData::packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination,
-                           AvatarTraits::TraitVersion traitVersion) {
-
-    qint64 bytesWritten = 0;
-
-    if (traitType == AvatarTraits::SkeletonModelURL) {
-
-        QByteArray encodedSkeletonURL = getWireSafeSkeletonModelURL().toEncoded();
-
-        if (encodedSkeletonURL.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) {
-            qWarning() << "Refusing to pack simple trait" << traitType << "of size" << encodedSkeletonURL.size()
-                << "bytes since it exceeds the maximum size" << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes";
-            return 0;
-        }
-
-        bytesWritten += destination.writePrimitive(traitType);
-
-        if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) {
-            bytesWritten += destination.writePrimitive(traitVersion);
-        }
-        
-        AvatarTraits::TraitWireSize encodedURLSize = encodedSkeletonURL.size();
-        bytesWritten += destination.writePrimitive(encodedURLSize);
-
-        bytesWritten += destination.write(encodedSkeletonURL);
-    }
-
-    return bytesWritten;
+QByteArray AvatarData::packSkeletonModelURL() const {
+    return getWireSafeSkeletonModelURL().toEncoded();
 }
 
+void AvatarData::unpackSkeletonModelURL(const QByteArray& data) {
+    auto skeletonModelURL = QUrl::fromEncoded(data);
+    setSkeletonModelURL(skeletonModelURL);
+}
 
-qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType,
-                                                 AvatarTraits::TraitInstanceID traitInstanceID,
-                                                 ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) {
-    qint64 bytesWritten = 0;
-
+QByteArray AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) {
     // grab a read lock on the avatar entities and check for entity data for the given ID
     QByteArray entityBinaryData;
     _avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] {
@@ -2034,104 +2009,48 @@ qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitTy
         }
     });
 
-    if (entityBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) {
-        qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << entityBinaryData.size()
-                   << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes";
-        return 0;
-    }
-
-    bytesWritten += destination.writePrimitive(traitType);
-
-    if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) {
-        bytesWritten += destination.writePrimitive(traitVersion);
-    }
-
-    bytesWritten += destination.write(traitInstanceID.toRfc4122());
-
-    if (!entityBinaryData.isNull()) {
-        AvatarTraits::TraitWireSize entityBinarySize = entityBinaryData.size();
-
-        bytesWritten += destination.writePrimitive(entityBinarySize);
-        bytesWritten += destination.write(entityBinaryData);
-    } else {
-        bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE);
-    }
-
-    return bytesWritten;
+    return entityBinaryData;
 }
 
-
-qint64 AvatarData::packGrabTraitInstance(AvatarTraits::TraitType traitType,
-                                         AvatarTraits::TraitInstanceID traitInstanceID,
-                                         ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) {
-    qint64 bytesWritten = 0;
-
+QByteArray AvatarData::packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) {
     // grab a read lock on the avatar grabs and check for grab data for the given ID
     QByteArray grabBinaryData;
-
     _avatarGrabsLock.withReadLock([this, &grabBinaryData, &traitInstanceID] {
         if (_avatarGrabData.contains(traitInstanceID)) {
             grabBinaryData = _avatarGrabData[traitInstanceID];
         }
     });
 
-    if (grabBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) {
-        qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << grabBinaryData.size()
-                   << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes";
-        return 0;
-    }
-
-    bytesWritten += destination.writePrimitive(traitType);
-
-    if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) {
-        bytesWritten += destination.writePrimitive(traitVersion);
-    }
-
-    bytesWritten += destination.write(traitInstanceID.toRfc4122());
-
-    if (!grabBinaryData.isNull()) {
-        AvatarTraits::TraitWireSize grabBinarySize = grabBinaryData.size();
-
-        bytesWritten += destination.writePrimitive(grabBinarySize);
-        bytesWritten += destination.write(grabBinaryData);
-    } else {
-        bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE);
-    }
-
-    return bytesWritten;
+    return grabBinaryData;
 }
 
-qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID,
-                                   ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) {
-    qint64 bytesWritten = 0;
+QByteArray AvatarData::packTrait(AvatarTraits::TraitType traitType) const {
+    QByteArray traitBinaryData;
 
+    // Call packer function
+    if (traitType == AvatarTraits::SkeletonModelURL) {
+        traitBinaryData = packSkeletonModelURL();
+    }
+
+    return traitBinaryData;
+}
+
+QByteArray AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID) {
+    QByteArray traitBinaryData;
+
+    // Call packer function
     if (traitType == AvatarTraits::AvatarEntity) {
-        bytesWritten += packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion);
+        traitBinaryData = packAvatarEntityTraitInstance(traitInstanceID);
     } else if (traitType == AvatarTraits::Grab) {
-        bytesWritten += packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion);
+        traitBinaryData = packGrabTraitInstance(traitInstanceID);
     }
 
-    return bytesWritten;
-}
-
-void AvatarData::prepareResetTraitInstances() {
-    if (_clientTraitsHandler) {
-        _avatarEntitiesLock.withReadLock([this]{
-            foreach (auto entityID, _packedAvatarEntityData.keys()) {
-                _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
-            }
-            foreach (auto grabID, _avatarGrabData.keys()) {
-                _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID);
-            }
-        });
-    }
+    return traitBinaryData;
 }
 
 void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData) {
     if (traitType == AvatarTraits::SkeletonModelURL) {
-        // get the URL from the binary data
-        auto skeletonModelURL = QUrl::fromEncoded(traitBinaryData);
-        setSkeletonModelURL(skeletonModelURL);
+        unpackSkeletonModelURL(traitBinaryData);
     }
 }
 
@@ -2152,6 +2071,19 @@ void AvatarData::processDeletedTraitInstance(AvatarTraits::TraitType traitType,
     }
 }
 
+void AvatarData::prepareResetTraitInstances() {
+    if (_clientTraitsHandler) {
+        _avatarEntitiesLock.withReadLock([this]{
+            foreach (auto entityID, _packedAvatarEntityData.keys()) {
+                _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
+            }
+            foreach (auto grabID, _avatarGrabData.keys()) {
+                _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID);
+            }
+        });
+    }
+}
+
 QByteArray AvatarData::identityByteArray(bool setIsReplicated) const {
     QByteArray identityData;
     QDataStream identityStream(&identityData, QIODevice::Append);
diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h
index 795939ea2d..1c4b0cfc53 100755
--- a/libraries/avatars/src/AvatarData.h
+++ b/libraries/avatars/src/AvatarData.h
@@ -479,6 +479,7 @@ class AvatarData : public QObject, public SpatiallyNestable {
      *     avatar. <em>Read-only.</em>
      * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's
      *     size in the virtual world. <em>Read-only.</em>
+     * @property {boolean} hasPriority - is the avatar in a Hero zone? <em>Read-only.</em>
      */
     Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript)
     Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale)
@@ -518,6 +519,8 @@ class AvatarData : public QObject, public SpatiallyNestable {
 
     Q_PROPERTY(float sensorToWorldScale READ getSensorToWorldScale)
 
+    Q_PROPERTY(bool hasPriority READ getHasPriority)
+
 public:
     virtual QString getName() const override { return QString("Avatar:") + _displayName; }
 
@@ -1134,18 +1137,16 @@ public:
     // identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange.
     void processAvatarIdentity(QDataStream& packetStream, bool& identityChanged, bool& displayNameChanged);
 
-    qint64 packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination,
-                     AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION);
-    qint64 packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID,
-                             ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION);
-
-    void prepareResetTraitInstances();
+    QByteArray packTrait(AvatarTraits::TraitType traitType) const;
+    QByteArray packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID);
 
     void processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData);
     void processTraitInstance(AvatarTraits::TraitType traitType,
                               AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData);
     void processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID);
 
+    void prepareResetTraitInstances();
+
     QByteArray identityByteArray(bool setIsReplicated = false) const;
 
     QUrl getWireSafeSkeletonModelURL() const;
@@ -1596,13 +1597,13 @@ protected:
     bool hasParent() const { return !getParentID().isNull(); }
     bool hasFaceTracker() const { return _headData ? _headData->_isFaceTrackerConnected : false; }
 
-    qint64 packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType,
-                                         AvatarTraits::TraitInstanceID traitInstanceID,
-                                         ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion);
-    qint64 packGrabTraitInstance(AvatarTraits::TraitType traitType,
-                                 AvatarTraits::TraitInstanceID traitInstanceID,
-                                 ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion);
+    QByteArray packSkeletonModelURL() const;
+    QByteArray packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID);
+    QByteArray packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID);
 
+    void unpackSkeletonModelURL(const QByteArray& data);
+
+    
     // isReplicated will be true on downstream Avatar Mixers and their clients, but false on the upstream "master"
     // Audio Mixer that the replicated avatar is connected to.
     bool _isReplicated{ false };
diff --git a/libraries/avatars/src/AvatarTraits.cpp b/libraries/avatars/src/AvatarTraits.cpp
new file mode 100644
index 0000000000..724f30e2f3
--- /dev/null
+++ b/libraries/avatars/src/AvatarTraits.cpp
@@ -0,0 +1,135 @@
+//
+//  AvatarTraits.cpp
+//  libraries/avatars/src
+//
+//  Created by Clement Brisset on 3/19/19.
+//  Copyright 2019 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 "AvatarTraits.h"
+
+#include <ExtendedIODevice.h>
+
+#include "AvatarData.h"
+
+namespace AvatarTraits {
+
+    qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarData& avatar) {
+        // Call packer function
+        auto traitBinaryData = avatar.packTrait(traitType);
+        auto traitBinaryDataSize = traitBinaryData.size();
+
+        // Verify packed data
+        if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) {
+            qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize
+                        << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes";
+            return 0;
+        }
+
+        // Write packed data to stream
+        qint64 bytesWritten = 0;
+        bytesWritten += destination.writePrimitive((TraitType)traitType);
+        bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize);
+        bytesWritten += destination.write(traitBinaryData);
+        return bytesWritten;
+    }
+
+    qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination,
+                              TraitVersion traitVersion, const AvatarData& avatar) {
+        // Call packer function
+        auto traitBinaryData = avatar.packTrait(traitType);
+        auto traitBinaryDataSize = traitBinaryData.size();
+
+        // Verify packed data
+        if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) {
+            qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize
+                        << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes";
+            return 0;
+        }
+
+        // Write packed data to stream
+        qint64 bytesWritten = 0;
+        bytesWritten += destination.writePrimitive((TraitType)traitType);
+        bytesWritten += destination.writePrimitive((TraitVersion)traitVersion);
+        bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize);
+        bytesWritten += destination.write(traitBinaryData);
+        return bytesWritten;
+    }
+
+
+    qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID,
+                             ExtendedIODevice& destination, AvatarData& avatar) {
+        // Call packer function
+        auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID);
+        auto traitBinaryDataSize = traitBinaryData.size();
+
+
+        // Verify packed data
+        if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) {
+            qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize
+                        << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes";
+            return 0;
+        }
+
+        // Write packed data to stream
+        qint64 bytesWritten = 0;
+        bytesWritten += destination.writePrimitive((TraitType)traitType);
+        bytesWritten += destination.write(traitInstanceID.toRfc4122());
+
+        if (!traitBinaryData.isNull()) {
+            bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize);
+            bytesWritten += destination.write(traitBinaryData);
+        } else {
+            bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE);
+        }
+
+        return bytesWritten;
+    }
+
+    qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID,
+                                      ExtendedIODevice& destination, TraitVersion traitVersion,
+                                      AvatarData& avatar) {
+        // Call packer function
+        auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID);
+        auto traitBinaryDataSize = traitBinaryData.size();
+
+
+        // Verify packed data
+        if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) {
+            qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize
+                        << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes";
+            return 0;
+        }
+
+        // Write packed data to stream
+        qint64 bytesWritten = 0;
+        bytesWritten += destination.writePrimitive((TraitType)traitType);
+        bytesWritten += destination.writePrimitive((TraitVersion)traitVersion);
+        bytesWritten += destination.write(traitInstanceID.toRfc4122());
+
+        if (!traitBinaryData.isNull()) {
+            bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize);
+            bytesWritten += destination.write(traitBinaryData);
+        } else {
+            bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE);
+        }
+
+        return bytesWritten;
+    }
+
+
+    qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination,
+                                         TraitVersion traitVersion) {
+        qint64 bytesWritten = 0;
+        bytesWritten += destination.writePrimitive(traitType);
+        if (traitVersion > DEFAULT_TRAIT_VERSION) {
+            bytesWritten += destination.writePrimitive(traitVersion);
+        }
+        bytesWritten += destination.write(instanceID.toRfc4122());
+        bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE);
+        return bytesWritten;
+    }
+};
diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h
index 4516572e42..13d64ec225 100644
--- a/libraries/avatars/src/AvatarTraits.h
+++ b/libraries/avatars/src/AvatarTraits.h
@@ -14,20 +14,35 @@
 
 #include <algorithm>
 #include <cstdint>
+#include <array>
 #include <vector>
 
 #include <QtCore/QUuid>
 
+class ExtendedIODevice;
+class AvatarData;
+
 namespace AvatarTraits {
     enum TraitType : int8_t {
+        // Null trait
         NullTrait = -1,
-        SkeletonModelURL,
+
+        // Simple traits
+        SkeletonModelURL = 0,
+
+        // Instanced traits
         FirstInstancedTrait,
         AvatarEntity = FirstInstancedTrait,
         Grab,
+
+        // Traits count
         TotalTraitTypes
     };
 
+    const int NUM_SIMPLE_TRAITS = (int)FirstInstancedTrait;
+    const int NUM_INSTANCED_TRAITS = (int)TotalTraitTypes - (int)FirstInstancedTrait;
+    const int NUM_TRAITS = (int)TotalTraitTypes;
+
     using TraitInstanceID = QUuid;
 
     inline bool isSimpleTrait(TraitType traitType) {
@@ -46,22 +61,19 @@ namespace AvatarTraits {
     const TraitMessageSequence FIRST_TRAIT_SEQUENCE = 0;
     const TraitMessageSequence MAX_TRAIT_SEQUENCE = INT64_MAX;
 
-    inline qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination,
-                                         TraitVersion traitVersion = NULL_TRAIT_VERSION) {
-        qint64 bytesWritten = 0;
+    qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarData& avatar);
+    qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination,
+                              TraitVersion traitVersion, const AvatarData& avatar);
 
-        bytesWritten += destination.writePrimitive(traitType);
+    qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID,
+                             ExtendedIODevice& destination, AvatarData& avatar);
+    qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID,
+                                      ExtendedIODevice& destination, TraitVersion traitVersion,
+                                      AvatarData& avatar);
 
-        if (traitVersion > DEFAULT_TRAIT_VERSION) {
-            bytesWritten += destination.writePrimitive(traitVersion);
-        }
+    qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination,
+                                           TraitVersion traitVersion = NULL_TRAIT_VERSION);
 
-        bytesWritten += destination.write(instanceID.toRfc4122());
-
-        bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE);
-
-        return bytesWritten;
-    }
 };
 
 #endif // hifi_AvatarTraits_h
diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp
index bcbe5308c7..f6bd66e89a 100644
--- a/libraries/avatars/src/ClientTraitsHandler.cpp
+++ b/libraries/avatars/src/ClientTraitsHandler.cpp
@@ -106,9 +106,10 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() {
             auto traitType = static_cast<AvatarTraits::TraitType>(std::distance(traitStatusesCopy.simpleCBegin(), simpleIt));
 
             if (initialSend || *simpleIt == Updated) {
-                if (traitType == AvatarTraits::SkeletonModelURL) {
-                    bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList);
+                bytesWritten += AvatarTraits::packTrait(traitType, *traitsPacketList, *_owningAvatar);
 
+
+                if (traitType == AvatarTraits::SkeletonModelURL) {
                     // keep track of our skeleton version in case we get an override back
                     _currentSkeletonVersion = _currentTraitVersion;
                 }
@@ -124,7 +125,9 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() {
                     || instanceIDValuePair.value == Updated) {
                     // this is a changed trait we need to send or we haven't send out trait information yet
                     // ask the owning avatar to pack it
-                    bytesWritten += _owningAvatar->packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList);
+                    bytesWritten += AvatarTraits::packTraitInstance(instancedIt->traitType, instanceIDValuePair.id,
+                                                                    *traitsPacketList, *_owningAvatar);
+
                 } else if (!initialSend && instanceIDValuePair.value == Deleted) {
                     // pack delete for this trait instance
                     bytesWritten += AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id,
@@ -162,11 +165,11 @@ void ClientTraitsHandler::processTraitOverride(QSharedPointer<ReceivedMessage> m
 
                 // override the skeleton URL but do not mark the trait as having changed
                 // so that we don't unecessarily send a new trait packet to the mixer with the overriden URL
-                auto encodedSkeletonURL = QUrl::fromEncoded(message->readWithoutCopy(traitBinarySize));
 
                 auto hasChangesBefore = _hasChangedTraits;
 
-                _owningAvatar->setSkeletonModelURL(encodedSkeletonURL);
+                auto traitBinaryData = message->readWithoutCopy(traitBinarySize);
+                _owningAvatar->processTrait(traitType, traitBinaryData);
 
                 // setSkeletonModelURL will flag us for changes to the SkeletonModelURL so we reset some state here to
                 // avoid unnecessarily sending the overriden skeleton model URL back to the mixer
diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp
index a716a40ad8..18717c8ca3 100644
--- a/libraries/avatars/src/ScriptAvatarData.cpp
+++ b/libraries/avatars/src/ScriptAvatarData.cpp
@@ -343,6 +343,14 @@ glm::mat4 ScriptAvatarData::getControllerRightHandMatrix() const {
 // END
 //
 
+bool ScriptAvatarData::getHasPriority() const {
+    if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) {
+        return sharedAvatarData->getHasPriority();
+    } else {
+        return false;
+    }
+}
+
 glm::quat ScriptAvatarData::getAbsoluteJointRotationInObjectFrame(int index) const {
     if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) {
         return sharedAvatarData->getAbsoluteJointRotationInObjectFrame(index);
diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h
index 91bac61728..01f7ff360a 100644
--- a/libraries/avatars/src/ScriptAvatarData.h
+++ b/libraries/avatars/src/ScriptAvatarData.h
@@ -68,6 +68,8 @@ class ScriptAvatarData : public QObject {
     Q_PROPERTY(glm::mat4 controllerLeftHandMatrix READ getControllerLeftHandMatrix)
     Q_PROPERTY(glm::mat4 controllerRightHandMatrix READ getControllerRightHandMatrix)
 
+    Q_PROPERTY(bool hasPriority READ getHasPriority)
+
 public:
     ScriptAvatarData(AvatarSharedPointer avatarData);
 
@@ -133,6 +135,8 @@ public:
     glm::mat4 getControllerLeftHandMatrix() const;
     glm::mat4 getControllerRightHandMatrix() const;
     
+    bool getHasPriority() const;
+
 signals:
     void displayNameChanged();
     void sessionDisplayNameChanged();
diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
index 33f4f2d751..d859d4b739 100644
--- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp
@@ -265,7 +265,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) {
         if (_procedural.isReady()) {
             outColor = _procedural.getColor(outColor);
             outColor.a *= _procedural.isFading() ? Interpolate::calculateFadeRatio(_procedural.getFadeStartTime()) : 1.0f;
-            _procedural.prepare(batch, _position, _dimensions, _orientation, ProceduralProgramKey(outColor.a < 1.0f));
+            _procedural.prepare(batch, _position, _dimensions, _orientation, _created, ProceduralProgramKey(outColor.a < 1.0f));
             proceduralRender = true;
         }
     });
diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
index 631148c27a..8a7fa3f8e7 100644
--- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp
@@ -33,7 +33,7 @@ using namespace render::entities;
 
 ZoneEntityRenderer::ZoneEntityRenderer(const EntityItemPointer& entity)
     : Parent(entity) {
-    _background->setSkybox(std::make_shared<ProceduralSkybox>());
+    _background->setSkybox(std::make_shared<ProceduralSkybox>(entity->getCreated()));
 }
 
 void ZoneEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entity) {
diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp
index f0bf13891b..bd4c6e5c71 100644
--- a/libraries/entities/src/EntityItem.cpp
+++ b/libraries/entities/src/EntityItem.cpp
@@ -789,8 +789,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
 
     auto lastEdited = lastEditedFromBufferAdjusted;
     bool otherOverwrites = overwriteLocalData && !weOwnSimulation;
-    auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) {
-        if (stillHasGrabActions()) {
+    // calculate hasGrab once outside the lambda rather than calling it every time inside
+    bool hasGrab = stillHasGrabAction();
+    auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection, hasGrab](quint64 updatedTimestamp, bool valueChanged) {
+        if (hasGrab) {
             return false;
         }
         bool simulationChanged = lastEdited > updatedTimestamp;
@@ -957,12 +959,18 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
     // by doing this parsing here... but it's not likely going to fully recover the content.
     //
 
-    if (overwriteLocalData && (getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES))) {
+    if (overwriteLocalData &&
+            !hasGrab &&
+            (getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES))) {
         // NOTE: This code is attempting to "repair" the old data we just got from the server to make it more
         // closely match where the entities should be if they'd stepped forward in time to "now". The server
         // is sending us data with a known "last simulated" time. That time is likely in the past, and therefore
         // this "new" data is actually slightly out of date. We calculate the time we need to skip forward and
         // use our simulation helper routine to get a best estimate of where the entity should be.
+        //
+        // NOTE: We don't want to do this in the hasGrab case because grabs "know best"
+        // (e.g. grabs will prevent drift between distributed physics simulations).
+        //
         float skipTimeForward = (float)(now - lastSimulatedFromBufferAdjusted) / (float)(USECS_PER_SECOND);
 
         // we want to extrapolate the motion forward to compensate for packet travel time, but
@@ -1426,7 +1434,7 @@ void EntityItem::getTransformAndVelocityProperties(EntityItemProperties& propert
 
 void EntityItem::upgradeScriptSimulationPriority(uint8_t priority) {
     uint8_t newPriority = glm::max(priority, _scriptSimulationPriority);
-    if (newPriority < SCRIPT_GRAB_SIMULATION_PRIORITY && stillHasGrabActions()) {
+    if (newPriority < SCRIPT_GRAB_SIMULATION_PRIORITY && stillHasMyGrabAction()) {
         newPriority = SCRIPT_GRAB_SIMULATION_PRIORITY;
     }
     if (newPriority != _scriptSimulationPriority) {
@@ -1439,7 +1447,7 @@ void EntityItem::upgradeScriptSimulationPriority(uint8_t priority) {
 void EntityItem::clearScriptSimulationPriority() {
     // DO NOT markDirtyFlags(Simulation::DIRTY_SIMULATION_OWNERSHIP_PRIORITY) here, because this
     // is only ever called from the code that actually handles the dirty flags, and it knows best.
-    _scriptSimulationPriority = stillHasGrabActions() ? SCRIPT_GRAB_SIMULATION_PRIORITY : 0;
+    _scriptSimulationPriority = stillHasMyGrabAction() ? SCRIPT_GRAB_SIMULATION_PRIORITY : 0;
 }
 
 void EntityItem::setPendingOwnershipPriority(uint8_t priority) {
@@ -2186,7 +2194,7 @@ void EntityItem::enableNoBootstrap() {
 }
 
 void EntityItem::disableNoBootstrap() {
-    if (!stillHasGrabActions()) {
+    if (!stillHasMyGrabAction()) {
         _flags &= ~Simulation::SPECIAL_FLAGS_NO_BOOTSTRAPPING;
         _flags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar
 
@@ -2272,7 +2280,13 @@ bool EntityItem::removeAction(EntitySimulationPointer simulation, const QUuid& a
     return success;
 }
 
-bool EntityItem::stillHasGrabActions() const {
+bool EntityItem::stillHasGrabAction() const {
+    return !_grabActions.empty();
+}
+
+// retutrns 'true' if there exists an action that returns 'true' for EntityActionInterface::isMine()
+// (e.g. the action belongs to the MyAvatar instance)
+bool EntityItem::stillHasMyGrabAction() const {
     QList<EntityDynamicPointer> holdActions = getActionsOfType(DYNAMIC_TYPE_HOLD);
     QList<EntityDynamicPointer>::const_iterator i = holdActions.begin();
     while (i != holdActions.end()) {
@@ -2700,20 +2714,6 @@ void EntityItem::setLastEdited(quint64 lastEdited) {
     });
 }
 
-quint64 EntityItem::getLastBroadcast() const {
-    quint64 result;
-    withReadLock([&] {
-        result = _lastBroadcast;
-    });
-    return result;
-}
-
-void EntityItem::setLastBroadcast(quint64 lastBroadcast) {
-    withWriteLock([&] {
-        _lastBroadcast = lastBroadcast;
-    });
-}
-
 void EntityItem::markAsChangedOnServer() {
     withWriteLock([&] {
         _changedOnServer = usecTimestampNow();
@@ -3479,6 +3479,9 @@ void EntityItem::addGrab(GrabPointer grab) {
         simulation->addDynamic(action);
         markDirtyFlags(Simulation::DIRTY_MOTION_TYPE);
         simulation->changeEntity(getThisPointer());
+
+        // don't forget to set isMine() for locally-created grabs
+        action->setIsMine(grab->getOwnerID() == Physics::getSessionUUID());
     }
 }
 
diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h
index fae871a124..01ed949a0c 100644
--- a/libraries/entities/src/EntityItem.h
+++ b/libraries/entities/src/EntityItem.h
@@ -124,8 +124,8 @@ public:
         { return (float)(usecTimestampNow() - getLastEdited()) / (float)USECS_PER_SECOND; }
 
     /// Last time we sent out an edit packet for this entity
-    quint64 getLastBroadcast() const;
-    void setLastBroadcast(quint64 lastBroadcast);
+    quint64 getLastBroadcast() const { return _lastBroadcast; }
+    void setLastBroadcast(quint64 lastBroadcast) { _lastBroadcast = lastBroadcast; }
 
     void markAsChangedOnServer();
     quint64 getLastChangedOnServer() const;
@@ -562,6 +562,8 @@ public:
     static void setPrimaryViewFrustumPositionOperator(std::function<glm::vec3()> getPrimaryViewFrustumPositionOperator) { _getPrimaryViewFrustumPositionOperator = getPrimaryViewFrustumPositionOperator; }
     static glm::vec3 getPrimaryViewFrustumPosition() { return _getPrimaryViewFrustumPositionOperator(); }
 
+    bool stillHasMyGrabAction() const;
+
 signals:
     void requestRenderUpdate();
     void spaceUpdate(std::pair<int32_t, glm::vec4> data);
@@ -574,7 +576,7 @@ protected:
     void setSimulated(bool simulated) { _simulated = simulated; }
 
     const QByteArray getDynamicDataInternal() const;
-    bool stillHasGrabActions() const;
+    bool stillHasGrabAction() const;
     void setDynamicDataInternal(QByteArray dynamicData);
 
     virtual void dimensionsChanged() override;
diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp
index aa4b3902c2..ca914731b5 100644
--- a/libraries/entities/src/EntityScriptingInterface.cpp
+++ b/libraries/entities/src/EntityScriptingInterface.cpp
@@ -1101,13 +1101,13 @@ void EntityScriptingInterface::handleEntityScriptCallMethodPacket(QSharedPointer
 
 void EntityScriptingInterface::onAddingEntity(EntityItem* entity) {
     if (entity->isWearable()) {
-        emit addingWearable(entity->getEntityItemID());
+        QMetaObject::invokeMethod(this, "addingWearable", Q_ARG(QUuid, entity->getEntityItemID()));
     }
 }
 
 void EntityScriptingInterface::onDeletingEntity(EntityItem* entity) {
     if (entity->isWearable()) {
-        emit deletingWearable(entity->getEntityItemID());
+        QMetaObject::invokeMethod(this, "deletingWearable", Q_ARG(QUuid, entity->getEntityItemID()));
     }
 }
 
diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp
index 5c5b5fa002..aed313f54e 100644
--- a/libraries/fbx/src/FBXSerializer.cpp
+++ b/libraries/fbx/src/FBXSerializer.cpp
@@ -443,6 +443,7 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
     QString hifiGlobalNodeID;
     unsigned int meshIndex = 0;
     haveReportedUnhandledRotationOrder = false;
+    int fbxVersionNumber = -1;
     foreach (const FBXNode& child, node.children) {
 
         if (child.name == "FBXHeaderExtension") {
@@ -465,6 +466,8 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
                             }
                         }
                     }
+                } else if (object.name == "FBXVersion") {
+                    fbxVersionNumber = object.properties.at(0).toInt();
                 }
             }
         } else if (child.name == "GlobalSettings") {
@@ -1161,8 +1164,14 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
                             counter++;
                         }
                     }
-                    _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2));
-                    _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1));
+                    if (_connectionParentMap.value(getID(connection.properties, 1)) == "0") {
+                        // don't assign the new parent
+                        qCDebug(modelformat) << "root node " << getID(connection.properties, 1) << "  has discarded parent " << getID(connection.properties, 2);
+                        _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1));
+                    } else {
+                        _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2));
+                        _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1));
+                    }
                 }
             }
         }
@@ -1311,8 +1320,6 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
 
         joint.bindTransformFoundInCluster = false;
 
-        hfmModel.joints.append(joint);
-
         QString rotationID = localRotations.value(modelID);
         AnimationCurve xRotCurve = animationCurves.value(xComponents.value(rotationID));
         AnimationCurve yRotCurve = animationCurves.value(yComponents.value(rotationID));
@@ -1335,7 +1342,13 @@ HFMModel* FBXSerializer::extractHFMModel(const hifi::VariantHash& mapping, const
                 xPosCurve.values.isEmpty() ? defaultPosValues.x : xPosCurve.values.at(i % xPosCurve.values.size()),
                 yPosCurve.values.isEmpty() ? defaultPosValues.y : yPosCurve.values.at(i % yPosCurve.values.size()),
                 zPosCurve.values.isEmpty() ? defaultPosValues.z : zPosCurve.values.at(i % zPosCurve.values.size()));
+            if ((fbxVersionNumber < 7500) && (i == 0)) {
+                joint.translation = hfmModel.animationFrames[i].translations[jointIndex];
+                joint.rotation = hfmModel.animationFrames[i].rotations[jointIndex];
+            }
+
         }
+        hfmModel.joints.append(joint);
     }
 
     // NOTE: shapeVertices are in joint-frame
diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp
index 4647c50496..226433e388 100644
--- a/libraries/networking/src/AccountManager.cpp
+++ b/libraries/networking/src/AccountManager.cpp
@@ -536,7 +536,7 @@ void AccountManager::requestAccessToken(const QString& login, const QString& pas
 
     QByteArray postData;
     postData.append("grant_type=password&");
-    postData.append("username=" + login + "&");
+    postData.append("username=" + QUrl::toPercentEncoding(login) + "&");
     postData.append("password=" + QUrl::toPercentEncoding(password) + "&");
     postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE);
 
diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp
index a9dbc12b09..18a180ad79 100644
--- a/libraries/networking/src/LimitedNodeList.cpp
+++ b/libraries/networking/src/LimitedNodeList.cpp
@@ -677,6 +677,9 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
     // If there is a new node with the same socket, this is a reconnection, kill the old node
     removeOldNode(findNodeWithAddr(publicSocket));
     removeOldNode(findNodeWithAddr(localSocket));
+    // If there is an old Connection to the new node's address kill it
+    _nodeSocket.cleanupConnection(publicSocket);
+    _nodeSocket.cleanupConnection(localSocket);
 
     auto it = _connectionIDs.find(uuid);
     if (it == _connectionIDs.end()) {
diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp
index 32ee72ea1c..20ba3cde60 100644
--- a/libraries/octree/src/OctreePersistThread.cpp
+++ b/libraries/octree/src/OctreePersistThread.cpp
@@ -82,11 +82,11 @@ void OctreePersistThread::start() {
         }
 
         if (data.readOctreeDataInfoFromData(_cachedJSONData)) {
-            qCDebug(octree) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.version << ")";
+            qCDebug(octree) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.dataVersion << ")";
             packet->writePrimitive(true);
             auto id = data.id.toRfc4122();
             packet->write(id);
-            packet->writePrimitive(data.version);
+            packet->writePrimitive(data.dataVersion);
         } else {
             _cachedJSONData.clear();
             qCWarning(octree) << "No octree data found";
@@ -144,8 +144,8 @@ void OctreePersistThread::handleOctreeDataFileReply(QSharedPointer<ReceivedMessa
     quint64 loadStarted = usecTimestampNow();
 
     if (hasValidOctreeData) {
-        qDebug() << "Setting entity version info to: " << data.id << data.version;
-        _tree->setOctreeVersionInfo(data.id, data.version);
+        qDebug() << "Setting entity version info to: " << data.id << data.dataVersion;
+        _tree->setOctreeVersionInfo(data.id, data.dataVersion);
     }
 
     bool persistentFileRead;
diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp
index ff8c270371..9940da0b9a 100644
--- a/libraries/procedural/src/procedural/Procedural.cpp
+++ b/libraries/procedural/src/procedural/Procedural.cpp
@@ -225,13 +225,15 @@ void Procedural::prepare(gpu::Batch& batch,
                          const glm::vec3& position,
                          const glm::vec3& size,
                          const glm::quat& orientation,
+                         const uint64_t& created,
                          const ProceduralProgramKey key) {
     std::lock_guard<std::mutex> lock(_mutex);
     _entityDimensions = size;
     _entityPosition = position;
     _entityOrientation = glm::mat3_cast(orientation);
+    _entityCreated = created;
     if (!_shaderPath.isEmpty()) {
-        auto lastModified = (quint64)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch();
+        auto lastModified = (uint64_t)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch();
         if (lastModified > _shaderModified) {
             QFile file(_shaderPath);
             file.open(QIODevice::ReadOnly);
@@ -278,7 +280,10 @@ void Procedural::prepare(gpu::Batch& batch,
 
         _proceduralPipelines[key] = gpu::Pipeline::create(program, key.isTransparent() ? _transparentState : _opaqueState);
 
-        _start = usecTimestampNow();
+        _lastCompile = usecTimestampNow();
+        if (_firstCompile == 0) {
+            _firstCompile = _lastCompile;
+        }
         _frameCount = 0;
         recompiledShader = true;
     }
@@ -371,7 +376,11 @@ void Procedural::setupUniforms() {
     _uniforms.push_back([=](gpu::Batch& batch) {
         _standardInputs.position = vec4(_entityPosition, 1.0f);
         // Minimize floating point error by doing an integer division to milliseconds, before the floating point division to seconds
-        _standardInputs.time = (float)((usecTimestampNow() - _start) / USECS_PER_MSEC) / MSECS_PER_SECOND;
+        auto now = usecTimestampNow();
+        _standardInputs.timeSinceLastCompile = (float)((now - _lastCompile) / USECS_PER_MSEC) / MSECS_PER_SECOND;
+        _standardInputs.timeSinceFirstCompile = (float)((now - _firstCompile) / USECS_PER_MSEC) / MSECS_PER_SECOND;
+        _standardInputs.timeSinceEntityCreation = (float)((now - _entityCreated) / USECS_PER_MSEC) / MSECS_PER_SECOND;
+
 
         // Date
         {
diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h
index 8477e69afc..956cef368f 100644
--- a/libraries/procedural/src/procedural/Procedural.h
+++ b/libraries/procedural/src/procedural/Procedural.h
@@ -82,10 +82,11 @@ public:
 
     bool isReady() const;
     bool isEnabled() const { return _enabled; }
-    void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, const ProceduralProgramKey key = ProceduralProgramKey());
+    void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation,
+                 const uint64_t& created, const ProceduralProgramKey key = ProceduralProgramKey());
 
     glm::vec4 getColor(const glm::vec4& entityColor) const;
-    quint64 getFadeStartTime() const { return _fadeStartTime; }
+    uint64_t getFadeStartTime() const { return _fadeStartTime; }
     bool isFading() const { return _doesFade && _isFading; }
     void setIsFading(bool isFading) { _isFading = isFading; }
     void setDoesFade(bool doesFade) { _doesFade = doesFade; }
@@ -106,9 +107,10 @@ protected:
         vec4 date;
         vec4 position; 
         vec4 scale;
-        float time;
+        float timeSinceLastCompile;
+        float timeSinceFirstCompile;
+        float timeSinceEntityCreation;
         int frameCount;
-        vec2 _spare1;
         vec4 resolution[4];
         mat4 orientation;
     };
@@ -116,9 +118,10 @@ protected:
     static_assert(0 == offsetof(StandardInputs, date), "ProceduralOffsets");
     static_assert(16 == offsetof(StandardInputs, position), "ProceduralOffsets");
     static_assert(32 == offsetof(StandardInputs, scale), "ProceduralOffsets");
-    static_assert(48 == offsetof(StandardInputs, time), "ProceduralOffsets");
-    static_assert(52 == offsetof(StandardInputs, frameCount), "ProceduralOffsets");
-    static_assert(56 == offsetof(StandardInputs, _spare1), "ProceduralOffsets");
+    static_assert(48 == offsetof(StandardInputs, timeSinceLastCompile), "ProceduralOffsets");
+    static_assert(52 == offsetof(StandardInputs, timeSinceFirstCompile), "ProceduralOffsets");
+    static_assert(56 == offsetof(StandardInputs, timeSinceEntityCreation), "ProceduralOffsets");
+    static_assert(60 == offsetof(StandardInputs, frameCount), "ProceduralOffsets");
     static_assert(64 == offsetof(StandardInputs, resolution), "ProceduralOffsets");
     static_assert(128 == offsetof(StandardInputs, orientation), "ProceduralOffsets");
 
@@ -126,13 +129,14 @@ protected:
     ProceduralData _data;
 
     bool _enabled { false };
-    uint64_t _start { 0 };
+    uint64_t _lastCompile { 0 };
+    uint64_t _firstCompile { 0 };
     int32_t _frameCount { 0 };
 
     // Rendering object descriptions, from userData
     QString _shaderSource;
     QString _shaderPath;
-    quint64 _shaderModified { 0 };
+    uint64_t _shaderModified { 0 };
     NetworkShaderPointer _networkShader;
     bool _shaderDirty { true };
     bool _uniformsDirty { true };
@@ -152,11 +156,12 @@ protected:
     glm::vec3 _entityDimensions;
     glm::vec3 _entityPosition;
     glm::mat3 _entityOrientation;
+    uint64_t _entityCreated;
 
 private:
     void setupUniforms();
 
-    mutable quint64 _fadeStartTime { 0 };
+    mutable uint64_t _fadeStartTime { 0 };
     mutable bool _hasStartedFade { false };
     mutable bool _isFading { false };
     bool _doesFade { true };
diff --git a/libraries/procedural/src/procedural/ProceduralCommon.slh b/libraries/procedural/src/procedural/ProceduralCommon.slh
index bd894a9e92..6e73534440 100644
--- a/libraries/procedural/src/procedural/ProceduralCommon.slh
+++ b/libraries/procedural/src/procedural/ProceduralCommon.slh
@@ -36,9 +36,11 @@ LAYOUT_STD140(binding=0) uniform standardInputsBuffer {
     // Offset 48
     float globalTime;
     // Offset 52
-    int frameCount;
+    float localCreatedTime;
     // Offset 56
-    vec2 _spare1;
+    float entityTime;
+    // Offset 60
+    int frameCount;
     // Offset 64, acts as vec4[4] for alignment purposes
     vec3 channelResolution[4];
     // Offset 128, acts as vec4[3] for alignment purposes
@@ -52,6 +54,8 @@ LAYOUT_STD140(binding=0) uniform standardInputsBuffer {
 #define iWorldPosition standardInputs.worldPosition
 #define iWorldScale standardInputs.worldScale
 #define iGlobalTime standardInputs.globalTime
+#define iLocalCreatedTime standardInputs.localCreatedTime
+#define iEntityTime standardInputs.entityTime
 #define iFrameCount standardInputs.frameCount
 #define iChannelResolution standardInputs.channelResolution
 #define iWorldOrientation standardInputs.worldOrientation
diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp
index 6eb6d531e1..53df1532dc 100644
--- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp
+++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp
@@ -17,7 +17,7 @@
 #include <ViewFrustum.h>
 #include <shaders/Shaders.h>
 
-ProceduralSkybox::ProceduralSkybox() : graphics::Skybox() {
+ProceduralSkybox::ProceduralSkybox(uint64_t created) : graphics::Skybox(), _created(created) {
     _procedural._vertexSource = gpu::Shader::createVertex(shader::graphics::vertex::skybox)->getSource();
     _procedural._opaqueFragmentSource = shader::Source::get(shader::procedural::fragment::proceduralSkybox);
     // Adjust the pipeline state for background using the stencil test
@@ -59,7 +59,7 @@ void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum,
     batch.setModelTransform(Transform()); // only for Mac
 
     auto& procedural = skybox._procedural;
-    procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat());
+    procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat(), skybox.getCreated());
     skybox.prepare(batch);
     batch.draw(gpu::TRIANGLE_STRIP, 4);
 }
diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.h b/libraries/procedural/src/procedural/ProceduralSkybox.h
index 5db1078a5f..a1d7ea8fa7 100644
--- a/libraries/procedural/src/procedural/ProceduralSkybox.h
+++ b/libraries/procedural/src/procedural/ProceduralSkybox.h
@@ -19,7 +19,7 @@
 
 class ProceduralSkybox: public graphics::Skybox {
 public:
-    ProceduralSkybox();
+    ProceduralSkybox(uint64_t created = 0);
     
     void parse(const QString& userData) { _procedural.setProceduralData(ProceduralData::parse(userData)); }
 
@@ -29,8 +29,11 @@ public:
     void render(gpu::Batch& batch, const ViewFrustum& frustum) const override;
     static void render(gpu::Batch& batch, const ViewFrustum& frustum, const ProceduralSkybox& skybox);
 
+    uint64_t getCreated() const { return _created; }
+
 protected:
     mutable Procedural _procedural;
+    uint64_t _created;
 };
 typedef std::shared_ptr< ProceduralSkybox > ProceduralSkyboxPointer;
 
diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js
index 3e8e0b1008..9771348377 100644
--- a/scripts/modules/appUi.js
+++ b/scripts/modules/appUi.js
@@ -353,10 +353,11 @@ function AppUi(properties) {
         // Close if necessary, clean up any remaining handlers, and remove the button.
         GlobalServices.myUsernameChanged.disconnect(restartNotificationPoll);
         GlobalServices.findableByChanged.disconnect(restartNotificationPoll);
+        that.tablet.screenChanged.disconnect(that.onScreenChanged);
         if (that.isOpen) {
             that.close();
+            that.onScreenChanged("", "");
         }
-        that.tablet.screenChanged.disconnect(that.onScreenChanged);
         if (that.button) {
             if (that.onClicked) {
                 that.button.clicked.disconnect(that.onClicked);
diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js
index fb61b914a3..6439d30023 100644
--- a/scripts/system/avatarapp.js
+++ b/scripts/system/avatarapp.js
@@ -1,6 +1,8 @@
 "use strict";
 /*jslint vars:true, plusplus:true, forin:true*/
-/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/
+/*global Tablet, Script, Entities, MyAvatar, Camera, Quat, HMD, Account, UserActivityLogger, Messages, print,
+  AvatarBookmarks, ContextOverlay, AddressManager
+*/
 /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
 //
 // avatarapp.js
@@ -14,7 +16,6 @@
 
 (function() { // BEGIN LOCAL_SCOPE
 
-var request = Script.require('request').request;
 var AVATARAPP_QML_SOURCE = "hifi/AvatarApp.qml";
 Script.include("/~/system/libraries/controllers.js");
 
@@ -22,7 +23,6 @@ Script.include("/~/system/libraries/controllers.js");
 var ENTRY_AVATAR_URL = "avatarUrl";
 var ENTRY_AVATAR_ENTITIES = "avatarEntites";
 var ENTRY_AVATAR_SCALE = "avatarScale";
-var ENTRY_VERSION = "version";
 
 function executeLater(callback) {
     Script.setTimeout(callback, 300);
@@ -44,7 +44,7 @@ function getMyAvatarWearables() {
         }
 
         var localRotation = entity.properties.localRotation;
-        entity.properties.localRotationAngles = Quat.safeEulerAngles(localRotation)
+        entity.properties.localRotationAngles = Quat.safeEulerAngles(localRotation);
         wearablesArray.push(entity);
     }
 
@@ -52,7 +52,7 @@ function getMyAvatarWearables() {
 }
 
 function getMyAvatar() {
-    var avatar = {}
+    var avatar = {};
     avatar[ENTRY_AVATAR_URL] = MyAvatar.skeletonModelURL;
     avatar[ENTRY_AVATAR_SCALE] = MyAvatar.getAvatarScale();
     avatar[ENTRY_AVATAR_ENTITIES] = getMyAvatarWearables();
@@ -68,7 +68,7 @@ function getMyAvatarSettings() {
         collisionSoundUrl : MyAvatar.collisionSoundURL,
         animGraphUrl: MyAvatar.getAnimGraphUrl(),
         animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(),
-    }
+    };
 }
 
 function updateAvatarWearables(avatar, callback, wearablesOverride) {
@@ -76,7 +76,8 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) {
         var wearables = wearablesOverride ? wearablesOverride : getMyAvatarWearables();
         avatar[ENTRY_AVATAR_ENTITIES] = wearables;
 
-        sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables})
+        sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables});
+        sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()});
 
         if(callback)
             callback();
@@ -101,7 +102,7 @@ var adjustWearables = {
             this.opened = value;
         }
     }
-}
+};
 
 var currentAvatarWearablesBackup = null;
 var currentAvatar = null;
@@ -112,7 +113,7 @@ function onTargetScaleChanged() {
     if(currentAvatar.scale !== MyAvatar.getAvatarScale()) {
         currentAvatar.scale = MyAvatar.getAvatarScale();
         if(notifyScaleChanged) {
-            sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale})
+            sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale});
         }
     }
 }
@@ -126,7 +127,7 @@ function onSkeletonModelURLChanged() {
 function onDominantHandChanged(dominantHand) {
     if(currentAvatarSettings.dominantHand !== dominantHand) {
         currentAvatarSettings.dominantHand = dominantHand;
-        sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand})
+        sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand});
     }
 }
 
@@ -140,37 +141,37 @@ function onHmdAvatarAlignmentTypeChanged(type) {
 function onCollisionsEnabledChanged(enabled) {
     if(currentAvatarSettings.collisionsEnabled !== enabled) {
         currentAvatarSettings.collisionsEnabled = enabled;
-        sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled})
+        sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled});
     }
 }
 
 function onOtherAvatarsCollisionsEnabledChanged(enabled) {
     if (currentAvatarSettings.otherAvatarsCollisionsEnabled !== enabled) {
         currentAvatarSettings.otherAvatarsCollisionsEnabled = enabled;
-        sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled })
+        sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled });
     }
 }
 
 function onNewCollisionSoundUrl(url) {
     if(currentAvatarSettings.collisionSoundUrl !== url) {
         currentAvatarSettings.collisionSoundUrl = url;
-        sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url})
+        sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url});
     }
 }
 
 function onAnimGraphUrlChanged(url) {
     if (currentAvatarSettings.animGraphUrl !== url) {
         currentAvatarSettings.animGraphUrl = url;
-        sendToQml({ 'method': 'settingChanged', 'name': 'animGraphUrl', 'value': currentAvatarSettings.animGraphUrl })
+        sendToQml({ 'method': 'settingChanged', 'name': 'animGraphUrl', 'value': currentAvatarSettings.animGraphUrl });
 
         if (currentAvatarSettings.animGraphOverrideUrl !== MyAvatar.getAnimGraphOverrideUrl()) {
             currentAvatarSettings.animGraphOverrideUrl = MyAvatar.getAnimGraphOverrideUrl();
-            sendToQml({ 'method': 'settingChanged', 'name': 'animGraphOverrideUrl', 'value': currentAvatarSettings.animGraphOverrideUrl })
+            sendToQml({ 'method': 'settingChanged', 'name': 'animGraphOverrideUrl',
+                        'value': currentAvatarSettings.animGraphOverrideUrl });
         }
     }
 }
 
-var selectedAvatarEntityGrabbable = false;
 var selectedAvatarEntityID = null;
 var grabbedAvatarEntityChangeNotifier = null;
 
@@ -178,6 +179,33 @@ var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml";
 var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace";
 var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js");
 
+function getWearablesFrozen() {
+    var wearablesFrozen = true;
+    var wearablesArray = getMyAvatarWearables();
+    wearablesArray.forEach(function(wearable) {
+        if (isGrabbable(wearable.id)) {
+            wearablesFrozen = false;
+        }
+    });
+
+    return wearablesFrozen;
+}
+
+function freezeWearables() {
+    var wearablesArray = getMyAvatarWearables();
+    wearablesArray.forEach(function(wearable) {
+        setGrabbable(wearable.id, false);
+    });
+}
+
+function unfreezeWearables() {
+    var wearablesArray = getMyAvatarWearables();
+    wearablesArray.forEach(function(wearable) {
+        setGrabbable(wearable.id, true);
+    });
+}
+
+
 function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml.
     switch (message.method) {
     case 'getAvatars':
@@ -201,7 +229,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
             }
         }
 
-        sendToQml(message)
+        sendToQml(message);
         break;
     case 'selectAvatar':
         Entities.addingWearable.disconnect(onAddingWearable);
@@ -209,6 +237,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
         AvatarBookmarks.loadBookmark(message.name);
         Entities.addingWearable.connect(onAddingWearable);
         Entities.deletingWearable.connect(onDeletingWearable);
+        sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()});
         break;
     case 'deleteAvatar':
         AvatarBookmarks.removeBookmark(message.name);
@@ -228,11 +257,12 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
             message.properties.localRotationAngles = Quat.safeEulerAngles(message.properties.localRotation);
         }
 
-        sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false})
+        sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false});
         break;
     case 'adjustWearablesOpened':
         currentAvatarWearablesBackup = getMyAvatarWearables();
         adjustWearables.setOpened(true);
+        unfreezeWearables();
 
         Entities.mousePressOnEntity.connect(onSelectedEntity);
         Messages.subscribe('Hifi-Object-Manipulation');
@@ -305,11 +335,11 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
         var currentAvatarURL = MyAvatar.getFullAvatarURLFromPreferences();
         if(currentAvatarURL !== message.avatarURL) {
             MyAvatar.useFullAvatarURL(message.avatarURL);
-            sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL})
+            sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL});
         }
         break;
     case 'navigate':
-        var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system")
+        var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
         if(message.url.indexOf('app://') === 0) {
             if (message.url === 'app://marketplace') {
                 tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL);
@@ -345,7 +375,17 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
         MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl;
         MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl);
 
-        settings = getMyAvatarSettings();
+        currentAvatarSettings = getMyAvatarSettings();
+        break;
+    case 'toggleWearablesFrozen':
+        var wearablesFrozen = getWearablesFrozen();
+        wearablesFrozen = !wearablesFrozen;
+        if (wearablesFrozen) {
+            freezeWearables();
+        } else {
+            unfreezeWearables();
+        }
+        sendToQml({'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : wearablesFrozen});
         break;
     default:
         print('Unrecognized message from AvatarApp.qml');
@@ -366,9 +406,11 @@ function isGrabbable(entityID) {
 }
 
 function setGrabbable(entityID, grabbable) {
-    var properties = Entities.getEntityProperties(entityID, ['avatarEntity']);
-    if (properties.avatarEntity) {
-        Entities.editEntity(entityID, { grab: { grabbable: grabbable }});
+    var properties = Entities.getEntityProperties(entityID, ['avatarEntity', 'grab.grabbable']);
+    if (properties.avatarEntity && properties.grab.grabbable != grabbable) {
+        var editProps = { grab: { grabbable: grabbable }};
+        Entities.editEntity(entityID, editProps);
+        sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()});
     }
 }
 
@@ -378,18 +420,7 @@ function ensureWearableSelected(entityID) {
             Script.clearInterval(grabbedAvatarEntityChangeNotifier);
             grabbedAvatarEntityChangeNotifier = null;
         }
-
-        if(selectedAvatarEntityID !== null) {
-            setGrabbable(selectedAvatarEntityID, selectedAvatarEntityGrabbable);
-        }
-
         selectedAvatarEntityID = entityID;
-        selectedAvatarEntityGrabbable = isGrabbable(entityID);
-
-        if(selectedAvatarEntityID !== null) {
-            setGrabbable(selectedAvatarEntityID, true);
-        }
-
         return true;
     }
 
@@ -398,7 +429,7 @@ function ensureWearableSelected(entityID) {
 
 function isEntityBeingWorn(entityID) {
     return Entities.getEntityProperties(entityID, 'parentID').parentID === MyAvatar.sessionUUID;
-};
+}
 
 function onSelectedEntity(entityID, pointerEvent) {
     if(selectedAvatarEntityID !== entityID && isEntityBeingWorn(entityID))
@@ -413,12 +444,14 @@ function onAddingWearable(entityID) {
     updateAvatarWearables(currentAvatar, function() {
         sendToQml({'method' : 'updateAvatarInBookmarks'});
     });
+    sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()});
 }
 
 function onDeletingWearable(entityID) {
     updateAvatarWearables(currentAvatar, function() {
         sendToQml({'method' : 'updateAvatarInBookmarks'});
     });
+    sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()});
 }
 
 function handleWearableMessages(channel, message, sender) {
@@ -435,30 +468,35 @@ function handleWearableMessages(channel, message, sender) {
     }
 
     var entityID = parsedMessage.grabbedEntity;
+
+    var updateWearable = function() {
+        // for some reasons Entities.getEntityProperties returns more than was asked..
+        var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions'];
+        var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames);
+        var properties = {};
+
+        propertyNames.forEach(function(propertyName) {
+            properties[propertyName] = entityProperties[propertyName];
+        });
+
+        properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation);
+        sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID,
+                   'wearableIndex' : -1, 'properties' : properties, updateUI : true});
+
+    };
+
     if(parsedMessage.action === 'grab') {
         if(selectedAvatarEntityID !== entityID) {
             ensureWearableSelected(entityID);
             sendToQml({'method' : 'selectAvatarEntity', 'entityID' : selectedAvatarEntityID});
         }
 
-        grabbedAvatarEntityChangeNotifier = Script.setInterval(function() {
-            // for some reasons Entities.getEntityProperties returns more than was asked..
-            var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions'];
-            var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames);
-            var properties = {}
-
-            propertyNames.forEach(function(propertyName) {
-                properties[propertyName] = entityProperties[propertyName];
-            })
-
-            properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation);
-            sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true})
-
-        }, 1000);
+        grabbedAvatarEntityChangeNotifier = Script.setInterval(updateWearable, 1000);
     } else if(parsedMessage.action === 'release') {
         if(grabbedAvatarEntityChangeNotifier !== null) {
             Script.clearInterval(grabbedAvatarEntityChangeNotifier);
             grabbedAvatarEntityChangeNotifier = null;
+            updateWearable();
         }
     }
 }
@@ -481,8 +519,8 @@ function onBookmarkDeleted(bookmarkName) {
 function onBookmarkAdded(bookmarkName) {
     var bookmark = AvatarBookmarks.getBookmark(bookmarkName);
     bookmark.avatarEntites.forEach(function(avatarEntity) {
-        avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation)
-    })
+        avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation);
+    });
 
     sendToQml({ 'method': 'bookmarkAdded', 'bookmarkName': bookmarkName, 'bookmark': bookmark });
 }
@@ -601,14 +639,8 @@ function onTabletScreenChanged(type, url) {
     onAvatarAppScreen = onAvatarAppScreenNow;
 
     if(onAvatarAppScreenNow) {
-        var message = {
-            'method' : 'initialize',
-            'data' : {
-                'jointNames' : MyAvatar.getJointNames()
-            }
-        };
-
-        sendToQml(message)
+        sendToQml({ 'method' : 'initialize', 'data' : { jointNames : MyAvatar.getJointNames() }});
+        sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()});
     }
 }
 
diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js
index 17ff918243..86806fd8b4 100644
--- a/scripts/system/commerce/wallet.js
+++ b/scripts/system/commerce/wallet.js
@@ -377,6 +377,9 @@ function deleteSendMoneyParticleEffect() {
 }
 
 function onUsernameChanged() {
+    if (ui.checkIsOpen()) {
+        ui.open(WALLET_QML_SOURCE);
+    }
 }    
 
 var MARKETPLACE_QML_PATH = "hifi/commerce/marketplace/Marketplace.qml";
diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js
index 5cb95f625d..51645e5502 100644
--- a/scripts/system/libraries/controllerDispatcherUtils.js
+++ b/scripts/system/libraries/controllerDispatcherUtils.js
@@ -341,8 +341,6 @@ entityIsGrabbable = function (eigProps) {
     var grabbable = getGrabbableData(eigProps).grabbable;
     if (!grabbable ||
         eigProps.locked ||
-        isAnothersAvatarEntity(eigProps) ||
-        isAnothersChildEntity(eigProps) ||
         FORBIDDEN_GRAB_TYPES.indexOf(eigProps.type) >= 0) {
         return false;
     }
diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js
index e059081741..38287e3af3 100644
--- a/scripts/system/marketplaces/marketplaces.js
+++ b/scripts/system/marketplaces/marketplaces.js
@@ -615,11 +615,10 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) {
         openMarketplace(message.itemId, message.itemEdition);
         break;
     case 'passphrasePopup_cancelClicked':
-    case 'needsLogIn_cancelClicked':
         // Should/must NOT check for wallet setup.
         openMarketplace();
         break;
-    case 'needsLogIn_loginClicked':
+    case 'marketplace_loginClicked':
         openLoginWindow();
         break;
     case 'disableHmdPreview':
diff --git a/tools/nitpick/compiledResources/.placeholder b/tools/nitpick/compiledResources/.placeholder
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tools/nitpick/compiledResources/resources.rcc b/tools/nitpick/compiledResources/resources.rcc
deleted file mode 100644
index 15f51ed7f4..0000000000
Binary files a/tools/nitpick/compiledResources/resources.rcc and /dev/null differ
diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp
index 62630cc7b3..53a74da82f 100644
--- a/tools/nitpick/src/TestRunnerMobile.cpp
+++ b/tools/nitpick/src/TestRunnerMobile.cpp
@@ -9,6 +9,7 @@
 //
 #include "TestRunnerMobile.h"
 
+#include <QNetworkInterface>
 #include <QThread>
 #include <QtWidgets/QMessageBox>
 #include <QtWidgets/QFileDialog>
@@ -69,13 +70,25 @@ void TestRunnerMobile::connectDevice() {
         _adbInterface = new AdbInterface();
     }
     
+    // Get list of devices
     QString devicesFullFilename{ _workingFolder + "/devices.txt" };
     QString command = _adbInterface->getAdbCommand() + " devices -l > " + devicesFullFilename;
     appendLog(command);
     system(command.toStdString().c_str());
 
     if (!QFile::exists(devicesFullFilename)) {
-        QMessageBox::critical(0, "Internal error", "devicesFullFilename not found");
+        QMessageBox::critical(0, "Internal error", "devices.txt not found");
+        exit (-1);
+    }
+
+    // Get device IP address
+    QString ifconfigFullFilename{ _workingFolder + "/ifconfig.txt" };
+    command = _adbInterface->getAdbCommand() + " shell ifconfig > " + ifconfigFullFilename;
+    appendLog(command);
+    system(command.toStdString().c_str());
+
+    if (!QFile::exists(ifconfigFullFilename)) {
+        QMessageBox::critical(0, "Internal error", "ifconfig.txt not found");
         exit (-1);
     }
 
@@ -86,6 +99,8 @@ void TestRunnerMobile::connectDevice() {
     QString line2 = devicesFile.readLine();
 
     const QString DEVICE{ "device" };
+    const QString MODEL{ "model" };
+
     if (line2.contains("unauthorized")) {
         QMessageBox::critical(0, "Unauthorized device detected", "Please allow USB debugging on device");
     } else if (line2.contains(DEVICE)) {
@@ -98,13 +113,20 @@ void TestRunnerMobile::connectDevice() {
             QStringList tokens = line2.split(QRegExp("[\r\n\t ]+"));
             QString deviceID = tokens[0];
             
-            QString modelID = tokens[3].split(':')[1];
-            QString modelName = "UNKNOWN";
-            if (modelNames.count(modelID) == 1) {
-                modelName = modelNames[modelID];
+            // Find the model entry
+            _modelName = "UNKNOWN";
+            for (int i = 0; i < tokens.size(); ++i) {
+                if (tokens[i].contains(MODEL)) {
+                    QString modelID = tokens[i].split(':')[1];
+
+                    if (modelNames.count(modelID) == 1) {
+                        _modelName = modelNames[modelID];
+                    }
+                    break;
+                }
             }
 
-            _detectedDeviceLabel->setText(modelName + " [" + deviceID + "]");
+            _detectedDeviceLabel->setText(_modelName + " [" + deviceID + "]");
             _pullFolderButton->setEnabled(true);
             _folderLineEdit->setEnabled(true);
             _downloadAPKPushbutton->setEnabled(true);
@@ -191,14 +213,28 @@ void TestRunnerMobile::runInterface() {
         ? QString("https://raw.githubusercontent.com/") + nitpick->getSelectedUser() + "/hifi_tests/" + nitpick->getSelectedBranch() + "/tests/testRecursive.js"
         : _scriptURL->text();
  
+    // Quest and Android have different commands to run interface
+    QString startCommand;
+    if (_modelName == "Quest") {
+        startCommand = "io.highfidelity.questInterface/.PermissionsChecker";
+    } else {
+        startCommand = "io.highfidelity.hifiinterface/.PermissionChecker";
+    }
+
+    QString serverIP { getServerIP() };
+    if (serverIP == NETWORK_NOT_FOUND) {
+        _runInterfacePushbutton->setEnabled(false);
+        return;
+    }
+
     QString command = _adbInterface->getAdbCommand() +
-        " shell am start -n io.highfidelity.hifiinterface/.PermissionChecker" + 
-        " --es args \\\"" + 
-            " --url file:///~/serverless/tutorial.json" + 
-            " --no-updater" + 
-            " --no-login-suggestion" + 
-            " --testScript " + testScript + " quitWhenFinished" + 
-            " --testResultsLocation /sdcard/snapshots" + 
+        " shell am start -n " + startCommand +
+        " --es args \\\"" +
+        " --url hifi://" + serverIP + "/0,0,0"
+        " --no-updater" +
+        " --no-login-suggestion" +
+        " --testScript " + testScript + " quitWhenFinished" +
+        " --testResultsLocation /sdcard/snapshots" +
         "\\\"";
 
     appendLog(command);
@@ -220,3 +256,85 @@ void TestRunnerMobile::pullFolder() {
     _statusLabel->setText("Pull complete");
 #endif
 }
+
+QString TestRunnerMobile::getServerIP() {
+    // Get device IP (ifconfig.txt was created when connecting)
+    QFile ifconfigFile{ _workingFolder + "/ifconfig.txt" };
+    if (!ifconfigFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+        QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
+            "Could not open 'ifconfig.txt'");
+        exit(-1);
+    }
+
+    QTextStream stream(&ifconfigFile);
+    QString line = ifconfigFile.readLine();
+    while (!line.isNull()) {
+        // The device IP is in the line following the "wlan0" line
+        if (line.left(6) == "wlan0 ") {
+            break;
+        }
+        line = ifconfigFile.readLine();
+    }
+
+    // The following line looks like this "inet addr:192.168.0.15  Bcast:192.168.0.255  Mask:255.255.255.0"
+    // Extract the address and mask
+    line = ifconfigFile.readLine();
+    QStringList lineParts = line.split(':');
+    if (lineParts.size() < 4) {
+        QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
+            "IP address line not in expected format: " + line + "(check that device WIFI is on)");
+
+        return NETWORK_NOT_FOUND;
+    }
+
+    qint64 deviceIP = convertToBinary(lineParts[1].split(' ')[0]);
+    qint64 deviceMask = convertToBinary(lineParts[3].split(' ')[0]);
+    qint64 deviceSubnet = deviceMask & deviceIP;
+
+    // The device needs to be on the same subnet as the server
+    // To find which of our IPs is the server - choose the 1st that is on the same subnet
+    // If more than one found then report an error
+
+    QString serverIP;
+
+    QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
+    for (int i = 0; i < interfaces.count(); i++) {
+        QList<QNetworkAddressEntry> entries = interfaces.at(i).addressEntries();
+        for (int j = 0; j < entries.count(); j++) {
+            if (entries.at(j).ip().protocol() == QAbstractSocket::IPv4Protocol) {
+                qint64 hostIP = convertToBinary(entries.at(j).ip().toString());
+                qint64 hostMask = convertToBinary(entries.at(j).netmask().toString());
+                qint64 hostSubnet = hostMask & hostIP;
+
+                if (hostSubnet == deviceSubnet) {
+                    if (!serverIP.isNull()) {
+                        QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__),
+                            "Cannot identify server IP (multiple interfaces on device submask)");
+                        return QString("CANNOT IDENTIFY SERVER IP");
+                    } else {
+                        union {
+                            uint32_t ip;
+                            uint8_t  bytes[4];
+                        } u;
+                        u.ip = hostIP;
+
+                        serverIP = QString::number(u.bytes[3]) + '.' + QString::number(u.bytes[2]) + '.' + QString::number(u.bytes[1]) + '.' + QString::number(u.bytes[0]);
+                    }
+                }
+            }
+        }
+    }
+
+    ifconfigFile.close();
+
+    return serverIP;
+}
+
+qint64 TestRunnerMobile::convertToBinary(const QString& str) {
+    QString binary;
+    foreach (const QString& s, str.split(".")) {
+        binary += QString::number(s.toInt(), 2).rightJustified(8, '0');
+    }
+
+    return binary.toLongLong(NULL, 2);
+}
diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h
index f7b16da6f8..09f847785b 100644
--- a/tools/nitpick/src/TestRunnerMobile.h
+++ b/tools/nitpick/src/TestRunnerMobile.h
@@ -52,6 +52,9 @@ public:
 
     void pullFolder();
 
+    QString getServerIP();
+    qint64 convertToBinary (const QString& str);
+
 private:
     QPushButton* _connectDeviceButton;
     QPushButton* _pullFolderButton;
@@ -75,5 +78,9 @@ private:
     std::map<QString, QString> modelNames;
 
     AdbInterface* _adbInterface;
+
+    QString _modelName;
+
+    QString NETWORK_NOT_FOUND{ "NETWORK NOT FOUND"};
 };
 #endif
diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h
index 7431f4979a..51f1f85113 100644
--- a/tools/nitpick/src/common.h
+++ b/tools/nitpick/src/common.h
@@ -60,5 +60,5 @@ const double R_Y = 0.212655f;
 const double G_Y = 0.715158f;
 const double B_Y = 0.072187f;
 
-const QString nitpickVersion { "v3.1.4" };
+const QString nitpickVersion{ "v3.1.5" }; 
 #endif // hifi_common_h
\ No newline at end of file