diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 1ec3ed54c4..4998a3b520 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -226,8 +226,9 @@ void AvatarMixer::start() { nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { std::for_each(cbegin, cend, [&](const SharedNodePointer& node) { if (node->getType() == NodeType::Agent && !node->isUpstream()) { - manageDisplayName(node); + manageIdentityData(node); } + ++_sumListeners; }); }, &lockWait, &nodeTransform, &functor); @@ -278,8 +279,9 @@ void AvatarMixer::start() { // NOTE: nodeData->getAvatar() might be side effected, must be called when access to node/nodeData // is guaranteed to not be accessed by other thread -void AvatarMixer::manageDisplayName(const SharedNodePointer& node) { +void AvatarMixer::manageIdentityData(const SharedNodePointer& node) { AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); + bool sendIdentity = false; if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) { AvatarData& avatar = nodeData->getAvatar(); const QString& existingBaseDisplayName = nodeData->getBaseDisplayName(); @@ -305,9 +307,39 @@ void AvatarMixer::manageDisplayName(const SharedNodePointer& node) { soFar.second++; // refcount nodeData->flagIdentityChange(); nodeData->setAvatarSessionDisplayNameMustChange(false); - sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name. + sendIdentity = true; qCDebug(avatars) << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID(); } + if (nodeData && nodeData->getAvatarSkeletonModelUrlMustChange()) { // never true for an empty _avatarWhitelist + nodeData->setAvatarSkeletonModelUrlMustChange(false); + AvatarData& avatar = nodeData->getAvatar(); + static const QUrl emptyURL(""); + QUrl url = avatar.cannonicalSkeletonModelURL(emptyURL); + if (!isAvatarInWhitelist(url)) { + qCDebug(avatars) << "Forbidden avatar" << nodeData->getNodeID() << avatar.getSkeletonModelURL() << "replaced with" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar); + avatar.setSkeletonModelURL(_replacementAvatar); + sendIdentity = true; + } + } + if (sendIdentity) { + sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name or avatar. + } +} + +bool AvatarMixer::isAvatarInWhitelist(const QUrl& url) { + // The avatar is in the whitelist if: + // 1. The avatar's URL's host matches one of the hosts of the URLs in the whitelist AND + // 2. The avatar's URL's path starts with the path of that same URL in the whitelist + for (const auto& whiteListedPrefix : _avatarWhitelist) { + auto whiteListURL = QUrl::fromUserInput(whiteListedPrefix); + // check if this script URL matches the whitelist domain and, optionally, is beneath the path + if (url.host().compare(whiteListURL.host(), Qt::CaseInsensitive) == 0 && + url.path().startsWith(whiteListURL.path(), Qt::CaseInsensitive)) { + return true; + } + } + + return false; } void AvatarMixer::throttle(std::chrono::microseconds duration, int frame) { @@ -520,13 +552,18 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer mes // parse the identity packet and update the change timestamp if appropriate bool identityChanged = false; bool displayNameChanged = false; - avatar.processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged); + bool skeletonModelUrlChanged = false; + avatar.processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged, skeletonModelUrlChanged); + if (identityChanged) { QMutexLocker nodeDataLocker(&nodeData->getMutex()); nodeData->flagIdentityChange(); if (displayNameChanged) { nodeData->setAvatarSessionDisplayNameMustChange(true); } + if (skeletonModelUrlChanged && !_avatarWhitelist.isEmpty()) { + nodeData->setAvatarSkeletonModelUrlMustChange(true); + } } } } @@ -882,4 +919,21 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { qCDebug(avatars) << "This domain requires a minimum avatar scale of" << _domainMinimumScale << "and a maximum avatar scale of" << _domainMaximumScale; + + const QString AVATAR_WHITELIST_DEFAULT{ "" }; + static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist"; + _avatarWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION].toString(AVATAR_WHITELIST_DEFAULT).split(',', QString::KeepEmptyParts); + + static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar"; + _replacementAvatar = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION].toString(REPLACEMENT_AVATAR_DEFAULT); + + if ((_avatarWhitelist.count() == 1) && _avatarWhitelist[0].isEmpty()) { + _avatarWhitelist.clear(); // KeepEmptyParts above will parse "," as ["", ""] (which is ok), but "" as [""] (which is not ok). + } + + if (_avatarWhitelist.isEmpty()) { + qCDebug(avatars) << "All avatars are allowed."; + } else { + qCDebug(avatars) << "Avatars other than" << _avatarWhitelist << "will be replaced by" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar); + } } diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index e0d073a281..42414dab55 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -61,7 +61,12 @@ private: void parseDomainServerSettings(const QJsonObject& domainSettings); void sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode); - void manageDisplayName(const SharedNodePointer& node); + void manageIdentityData(const SharedNodePointer& node); + bool isAvatarInWhitelist(const QUrl& url); + + const QString REPLACEMENT_AVATAR_DEFAULT{ "" }; + QStringList _avatarWhitelist { }; + QString _replacementAvatar { REPLACEMENT_AVATAR_DEFAULT }; void optionallyReplicatePacket(ReceivedMessage& message, const Node& node); diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index c905b10251..12b0286088 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -65,6 +65,8 @@ public: void flagIdentityChange() { _identityChangeTimestamp = usecTimestampNow(); } bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; } void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; } + bool getAvatarSkeletonModelUrlMustChange() const { return _avatarSkeletonModelUrlMustChange; } + void setAvatarSkeletonModelUrlMustChange(bool set = true) { _avatarSkeletonModelUrlMustChange = set; } void resetNumAvatarsSentLastFrame() { _numAvatarsSentLastFrame = 0; } void incrementNumAvatarsSentLastFrame() { ++_numAvatarsSentLastFrame; } @@ -146,6 +148,7 @@ private: uint64_t _identityChangeTimestamp; bool _avatarSessionDisplayNameMustChange{ true }; + bool _avatarSkeletonModelUrlMustChange{ false }; int _numAvatarsSentLastFrame = 0; int _numFramesSinceAdjustment = 0; diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 10da5b55fc..363a370b40 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -866,6 +866,22 @@ "help": "Limits the scale of avatars in your domain. Cannot be greater than 1000.", "placeholder": 3.0, "default": 3.0 + }, + { + "name": "avatar_whitelist", + "label": "Avatars Allowed from:", + "help": "Comma separated list of URLs (with optional paths) that avatar .fst files are allowed from. If someone attempts to use an avatar with a different domain, it will be rejected and the replacement avatar will be used. If left blank, any domain is allowed.", + "placeholder": "", + "default": "", + "advanced": true + }, + { + "name": "replacement_avatar", + "label": "Replacement Avatar for disallowed avatars", + "help": "A URL for an avatar .fst to be used when someone tries to use an avatar that is not allowed. If left blank, the generic default avatar is used.", + "placeholder": "", + "default": "", + "advanced": true } ] }, diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9ce6cc9b25..5e168c6620 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1721,6 +1721,10 @@ void Application::cleanupBeforeQuit() { // Cleanup all overlays after the scripts, as scripts might add more _overlays.cleanupAllOverlays(); + // The cleanup process enqueues the transactions but does not process them. Calling this here will force the actual + // removal of the items. + // See https://highfidelity.fogbugz.com/f/cases/5328 + _main3DScene->processTransactionQueue(); // first stop all timers directly or by invokeMethod // depending on what thread they run in @@ -5292,6 +5296,11 @@ void Application::nodeActivated(SharedNodePointer node) { if (node->getType() == NodeType::AvatarMixer) { // new avatar mixer, send off our identity packet on next update loop + // Reset skeletonModelUrl if the last server modified our choice. + static const QUrl empty{}; + if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->cannonicalSkeletonModelURL(empty)) { + getMyAvatar()->resetFullAvatarURL(); + } getMyAvatar()->markIdentityDataChanged(); getMyAvatar()->resetLastSent(); } diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 24a25f314d..1adcfbd345 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1961,6 +1961,32 @@ void MyAvatar::updateOrientation(float deltaTime) { totalBodyYaw += (speedFactor * deltaAngle * (180.0f / PI)); } + // Use head/HMD roll to turn while walking or flying. + if (qApp->isHMDMode() && _hmdRollControlEnabled) { + // Turn with head roll. + const float MIN_CONTROL_SPEED = 0.01f; + float speed = glm::length(getVelocity()); + if (speed >= MIN_CONTROL_SPEED) { + // Feather turn when stopping moving. + float speedFactor; + if (getDriveKey(TRANSLATE_Z) != 0.0f || _lastDrivenSpeed == 0.0f) { + _lastDrivenSpeed = speed; + speedFactor = 1.0f; + } else { + speedFactor = glm::min(speed / _lastDrivenSpeed, 1.0f); + } + + float direction = glm::dot(getVelocity(), getRotation() * Vectors::UNIT_NEG_Z) > 0.0f ? 1.0f : -1.0f; + + float rollAngle = glm::degrees(asinf(glm::dot(IDENTITY_UP, _hmdSensorOrientation * IDENTITY_RIGHT))); + float rollSign = rollAngle < 0.0f ? -1.0f : 1.0f; + rollAngle = fabsf(rollAngle); + rollAngle = rollAngle > _hmdRollControlDeadZone ? rollSign * (rollAngle - _hmdRollControlDeadZone) : 0.0f; + + totalBodyYaw += speedFactor * direction * rollAngle * deltaTime * _hmdRollControlRate; + } + } + // update body orientation by movement inputs glm::quat initialOrientation = getOrientationOutbound(); setOrientation(getOrientation() * glm::quat(glm::radians(glm::vec3(0.0f, totalBodyYaw, 0.0f)))); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index cfe66eb10e..f61f24fb11 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -132,6 +132,10 @@ class MyAvatar : public Avatar { Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled) Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls) + Q_PROPERTY(bool hmdRollControlEnabled READ getHMDRollControlEnabled WRITE setHMDRollControlEnabled) + Q_PROPERTY(float hmdRollControlDeadZone READ getHMDRollControlDeadZone WRITE setHMDRollControlDeadZone) + Q_PROPERTY(float hmdRollControlRate READ getHMDRollControlRate WRITE setHMDRollControlRate) + public: enum DriveKeys { TRANSLATE_X = 0, @@ -337,6 +341,13 @@ public: void setUseAdvancedMovementControls(bool useAdvancedMovementControls) { _useAdvancedMovementControls.set(useAdvancedMovementControls); } + void setHMDRollControlEnabled(bool value) { _hmdRollControlEnabled = value; } + bool getHMDRollControlEnabled() const { return _hmdRollControlEnabled; } + void setHMDRollControlDeadZone(float value) { _hmdRollControlDeadZone = value; } + float getHMDRollControlDeadZone() const { return _hmdRollControlDeadZone; } + void setHMDRollControlRate(float value) { _hmdRollControlRate = value; } + float getHMDRollControlRate() const { return _hmdRollControlRate; } + // get/set avatar data void saveData(); void loadData(); @@ -687,6 +698,13 @@ private: bool _useSnapTurn { true }; bool _clearOverlayWhenMoving { true }; + const float ROLL_CONTROL_DEAD_ZONE_DEFAULT = 8.0f; // deg + const float ROLL_CONTROL_RATE_DEFAULT = 2.5f; // deg/sec/deg + bool _hmdRollControlEnabled { true }; + float _hmdRollControlDeadZone { ROLL_CONTROL_DEAD_ZONE_DEFAULT }; + float _hmdRollControlRate { ROLL_CONTROL_RATE_DEFAULT }; + float _lastDrivenSpeed { 0.0f }; + // working copies -- see AvatarData for thread-safe _sensorToWorldMatrixCache, used for outward facing access glm::mat4 _sensorToWorldMatrix { glm::mat4() }; diff --git a/interface/src/scripting/AudioDeviceScriptingInterface.cpp b/interface/src/scripting/AudioDeviceScriptingInterface.cpp index d22f948344..1fd30af0e3 100644 --- a/interface/src/scripting/AudioDeviceScriptingInterface.cpp +++ b/interface/src/scripting/AudioDeviceScriptingInterface.cpp @@ -48,15 +48,31 @@ AudioDeviceScriptingInterface::AudioDeviceScriptingInterface(): QAbstractListMod SettingsScriptingInterface* settings = SettingsScriptingInterface::getInstance(); const QString inDevice = settings->getValue("audio_input_device", _currentInputDevice).toString(); if (inDevice != _currentInputDevice) { - qCDebug(audioclient) << __FUNCTION__ << "about to call setInputDeviceAsync() device: [" << inDevice << "] _currentInputDevice:" << _currentInputDevice; - setInputDeviceAsync(inDevice); + // before using the old setting, check to make sure the device still exists.... + bool inDeviceExists = DependencyManager::get()->getNamedAudioDeviceForModeExists(QAudio::AudioInput, inDevice); + + if (inDeviceExists) { + qCDebug(audioclient) << __FUNCTION__ << "about to call setInputDeviceAsync() device: [" << inDevice << "] _currentInputDevice:" << _currentInputDevice; + setInputDeviceAsync(inDevice); + } else { + qCDebug(audioclient) << __FUNCTION__ << "previously selected device no longer exists inDevice: [" << inDevice << "] keeping device _currentInputDevice:" << _currentInputDevice; + } } // If the audio_output_device setting is not available, use the _currentOutputDevice auto outDevice = settings->getValue("audio_output_device", _currentOutputDevice).toString(); + if (outDevice != _currentOutputDevice) { - qCDebug(audioclient) << __FUNCTION__ << "about to call setOutputDeviceAsync() outDevice: [" << outDevice << "] _currentOutputDevice:" << _currentOutputDevice; - setOutputDeviceAsync(outDevice); + // before using the old setting, check to make sure the device still exists.... + bool outDeviceExists = DependencyManager::get()->getNamedAudioDeviceForModeExists(QAudio::AudioOutput, outDevice); + + if (outDeviceExists) { + qCDebug(audioclient) << __FUNCTION__ << "about to call setOutputDeviceAsync() outDevice: [" << outDevice << "] _currentOutputDevice:" << _currentOutputDevice; + setOutputDeviceAsync(outDevice); + } else { + qCDebug(audioclient) << __FUNCTION__ << "previously selected device no longer exists outDevice: [" << outDevice << "] keeping device _currentOutputDevice:" << _currentOutputDevice; + } + } } diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 20a2aab2b6..fbb3d24298 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1164,9 +1164,32 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f bool bodySensorTrackingEnabled = params.isLeftFootEnabled || params.isRightFootEnabled; const float RELAX_DURATION = 0.6f; + const float CONTROL_DURATION = 0.4f; + const bool TO_CONTROLLED = true; + const bool FROM_CONTROLLED = false; + const bool LEFT_HAND = true; + const bool RIGHT_HAND = false; if (params.isLeftEnabled) { + if (!_isLeftHandControlled) { + _leftHandControlTimeRemaining = CONTROL_DURATION; + _isLeftHandControlled = true; + } + glm::vec3 handPosition = params.leftPosition; + glm::quat handRotation = params.leftOrientation; + + if (_leftHandControlTimeRemaining > 0.0f) { + // Move hand from non-controlled position to controlled position. + _leftHandControlTimeRemaining = std::max(_leftHandControlTimeRemaining - dt, 0.0f); + AnimPose handPose(Vectors::ONE, handRotation, handPosition); + if (transitionHandPose(_leftHandControlTimeRemaining, CONTROL_DURATION, handPose, LEFT_HAND, TO_CONTROLLED, + handPose)) { + handPosition = handPose.trans(); + handRotation = handPose.rot(); + } + } + if (!bodySensorTrackingEnabled) { // prevent the hand IK targets from intersecting the body capsule glm::vec3 displacement; @@ -1176,27 +1199,22 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f } _animVars.set("leftHandPosition", handPosition); - _animVars.set("leftHandRotation", params.leftOrientation); + _animVars.set("leftHandRotation", handRotation); _animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); - _isLeftHandControlled = true; - _lastLeftHandControlledPose = AnimPose(glm::vec3(1.0f), params.leftOrientation, handPosition); + _lastLeftHandControlledPose = AnimPose(Vectors::ONE, handRotation, handPosition); } else { if (_isLeftHandControlled) { - _leftHandRelaxDuration = RELAX_DURATION; + _leftHandRelaxTimeRemaining = RELAX_DURATION; _isLeftHandControlled = false; } - if (_leftHandRelaxDuration > 0) { + if (_leftHandRelaxTimeRemaining > 0.0f) { // Move hand from controlled position to non-controlled position. - _leftHandRelaxDuration = std::max(_leftHandRelaxDuration - dt, 0.0f); - auto ikNode = getAnimInverseKinematicsNode(); - if (ikNode) { - float alpha = 1.0f - _leftHandRelaxDuration / RELAX_DURATION; - const AnimPose geometryToRigTransform(_geometryToRigTransform); - AnimPose uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledLeftHandPose(); - AnimPose handPose; - ::blend(1, &_lastLeftHandControlledPose, &uncontrolledHandPose, alpha, &handPose); + _leftHandRelaxTimeRemaining = std::max(_leftHandRelaxTimeRemaining - dt, 0.0f); + AnimPose handPose; + if (transitionHandPose(_leftHandRelaxTimeRemaining, RELAX_DURATION, _lastLeftHandControlledPose, LEFT_HAND, + FROM_CONTROLLED, handPose)) { _animVars.set("leftHandPosition", handPose.trans()); _animVars.set("leftHandRotation", handPose.rot()); _animVars.set("leftHandType", (int)IKTarget::Type::RotationAndPosition); @@ -1209,7 +1227,25 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f } if (params.isRightEnabled) { + if (!_isRightHandControlled) { + _rightHandControlTimeRemaining = CONTROL_DURATION; + _isRightHandControlled = true; + } + glm::vec3 handPosition = params.rightPosition; + glm::quat handRotation = params.rightOrientation; + + if (_rightHandControlTimeRemaining > 0.0f) { + // Move hand from non-controlled position to controlled position. + _rightHandControlTimeRemaining = std::max(_rightHandControlTimeRemaining - dt, 0.0f); + AnimPose handPose(Vectors::ONE, handRotation, handPosition); + if (transitionHandPose(_rightHandControlTimeRemaining, CONTROL_DURATION, handPose, RIGHT_HAND, TO_CONTROLLED, + handPose)) { + handPosition = handPose.trans(); + handRotation = handPose.rot(); + } + } + if (!bodySensorTrackingEnabled) { // prevent the hand IK targets from intersecting the body capsule glm::vec3 displacement; @@ -1219,27 +1255,22 @@ void Rig::updateFromHandAndFeetParameters(const HandAndFeetParameters& params, f } _animVars.set("rightHandPosition", handPosition); - _animVars.set("rightHandRotation", params.rightOrientation); + _animVars.set("rightHandRotation", handRotation); _animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); - _isRightHandControlled = true; - _lastRightHandControlledPose = AnimPose(glm::vec3(1.0f), params.rightOrientation, handPosition); + _lastRightHandControlledPose = AnimPose(Vectors::ONE, handRotation, handPosition); } else { if (_isRightHandControlled) { - _rightHandRelaxDuration = RELAX_DURATION; + _rightHandRelaxTimeRemaining = RELAX_DURATION; _isRightHandControlled = false; } - if (_rightHandRelaxDuration > 0) { + if (_rightHandRelaxTimeRemaining > 0.0f) { // Move hand from controlled position to non-controlled position. - _rightHandRelaxDuration = std::max(_rightHandRelaxDuration - dt, 0.0f); - auto ikNode = getAnimInverseKinematicsNode(); - if (ikNode) { - float alpha = 1.0f - _rightHandRelaxDuration / RELAX_DURATION; - const AnimPose geometryToRigTransform(_geometryToRigTransform); - AnimPose uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledRightHandPose(); - AnimPose handPose; - ::blend(1, &_lastRightHandControlledPose, &uncontrolledHandPose, alpha, &handPose); + _rightHandRelaxTimeRemaining = std::max(_rightHandRelaxTimeRemaining - dt, 0.0f); + AnimPose handPose; + if (transitionHandPose(_rightHandRelaxTimeRemaining, RELAX_DURATION, _lastRightHandControlledPose, RIGHT_HAND, + FROM_CONTROLLED, handPose)) { _animVars.set("rightHandPosition", handPose.trans()); _animVars.set("rightHandRotation", handPose.rot()); _animVars.set("rightHandType", (int)IKTarget::Type::RotationAndPosition); @@ -1545,3 +1576,25 @@ void Rig::computeAvatarBoundingCapsule( glm::vec3 rigCenter = (geometryToRig * (0.5f * (totalExtents.maximum + totalExtents.minimum))); localOffsetOut = rigCenter - (geometryToRig * rootPosition); } + +bool Rig::transitionHandPose(float deltaTime, float totalDuration, AnimPose& controlledHandPose, bool isLeftHand, + bool isToControlled, AnimPose& returnHandPose) { + auto ikNode = getAnimInverseKinematicsNode(); + if (ikNode) { + float alpha = 1.0f - deltaTime / totalDuration; + const AnimPose geometryToRigTransform(_geometryToRigTransform); + AnimPose uncontrolledHandPose; + if (isLeftHand) { + uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledLeftHandPose(); + } else { + uncontrolledHandPose = geometryToRigTransform * ikNode->getUncontrolledRightHandPose(); + } + if (isToControlled) { + ::blend(1, &uncontrolledHandPose, &controlledHandPose, alpha, &returnHandPose); + } else { + ::blend(1, &controlledHandPose, &uncontrolledHandPose, alpha, &returnHandPose); + } + return true; + } + return false; +} diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 994bd4b074..b5b69fc018 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -352,10 +352,15 @@ private: int _nextStateHandlerId { 0 }; QMutex _stateMutex; + bool transitionHandPose(float deltaTime, float totalDuration, AnimPose& controlledHandPose, bool isLeftHand, + bool isToControlled, AnimPose& returnHandPose); + bool _isLeftHandControlled { false }; bool _isRightHandControlled { false }; - float _leftHandRelaxDuration { 0.0f }; - float _rightHandRelaxDuration { 0.0f }; + float _leftHandControlTimeRemaining { 0.0f }; + float _rightHandControlTimeRemaining { 0.0f }; + float _leftHandRelaxTimeRemaining { 0.0f }; + float _rightHandRelaxTimeRemaining { 0.0f }; AnimPose _lastLeftHandControlledPose; AnimPose _lastRightHandControlledPose; }; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 0bc72ae689..5297426840 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -395,6 +395,11 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { return (mode == QAudio::AudioInput) ? QAudioDeviceInfo::defaultInputDevice() : QAudioDeviceInfo::defaultOutputDevice(); } +bool AudioClient::getNamedAudioDeviceForModeExists(QAudio::Mode mode, const QString& deviceName) { + return (getNamedAudioDeviceForMode(mode, deviceName).deviceName() == deviceName); +} + + // attempt to use the native sample rate and channel count bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, QAudioFormat& audioFormat) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index bec2fd2cc9..f3e7e418b2 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -149,6 +149,8 @@ public: static const float CALLBACK_ACCELERATOR_RATIO; + bool getNamedAudioDeviceForModeExists(QAudio::Mode mode, const QString& deviceName); + #ifdef Q_OS_WIN static QString friendlyNameForAudioDevice(wchar_t* guid); #endif diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 3c048e590d..6ec2b45c89 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1483,7 +1483,8 @@ QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const { return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL; } -void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged) { +void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& identityChanged, + bool& displayNameChanged, bool& skeletonModelUrlChanged) { QDataStream packetStream(identityData); @@ -1516,6 +1517,7 @@ void AvatarData::processAvatarIdentity(const QByteArray& identityData, bool& ide if (_firstSkeletonCheck || (identity.skeletonModelURL != cannonicalSkeletonModelURL(emptyURL))) { setSkeletonModelURL(identity.skeletonModelURL); identityChanged = true; + skeletonModelUrlChanged = true; if (_firstSkeletonCheck) { displayNameChanged = true; } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 2d43c4d412..44d910b571 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -369,6 +369,7 @@ public: virtual ~AvatarData(); static const QUrl& defaultFullAvatarModelUrl(); + QUrl cannonicalSkeletonModelURL(const QUrl& empty) const; virtual bool isMyAvatar() const { return false; } @@ -534,8 +535,9 @@ public: }; // identityChanged returns true if identity has changed, false otherwise. - // displayNameChanged returns true if displayName has changed, false otherwise. - void processAvatarIdentity(const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged); + // identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange. + void processAvatarIdentity(const QByteArray& identityData, bool& identityChanged, + bool& displayNameChanged, bool& skeletonModelUrlChanged); QByteArray identityByteArray(bool shouldForwardIncomingSequenceNumber = false) const; @@ -691,7 +693,6 @@ protected: QVector _attachmentData; QString _displayName; QString _sessionDisplayName { }; - QUrl cannonicalSkeletonModelURL(const QUrl& empty) const; QHash _jointIndices; ///< 1-based, since zero is returned for missing keys QStringList _jointNames; ///< in order of depth-first traversal diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 06ead63de1..e8c37bdaa8 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -155,8 +155,9 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer auto avatar = newOrExistingAvatar(identityUUID, sendingNode); bool identityChanged = false; bool displayNameChanged = false; + bool skeletonModelUrlChanged = false; // In this case, the "sendingNode" is the Avatar Mixer. - avatar->processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged); + avatar->processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged, skeletonModelUrlChanged); } } diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index aef8d9d85b..319a821eb6 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -33,7 +33,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", - "system/chat.js" + // "system/chat.js" ]; // add a menu item for debugging diff --git a/scripts/developer/hmdRollControlDisable.js b/scripts/developer/hmdRollControlDisable.js new file mode 100644 index 0000000000..fe8a85e3e5 --- /dev/null +++ b/scripts/developer/hmdRollControlDisable.js @@ -0,0 +1,17 @@ +// +// hmdRollControlDisable.js +// +// Created by David Rowe on 4 Jun 2017. +// Copyright 2017 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 +// + +var hmdRollControlEnabled = false; + +//print("HMD roll control: " + hmdRollControlEnabled); + +MyAvatar.hmdRollControlEnabled = hmdRollControlEnabled; + +Script.stop(); diff --git a/scripts/developer/hmdRollControlEnable.js b/scripts/developer/hmdRollControlEnable.js new file mode 100644 index 0000000000..81318b7ddd --- /dev/null +++ b/scripts/developer/hmdRollControlEnable.js @@ -0,0 +1,21 @@ +// +// hmdRollControlEnable.js +// +// Created by David Rowe on 4 Jun 2017. +// Copyright 2017 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 +// + +var hmdRollControlEnabled = true; +var hmdRollControlDeadZone = 8.0; // deg +var hmdRollControlRate = 2.5; // deg/sec/deg + +//print("HMD roll control: " + hmdRollControlEnabled + ", " + hmdRollControlDeadZone + ", " + hmdRollControlRate); + +MyAvatar.hmdRollControlEnabled = hmdRollControlEnabled; +MyAvatar.hmdRollControlDeadZone = hmdRollControlDeadZone; +MyAvatar.hmdRollControlRate = hmdRollControlRate; + +Script.stop(); diff --git a/server-console/src/main.js b/server-console/src/main.js index 408a17bd56..95fb0d81b2 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -101,6 +101,10 @@ function getApplicationDataDirectory() { return path.join(getRootHifiDataDirectory(), '/Server Console'); } +// Update lock filepath +const UPDATER_LOCK_FILENAME = ".updating"; +const UPDATER_LOCK_FULL_PATH = getRootHifiDataDirectory() + "/" + UPDATER_LOCK_FILENAME; + // Configure log global.log = require('electron-log'); const logFile = getApplicationDataDirectory() + '/log.txt'; @@ -630,11 +634,22 @@ function checkNewContent() { userConfig.save(configPath); } }); + } else if (fs.existsSync(UPDATER_LOCK_FULL_PATH)) { + backupResourceDirectoriesAndRestart(); } } }); } +function removeIncompleteUpdate(acResourceDirectory, dsResourceDirectory) { + if (fs.existsSync(UPDATER_LOCK_FULL_PATH)) { + log.debug('Removing incomplete content update files before copying new update'); + fs.emptyDirSync(dsResourceDirectory); + fs.emptyDirSync(acResourceDirectory); + } else { + fs.ensureFileSync(UPDATER_LOCK_FULL_PATH); + } +} function maybeInstallDefaultContentSet(onComplete) { // Check for existing data @@ -673,7 +688,11 @@ function maybeInstallDefaultContentSet(onComplete) { } log.debug("Found contentPath:" + argv.contentPath); + if (argv.contentPath) { + // check if we're updating a data folder whose update is incomplete + removeIncompleteUpdate(acResourceDirectory, dsResourceDirectory); + fs.copy(argv.contentPath, getRootHifiDataDirectory(), function (err) { if (err) { log.debug('Could not copy home content: ' + err); @@ -682,12 +701,12 @@ function maybeInstallDefaultContentSet(onComplete) { log.debug('Copied home content over to: ' + getRootHifiDataDirectory()); userConfig.set('homeContentLastModified', new Date()); userConfig.save(configPath); + fs.removeSync(UPDATER_LOCK_FULL_PATH); onComplete(); }); return; } - // Show popup var window = new BrowserWindow({ icon: appIcon, @@ -718,6 +737,9 @@ function maybeInstallDefaultContentSet(onComplete) { var aborted = false; + // check if we're updating a data folder whose update is incomplete + removeIncompleteUpdate(acResourceDirectory, dsResourceDirectory); + // Start downloading content set var req = progress(request.get({ url: HOME_CONTENT_URL @@ -763,6 +785,7 @@ function maybeInstallDefaultContentSet(onComplete) { log.debug("Finished unarchiving home content set"); userConfig.set('homeContentLastModified', new Date()); userConfig.save(configPath); + fs.removeSync(UPDATER_LOCK_FULL_PATH); sendStateUpdate('complete'); });