diff --git a/BUILD.md b/BUILD.md index c51e40cb58..9c56574cbb 100644 --- a/BUILD.md +++ b/BUILD.md @@ -2,8 +2,8 @@ * [cmake](http://www.cmake.org/cmake/resources/software.html) ~> 3.3.2 * [Qt](http://www.qt.io/download-open-source) ~> 5.6.1 -* [OpenSSL](https://www.openssl.org/community/binaries.html) ~> 1.0.1m - * IMPORTANT: Using the recommended version of OpenSSL is critical to avoid security vulnerabilities. +* [OpenSSL](https://www.openssl.org/community/binaries.html) + * IMPORTANT: Use the latest available version of OpenSSL to avoid security vulnerabilities. * [VHACD](https://github.com/virneo/v-hacd)(clone this repository)(Optional) ####CMake External Project Dependencies diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index ca1b57ad6a..b9cac208b7 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -191,8 +191,7 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) { nodeList->eachNode([&killedNode](const SharedNodePointer& node) { auto clientData = dynamic_cast(node->getLinkedData()); if (clientData) { - QUuid killedUUID = killedNode->getUUID(); - clientData->removeHRTFsForNode(killedUUID); + clientData->removeNode(killedNode->getUUID()); } }); } @@ -325,8 +324,8 @@ void AudioMixer::sendStatsPacket() { addTiming(_mixTiming, "mix"); addTiming(_eventsTiming, "events"); -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - timingStats["ns_per_throttle"] = (_stats.totalMixes > 0) ? (float)(_stats.throttleTime / _stats.totalMixes) : 0; +#ifdef HIFI_AUDIO_MIXER_DEBUG + timingStats["ns_per_mix"] = (_stats.totalMixes > 0) ? (float)(_stats.mixTime / _stats.totalMixes) : 0; #endif // call it "avg_..." to keep it higher in the display, sorted alphabetically diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 70d6a67b5b..791ccb8b03 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -26,6 +26,7 @@ AudioMixerClientData::AudioMixerClientData(const QUuid& nodeID) : NodeData(nodeID), audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO), + _ignoreZone(*this), _outgoingMixedAudioSequenceNumber(0), _downstreamAudioStreamStats() { @@ -427,3 +428,99 @@ void AudioMixerClientData::cleanupCodec() { } } } + +AudioMixerClientData::IgnoreZone& AudioMixerClientData::IgnoreZoneMemo::get(unsigned int frame) { + // check for a memoized zone + if (frame != _frame.load(std::memory_order_acquire)) { + AvatarAudioStream* stream = _data.getAvatarAudioStream(); + + // get the initial dimensions from the stream + glm::vec3 corner = stream ? stream->getAvatarBoundingBoxCorner() : glm::vec3(0); + glm::vec3 scale = stream ? stream->getAvatarBoundingBoxScale() : glm::vec3(0); + + // enforce a minimum scale + static const glm::vec3 MIN_IGNORE_BOX_SCALE = glm::vec3(0.3f, 1.3f, 0.3f); + if (glm::any(glm::lessThan(scale, MIN_IGNORE_BOX_SCALE))) { + scale = MIN_IGNORE_BOX_SCALE; + } + + // quadruple the scale (this is arbitrary number chosen for comfort) + const float IGNORE_BOX_SCALE_FACTOR = 4.0f; + scale *= IGNORE_BOX_SCALE_FACTOR; + + // create the box (we use a box for the zone for convenience) + AABox box(corner, scale); + + // update the memoized zone + // This may be called by multiple threads concurrently, + // so take a lock and only update the memo if this call is first. + // This prevents concurrent updates from invalidating the returned reference + // (contingent on the preconditions listed in the header). + std::lock_guard lock(_mutex); + if (frame != _frame.load(std::memory_order_acquire)) { + _zone = box; + unsigned int oldFrame = _frame.exchange(frame, std::memory_order_release); + Q_UNUSED(oldFrame); + + // check the precondition + assert(oldFrame == 0 || frame == (oldFrame + 1)); + } + } + + return _zone; +} + +void AudioMixerClientData::IgnoreNodeCache::cache(bool shouldIgnore) { + if (!_isCached) { + _shouldIgnore = shouldIgnore; + _isCached = true; + } +} + +bool AudioMixerClientData::IgnoreNodeCache::isCached() { + return _isCached; +} + +bool AudioMixerClientData::IgnoreNodeCache::shouldIgnore() { + bool ignore = _shouldIgnore; + _isCached = false; + return ignore; +} + +bool AudioMixerClientData::shouldIgnore(const SharedNodePointer self, const SharedNodePointer node, unsigned int frame) { + // this is symmetric over self / node; if computed, it is cached in the other + + // check the cache to avoid computation + auto& cache = _nodeSourcesIgnoreMap[node->getUUID()]; + if (cache.isCached()) { + return cache.shouldIgnore(); + } + + AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + if (!nodeData) { + return false; + } + + // compute shouldIgnore + bool shouldIgnore = true; + if ( // the nodes are not ignoring each other explicitly (or are but get data regardless) + (!self->isIgnoringNodeWithID(node->getUUID()) || + (nodeData->getRequestsDomainListData() && node->getCanKick())) && + (!node->isIgnoringNodeWithID(self->getUUID()) || + (getRequestsDomainListData() && self->getCanKick()))) { + + // if either node is enabling an ignore radius, check their proximity + if ((self->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) { + auto& zone = _ignoreZone.get(frame); + auto& nodeZone = nodeData->_ignoreZone.get(frame); + shouldIgnore = zone.touches(nodeZone); + } else { + shouldIgnore = false; + } + } + + // cache in node + nodeData->_nodeSourcesIgnoreMap[self->getUUID()].cache(shouldIgnore); + + return shouldIgnore; +} diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index e637fd0409..c30923f411 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -38,18 +38,22 @@ public: AudioStreamMap getAudioStreams() { QReadLocker readLock { &_streamsLock }; return _audioStreams; } AvatarAudioStream* getAvatarAudioStream(); + // returns whether self (this data's node) should ignore node, memoized by frame + // precondition: frame is monotonically increasing after first call + bool shouldIgnore(SharedNodePointer self, SharedNodePointer node, unsigned int frame); + // the following methods should be called from the AudioMixer assignment thread ONLY // they are not thread-safe // returns a new or existing HRTF object for the given stream from the given node AudioHRTF& hrtfForStream(const QUuid& nodeID, const QUuid& streamID = QUuid()) { return _nodeSourcesHRTFMap[nodeID][streamID]; } - // remove HRTFs for all sources from this node - void removeHRTFsForNode(const QUuid& nodeID) { _nodeSourcesHRTFMap.erase(nodeID); } - // removes an AudioHRTF object for a given stream void removeHRTFForStream(const QUuid& nodeID, const QUuid& streamID = QUuid()); + // remove all sources and data from this node + void removeNode(const QUuid& nodeID) { _nodeSourcesIgnoreMap.unsafe_erase(nodeID); _nodeSourcesHRTFMap.erase(nodeID); } + void removeAgentAvatarAudioStream(); int parseData(ReceivedMessage& message) override; @@ -86,12 +90,10 @@ public: bool shouldFlushEncoder() { return _shouldFlushEncoder; } QString getCodecName() { return _selectedCodecName; } - + bool shouldMuteClient() { return _shouldMuteClient; } void setShouldMuteClient(bool shouldMuteClient) { _shouldMuteClient = shouldMuteClient; } glm::vec3 getPosition() { return getAvatarAudioStream() ? getAvatarAudioStream()->getPosition() : glm::vec3(0); } - glm::vec3 getAvatarBoundingBoxCorner() { return getAvatarAudioStream() ? getAvatarAudioStream()->getAvatarBoundingBoxCorner() : glm::vec3(0); } - glm::vec3 getAvatarBoundingBoxScale() { return getAvatarAudioStream() ? getAvatarAudioStream()->getAvatarBoundingBoxScale() : glm::vec3(0); } bool getRequestsDomainListData() { return _requestsDomainListData; } void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; } @@ -103,9 +105,48 @@ public slots: void sendSelectAudioFormat(SharedNodePointer node, const QString& selectedCodecName); private: + using IgnoreZone = AABox; + QReadWriteLock _streamsLock; AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID + class IgnoreZoneMemo { + public: + IgnoreZoneMemo(AudioMixerClientData& data) : _data(data) {} + + // returns an ignore zone, memoized by frame (lockless if the zone is already memoized) + // preconditions: + // - frame is monotonically increasing after first call + // - there are no references left from calls to getIgnoreZone(frame - 1) + IgnoreZone& get(unsigned int frame); + + private: + AudioMixerClientData& _data; + IgnoreZone _zone; + std::atomic _frame { 0 }; + std::mutex _mutex; + }; + IgnoreZoneMemo _ignoreZone; + + class IgnoreNodeCache { + public: + // std::atomic is not copyable - always initialize uncached + IgnoreNodeCache() {} + IgnoreNodeCache(const IgnoreNodeCache& other) {} + + void cache(bool shouldIgnore); + bool isCached(); + bool shouldIgnore(); + + private: + std::atomic _isCached { false }; + bool _shouldIgnore { false }; + }; + struct IgnoreNodeCacheHasher { std::size_t operator()(const QUuid& key) const { return qHash(key); } }; + + using NodeSourcesIgnoreMap = tbb::concurrent_unordered_map; + NodeSourcesIgnoreMap _nodeSourcesIgnoreMap; + using HRTFMap = std::unordered_map; using NodeSourcesHRTFMap = std::unordered_map; NodeSourcesHRTFMap _nodeSourcesHRTFMap; diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index adc6413316..370df60ec5 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -46,7 +46,6 @@ void sendMutePacket(const SharedNodePointer& node, AudioMixerClientData&); void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& data); // mix helpers -inline bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node); inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); inline float computeGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, @@ -126,8 +125,7 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { typedef void (AudioMixerSlave::*MixFunctor)( AudioMixerClientData&, const QUuid&, const AvatarAudioStream&, const PositionalAudioStream&); - auto allStreams = [&](const SharedNodePointer& node, MixFunctor mixFunctor) { - AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + auto forAllStreams = [&](const SharedNodePointer& node, AudioMixerClientData* nodeData, MixFunctor mixFunctor) { auto nodeID = node->getUUID(); for (auto& streamPair : nodeData->getAudioStreams()) { auto nodeStream = streamPair.second; @@ -135,10 +133,17 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { } }; - std::for_each(_begin, _end, [&](const SharedNodePointer& node) { - if (*node == *listener) { - AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); +#ifdef HIFI_AUDIO_MIXER_DEBUG + auto mixStart = p_high_resolution_clock::now(); +#endif + std::for_each(_begin, _end, [&](const SharedNodePointer& node) { + AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + if (!nodeData) { + return; + } + + if (*node == *listener) { // only mix the echo, if requested for (auto& streamPair : nodeData->getAudioStreams()) { auto nodeStream = streamPair.second; @@ -146,15 +151,10 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { mixStream(*listenerData, node->getUUID(), *listenerAudioStream, *nodeStream); } } - } else if (!shouldIgnoreNode(listener, node)) { + } else if (!listenerData->shouldIgnore(listener, node, _frame)) { if (!isThrottling) { - allStreams(node, &AudioMixerSlave::mixStream); + forAllStreams(node, nodeData, &AudioMixerSlave::mixStream); } else { -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - auto throttleStart = p_high_resolution_clock::now(); -#endif - - AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); auto nodeID = node->getUUID(); // compute the node's max relative volume @@ -179,13 +179,6 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { if (!throttledNodes.empty()) { std::push_heap(throttledNodes.begin(), throttledNodes.end()); } - -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - auto throttleEnd = p_high_resolution_clock::now(); - uint64_t throttleTime = - std::chrono::duration_cast(throttleEnd - throttleStart).count(); - stats.throttleTime += throttleTime; -#endif } } }); @@ -201,7 +194,8 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { std::pop_heap(throttledNodes.begin(), throttledNodes.end()); auto& node = throttledNodes.back().second; - allStreams(node, &AudioMixerSlave::mixStream); + AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + forAllStreams(node, nodeData, &AudioMixerSlave::mixStream); throttledNodes.pop_back(); } @@ -209,10 +203,17 @@ bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) { // throttle the remaining nodes' streams for (const std::pair& nodePair : throttledNodes) { auto& node = nodePair.second; - allStreams(node, &AudioMixerSlave::throttleStream); + AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); + forAllStreams(node, nodeData, &AudioMixerSlave::throttleStream); } } +#ifdef HIFI_AUDIO_MIXER_DEBUG + auto mixEnd = p_high_resolution_clock::now(); + auto mixTime = std::chrono::duration_cast(mixEnd - mixStart); + stats.mixTime += mixTime.count(); +#endif + // use the per listener AudioLimiter to render the mixed data... listenerData->audioLimiter.render(_mixSamples, _bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); @@ -452,55 +453,6 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& } } -bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node) { - AudioMixerClientData* listenerData = static_cast(listener->getLinkedData()); - AudioMixerClientData* nodeData = static_cast(node->getLinkedData()); - - // when this is true, the AudioMixer will send Audio data to a client about avatars that have ignored them - bool getsAnyIgnored = listenerData->getRequestsDomainListData() && listener->getCanKick(); - - bool ignore = true; - - if (nodeData && - // make sure that it isn't being ignored by our listening node - (!listener->isIgnoringNodeWithID(node->getUUID()) || (nodeData->getRequestsDomainListData() && node->getCanKick())) && - // and that it isn't ignoring our listening node - (!node->isIgnoringNodeWithID(listener->getUUID()) || getsAnyIgnored)) { - - // is either node enabling the space bubble / ignore radius? - if ((listener->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) { - // define the minimum bubble size - static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f); - - // set up the bounding box for the listener - AABox listenerBox(listenerData->getAvatarBoundingBoxCorner(), listenerData->getAvatarBoundingBoxScale()); - if (glm::any(glm::lessThan(listenerData->getAvatarBoundingBoxScale(), minBubbleSize))) { - listenerBox.setScaleStayCentered(minBubbleSize); - } - - // set up the bounding box for the node - AABox nodeBox(nodeData->getAvatarBoundingBoxCorner(), nodeData->getAvatarBoundingBoxScale()); - // Clamp the size of the bounding box to a minimum scale - if (glm::any(glm::lessThan(nodeData->getAvatarBoundingBoxScale(), minBubbleSize))) { - nodeBox.setScaleStayCentered(minBubbleSize); - } - - // quadruple the scale of both bounding boxes - listenerBox.embiggen(4.0f); - nodeBox.embiggen(4.0f); - - // perform the collision check between the two bounding boxes - ignore = listenerBox.touches(nodeBox); - } else { - ignore = false; - } - } - - return ignore; -} - -static const float ATTENUATION_START_DISTANCE = 1.0f; - float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition) { float gain = 1.0f; @@ -556,6 +508,7 @@ float computeGain(const AvatarAudioStream& listeningNodeStream, const Positional } // distance attenuation + const float ATTENUATION_START_DISTANCE = 1.0f; float distance = glm::length(relativePosition); assert(ATTENUATION_START_DISTANCE > EPSILON); if (distance >= ATTENUATION_START_DISTANCE) { diff --git a/assignment-client/src/audio/AudioMixerStats.cpp b/assignment-client/src/audio/AudioMixerStats.cpp index a3a3a215bc..a831210871 100644 --- a/assignment-client/src/audio/AudioMixerStats.cpp +++ b/assignment-client/src/audio/AudioMixerStats.cpp @@ -20,8 +20,8 @@ void AudioMixerStats::reset() { hrtfThrottleRenders = 0; manualStereoMixes = 0; manualEchoMixes = 0; -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - throttleTime = 0; +#ifdef HIFI_AUDIO_MIXER_DEBUG + mixTime = 0; #endif } @@ -34,7 +34,7 @@ void AudioMixerStats::accumulate(const AudioMixerStats& otherStats) { hrtfThrottleRenders += otherStats.hrtfThrottleRenders; manualStereoMixes += otherStats.manualStereoMixes; manualEchoMixes += otherStats.manualEchoMixes; -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - throttleTime += otherStats.throttleTime; +#ifdef HIFI_AUDIO_MIXER_DEBUG + mixTime += otherStats.mixTime; #endif } diff --git a/assignment-client/src/audio/AudioMixerStats.h b/assignment-client/src/audio/AudioMixerStats.h index f7e3ed1525..77ac8b985d 100644 --- a/assignment-client/src/audio/AudioMixerStats.h +++ b/assignment-client/src/audio/AudioMixerStats.h @@ -12,7 +12,7 @@ #ifndef hifi_AudioMixerStats_h #define hifi_AudioMixerStats_h -#ifdef HIFI_AUDIO_THROTTLE_DEBUG +#ifdef HIFI_AUDIO_MIXER_DEBUG #include #endif @@ -29,8 +29,8 @@ struct AudioMixerStats { int manualStereoMixes { 0 }; int manualEchoMixes { 0 }; -#ifdef HIFI_AUDIO_THROTTLE_DEBUG - uint64_t throttleTime { 0 }; +#ifdef HIFI_AUDIO_MIXER_DEBUG + uint64_t mixTime { 0 }; #endif void reset(); diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 31d6845972..379f812923 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -667,7 +667,17 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointergetActiveSocket() ? matchingNode->getActiveSocket()->getAddress() : matchingNode->getPublicSocket().getAddress(); - + + // probably isLoopback covers it, as whenever I try to ban an agent on same machine as the domain-server + // it is always 127.0.0.1, but looking at the public and local addresses just to be sure + // TODO: soon we will have feedback (in the form of a message to the client) after we kick. When we + // do, we will have a success flag, and perhaps a reason for failure. For now, just don't do it. + if (kickAddress == limitedNodeList->getPublicSockAddr().getAddress() || + kickAddress == limitedNodeList->getLocalSockAddr().getAddress() || + kickAddress.isLoopback() ) { + qWarning() << "attempt to kick node running on same machine as domain server, ignoring KickRequest"; + return; + } NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid()); // check if there were already permissions for the IP diff --git a/interface/resources/icons/tablet-icons/bubble-a.svg b/interface/resources/icons/tablet-icons/bubble-a.svg new file mode 100644 index 0000000000..553636bfbb --- /dev/null +++ b/interface/resources/icons/tablet-icons/bubble-a.svg @@ -0,0 +1,96 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/edit-a.svg b/interface/resources/icons/tablet-icons/edit-a.svg new file mode 100644 index 0000000000..045887c47d --- /dev/null +++ b/interface/resources/icons/tablet-icons/edit-a.svg @@ -0,0 +1,62 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/goto-a.svg b/interface/resources/icons/tablet-icons/goto-a.svg new file mode 100644 index 0000000000..1c95460040 --- /dev/null +++ b/interface/resources/icons/tablet-icons/goto-a.svg @@ -0,0 +1,54 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/help-a.svg b/interface/resources/icons/tablet-icons/help-a.svg new file mode 100644 index 0000000000..84a2c86791 --- /dev/null +++ b/interface/resources/icons/tablet-icons/help-a.svg @@ -0,0 +1,65 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/ignore-a.svg b/interface/resources/icons/tablet-icons/ignore-a.svg new file mode 100644 index 0000000000..c046799f92 --- /dev/null +++ b/interface/resources/icons/tablet-icons/ignore-a.svg @@ -0,0 +1,74 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/market-a.svg b/interface/resources/icons/tablet-icons/market-a.svg new file mode 100644 index 0000000000..f8ba17301e --- /dev/null +++ b/interface/resources/icons/tablet-icons/market-a.svg @@ -0,0 +1,64 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/menu-a.svg b/interface/resources/icons/tablet-icons/menu-a.svg new file mode 100644 index 0000000000..fe2c9178d6 --- /dev/null +++ b/interface/resources/icons/tablet-icons/menu-a.svg @@ -0,0 +1,62 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-mute-a.svg b/interface/resources/icons/tablet-icons/mic-mute-a.svg new file mode 100644 index 0000000000..4b199c8e01 --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-mute-a.svg @@ -0,0 +1,70 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-a.svg b/interface/resources/icons/tablet-icons/mic-mute-i.svg similarity index 100% rename from interface/resources/icons/tablet-icons/mic-a.svg rename to interface/resources/icons/tablet-icons/mic-mute-i.svg diff --git a/interface/resources/icons/tablet-icons/mic-unmute-a.svg b/interface/resources/icons/tablet-icons/mic-unmute-a.svg new file mode 100644 index 0000000000..b1464f207d --- /dev/null +++ b/interface/resources/icons/tablet-icons/mic-unmute-a.svg @@ -0,0 +1,70 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/mic-i.svg b/interface/resources/icons/tablet-icons/mic-unmute-i.svg similarity index 100% rename from interface/resources/icons/tablet-icons/mic-i.svg rename to interface/resources/icons/tablet-icons/mic-unmute-i.svg diff --git a/interface/resources/icons/tablet-icons/people-a.svg b/interface/resources/icons/tablet-icons/people-a.svg new file mode 100644 index 0000000000..bed652f410 --- /dev/null +++ b/interface/resources/icons/tablet-icons/people-a.svg @@ -0,0 +1,80 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/scripts-a.svg b/interface/resources/icons/tablet-icons/scripts-a.svg new file mode 100644 index 0000000000..c285d83c8b --- /dev/null +++ b/interface/resources/icons/tablet-icons/scripts-a.svg @@ -0,0 +1,68 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/snap-a.svg b/interface/resources/icons/tablet-icons/snap-a.svg new file mode 100644 index 0000000000..2fe966543f --- /dev/null +++ b/interface/resources/icons/tablet-icons/snap-a.svg @@ -0,0 +1,62 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/switch-desk-a.svg b/interface/resources/icons/tablet-icons/switch-desk-a.svg new file mode 100644 index 0000000000..7b1d9f2f0a --- /dev/null +++ b/interface/resources/icons/tablet-icons/switch-desk-a.svg @@ -0,0 +1,54 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/switch-a.svg b/interface/resources/icons/tablet-icons/switch-desk-i.svg similarity index 100% rename from interface/resources/icons/tablet-icons/switch-a.svg rename to interface/resources/icons/tablet-icons/switch-desk-i.svg diff --git a/interface/resources/icons/tablet-icons/switch-vr-a.svg b/interface/resources/icons/tablet-icons/switch-vr-a.svg new file mode 100644 index 0000000000..1aa961f6f5 --- /dev/null +++ b/interface/resources/icons/tablet-icons/switch-vr-a.svg @@ -0,0 +1,54 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/switch-i.svg b/interface/resources/icons/tablet-icons/switch-vr-i.svg similarity index 100% rename from interface/resources/icons/tablet-icons/switch-i.svg rename to interface/resources/icons/tablet-icons/switch-vr-i.svg diff --git a/interface/resources/icons/tablet-icons/users-a.svg b/interface/resources/icons/tablet-icons/users-a.svg new file mode 100644 index 0000000000..c06e95b91a --- /dev/null +++ b/interface/resources/icons/tablet-icons/users-a.svg @@ -0,0 +1,94 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/interface/resources/qml/hifi/tablet/TabletButton.qml b/interface/resources/qml/hifi/tablet/TabletButton.qml index 945d2769dd..5e7e0f5709 100644 --- a/interface/resources/qml/hifi/tablet/TabletButton.qml +++ b/interface/resources/qml/hifi/tablet/TabletButton.qml @@ -4,10 +4,14 @@ import QtGraphicalEffects 1.0 Item { id: tabletButton property var uuid; - property string text: "EDIT" - property string icon: "icons/edit-icon.svg" - property string activeText: tabletButton.text + property string icon: "icons/tablet-icons/edit-i.svg" + property string hoverIcon: tabletButton.icon property string activeIcon: tabletButton.icon + property string activeHoverIcon: tabletButton.activeIcon + property string text: "EDIT" + property string hoverText: tabletButton.text + property string activeText: tabletButton.text + property string activeHoverText: tabletButton.activeText property bool isActive: false property bool inDebugMode: false property bool isEntered: false @@ -25,9 +29,9 @@ Item { onIsActiveChanged: { if (tabletButton.isEntered) { - tabletButton.state = (tabletButton.isActive) ? "hover active state" : "hover sate"; + tabletButton.state = (tabletButton.isActive) ? "hover active state" : "hover state"; } else { - tabletButton.state = (tabletButton.isActive) ? "active state" : "base sate"; + tabletButton.state = (tabletButton.isActive) ? "active state" : "base state"; } } @@ -89,7 +93,6 @@ Item { id: icon width: 50 height: 50 - visible: false anchors.bottom: text.top anchors.bottomMargin: 5 anchors.horizontalCenter: parent.horizontalCenter @@ -97,13 +100,6 @@ Item { source: tabletButton.urlHelper(tabletButton.icon) } - ColorOverlay { - id: iconColorOverlay - anchors.fill: icon - source: icon - color: "#ffffff" - } - Text { id: text color: "#ffffff" @@ -166,6 +162,17 @@ Item { target: glow visible: true } + + PropertyChanges { + target: text + color: "#ffffff" + text: tabletButton.hoverText + } + + PropertyChanges { + target: icon + source: tabletButton.urlHelper(tabletButton.hoverIcon) + } }, State { name: "active state" @@ -188,11 +195,6 @@ Item { text: tabletButton.activeText } - PropertyChanges { - target: iconColorOverlay - color: "#333333" - } - PropertyChanges { target: icon source: tabletButton.urlHelper(tabletButton.activeIcon) @@ -221,13 +223,13 @@ Item { PropertyChanges { target: text color: "#333333" + text: tabletButton.activeHoverText } PropertyChanges { - target: iconColorOverlay - color: "#333333" + target: icon + source: tabletButton.urlHelper(tabletButton.activeHoverIcon) } - } ] } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 1441ae9001..b1808c9323 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3111,7 +3111,10 @@ void Application::mousePressEvent(QMouseEvent* event) { if (!_aboutToQuit) { getOverlays().mousePressEvent(&mappedEvent); - getEntities()->mousePressEvent(&mappedEvent); + + if (!_controllerScriptingInterface->areEntityClicksCaptured()) { + getEntities()->mousePressEvent(&mappedEvent); + } } _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts diff --git a/interface/src/scripting/ControllerScriptingInterface.cpp b/interface/src/scripting/ControllerScriptingInterface.cpp index d28c209a52..0d0c2ef668 100644 --- a/interface/src/scripting/ControllerScriptingInterface.cpp +++ b/interface/src/scripting/ControllerScriptingInterface.cpp @@ -60,6 +60,18 @@ void ControllerScriptingInterface::releaseKeyEvents(const KeyEvent& event) { } } +bool ControllerScriptingInterface::areEntityClicksCaptured() const { + return _captureEntityClicks; +} + +void ControllerScriptingInterface::captureEntityClickEvents() { + _captureEntityClicks = true; +} + +void ControllerScriptingInterface::releaseEntityClickEvents() { + _captureEntityClicks = false; +} + bool ControllerScriptingInterface::isJoystickCaptured(int joystickIndex) const { return _capturedJoysticks.contains(joystickIndex); } diff --git a/interface/src/scripting/ControllerScriptingInterface.h b/interface/src/scripting/ControllerScriptingInterface.h index 50539e7a05..996ccabb20 100644 --- a/interface/src/scripting/ControllerScriptingInterface.h +++ b/interface/src/scripting/ControllerScriptingInterface.h @@ -84,6 +84,7 @@ public: bool isKeyCaptured(QKeyEvent* event) const; bool isKeyCaptured(const KeyEvent& event) const; bool isJoystickCaptured(int joystickIndex) const; + bool areEntityClicksCaptured() const; void updateInputControllers(); @@ -95,6 +96,9 @@ public slots: virtual void captureJoystick(int joystickIndex); virtual void releaseJoystick(int joystickIndex); + virtual void captureEntityClickEvents(); + virtual void releaseEntityClickEvents(); + virtual glm::vec2 getViewportDimensions() const; virtual QVariant getRecommendedOverlayRect() const; @@ -128,6 +132,7 @@ private: QMultiMap _capturedKeys; QSet _capturedJoysticks; + bool _captureEntityClicks; using InputKey = controller::InputController::Key; using InputControllerMap = std::map; diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index 1265aabbf2..4f4f3bf67f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -938,17 +938,19 @@ void EntityTreeRenderer::addEntityToScene(EntityItemPointer entity) { void EntityTreeRenderer::entityScriptChanging(const EntityItemID& entityID, const bool reload) { - if (_tree && !_shuttingDown) { - _entitiesScriptEngine->unloadEntityScript(entityID); - checkAndCallPreload(entityID, reload); - } + checkAndCallPreload(entityID, reload, true); } -void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload) { +void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const bool reload, const bool unloadFirst) { if (_tree && !_shuttingDown) { EntityItemPointer entity = getTree()->findEntityByEntityItemID(entityID); - if (entity && entity->shouldPreloadScript() && _entitiesScriptEngine) { - QString scriptUrl = entity->getScript(); + bool shouldLoad = entity && entity->shouldPreloadScript() && _entitiesScriptEngine; + QString scriptUrl = entity->getScript(); + if ((unloadFirst && shouldLoad) || scriptUrl.isEmpty()) { + _entitiesScriptEngine->unloadEntityScript(entityID); + entity->scriptHasUnloaded(); + } + if (shouldLoad && !scriptUrl.isEmpty()) { scriptUrl = ResourceManager::normalizeURL(scriptUrl); ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload); entity->scriptHasPreloaded(); diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 8669a1c4d3..c11738c459 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -148,7 +148,7 @@ private: bool layerZoneAndHasSkybox(const std::shared_ptr& zone); bool applySkyboxAndHasAmbient(); - void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false); + void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false, const bool unloadFirst = false); QList _releasedModels; RayToEntityIntersectionResult findRayIntersectionWorker(const PickRay& ray, Octree::lockType lockType, diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index e69195d53d..163b4d9e45 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -446,6 +446,7 @@ public: bool shouldPreloadScript() const { return !_script.isEmpty() && ((_loadedScript != _script) || (_loadedScriptTimestamp != _scriptTimestamp)); } void scriptHasPreloaded() { _loadedScript = _script; _loadedScriptTimestamp = _scriptTimestamp; } + void scriptHasUnloaded() { _loadedScript = ""; _loadedScriptTimestamp = 0; } bool getClientOnly() const { return _clientOnly; } void setClientOnly(bool clientOnly) { _clientOnly = clientOnly; } diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 4e92b2a572..427f6b4af0 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1119,7 +1119,9 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c endLogging = usecTimestampNow(); startUpdate = usecTimestampNow(); - properties.setLastEditedBy(senderNode->getUUID()); + if (!isPhysics) { + properties.setLastEditedBy(senderNode->getUUID()); + } updateEntity(entityItemID, properties, senderNode); existingEntity->markAsChangedOnServer(); endUpdate = usecTimestampNow(); diff --git a/libraries/script-engine/src/TabletScriptingInterface.h b/libraries/script-engine/src/TabletScriptingInterface.h index 93f5bcf6ba..9986fc78b0 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.h +++ b/libraries/script-engine/src/TabletScriptingInterface.h @@ -227,10 +227,14 @@ protected: /**jsdoc * @typedef TabletButtonProxy.ButtonProperties - * @property {string} text - button caption * @property {string} icon - url to button icon. (50 x 50) - * @property {string} activeText - button caption when button is active + * @property {string} hoverIcon - url to button icon, displayed during mouse hover. (50 x 50) + * @property {string} activeHoverIcon - url to button icon used when button is active, and during mouse hover. (50 x 50) * @property {string} activeIcon - url to button icon used when button is active. (50 x 50) + * @property {string} text - button caption + * @property {string} hoverText - button caption when button is not-active but during mouse hover. + * @property {string} activeText - button caption when button is active + * @property {string} activeHoverText - button caption when button is active and during mouse hover. * @property {string} isActive - true when button is active. * @property {number} sortOrder - determines sort order on tablet. lower numbers will appear before larger numbers. default is 100 */ diff --git a/scripts/system/bubble.js b/scripts/system/bubble.js index 87043ccc8a..ff262e3d6e 100644 --- a/scripts/system/bubble.js +++ b/scripts/system/bubble.js @@ -177,6 +177,7 @@ var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/bubble-i.svg", + activeIcon: "icons/tablet-icons/bubble-a.svg", text: buttonName, sortOrder: 4 }); diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index e495ccc67b..74a3c3d25b 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -404,7 +404,7 @@ Grabber.prototype.pressEvent = function(event) { }; Grabber.prototype.releaseEvent = function(event) { - if (event.isLeftButton!==true ||event.isRightButton===true || event.isMiddleButton===true) { + if (event.isLeftButton!==true ||event.isRightButton===true || event.isMiddleButton===true) { return; } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index f8cce6a544..92c5f2c9d3 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -253,6 +253,7 @@ var toolBar = (function () { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); activeButton = tablet.addButton({ icon: "icons/tablet-icons/edit-i.svg", + activeIcon: "icons/tablet-icons/edit-a.svg", text: "EDIT", sortOrder: 10 }); @@ -462,6 +463,11 @@ var toolBar = (function () { that.setActive = function (active) { Settings.setValue(EDIT_SETTING, active); + if (active) { + Controller.captureEntityClickEvents(); + } else { + Controller.releaseEntityClickEvents(); + } if (active === isActive) { return; } @@ -965,6 +971,7 @@ function cleanupModelMenus() { } Script.scriptEnding.connect(function () { + toolBar.setActive(false); Settings.setValue(SETTING_AUTO_FOCUS_ON_SELECT, Menu.isOptionChecked(MENU_AUTO_FOCUS_ON_SELECT)); Settings.setValue(SETTING_EASE_ON_FOCUS, Menu.isOptionChecked(MENU_EASE_ON_FOCUS)); Settings.setValue(SETTING_SHOW_LIGHTS_IN_EDIT_MODE, Menu.isOptionChecked(MENU_SHOW_LIGHTS_IN_EDIT_MODE)); diff --git a/scripts/system/goto.js b/scripts/system/goto.js index 092abd0369..0e09ea3d79 100644 --- a/scripts/system/goto.js +++ b/scripts/system/goto.js @@ -39,6 +39,7 @@ if (Settings.getValue("HUDUIEnabled")) { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/goto-i.svg", + activeIcon: "icons/tablet-icons/goto-a.svg", text: buttonName, sortOrder: 8 }); diff --git a/scripts/system/help.js b/scripts/system/help.js index 19c4b04363..4e7788a758 100644 --- a/scripts/system/help.js +++ b/scripts/system/help.js @@ -30,6 +30,7 @@ tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/help-i.svg", + activeIcon: "icons/tablet-icons/help-a.svg", text: buttonName, sortOrder: 6 }); diff --git a/scripts/system/hmd.js b/scripts/system/hmd.js index c755454fbb..3493215ba3 100644 --- a/scripts/system/hmd.js +++ b/scripts/system/hmd.js @@ -53,14 +53,13 @@ function onHmdChanged(isHmd) { //TODO change button icon when the hmd changes if (isHmd) { button.editProperties({ - icon: "icons/tablet-icons/switch-a.svg", + icon: "icons/tablet-icons/switch-desk-i.svg", text: "DESKTOP" }); } else { button.editProperties({ - icon: "icons/tablet-icons/switch-i.svg", - text: "VR", - sortOrder: 2 + icon: "icons/tablet-icons/switch-vr-i.svg", + text: "VR" }); } desktopOnlyViews.forEach(function (view) { @@ -82,8 +81,8 @@ if (headset) { }); } else { button = tablet.addButton({ - icon: "icons/tablet-icons/switch-a.svg", - text: "SWITCH", + icon: HMD.active ? "icons/tablet-icons/switch-desk-i.svg" : "icons/tablet-icons/switch-vr-i.svg", + text: HMD.active ? "DESKTOP" : "VR", sortOrder: 2 }); } diff --git a/scripts/system/libraries/utils.js b/scripts/system/libraries/utils.js index 2e490e5c30..a5e97d8949 100644 --- a/scripts/system/libraries/utils.js +++ b/scripts/system/libraries/utils.js @@ -9,7 +9,7 @@ // note: this constant is currently duplicated in edit.js EDIT_SETTING = "io.highfidelity.isEditting"; isInEditMode = function isInEditMode() { - return Settings.getValue(EDIT_SETTING) === "false" ? false : !!Settings.getValue(EDIT_SETTING); + return Settings.getValue(EDIT_SETTING); }; if (!Function.prototype.bind) { diff --git a/scripts/system/mute.js b/scripts/system/mute.js index f28a2eb9a5..822a8f127a 100644 --- a/scripts/system/mute.js +++ b/scripts/system/mute.js @@ -37,9 +37,11 @@ if (Settings.getValue("HUDUIEnabled")) { } else { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ - icon: "icons/tablet-icons/mic-a.svg", - text: buttonName, - activeIcon: "icons/tablet-icons/mic-i.svg", + icon: "icons/tablet-icons/mic-unmute-i.svg", + hoverIcon: "icons/tablet-icons/mic-mute-i.svg", + activeIcon: "icons/tablet-icons/mic-mute-a.svg", + activeHoverIcon: "icons/tablet-icons/mic-unmute-a.svg", + text: "MUTE", activeText: "UNMUTE", sortOrder: 1 }); diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 1a3fbab3ea..5283df6127 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -42,6 +42,7 @@ tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ icon: "icons/tablet-icons/goto-i.svg", + activeIcon: "icons/tablet-icons/goto-a.svg", text: buttonName, sortOrder: 8 });