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/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml index 4c937d6139..802038d806 100644 --- a/domain-server/resources/web/settings/index.shtml +++ b/domain-server/resources/web/settings/index.shtml @@ -25,7 +25,7 @@ - + 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()->setSessionUUID(QUuid { id }); - // store the new token to the account info auto accountManager = DependencyManager::get(); accountManager->setTemporaryDomain(id, key); + // change our domain ID immediately + DependencyManager::get()->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()->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(); - 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(); + + // 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& groupsForName = _groupMembership[name]; + const QHash& 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& groupsForUser = _groupMembership[userName]; + QHash& 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 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 4d9481f002..b1b30a1acd 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -98,6 +98,7 @@ Avatar::Avatar(RigPointer rig) : _headData = static_cast(new Head(this)); _skeletonModel = std::make_shared(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 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(*_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 581bd285e2..d755dc3aca 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() { @@ -822,7 +822,7 @@ void Model::setURL(const QUrl& url) { _needsReload = true; _needsUpdateTextures = true; _meshGroupsKnown = false; - _geometryRequestFailed = false; + _visualGeometryRequestFailed = false; invalidCalculatedMeshBoxes(); deleteGeometry(); @@ -830,14 +830,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()->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 #include #include +#include #include #include @@ -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/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)); diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 9e11f839b3..f29fdfb00a 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -107,7 +107,10 @@ var NEAR_GRAB_PICK_RADIUS = 0.25; // radius used for search ray vs object for ne var PICK_BACKOFF_DISTANCE = 0.2; // helps when hand is intersecting the grabble object var NEAR_GRABBING_KINEMATIC = true; // force objects to be kinematic when near-grabbed -var CHECK_TOO_FAR_UNEQUIP_TIME = 1.0; // seconds + +// if an equipped item is "adjusted" to be too far from the hand it's in, it will be unequipped. +var CHECK_TOO_FAR_UNEQUIP_TIME = 0.3; // seconds, duration between checks +var AUTO_UNEQUIP_DISTANCE_FACTOR = 1.2; // multiplied by maximum dimension of held item, > this means drop // // other constants @@ -1818,7 +1821,8 @@ function MyController(hand) { this.heartBeat(this.grabbedEntity); - var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "position", "rotation"]); + var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", + "position", "rotation", "dimensions"]); if (!props.position) { // server may have reset, taking our equipped entity with it. move back to "off" stte this.callEntityMethodOnGrabbed("releaseGrab"); @@ -1830,10 +1834,9 @@ function MyController(hand) { if (now - this.lastUnequipCheckTime > MSECS_PER_SEC * CHECK_TOO_FAR_UNEQUIP_TIME) { this.lastUnequipCheckTime = now; - if (props.parentID == MyAvatar.sessionUUID && - Vec3.length(props.localPosition) > NEAR_GRAB_MAX_DISTANCE) { + if (props.parentID == MyAvatar.sessionUUID) { var handPosition = this.getHandPosition(); - // the center of the equipped object being far from the hand isn't enough to autoequip -- we also + // the center of the equipped object being far from the hand isn't enough to auto-unequip -- we also // need to fail the findEntities test. var nearPickedCandidateEntities = Entities.findEntities(handPosition, NEAR_GRAB_RADIUS); if (nearPickedCandidateEntities.indexOf(this.grabbedEntity) == -1) { @@ -2416,4 +2419,4 @@ function cleanup() { } Script.scriptEnding.connect(cleanup); -Script.update.connect(update); \ No newline at end of file +Script.update.connect(update);