diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp
index 8f752e70d0..eabb4955d9 100644
--- a/assignment-client/src/audio/AudioMixer.cpp
+++ b/assignment-client/src/audio/AudioMixer.cpp
@@ -100,6 +100,63 @@ AudioMixer::AudioMixer(ReceivedMessage& message) :
 
 const float ATTENUATION_BEGINS_AT_DISTANCE = 1.0f;
 
+const int IEEE754_MANT_BITS = 23;
+const int IEEE754_EXPN_BIAS = 127;
+
+//
+// for x  > 0.0f, returns log2(x)
+// for x <= 0.0f, returns large negative value
+//
+// abs |error| < 8e-3, smooth (exact for x=2^N) for NPOLY=3
+// abs |error| < 2e-4, smooth (exact for x=2^N) for NPOLY=5
+// rel |error| < 0.4 from precision loss very close to 1.0f
+//
+static inline float fastlog2(float x) {
+
+    union { float f; int32_t i; } mant, bits = { x };
+
+    // split into mantissa and exponent
+    mant.i = (bits.i & ((1 << IEEE754_MANT_BITS) - 1)) | (IEEE754_EXPN_BIAS << IEEE754_MANT_BITS);
+    int32_t expn = (bits.i >> IEEE754_MANT_BITS) - IEEE754_EXPN_BIAS;
+
+    mant.f -= 1.0f;
+
+    // polynomial for log2(1+x) over x=[0,1]
+    //x = (-0.346555386f * mant.f + 1.346555386f) * mant.f;
+    x = (((-0.0821307180f * mant.f + 0.321188984f) * mant.f - 0.677784014f) * mant.f + 1.43872575f) * mant.f;
+
+    return x + expn;
+}
+
+//
+// for -126 <= x < 128, returns exp2(x)
+//
+// rel |error| < 3e-3, smooth (exact for x=N) for NPOLY=3
+// rel |error| < 9e-6, smooth (exact for x=N) for NPOLY=5
+//
+static inline float fastexp2(float x) {
+
+    union { float f; int32_t i; } xi;
+
+    // bias such that x > 0
+    x += IEEE754_EXPN_BIAS;
+    //x = MAX(x, 1.0f);
+    //x = MIN(x, 254.9999f);
+
+    // split into integer and fraction
+    xi.i = (int32_t)x;
+    x -= xi.i;
+
+    // construct exp2(xi) as a float
+    xi.i <<= IEEE754_MANT_BITS;
+
+    // polynomial for exp2(x) over x=[0,1]
+    //x = (0.339766028f * x + 0.660233972f) * x + 1.0f;
+    x = (((0.0135557472f * x + 0.0520323690f) * x + 0.241379763f) * x + 0.693032121f) * x + 1.0f;
+
+    return x * xi.f;
+}
+
 float AudioMixer::gainForSource(const PositionalAudioStream& streamToAdd,
                                 const AvatarAudioStream& listeningNodeStream, const glm::vec3& relativePosition, bool isEcho) {
     float gain = 1.0f;
@@ -148,7 +205,7 @@ float AudioMixer::gainForSource(const PositionalAudioStream& streamToAdd,
         g = (g > 1.0f) ? 1.0f : g;
 
         // calculate the distance coefficient using the distance to this node
-        float distanceCoefficient = exp2f(log2f(g) * log2f(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE));
+        float distanceCoefficient = fastexp2(fastlog2(g) * fastlog2(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE));
 
         // multiply the current attenuation coefficient by the distance coefficient
         gain *= distanceCoefficient;
diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp
index 8f8c8e001c..c827e79223 100644
--- a/domain-server/src/DomainGatekeeper.cpp
+++ b/domain-server/src/DomainGatekeeper.cpp
@@ -182,7 +182,7 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin
 
                     GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID);
 #ifdef WANT_DEBUG
-                    qDebug() << "|  user-permissions: user is in group:" << groupID << " rank:"
+                    qDebug() << "|  user-permissions: user " << verifiedUsername << "is in group:" << groupID << " rank:"
                              << rank.name << "so:" << userPerms;
 #endif
                 }
diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp
index 23e37efaf1..6c4b12d4c0 100644
--- a/domain-server/src/DomainServer.cpp
+++ b/domain-server/src/DomainServer.cpp
@@ -117,9 +117,18 @@ DomainServer::DomainServer(int argc, char* argv[]) :
     _settingsManager.apiRefreshGroupInformation();
 
     setupNodeListAndAssignments();
+
+    if (_type == MetaverseDomain) {
+        // if we have a metaverse domain, we'll need an access token to heartbeat handle auto-networking
+        resetAccountManagerAccessToken();
+    }
+
     setupAutomaticNetworking();
-    if (!getID().isNull()) {
+
+    if (!getID().isNull() && _type != NonMetaverse) {
+        // setup periodic heartbeats to metaverse API
         setupHeartbeatToMetaverse();
+
         // send the first heartbeat immediately
         sendHeartbeatToMetaverse();
     }
@@ -301,16 +310,22 @@ void DomainServer::handleTempDomainSuccess(QNetworkReply& requestReply) {
         // store the new ID and auto networking setting on disk
         _settingsManager.persistToFile();
 
-        // change our domain ID immediately
-        DependencyManager::get<LimitedNodeList>()->setSessionUUID(QUuid { id });
-
         // store the new token to the account info
         auto accountManager = DependencyManager::get<AccountManager>();
         accountManager->setTemporaryDomain(id, key);
 
+        // change our domain ID immediately
+        DependencyManager::get<LimitedNodeList>()->setSessionUUID(QUuid { id });
+
+        // change our type to reflect that we are a temporary domain now
+        _type = MetaverseTemporaryDomain;
+
         // update our heartbeats to use the correct id
         setupICEHeartbeatForFullNetworking();
         setupHeartbeatToMetaverse();
+
+        // if we have a current ICE server address, update it in the API for the new temporary domain
+        sendICEServerAddressToMetaverseAPI();
     } else {
         qWarning() << "There were problems parsing the API response containing a temporary domain name. Please try again"
             << "via domain-server relaunch or from the domain-server settings.";
@@ -394,6 +409,16 @@ void DomainServer::setupNodeListAndAssignments() {
     const QVariant* idValueVariant = valueForKeyPath(settingsMap, METAVERSE_DOMAIN_ID_KEY_PATH);
     if (idValueVariant) {
         nodeList->setSessionUUID(idValueVariant->toString());
+
+        // if we have an ID, we'll assume we're a metaverse domain
+        // now see if we think we're a temp domain (we have an API key) or a full domain
+        const auto& temporaryDomainKey = DependencyManager::get<AccountManager>()->getTemporaryDomainKey(getID());
+        if (temporaryDomainKey.isEmpty()) {
+            _type = MetaverseDomain;
+        } else {
+            _type = MetaverseTemporaryDomain;
+        }
+
     } else {
         nodeList->setSessionUUID(QUuid::createUuid()); // Use random UUID
     }
@@ -477,42 +502,46 @@ bool DomainServer::resetAccountManagerAccessToken() {
 }
 
 void DomainServer::setupAutomaticNetworking() {
-    qDebug() << "Updating automatic networking setting in domain-server to" << _automaticNetworkingSetting;
-
-    resetAccountManagerAccessToken();
 
     _automaticNetworkingSetting =
         _settingsManager.valueOrDefaultValueForKeyPath(METAVERSE_AUTOMATIC_NETWORKING_KEY_PATH).toString();
 
-    auto nodeList = DependencyManager::get<LimitedNodeList>();
-    const QUuid& domainID = getID();
+    qDebug() << "Configuring automatic networking in domain-server as" << _automaticNetworkingSetting;
 
-    if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) {
-        setupICEHeartbeatForFullNetworking();
-    }
+    if (_automaticNetworkingSetting != DISABLED_AUTOMATIC_NETWORKING_VALUE) {
+        const QUuid& domainID = getID();
 
-    if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE ||
-        _automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) {
+        if (_automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) {
+            setupICEHeartbeatForFullNetworking();
+        }
 
-        if (!domainID.isNull()) {
-            qDebug() << "domain-server" << _automaticNetworkingSetting << "automatic networking enabled for ID"
-                << uuidStringWithoutCurlyBraces(domainID) << "via" << _oauthProviderURL.toString();
+        if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE ||
+            _automaticNetworkingSetting == FULL_AUTOMATIC_NETWORKING_VALUE) {
 
-            if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE) {
-                // send any public socket changes to the data server so nodes can find us at our new IP
-                connect(nodeList.data(), &LimitedNodeList::publicSockAddrChanged,
-                        this, &DomainServer::performIPAddressUpdate);
+            if (!domainID.isNull()) {
+                qDebug() << "domain-server" << _automaticNetworkingSetting << "automatic networking enabled for ID"
+                    << uuidStringWithoutCurlyBraces(domainID) << "via" << _oauthProviderURL.toString();
 
-                // have the LNL enable public socket updating via STUN
-                nodeList->startSTUNPublicSocketUpdate();
+                if (_automaticNetworkingSetting == IP_ONLY_AUTOMATIC_NETWORKING_VALUE) {
+
+                    auto nodeList = DependencyManager::get<LimitedNodeList>();
+
+                    // send any public socket changes to the data server so nodes can find us at our new IP
+                    connect(nodeList.data(), &LimitedNodeList::publicSockAddrChanged,
+                            this, &DomainServer::performIPAddressUpdate);
+
+                    // have the LNL enable public socket updating via STUN
+                    nodeList->startSTUNPublicSocketUpdate();
+                }
+            } else {
+                qDebug() << "Cannot enable domain-server automatic networking without a domain ID."
+                << "Please add an ID to your config file or via the web interface.";
+                
+                return;
             }
-        } else {
-            qDebug() << "Cannot enable domain-server automatic networking without a domain ID."
-            << "Please add an ID to your config file or via the web interface.";
-
-            return;
         }
     }
+
 }
 
 void DomainServer::setupHeartbeatToMetaverse() {
@@ -1139,42 +1168,45 @@ void DomainServer::handleMetaverseHeartbeatError(QNetworkReply& requestReply) {
         return;
     }
 
-    // check if we need to force a new temporary domain name
-    switch (requestReply.error()) {
-        // if we have a temporary domain with a bad token, we get a 401
-        case QNetworkReply::NetworkError::AuthenticationRequiredError: {
-            static const QString DATA_KEY = "data";
-            static const QString TOKEN_KEY = "api_key";
+    // only attempt to grab a new temporary name if we're already a temporary domain server
+    if (_type == MetaverseTemporaryDomain) {
+        // check if we need to force a new temporary domain name
+        switch (requestReply.error()) {
+                // if we have a temporary domain with a bad token, we get a 401
+            case QNetworkReply::NetworkError::AuthenticationRequiredError: {
+                static const QString DATA_KEY = "data";
+                static const QString TOKEN_KEY = "api_key";
 
-            QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
-            auto tokenFailure = jsonObject[DATA_KEY].toObject()[TOKEN_KEY];
+                QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
+                auto tokenFailure = jsonObject[DATA_KEY].toObject()[TOKEN_KEY];
 
-            if (!tokenFailure.isNull()) {
-                qWarning() << "Temporary domain name lacks a valid API key, and is being reset.";
+                if (!tokenFailure.isNull()) {
+                    qWarning() << "Temporary domain name lacks a valid API key, and is being reset.";
+                }
+                break;
             }
-            break;
+                // if the domain does not (or no longer) exists, we get a 404
+            case QNetworkReply::NetworkError::ContentNotFoundError:
+                qWarning() << "Domain not found, getting a new temporary domain.";
+                break;
+                // otherwise, we erred on something else, and should not force a temporary domain
+            default:
+                return;
         }
-        // if the domain does not (or no longer) exists, we get a 404
-        case QNetworkReply::NetworkError::ContentNotFoundError:
-            qWarning() << "Domain not found, getting a new temporary domain.";
-            break;
-        // otherwise, we erred on something else, and should not force a temporary domain
-        default:
-            return;
-    }
 
-    // halt heartbeats until we have a token
-    _metaverseHeartbeatTimer->deleteLater();
-    _metaverseHeartbeatTimer = nullptr;
+        // halt heartbeats until we have a token
+        _metaverseHeartbeatTimer->deleteLater();
+        _metaverseHeartbeatTimer = nullptr;
 
-    // give up eventually to avoid flooding traffic
-    static const int MAX_ATTEMPTS = 5;
-    static int attempt = 0;
-    if (++attempt < MAX_ATTEMPTS) {
-        // get a new temporary name and token
-        getTemporaryName(true);
-    } else {
-        qWarning() << "Already attempted too many temporary domain requests. Please set a domain ID manually or restart.";
+        // give up eventually to avoid flooding traffic
+        static const int MAX_ATTEMPTS = 5;
+        static int attempt = 0;
+        if (++attempt < MAX_ATTEMPTS) {
+            // get a new temporary name and token
+            getTemporaryName(true);
+        } else {
+            qWarning() << "Already attempted too many temporary domain requests. Please set a domain ID manually or restart.";
+        }
     }
 }
 
diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h
index 4004333789..06b3ed2c0a 100644
--- a/domain-server/src/DomainServer.h
+++ b/domain-server/src/DomainServer.h
@@ -42,6 +42,12 @@ public:
     DomainServer(int argc, char* argv[]);
     ~DomainServer();
 
+    enum DomainType {
+        NonMetaverse,
+        MetaverseDomain,
+        MetaverseTemporaryDomain
+    };
+
     static int const EXIT_CODE_REBOOT;
 
     bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false);
@@ -195,6 +201,8 @@ private:
     int _numHeartbeatDenials { 0 };
     bool _connectedToICEServer { false };
 
+    DomainType _type { DomainType::NonMetaverse };
+
     friend class DomainGatekeeper;
     friend class DomainMetadata;
 };
diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp
index dc49bc6126..47187fac5c 100644
--- a/domain-server/src/DomainServerSettingsManager.cpp
+++ b/domain-server/src/DomainServerSettingsManager.cpp
@@ -356,7 +356,7 @@ void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap&
         if (nameKey.first.toLower() != groupNameLower) {
             continue;
         }
-        QUuid groupID = _groupIDs[groupNameLower];
+        QUuid groupID = _groupIDs[groupNameLower.toLower()];
         QUuid rankID = nameKey.second;
         GroupRank rank = _groupRanks[groupID][rankID];
         if (rank.order == 0) {
@@ -1477,14 +1477,14 @@ void DomainServerSettingsManager::apiGetGroupRanksErrorCallback(QNetworkReply& r
 
 void DomainServerSettingsManager::recordGroupMembership(const QString& name, const QUuid groupID, QUuid rankID) {
     if (rankID != QUuid()) {
-        _groupMembership[name][groupID] = rankID;
+        _groupMembership[name.toLower()][groupID] = rankID;
     } else {
-        _groupMembership[name].remove(groupID);
+        _groupMembership[name.toLower()].remove(groupID);
     }
 }
 
 QUuid DomainServerSettingsManager::isGroupMember(const QString& name, const QUuid& groupID) {
-    const QHash<QUuid, QUuid>& groupsForName = _groupMembership[name];
+    const QHash<QUuid, QUuid>& groupsForName = _groupMembership[name.toLower()];
     if (groupsForName.contains(groupID)) {
         return groupsForName[groupID];
     }
@@ -1528,7 +1528,7 @@ void DomainServerSettingsManager::debugDumpGroupsState() {
 
     qDebug() << "_groupIDs:";
     foreach (QString groupName, _groupIDs.keys()) {
-        qDebug() << "|  " << groupName << "==>" << _groupIDs[groupName];
+        qDebug() << "|  " << groupName << "==>" << _groupIDs[groupName.toLower()];
     }
 
     qDebug() << "_groupNames:";
@@ -1548,7 +1548,7 @@ void DomainServerSettingsManager::debugDumpGroupsState() {
 
     qDebug() << "_groupMembership";
     foreach (QString userName, _groupMembership.keys()) {
-        QHash<QUuid, QUuid>& groupsForUser = _groupMembership[userName];
+        QHash<QUuid, QUuid>& groupsForUser = _groupMembership[userName.toLower()];
         QString line = "";
         foreach (QUuid groupID, groupsForUser.keys()) {
             line += " g=" + groupID.toString() + ",r=" + groupsForUser[groupID].toString();
diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h
index 144589326c..c067377ffc 100644
--- a/domain-server/src/DomainServerSettingsManager.h
+++ b/domain-server/src/DomainServerSettingsManager.h
@@ -84,7 +84,7 @@ public:
     QList<QUuid> getBlacklistGroupIDs();
 
     // these are used to locally cache the result of calling "api/v1/groups/.../is_member/..." on metaverse's api
-    void clearGroupMemberships(const QString& name) { _groupMembership[name].clear(); }
+    void clearGroupMemberships(const QString& name) { _groupMembership[name.toLower()].clear(); }
     void recordGroupMembership(const QString& name, const QUuid groupID, QUuid rankID);
     QUuid isGroupMember(const QString& name, const QUuid& groupID); // returns rank or -1 if not a member
 
diff --git a/interface/resources/qml/AssetServer.qml b/interface/resources/qml/AssetServer.qml
index 8d971e48d3..1ad2d1a1e4 100644
--- a/interface/resources/qml/AssetServer.qml
+++ b/interface/resources/qml/AssetServer.qml
@@ -314,6 +314,14 @@ ScrollingWindow {
         });
     }
 
+    Timer {
+        id: doUploadTimer
+        property var url
+        property bool isConnected: false
+        interval: 5
+        repeat: false
+        running: false
+    }
 
     property var uploadOpen: false;
     Timer {
@@ -366,6 +374,10 @@ ScrollingWindow {
             }, dropping);
         }
 
+        function initiateUpload(url) {
+            doUpload(doUploadTimer.url, false);
+        }
+
         if (fileUrl) {
             doUpload(fileUrl, true);
         } else {
@@ -373,12 +385,21 @@ ScrollingWindow {
                 selectDirectory: false,
                 dir: currentDirectory
             });
+
             browser.canceled.connect(function() {
                 uploadOpen = false;
             });
+
             browser.selectedFile.connect(function(url) {
                 currentDirectory = browser.dir;
-                doUpload(url, false);
+
+                // Initiate upload from a timer so that file browser dialog can close beforehand.
+                doUploadTimer.url = url;
+                if (!doUploadTimer.isConnected) {
+                    doUploadTimer.triggered.connect(function() { initiateUpload(); });
+                    doUploadTimer.isConnected = true;
+                }
+                doUploadTimer.start();
             });
         }
     }
diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp
index 018a9bb2fe..d3a38271a0 100644
--- a/interface/src/avatar/Avatar.cpp
+++ b/interface/src/avatar/Avatar.cpp
@@ -98,6 +98,7 @@ Avatar::Avatar(RigPointer rig) :
     _headData = static_cast<HeadData*>(new Head(this));
 
     _skeletonModel = std::make_shared<SkeletonModel>(this, nullptr, rig);
+    connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished);
 }
 
 Avatar::~Avatar() {
@@ -298,7 +299,9 @@ void Avatar::simulate(float deltaTime) {
         {
             PerformanceTimer perfTimer("head");
             glm::vec3 headPosition = getPosition();
-            _skeletonModel->getHeadPosition(headPosition);
+            if (!_skeletonModel->getHeadPosition(headPosition)) {
+                headPosition = getPosition();
+            }
             Head* head = getHead();
             head->setPosition(headPosition);
             head->setScale(getUniformScale());
@@ -306,6 +309,7 @@ void Avatar::simulate(float deltaTime) {
         }
     } else {
         // a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.
+        getHead()->setPosition(getPosition());
         _skeletonModel->simulate(deltaTime, false);
     }
 
@@ -916,6 +920,17 @@ void Avatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
     }
 }
 
+void Avatar::setModelURLFinished(bool success) {
+    if (!success && _skeletonModelURL != AvatarData::defaultFullAvatarModelUrl()) {
+        qDebug() << "Using default after failing to load Avatar model: " << _skeletonModelURL;
+        // call _skeletonModel.setURL, but leave our copy of _skeletonModelURL alone.  This is so that
+        // we don't redo this every time we receive an identity packet from the avatar with the bad url.
+        QMetaObject::invokeMethod(_skeletonModel.get(), "setURL",
+                                  Qt::QueuedConnection, Q_ARG(QUrl, AvatarData::defaultFullAvatarModelUrl()));
+    }
+}
+
+
 // create new model, can return an instance of a SoftAttachmentModel rather then Model
 static std::shared_ptr<Model> allocateAttachmentModel(bool isSoft, RigPointer rigOverride) {
     if (isSoft) {
diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h
index b9f44613c7..910f2cc1e6 100644
--- a/interface/src/avatar/Avatar.h
+++ b/interface/src/avatar/Avatar.h
@@ -184,6 +184,8 @@ public slots:
     glm::vec3 getRightPalmPosition() const;
     glm::quat getRightPalmRotation() const;
 
+    void setModelURLFinished(bool success);
+
 protected:
     friend class AvatarManager;
 
diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index eef176338f..de5455fc14 100644
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -430,6 +430,7 @@ void MyAvatar::simulate(float deltaTime) {
 
     if (!_skeletonModel->hasSkeleton()) {
         // All the simulation that can be done has been done
+        getHead()->setPosition(getPosition()); // so audio-position isn't 0,0,0
         return;
     }
 
diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
index 564a58708f..eba2d4cf4b 100644
--- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
+++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp
@@ -919,7 +919,9 @@ bool RenderableModelEntityItem::contains(const glm::vec3& point) const {
 bool RenderableModelEntityItem::shouldBePhysical() const {
     // If we have a model, make sure it hasn't failed to download.
     // If it has, we'll report back that we shouldn't be physical so that physics aren't held waiting for us to be ready.
-    if (_model && _model->didGeometryRequestFail()) {
+    if (_model && getShapeType() == SHAPE_TYPE_COMPOUND && _model->didCollisionGeometryRequestFail()) {
+        return false;
+    } else if (_model && getShapeType() != SHAPE_TYPE_NONE && _model->didVisualGeometryRequestFail()) {
         return false;
     } else {
         return ModelEntityItem::shouldBePhysical();
diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp
index c4b2a6dd22..3b7092ce8d 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.cpp
+++ b/libraries/model-networking/src/model-networking/ModelCache.cpp
@@ -403,9 +403,8 @@ void GeometryResourceWatcher::setResource(GeometryResource::Pointer resource) {
 void GeometryResourceWatcher::resourceFinished(bool success) {
     if (success) {
         _geometryRef = std::make_shared<Geometry>(*_resource);
-    } else {
-        emit resourceFailed();
     }
+    emit finished(success);
 }
 
 void GeometryResourceWatcher::resourceRefreshed() {
diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h
index 62037d67bc..962a919d6c 100644
--- a/libraries/model-networking/src/model-networking/ModelCache.h
+++ b/libraries/model-networking/src/model-networking/ModelCache.h
@@ -112,7 +112,7 @@ public:
     QUrl getURL() const { return (bool)_resource ? _resource->getURL() : QUrl(); }
 
 signals:
-    void resourceFailed();
+    void finished(bool success);
 
 private:
     void startWatching();
diff --git a/libraries/physics/src/CharacterController.cpp b/libraries/physics/src/CharacterController.cpp
index 30fb3536ca..b5ef03be0f 100644
--- a/libraries/physics/src/CharacterController.cpp
+++ b/libraries/physics/src/CharacterController.cpp
@@ -534,13 +534,14 @@ void CharacterController::preSimulation() {
 
         // scan for distant floor
         // rayStart is at center of bottom sphere
-        btVector3 rayStart = _characterBodyTransform.getOrigin() - _halfHeight * _currentUp;
+        btVector3 rayStart = _characterBodyTransform.getOrigin();
 
         // rayEnd is straight down MAX_FALL_HEIGHT
         btScalar rayLength = _radius + MAX_FALL_HEIGHT;
         btVector3 rayEnd = rayStart - rayLength * _currentUp;
 
-        const btScalar JUMP_PROXIMITY_THRESHOLD = 0.1f * _radius;
+        const btScalar FLY_TO_GROUND_THRESHOLD = 0.1f * _radius;
+        const btScalar GROUND_TO_FLY_THRESHOLD = 0.8f * _radius + _halfHeight;
         const quint64 TAKE_OFF_TO_IN_AIR_PERIOD = 250 * MSECS_PER_SECOND;
         const btScalar MIN_HOVER_HEIGHT = 2.5f;
         const quint64 JUMP_TO_HOVER_PERIOD = 1100 * MSECS_PER_SECOND;
@@ -553,7 +554,7 @@ void CharacterController::preSimulation() {
         bool rayHasHit = rayCallback.hasHit();
         if (rayHasHit) {
             _rayHitStartTime = now;
-            _floorDistance = rayLength * rayCallback.m_closestHitFraction - _radius;
+            _floorDistance = rayLength * rayCallback.m_closestHitFraction - (_radius + _halfHeight);
         } else if ((now - _rayHitStartTime) < RAY_HIT_START_PERIOD) {
             rayHasHit = true;
         } else {
@@ -581,7 +582,7 @@ void CharacterController::preSimulation() {
                 _takeoffJumpButtonID = _jumpButtonDownCount;
                 _takeoffToInAirStartTime = now;
                 SET_STATE(State::Takeoff, "jump pressed");
-            } else if (rayHasHit && !_hasSupport && _floorDistance > JUMP_PROXIMITY_THRESHOLD) {
+            } else if (rayHasHit && !_hasSupport && _floorDistance > GROUND_TO_FLY_THRESHOLD) {
                 SET_STATE(State::InAir, "falling");
             }
             break;
@@ -595,7 +596,7 @@ void CharacterController::preSimulation() {
             }
             break;
         case State::InAir: {
-            if ((velocity.dot(_currentUp) <= (JUMP_SPEED / 2.0f)) && ((_floorDistance < JUMP_PROXIMITY_THRESHOLD) || _hasSupport)) {
+            if ((velocity.dot(_currentUp) <= (JUMP_SPEED / 2.0f)) && ((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport)) {
                 SET_STATE(State::Ground, "hit ground");
             } else {
                 btVector3 desiredVelocity = _targetVelocity;
@@ -614,7 +615,7 @@ void CharacterController::preSimulation() {
         case State::Hover:
             if ((_floorDistance < MIN_HOVER_HEIGHT) && !jumpButtonHeld && !flyingFast) {
                 SET_STATE(State::InAir, "near ground");
-            } else if (((_floorDistance < JUMP_PROXIMITY_THRESHOLD) || _hasSupport) && !flyingFast) {
+            } else if (((_floorDistance < FLY_TO_GROUND_THRESHOLD) || _hasSupport) && !flyingFast) {
                 SET_STATE(State::Ground, "touching ground");
             }
             break;
diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp
index 2140f2a803..0e67bfaa47 100644
--- a/libraries/render-utils/src/Model.cpp
+++ b/libraries/render-utils/src/Model.cpp
@@ -111,8 +111,8 @@ Model::Model(RigPointer rig, QObject* parent) :
 
     setSnapModelToRegistrationPoint(true, glm::vec3(0.5f));
 
-    // handle download failure reported by the GeometryResourceWatcher
-    connect(&_renderWatcher, &GeometryResourceWatcher::resourceFailed, this, &Model::handleGeometryResourceFailure);
+    connect(&_renderWatcher, &GeometryResourceWatcher::finished, this, &Model::loadURLFinished);
+    connect(&_collisionWatcher, &GeometryResourceWatcher::finished, this, &Model::loadCollisionModelURLFinished);
 }
 
 Model::~Model() {
@@ -826,7 +826,7 @@ void Model::setURL(const QUrl& url) {
     _needsReload = true;
     _needsUpdateTextures = true;
     _meshGroupsKnown = false;
-    _geometryRequestFailed = false;
+    _visualGeometryRequestFailed = false;
     invalidCalculatedMeshBoxes();
     deleteGeometry();
 
@@ -834,14 +834,30 @@ void Model::setURL(const QUrl& url) {
     onInvalidate();
 }
 
+void Model::loadURLFinished(bool success) {
+    if (!success) {
+        _visualGeometryRequestFailed = true;
+    }
+    emit setURLFinished(success);
+}
+
 void Model::setCollisionModelURL(const QUrl& url) {
     if (_collisionUrl == url && _collisionWatcher.getURL() == url) {
         return;
     }
     _collisionUrl = url;
+    _collisionGeometryRequestFailed = false;
     _collisionWatcher.setResource(DependencyManager::get<ModelCache>()->getGeometryResource(url));
 }
 
+void Model::loadCollisionModelURLFinished(bool success) {
+    if (!success) {
+        _collisionGeometryRequestFailed  = true;
+    }
+
+    emit setCollisionModelURLFinished(success);
+}
+
 bool Model::getJointPositionInWorldFrame(int jointIndex, glm::vec3& position) const {
     return _rig->getJointPositionInWorldFrame(jointIndex, position, _translation, _rotation);
 }
diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h
index b95c0318b4..f7bf83ca5b 100644
--- a/libraries/render-utils/src/Model.h
+++ b/libraries/render-utils/src/Model.h
@@ -148,8 +148,9 @@ public:
     const QUrl& getCollisionURL() const { return _collisionUrl; }
 
     bool isActive() const { return isLoaded(); }
-    
-    bool didGeometryRequestFail() const { return _geometryRequestFailed; }
+
+    bool didVisualGeometryRequestFail() const { return _visualGeometryRequestFailed; }
+    bool didCollisionGeometryRequestFail() const { return _collisionGeometryRequestFailed; }
 
     bool convexHullContains(glm::vec3 point);
 
@@ -237,6 +238,14 @@ public:
     // returns 'true' if needs fullUpdate after geometry change
     bool updateGeometry();
 
+public slots:
+    void loadURLFinished(bool success);
+    void loadCollisionModelURLFinished(bool success);
+
+signals:
+    void setURLFinished(bool success);
+    void setCollisionModelURLFinished(bool success);
+
 protected:
 
     void setPupilDilation(float dilation) { _pupilDilation = dilation; }
@@ -394,10 +403,8 @@ protected:
 
     uint32_t _deleteGeometryCounter { 0 };
 
-    bool _geometryRequestFailed { false };
-
-private slots:
-    void handleGeometryResourceFailure() { _geometryRequestFailed = true; }
+    bool _visualGeometryRequestFailed { false };
+    bool _collisionGeometryRequestFailed { false };
 };
 
 Q_DECLARE_METATYPE(ModelPointer)
diff --git a/libraries/script-engine/src/ScriptCache.cpp b/libraries/script-engine/src/ScriptCache.cpp
index 2114289095..40234e8134 100644
--- a/libraries/script-engine/src/ScriptCache.cpp
+++ b/libraries/script-engine/src/ScriptCache.cpp
@@ -18,6 +18,7 @@
 #include <QNetworkReply>
 #include <QObject>
 #include <QThread>
+#include <QRegularExpression>
 
 #include <assert.h>
 #include <SharedUtil.h>
@@ -109,12 +110,22 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable
     QUrl unnormalizedURL(scriptOrURL);
     QUrl url = ResourceManager::normalizeURL(unnormalizedURL);
 
-    // attempt to determine if this is a URL to a script, or if this is actually a script itself (which is valid in the entityScript use case)
-    if (url.scheme().isEmpty() && scriptOrURL.simplified().replace(" ", "").contains("(function(){")) {
+    // attempt to determine if this is a URL to a script, or if this is actually a script itself (which is valid in the
+    // entityScript use case)
+    if (unnormalizedURL.scheme().isEmpty() &&
+            scriptOrURL.simplified().replace(" ", "").contains(QRegularExpression(R"(\(function\([a-z]?[\w,]*\){)"))) {
         contentAvailable(scriptOrURL, scriptOrURL, false, true);
         return;
     }
 
+    // give a similar treatment to javacript: urls
+    if (unnormalizedURL.scheme() == "javascript") {
+        QString contents { scriptOrURL };
+        contents.replace(QRegularExpression("^javascript:"), "");
+        contentAvailable(scriptOrURL, contents, false, true);
+        return;
+    }
+
     Lock lock(_containerLock);
     if (_scriptCache.contains(url) && !forceDownload) {
         auto scriptContent = _scriptCache[url];
@@ -133,6 +144,7 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable
             qCDebug(scriptengine) << "about to call: ResourceManager::createResourceRequest(this, url); on thread [" << QThread::currentThread() << "] expected thread [" << thread() << "]";
             #endif
             auto request = ResourceManager::createResourceRequest(nullptr, url);
+            Q_ASSERT(request);
             request->setCacheEnabled(!forceDownload);
             connect(request, &ResourceRequest::finished, this, &ScriptCache::scriptContentAvailable);
             request->send();
diff --git a/libraries/shared/src/SettingManager.cpp b/libraries/shared/src/SettingManager.cpp
index abb8525b03..a42e62c1c8 100644
--- a/libraries/shared/src/SettingManager.cpp
+++ b/libraries/shared/src/SettingManager.cpp
@@ -53,7 +53,7 @@ namespace Setting {
         const auto& key = handle->getKey();
         withWriteLock([&] {
             QVariant loadedValue;
-            if (_pendingChanges.contains(key)) {
+            if (_pendingChanges.contains(key) && _pendingChanges[key] != UNSET_VALUE) {
                 loadedValue = _pendingChanges[key];
             } else {
                 loadedValue = value(key);
diff --git a/libraries/shared/src/SettingManager.h b/libraries/shared/src/SettingManager.h
index 1f309c966f..ffdd4ba42a 100644
--- a/libraries/shared/src/SettingManager.h
+++ b/libraries/shared/src/SettingManager.h
@@ -46,7 +46,7 @@ namespace Setting {
     private:
         QHash<QString, Interface*> _handles;
         QPointer<QTimer> _saveTimer = nullptr;
-        const QVariant UNSET_VALUE { QUuid::createUuid().variant() };
+        const QVariant UNSET_VALUE { QUuid::createUuid() };
         QHash<QString, QVariant> _pendingChanges;
 
         friend class Interface;
diff --git a/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js b/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js
index 30567b4fc7..daef1d5db3 100644
--- a/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js
+++ b/script-archive/audioExamples/acAudioSearching/ACAudioSearchAndInject.js
@@ -40,7 +40,7 @@ var DEFAULT_SOUND_DATA = {
 
 Script.include("../../libraries/utils.js");
 Agent.isAvatar = true; // This puts a robot at 0,0,0, but is currently necessary in order to use AvatarList.
-Avatar.skeletonModelURL = "http://invalid-url";
+Avatar.skeletonModelURL = "http://hifi-content.s3.amazonaws.com/ozan/dev/avatars/invisible_avatar/invisible_avatar.fst";
 function ignore() {}
 function debug() { // Display the arguments not just [Object object].
     //print.apply(null, [].map.call(arguments, JSON.stringify));