diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index b9cac208b7..2583e15760 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -47,40 +47,54 @@ static const QString AUDIO_THREADING_GROUP_KEY = "audio_threading"; int AudioMixer::_numStaticJitterFrames{ -1 }; float AudioMixer::_noiseMutingThreshold{ DEFAULT_NOISE_MUTING_THRESHOLD }; float AudioMixer::_attenuationPerDoublingInDistance{ DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE }; +std::map> AudioMixer::_availableCodecs{ }; +QStringList AudioMixer::_codecPreferenceOrder{}; QHash AudioMixer::_audioZones; QVector AudioMixer::_zoneSettings; QVector AudioMixer::_zoneReverbSettings; AudioMixer::AudioMixer(ReceivedMessage& message) : ThreadedAssignment(message) { + + // hash the available codecs (on the mixer) + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + std::for_each(codecPlugins.cbegin(), codecPlugins.cend(), + [&](const CodecPluginPointer& codec) { + _availableCodecs[codec->getName()] = codec; + }); + auto nodeList = DependencyManager::get(); auto& packetReceiver = nodeList->getPacketReceiver(); - packetReceiver.registerListenerForTypes({ PacketType::MicrophoneAudioNoEcho, PacketType::MicrophoneAudioWithEcho, - PacketType::InjectAudio, PacketType::AudioStreamStats }, - this, "handleAudioPacket"); - packetReceiver.registerListenerForTypes({ PacketType::SilentAudioFrame }, this, "handleSilentAudioPacket"); - packetReceiver.registerListener(PacketType::NegotiateAudioFormat, this, "handleNegotiateAudioFormat"); + // packets whose consequences are limited to their own node can be parallelized + packetReceiver.registerListenerForTypes({ + PacketType::MicrophoneAudioNoEcho, + PacketType::MicrophoneAudioWithEcho, + PacketType::InjectAudio, + PacketType::AudioStreamStats, + PacketType::SilentAudioFrame, + PacketType::NegotiateAudioFormat, + PacketType::MuteEnvironment, + PacketType::NodeIgnoreRequest, + PacketType::RadiusIgnoreRequest, + PacketType::RequestsDomainListData, + PacketType::PerAvatarGainSet }, + this, "queueAudioPacket"); + + // packets whose consequences are global should be processed on the main thread packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); - packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); - packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); packetReceiver.registerListener(PacketType::NodeMuteRequest, this, "handleNodeMuteRequestPacket"); - packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket"); - packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket"); - packetReceiver.registerListener(PacketType::PerAvatarGainSet, this, "handlePerAvatarGainSetDataPacket"); + packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); } -void AudioMixer::handleAudioPacket(QSharedPointer message, SharedNodePointer sendingNode) { - getOrCreateClientData(sendingNode.data()); - DependencyManager::get()->updateNodeWithDataFromPacket(message, sendingNode); -} +void AudioMixer::queueAudioPacket(QSharedPointer message, SharedNodePointer node) { + if (message->getType() == PacketType::SilentAudioFrame) { + _numSilentPackets++; + } -void AudioMixer::handleSilentAudioPacket(QSharedPointer message, SharedNodePointer sendingNode) { - _numSilentPackets++; - getOrCreateClientData(sendingNode.data()); - DependencyManager::get()->updateNodeWithDataFromPacket(message, sendingNode); + getOrCreateClientData(node.data())->queuePacket(message, node); } void AudioMixer::handleMuteEnvironmentPacket(QSharedPointer message, SharedNodePointer sendingNode) { @@ -119,69 +133,28 @@ InputPluginList getInputPlugins() { return result; } -void saveInputPluginSettings(const InputPluginList& plugins) { -} +// must be here to satisfy a reference in PluginManager::saveSettings() +void saveInputPluginSettings(const InputPluginList& plugins) {} - -void AudioMixer::handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode) { - QStringList availableCodecs; - auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); - if (codecPlugins.size() > 0) { - for (auto& plugin : codecPlugins) { - auto codecName = plugin->getName(); - qDebug() << "Codec available:" << codecName; - availableCodecs.append(codecName); - } - } else { - qDebug() << "No Codecs available..."; - } - - CodecPluginPointer selectedCodec; +const std::pair AudioMixer::negotiateCodec(std::vector codecs) { QString selectedCodecName; + CodecPluginPointer selectedCodec; - QStringList codecPreferenceList = _codecPreferenceOrder.split(","); + // read the codecs requested (by the client) + int minPreference = std::numeric_limits::max(); + for (auto& codec : codecs) { + if (_availableCodecs.count(codec) > 0) { + int preference = _codecPreferenceOrder.indexOf(codec); - // read the codecs requested by the client - const int MAX_PREFERENCE = 99999; - int preferredCodecIndex = MAX_PREFERENCE; - QString preferredCodec; - quint8 numberOfCodecs = 0; - message->readPrimitive(&numberOfCodecs); - qDebug() << "numberOfCodecs:" << numberOfCodecs; - QStringList codecList; - for (quint16 i = 0; i < numberOfCodecs; i++) { - QString requestedCodec = message->readString(); - int preferenceOfThisCodec = codecPreferenceList.indexOf(requestedCodec); - bool codecAvailable = availableCodecs.contains(requestedCodec); - qDebug() << "requestedCodec:" << requestedCodec << "preference:" << preferenceOfThisCodec << "available:" << codecAvailable; - if (codecAvailable) { - codecList.append(requestedCodec); - if (preferenceOfThisCodec >= 0 && preferenceOfThisCodec < preferredCodecIndex) { - qDebug() << "This codec is preferred..."; - selectedCodecName = requestedCodec; - preferredCodecIndex = preferenceOfThisCodec; - } - } - } - qDebug() << "all requested and available codecs:" << codecList; - - // choose first codec - if (!selectedCodecName.isEmpty()) { - if (codecPlugins.size() > 0) { - for (auto& plugin : codecPlugins) { - if (selectedCodecName == plugin->getName()) { - qDebug() << "Selecting codec:" << selectedCodecName; - selectedCodec = plugin; - break; - } + // choose the preferred, available codec + if (preference >= 0 && preference < minPreference) { + minPreference = preference; + selectedCodecName = codec; } } } - auto clientData = getOrCreateClientData(sendingNode.data()); - clientData->setupCodec(selectedCodec, selectedCodecName); - qDebug() << "selectedCodecName:" << selectedCodecName; - clientData->sendSelectAudioFormat(sendingNode, selectedCodecName); + return std::make_pair(selectedCodecName, _availableCodecs[selectedCodecName]); } void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) { @@ -227,42 +200,6 @@ void AudioMixer::handleKillAvatarPacket(QSharedPointer packet, } } -void AudioMixer::handleRequestsDomainListDataPacket(QSharedPointer message, SharedNodePointer senderNode) { - auto nodeList = DependencyManager::get(); - nodeList->getOrCreateLinkedData(senderNode); - - if (senderNode->getLinkedData()) { - AudioMixerClientData* nodeData = dynamic_cast(senderNode->getLinkedData()); - if (nodeData != nullptr) { - bool isRequesting; - message->readPrimitive(&isRequesting); - nodeData->setRequestsDomainListData(isRequesting); - } - } -} - -void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { - sendingNode->parseIgnoreRequestMessage(packet); -} - -void AudioMixer::handlePerAvatarGainSetDataPacket(QSharedPointer packet, SharedNodePointer sendingNode) { - auto clientData = dynamic_cast(sendingNode->getLinkedData()); - if (clientData) { - QUuid listeningNodeUUID = sendingNode->getUUID(); - // parse the UUID from the packet - QUuid audioSourceUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); - uint8_t packedGain; - packet->readPrimitive(&packedGain); - float gain = unpackFloatGainFromByte(packedGain); - clientData->hrtfForStream(audioSourceUUID, QUuid()).setGainAdjustment(gain); - qDebug() << "Setting gain adjustment for hrtf[" << listeningNodeUUID << "][" << audioSourceUUID << "] to " << gain; - } -} - -void AudioMixer::handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { - sendingNode->parseIgnoreRadiusRequestMessage(packet); -} - void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) { auto injectorClientData = qobject_cast(sender()); if (injectorClientData) { @@ -323,6 +260,7 @@ void AudioMixer::sendStatsPacket() { addTiming(_prepareTiming, "prepare"); addTiming(_mixTiming, "mix"); addTiming(_eventsTiming, "events"); + addTiming(_packetsTiming, "packets"); #ifdef HIFI_AUDIO_MIXER_DEBUG timingStats["ns_per_mix"] = (_stats.totalMixes > 0) ? (float)(_stats.mixTime / _stats.totalMixes) : 0; @@ -452,19 +390,27 @@ void AudioMixer::start() { ++frame; ++_numStatFrames; - // play nice with qt event-looping + // process queued events (networking, global audio packets, &c.) { auto eventsTimer = _eventsTiming.timer(); // since we're a while loop we need to yield to qt's event processing QCoreApplication::processEvents(); - if (_isFinished) { - // alert qt eventing that this is finished - QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete); - break; + // process (node-isolated) audio packets across slave threads + { + nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + auto packetsTimer = _packetsTiming.timer(); + _slavePool.processPackets(cbegin, cend); + }); } } + + if (_isFinished) { + // alert qt eventing that this is finished + QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete); + break; + } } } @@ -629,7 +575,8 @@ void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) { const QString CODEC_PREFERENCE_ORDER = "codec_preference_order"; if (audioEnvGroupObject[CODEC_PREFERENCE_ORDER].isString()) { - _codecPreferenceOrder = audioEnvGroupObject[CODEC_PREFERENCE_ORDER].toString(); + QString codecPreferenceOrder = audioEnvGroupObject[CODEC_PREFERENCE_ORDER].toString(); + _codecPreferenceOrder = codecPreferenceOrder.split(","); qDebug() << "Codec preference order changed to" << _codecPreferenceOrder; } diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 07359f4aef..0641c04a6c 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -49,6 +49,7 @@ public: static const QHash& getAudioZones() { return _audioZones; } static const QVector& getZoneSettings() { return _zoneSettings; } static const QVector& getReverbSettings() { return _zoneReverbSettings; } + static const std::pair negotiateCodec(std::vector codecs); public slots: void run() override; @@ -56,20 +57,14 @@ public slots: private slots: // packet handlers - void handleAudioPacket(QSharedPointer packet, SharedNodePointer sendingNode); - void handleSilentAudioPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleMuteEnvironmentPacket(QSharedPointer packet, SharedNodePointer sendingNode); - void handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode); - void handleNodeKilled(SharedNodePointer killedNode); - void handleRequestsDomainListDataPacket(QSharedPointer message, SharedNodePointer senderNode); - void handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); - void handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); - void handleKillAvatarPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleNodeMuteRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); - void handlePerAvatarGainSetDataPacket(QSharedPointer packet, SharedNodePointer sendingNode); + void handleNodeKilled(SharedNodePointer killedNode); + void handleKillAvatarPacket(QSharedPointer packet, SharedNodePointer sendingNode); - void start(); + void queueAudioPacket(QSharedPointer packet, SharedNodePointer sendingNode); void removeHRTFsForFinishedInjector(const QUuid& streamID); + void start(); private: // mixing helpers @@ -93,8 +88,6 @@ private: int _numStatFrames { 0 }; AudioMixerStats _stats; - QString _codecPreferenceOrder; - AudioMixerSlavePool _slavePool; class Timer { @@ -124,13 +117,17 @@ private: Timer _prepareTiming; Timer _mixTiming; Timer _eventsTiming; + Timer _packetsTiming; static int _numStaticJitterFrames; // -1 denotes dynamic jitter buffering static float _noiseMutingThreshold; static float _attenuationPerDoublingInDistance; + static std::map _availableCodecs; + static QStringList _codecPreferenceOrder; static QHash _audioZones; static QVector _zoneSettings; static QVector _zoneReverbSettings; + }; #endif // hifi_AudioMixer_h diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 791ccb8b03..fe43e6f730 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -19,6 +19,7 @@ #include "InjectedAudioStream.h" +#include "AudioHelpers.h" #include "AudioMixer.h" #include "AudioMixerClientData.h" @@ -47,6 +48,92 @@ AudioMixerClientData::~AudioMixerClientData() { } } +void AudioMixerClientData::queuePacket(QSharedPointer message, SharedNodePointer node) { + if (!_packetQueue.node) { + _packetQueue.node = node; + } + _packetQueue.push(message); +} + +void AudioMixerClientData::processPackets() { + SharedNodePointer node = _packetQueue.node; + assert(_packetQueue.empty() || node); + _packetQueue.node.clear(); + + while (!_packetQueue.empty()) { + auto& packet = _packetQueue.back(); + + switch (packet->getType()) { + case PacketType::MicrophoneAudioNoEcho: + case PacketType::MicrophoneAudioWithEcho: + case PacketType::InjectAudio: + case PacketType::AudioStreamStats: + case PacketType::SilentAudioFrame: { + QMutexLocker lock(&getMutex()); + parseData(*packet); + break; + } + case PacketType::NegotiateAudioFormat: + negotiateAudioFormat(*packet, node); + break; + case PacketType::RequestsDomainListData: + parseRequestsDomainListData(*packet); + break; + case PacketType::PerAvatarGainSet: + parsePerAvatarGainSet(*packet, node); + break; + case PacketType::NodeIgnoreRequest: + parseNodeIgnoreRequest(packet, node); + break; + case PacketType::RadiusIgnoreRequest: + parseRadiusIgnoreRequest(packet, node); + break; + default: + Q_UNREACHABLE(); + } + + _packetQueue.pop(); + } + assert(_packetQueue.empty()); +} + +void AudioMixerClientData::negotiateAudioFormat(ReceivedMessage& message, const SharedNodePointer& node) { + quint8 numberOfCodecs; + message.readPrimitive(&numberOfCodecs); + std::vector codecs; + for (auto i = 0; i < numberOfCodecs; i++) { + codecs.push_back(message.readString()); + } + const std::pair codec = AudioMixer::negotiateCodec(codecs); + + setupCodec(codec.second, codec.first); + sendSelectAudioFormat(node, codec.first); +} + +void AudioMixerClientData::parseRequestsDomainListData(ReceivedMessage& message) { + bool isRequesting; + message.readPrimitive(&isRequesting); + setRequestsDomainListData(isRequesting); +} + +void AudioMixerClientData::parsePerAvatarGainSet(ReceivedMessage& message, const SharedNodePointer& node) { + QUuid uuid = node->getUUID(); + // parse the UUID from the packet + QUuid avatarUuid = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + uint8_t packedGain; + message.readPrimitive(&packedGain); + float gain = unpackFloatGainFromByte(packedGain); + hrtfForStream(avatarUuid, QUuid()).setGainAdjustment(gain); + qDebug() << "Setting gain adjustment for hrtf[" << uuid << "][" << avatarUuid << "] to " << gain; +} + +void AudioMixerClientData::parseNodeIgnoreRequest(QSharedPointer message, const SharedNodePointer& node) { + node->parseIgnoreRequestMessage(message); +} + +void AudioMixerClientData::parseRadiusIgnoreRequest(QSharedPointer message, const SharedNodePointer& node) { + node->parseIgnoreRadiusRequestMessage(message); +} AvatarAudioStream* AudioMixerClientData::getAvatarAudioStream() { QReadLocker readLocker { &_streamsLock }; @@ -461,9 +548,6 @@ AudioMixerClientData::IgnoreZone& AudioMixerClientData::IgnoreZoneMemo::get(unsi _zone = box; unsigned int oldFrame = _frame.exchange(frame, std::memory_order_release); Q_UNUSED(oldFrame); - - // check the precondition - assert(oldFrame == 0 || frame == (oldFrame + 1)); } } diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index c30923f411..8d76cda2f1 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -12,6 +12,8 @@ #ifndef hifi_AudioMixerClientData_h #define hifi_AudioMixerClientData_h +#include + #include #include @@ -34,12 +36,15 @@ public: using SharedStreamPointer = std::shared_ptr; using AudioStreamMap = std::unordered_map; + void queuePacket(QSharedPointer packet, SharedNodePointer node); + void processPackets(); + // locks the mutex to make a copy 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 + // precondition: frame is increasing after first call (including overflow wrap) bool shouldIgnore(SharedNodePointer self, SharedNodePointer node, unsigned int frame); // the following methods should be called from the AudioMixer assignment thread ONLY @@ -56,7 +61,13 @@ public: void removeAgentAvatarAudioStream(); + // packet parsers int parseData(ReceivedMessage& message) override; + void negotiateAudioFormat(ReceivedMessage& message, const SharedNodePointer& node); + void parseRequestsDomainListData(ReceivedMessage& message); + void parsePerAvatarGainSet(ReceivedMessage& message, const SharedNodePointer& node); + void parseNodeIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); + void parseRadiusIgnoreRequest(QSharedPointer message, const SharedNodePointer& node); // attempt to pop a frame from each audio stream, and return the number of streams from this client int checkBuffersBeforeFrameSend(); @@ -105,18 +116,22 @@ public slots: void sendSelectAudioFormat(SharedNodePointer node, const QString& selectedCodecName); private: - using IgnoreZone = AABox; + struct PacketQueue : public std::queue> { + QWeakPointer node; + }; + PacketQueue _packetQueue; QReadWriteLock _streamsLock; AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID + using IgnoreZone = AABox; 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 + // - frame is increasing after first call (including overflow wrap) // - there are no references left from calls to getIgnoreZone(frame - 1) IgnoreZone& get(unsigned int frame); diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index 370df60ec5..6b53de89c2 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -53,7 +53,14 @@ inline float computeGain(const AvatarAudioStream& listeningNodeStream, const Pos inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); -void AudioMixerSlave::configure(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) { +void AudioMixerSlave::processPackets(const SharedNodePointer& node) { + AudioMixerClientData* data = (AudioMixerClientData*)node->getLinkedData(); + if (data) { + data->processPackets(); + } +} + +void AudioMixerSlave::configureMix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) { _begin = begin; _end = end; _frame = frame; diff --git a/assignment-client/src/audio/AudioMixerSlave.h b/assignment-client/src/audio/AudioMixerSlave.h index 7b59500629..074d10ff40 100644 --- a/assignment-client/src/audio/AudioMixerSlave.h +++ b/assignment-client/src/audio/AudioMixerSlave.h @@ -30,9 +30,13 @@ class AudioMixerSlave { public: using ConstIter = NodeList::const_iterator; - void configure(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio); + // process packets for a given node (requires no configuration) + void processPackets(const SharedNodePointer& node); - // mix and broadcast non-ignored streams to the node + // configure a round of mixing + void configureMix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio); + + // mix and broadcast non-ignored streams to the node (requires configuration using configureMix, above) // returns true if a mixed packet was sent to the node void mix(const SharedNodePointer& node); diff --git a/assignment-client/src/audio/AudioMixerSlavePool.cpp b/assignment-client/src/audio/AudioMixerSlavePool.cpp index 9b20572b84..643361ac5d 100644 --- a/assignment-client/src/audio/AudioMixerSlavePool.cpp +++ b/assignment-client/src/audio/AudioMixerSlavePool.cpp @@ -21,7 +21,7 @@ void AudioMixerSlaveThread::run() { // iterate over all available nodes SharedNodePointer node; while (try_pop(node)) { - mix(node); + (this->*_function)(node); } bool stopping = _stop; @@ -41,7 +41,11 @@ void AudioMixerSlaveThread::wait() { }); ++_pool._numStarted; } - configure(_pool._begin, _pool._end, _pool._frame, _pool._throttlingRatio); + + if (_pool._configure) { + _pool._configure(*this); + } + _function = _pool._function; } void AudioMixerSlaveThread::notify(bool stopping) { @@ -64,16 +68,31 @@ bool AudioMixerSlaveThread::try_pop(SharedNodePointer& node) { static AudioMixerSlave slave; #endif +void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) { + _function = &AudioMixerSlave::processPackets; + _configure = [](AudioMixerSlave& slave) {}; + run(begin, end); +} + void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) { - _begin = begin; - _end = end; + _function = &AudioMixerSlave::mix; + _configure = [&](AudioMixerSlave& slave) { + slave.configureMix(_begin, _end, _frame, _throttlingRatio); + }; _frame = frame; _throttlingRatio = throttlingRatio; + run(begin, end); +} + +void AudioMixerSlavePool::run(ConstIter begin, ConstIter end) { + _begin = begin; + _end = end; + #ifdef AUDIO_SINGLE_THREADED - slave.configure(_begin, _end, frame, throttlingRatio); + _configure(slave); std::for_each(begin, end, [&](const SharedNodePointer& node) { - slave.mix(node); + _function(slave, node); }); #else // fill the queue @@ -84,7 +103,7 @@ void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame { Lock lock(_mutex); - // mix + // run _numStarted = _numFinished = 0; _slaveCondition.notify_all(); diff --git a/assignment-client/src/audio/AudioMixerSlavePool.h b/assignment-client/src/audio/AudioMixerSlavePool.h index 19d2315d12..4082ea18d9 100644 --- a/assignment-client/src/audio/AudioMixerSlavePool.h +++ b/assignment-client/src/audio/AudioMixerSlavePool.h @@ -43,6 +43,7 @@ private: bool try_pop(SharedNodePointer& node); AudioMixerSlavePool& _pool; + void (AudioMixerSlave::*_function)(const SharedNodePointer& node) { nullptr }; bool _stop { false }; }; @@ -60,6 +61,9 @@ public: AudioMixerSlavePool(int numThreads = QThread::idealThreadCount()) { setNumThreads(numThreads); } ~AudioMixerSlavePool() { resize(0); } + // process packets on slave threads + void processPackets(ConstIter begin, ConstIter end); + // mix on slave threads void mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio); @@ -70,6 +74,7 @@ public: int numThreads() { return _numThreads; } private: + void run(ConstIter begin, ConstIter end); void resize(int numThreads); std::vector> _slaves; @@ -82,6 +87,8 @@ private: Mutex _mutex; ConditionVariable _slaveCondition; ConditionVariable _poolCondition; + void (AudioMixerSlave::*_function)(const SharedNodePointer& node); + std::function _configure; int _numThreads { 0 }; int _numStarted { 0 }; // guarded by _mutex int _numFinished { 0 }; // guarded by _mutex diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index 02dc552dae..425bea2c38 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "EntityServer.h" #include "EntityServerConsts.h" @@ -71,6 +72,7 @@ OctreePointer EntityServer::createTree() { DependencyManager::registerInheritance(); DependencyManager::set(tree); + DependencyManager::set(std::static_pointer_cast(tree)); return tree; } @@ -292,96 +294,26 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio } else { tree->setEntityScriptSourceWhitelist(""); } - - if (readOptionString("entityEditFilter", settingsSectionObject, _entityEditFilter) && !_entityEditFilter.isEmpty()) { - // Tell the tree that we have a filter, so that it doesn't accept edits until we have a filter function set up. - std::static_pointer_cast(_tree)->setHasEntityFilter(true); - // Now fetch script from file asynchronously. - QUrl scriptURL(_entityEditFilter); - - // The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp) - if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == URL_SCHEME_FILE)) { - qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer."; - scriptRequestFinished(); - return; - } - auto scriptRequest = ResourceManager::createResourceRequest(this, scriptURL); - if (!scriptRequest) { - qWarning() << "Could not create ResourceRequest for Agent script at" << scriptURL.toString(); - scriptRequestFinished(); - return; - } - // Agent.cpp sets up a timeout here, but that is unnecessary, as ResourceRequest has its own. - connect(scriptRequest, &ResourceRequest::finished, this, &EntityServer::scriptRequestFinished); - // FIXME: handle atp rquests setup here. See Agent::requestScript() - qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString()); - scriptRequest->send(); - qDebug() << "script request sent"; + + auto entityEditFilters = DependencyManager::get(); + + QString filterURL; + if (readOptionString("entityEditFilter", settingsSectionObject, filterURL) && !filterURL.isEmpty()) { + // connect the filterAdded signal, and block edits until you hear back + connect(entityEditFilters.data(), &EntityEditFilters::filterAdded, this, &EntityServer::entityFilterAdded); + + entityEditFilters->addFilter(EntityItemID(), filterURL); } } -// Copied from ScriptEngine.cpp. We should make this a class method for reuse. -// Note: I've deliberately stopped short of using ScriptEngine instead of QScriptEngine, as that is out of project scope at this point. -static bool hasCorrectSyntax(const QScriptProgram& program) { - const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode()); - if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { - const auto error = syntaxCheck.errorMessage(); - const auto line = QString::number(syntaxCheck.errorLineNumber()); - const auto column = QString::number(syntaxCheck.errorColumnNumber()); - const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column); - qCritical() << qPrintable(message); - return false; - } - return true; -} -static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) { - if (engine.hasUncaughtException()) { - const auto backtrace = engine.uncaughtExceptionBacktrace(); - const auto exception = engine.uncaughtException().toString(); - const auto line = QString::number(engine.uncaughtExceptionLineNumber()); - engine.clearExceptions(); - - static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3"; - auto message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line); - if (!backtrace.empty()) { - static const auto lineSeparator = "\n "; - message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); +void EntityServer::entityFilterAdded(EntityItemID id, bool success) { + if (id.isInvalidID()) { + if (success) { + qDebug() << "entity edit filter for " << id << "added successfully"; + } else { + qDebug() << "entity edit filter unsuccessfully added, all edits will be rejected for those without lock rights."; } - qCritical() << qPrintable(message); - return true; } - return false; -} -void EntityServer::scriptRequestFinished() { - qDebug() << "script request completed"; - auto scriptRequest = qobject_cast(sender()); - const QString urlString = scriptRequest->getUrl().toString(); - if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) { - auto scriptContents = scriptRequest->getData(); - qInfo() << "Downloaded script:" << scriptContents; - QScriptProgram program(scriptContents, urlString); - if (hasCorrectSyntax(program)) { - _entityEditFilterEngine.evaluate(scriptContents); - if (!hadUncaughtExceptions(_entityEditFilterEngine, urlString)) { - std::static_pointer_cast(_tree)->initEntityEditFilterEngine(&_entityEditFilterEngine, [this]() { - return hadUncaughtExceptions(_entityEditFilterEngine, _entityEditFilter); - }); - scriptRequest->deleteLater(); - qDebug() << "script request filter processed"; - return; - } - } - } else if (scriptRequest) { - qCritical() << "Failed to download script at" << urlString; - // See HTTPResourceRequest::onRequestFinished for interpretation of codes. For example, a 404 is code 6 and 403 is 3. A timeout is 2. Go figure. - qCritical() << "ResourceRequest error was" << scriptRequest->getResult(); - } else { - qCritical() << "Failed to create script request."; - } - // Hard stop of the assignment client on failure. We don't want anyone to think they have a filter in place when they don't. - // Alas, only indications will be the above logging with assignment client restarting repeatedly, and clients will not see any entities. - qDebug() << "script request failure causing stop"; - stop(); } void EntityServer::nodeAdded(SharedNodePointer node) { diff --git a/assignment-client/src/entities/EntityServer.h b/assignment-client/src/entities/EntityServer.h index f142145d5f..325435fe7e 100644 --- a/assignment-client/src/entities/EntityServer.h +++ b/assignment-client/src/entities/EntityServer.h @@ -63,13 +63,13 @@ public slots: virtual void nodeAdded(SharedNodePointer node) override; virtual void nodeKilled(SharedNodePointer node) override; void pruneDeletedEntities(); + void entityFilterAdded(EntityItemID id, bool success); protected: virtual OctreePointer createTree() override; private slots: void handleEntityPacket(QSharedPointer message, SharedNodePointer senderNode); - void scriptRequestFinished(); private: SimpleEntitySimulationPointer _entitySimulation; @@ -77,9 +77,6 @@ private: QReadWriteLock _viewerSendingStatsLock; QMap> _viewerSendingStats; - - QString _entityEditFilter{}; - QScriptEngine _entityEditFilterEngine{}; }; #endif // hifi_EntityServer_h diff --git a/domain-server/CMakeLists.txt b/domain-server/CMakeLists.txt index 746e599d4e..2ce537a5a0 100644 --- a/domain-server/CMakeLists.txt +++ b/domain-server/CMakeLists.txt @@ -20,7 +20,7 @@ endif () symlink_or_copy_directory_beside_target(${_SHOULD_SYMLINK_RESOURCES} "${CMAKE_CURRENT_SOURCE_DIR}/resources" "resources") # link the shared hifi libraries -link_hifi_libraries(embedded-webserver networking shared) +link_hifi_libraries(embedded-webserver networking shared avatars) # find OpenSSL find_package(OpenSSL REQUIRED) diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 379f812923..23f663817d 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -29,7 +29,7 @@ #include #include #include - +#include //for KillAvatarReason #include "DomainServerNodeData.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; @@ -299,7 +299,14 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList } QVariantMap& DomainServerSettingsManager::getDescriptorsMap() { + static const QString DESCRIPTORS{ "descriptors" }; + + auto& settingsMap = getSettingsMap(); + if (!getSettingsMap().contains(DESCRIPTORS)) { + settingsMap.insert(DESCRIPTORS, QVariantMap()); + } + return *static_cast(getSettingsMap()[DESCRIPTORS].data()); } @@ -721,6 +728,13 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointerwrite(nodeUUID.toRfc4122()); + packet->writePrimitive(KillAvatarReason::NoReason); + + // send to avatar mixer, it sends the kill to everyone else + limitedNodeList->broadcastToNodes(std::move(packet), NodeSet() << NodeType::AvatarMixer); if (newPermissions) { qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID()) @@ -728,9 +742,12 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointergetJointTransform(cluster.jointIndex); -#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MACOS) +#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MAC) glm::mat4 out, inverseBindMatrix = cluster.inverseBindMatrix; glm_mat4_mul((glm_vec4*)&jointMatrix, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out); state.clusterMatrices[j] = out; @@ -155,7 +155,7 @@ void CauterizedModel::updateClusterMatrices() { if (_cauterizeBoneSet.find(cluster.jointIndex) != _cauterizeBoneSet.end()) { jointMatrix = cauterizeMatrix; } -#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MACOS) +#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MAC) glm::mat4 out, inverseBindMatrix = cluster.inverseBindMatrix; glm_mat4_mul((glm_vec4*)&jointMatrix, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out); state.clusterMatrices[j] = out; diff --git a/interface/src/avatar/SoftAttachmentModel.cpp b/interface/src/avatar/SoftAttachmentModel.cpp index da7ca0b87d..6ed54afb27 100644 --- a/interface/src/avatar/SoftAttachmentModel.cpp +++ b/interface/src/avatar/SoftAttachmentModel.cpp @@ -60,7 +60,7 @@ void SoftAttachmentModel::updateClusterMatrices() { } else { jointMatrix = _rig->getJointTransform(cluster.jointIndex); } -#if GLM_ARCH & GLM_ARCH_SSE2 +#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MAC) glm::mat4 out, inverseBindMatrix = cluster.inverseBindMatrix; glm_mat4_mul((glm_vec4*)&jointMatrix, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out); state.clusterMatrices[j] = out; diff --git a/interface/src/ui/BaseLogDialog.cpp b/interface/src/ui/BaseLogDialog.cpp index 47cea9c26a..7e0027e0a8 100644 --- a/interface/src/ui/BaseLogDialog.cpp +++ b/interface/src/ui/BaseLogDialog.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -22,9 +23,10 @@ const int TOP_BAR_HEIGHT = 46; const int INITIAL_WIDTH = 720; const int INITIAL_HEIGHT = 480; -const int MINIMAL_WIDTH = 570; +const int MINIMAL_WIDTH = 700; const int SEARCH_BUTTON_LEFT = 25; const int SEARCH_BUTTON_WIDTH = 20; +const int SEARCH_TOGGLE_BUTTON_WIDTH = 50; const int SEARCH_TEXT_WIDTH = 240; const QColor HIGHLIGHT_COLOR = QColor("#3366CC"); @@ -75,14 +77,32 @@ void BaseLogDialog::initControls() { // disable blue outline in Mac _searchTextBox->setAttribute(Qt::WA_MacShowFocusRect, false); _searchTextBox->setGeometry(_leftPad, ELEMENT_MARGIN, SEARCH_TEXT_WIDTH, ELEMENT_HEIGHT); - _leftPad += SEARCH_TEXT_WIDTH + CHECKBOX_MARGIN; + _leftPad += SEARCH_TEXT_WIDTH + BUTTON_MARGIN; _searchTextBox->show(); connect(_searchTextBox, SIGNAL(textChanged(QString)), SLOT(handleSearchTextChanged(QString))); + connect(_searchTextBox, SIGNAL(returnPressed()), SLOT(toggleSearchNext())); + + _searchPrevButton = new QPushButton(this); + _searchPrevButton->setObjectName("searchPrevButton"); + _searchPrevButton->setGeometry(_leftPad, ELEMENT_MARGIN, SEARCH_TOGGLE_BUTTON_WIDTH, ELEMENT_HEIGHT); + _searchPrevButton->setText("Prev"); + _leftPad += SEARCH_TOGGLE_BUTTON_WIDTH + BUTTON_MARGIN; + _searchPrevButton->show(); + connect(_searchPrevButton, SIGNAL(clicked()), SLOT(toggleSearchPrev())); + + _searchNextButton = new QPushButton(this); + _searchNextButton->setObjectName("searchNextButton"); + _searchNextButton->setGeometry(_leftPad, ELEMENT_MARGIN, SEARCH_TOGGLE_BUTTON_WIDTH, ELEMENT_HEIGHT); + _searchNextButton->setText("Next"); + _leftPad += SEARCH_TOGGLE_BUTTON_WIDTH + CHECKBOX_MARGIN; + _searchNextButton->show(); + connect(_searchNextButton, SIGNAL(clicked()), SLOT(toggleSearchNext())); _logTextBox = new QPlainTextEdit(this); _logTextBox->setReadOnly(true); _logTextBox->show(); _highlighter = new KeywordHighlighter(_logTextBox->document()); + connect(_logTextBox, SIGNAL(selectionChanged()), SLOT(updateSelection())); } @@ -105,17 +125,68 @@ void BaseLogDialog::handleSearchButton() { } void BaseLogDialog::handleSearchTextChanged(QString searchText) { + if (searchText.isEmpty()) { + return; + } + + QTextCursor cursor = _logTextBox->textCursor(); + if (cursor.hasSelection()) { + QString selectedTerm = cursor.selectedText(); + if (selectedTerm == searchText) { + return; + } + } + + cursor.setPosition(0, QTextCursor::MoveAnchor); + _logTextBox->setTextCursor(cursor); + bool foundTerm = _logTextBox->find(searchText); + + if (!foundTerm) { + cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); + _logTextBox->setTextCursor(cursor); + } + _searchTerm = searchText; _highlighter->keyword = searchText; _highlighter->rehighlight(); } +void BaseLogDialog::toggleSearchPrev() { + QTextCursor searchCursor = _logTextBox->textCursor(); + if (searchCursor.hasSelection()) { + QString selectedTerm = searchCursor.selectedText(); + _logTextBox->find(selectedTerm, QTextDocument::FindBackward); + } else { + handleSearchTextChanged(_searchTextBox->text()); + } +} + +void BaseLogDialog::toggleSearchNext() { + QTextCursor searchCursor = _logTextBox->textCursor(); + if (searchCursor.hasSelection()) { + QString selectedTerm = searchCursor.selectedText(); + _logTextBox->find(selectedTerm); + } else { + handleSearchTextChanged(_searchTextBox->text()); + } +} + void BaseLogDialog::showLogData() { _logTextBox->clear(); _logTextBox->appendPlainText(getCurrentLog()); _logTextBox->ensureCursorVisible(); } +void BaseLogDialog::updateSelection() { + QTextCursor cursor = _logTextBox->textCursor(); + if (cursor.hasSelection()) { + QString selectionTerm = cursor.selectedText(); + if (QString::compare(selectionTerm, _searchTextBox->text(), Qt::CaseInsensitive) != 0) { + _searchTextBox->setText(selectionTerm); + } + } +} + KeywordHighlighter::KeywordHighlighter(QTextDocument* parent) : QSyntaxHighlighter(parent) { keywordFormat.setForeground(HIGHLIGHT_COLOR); } diff --git a/interface/src/ui/BaseLogDialog.h b/interface/src/ui/BaseLogDialog.h index 72fe83cd82..d097010bae 100644 --- a/interface/src/ui/BaseLogDialog.h +++ b/interface/src/ui/BaseLogDialog.h @@ -18,6 +18,7 @@ const int ELEMENT_MARGIN = 7; const int ELEMENT_HEIGHT = 32; const int CHECKBOX_MARGIN = 12; const int CHECKBOX_WIDTH = 140; +const int BUTTON_MARGIN = 8; class QPushButton; class QLineEdit; @@ -35,8 +36,11 @@ public slots: void appendLogLine(QString logLine); private slots: + void updateSelection(); void handleSearchButton(); void handleSearchTextChanged(QString text); + void toggleSearchPrev(); + void toggleSearchNext(); protected: int _leftPad { 0 }; @@ -49,6 +53,8 @@ private: QPushButton* _searchButton { nullptr }; QLineEdit* _searchTextBox { nullptr }; QPlainTextEdit* _logTextBox { nullptr }; + QPushButton* _searchPrevButton { nullptr }; + QPushButton* _searchNextButton { nullptr }; QString _searchTerm; KeywordHighlighter* _highlighter { nullptr }; diff --git a/libraries/animation/src/AnimPose.cpp b/libraries/animation/src/AnimPose.cpp index ba97f70518..5638cacabc 100644 --- a/libraries/animation/src/AnimPose.cpp +++ b/libraries/animation/src/AnimPose.cpp @@ -50,7 +50,7 @@ glm::vec3 AnimPose::xformVector(const glm::vec3& rhs) const { } AnimPose AnimPose::operator*(const AnimPose& rhs) const { -#if GLM_ARCH & GLM_ARCH_SSE2 +#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MAC) glm::mat4 result; glm::mat4 lhsMat = *this; glm::mat4 rhsMat = rhs; diff --git a/libraries/entities/src/EntityEditFilters.cpp b/libraries/entities/src/EntityEditFilters.cpp new file mode 100644 index 0000000000..d62495d95e --- /dev/null +++ b/libraries/entities/src/EntityEditFilters.cpp @@ -0,0 +1,237 @@ +// +// EntityEditFilters.cpp +// libraries/entities/src +// +// Created by David Kelly on 2/7/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 +// + + +#include + +#include +#include "EntityEditFilters.h" + +QList EntityEditFilters::getZonesByPosition(glm::vec3& position) { + QList zones; + QList missingZones; + _lock.lockForRead(); + auto zoneIDs = _filterDataMap.keys(); + _lock.unlock(); + for (auto id : zoneIDs) { + if (!id.isInvalidID()) { + // for now, look it up in the tree (soon we need to cache or similar?) + EntityItemPointer itemPtr = _tree->findEntityByEntityItemID(id); + auto zone = std::dynamic_pointer_cast(itemPtr); + if (!zone) { + // TODO: maybe remove later? + removeFilter(id); + } else if (zone->contains(position)) { + zones.append(id); + } + } else { + // the null id is the global filter we put in the domain server's + // advanced entity server settings + zones.append(id); + } + } + return zones; +} + +bool EntityEditFilters::filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, + EntityTree::FilterType filterType, EntityItemID& itemID) { + + // get the ids of all the zones (plus the global entity edit filter) that the position + // lies within + auto zoneIDs = getZonesByPosition(position); + for (auto id : zoneIDs) { + if (!itemID.isInvalidID() && id == itemID) { + continue; + } + + // get the filter pair, etc... + _lock.lockForRead(); + FilterData filterData = _filterDataMap.value(id); + _lock.unlock(); + + if (filterData.valid()) { + if (filterData.rejectAll) { + return false; + } + auto oldProperties = propertiesIn.getDesiredProperties(); + auto specifiedProperties = propertiesIn.getChangedProperties(); + propertiesIn.setDesiredProperties(specifiedProperties); + QScriptValue inputValues = propertiesIn.copyToScriptValue(filterData.engine, false, true, true); + propertiesIn.setDesiredProperties(oldProperties); + + auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter. + QScriptValueList args; + args << inputValues; + args << filterType; + + QScriptValue result = filterData.filterFn.call(_nullObjectForFilter, args); + if (filterData.uncaughtExceptions()) { + return false; + } + + if (result.isObject()){ + // make propertiesIn reflect the changes, for next filter... + propertiesIn.copyFromScriptValue(result, false); + + // and update propertiesOut too. TODO: this could be more efficient... + propertiesOut.copyFromScriptValue(result, false); + // Javascript objects are == only if they are the same object. To compare arbitrary values, we need to use JSON. + auto out = QJsonValue::fromVariant(result.toVariant()); + wasChanged |= (in != out); + } else { + return false; + } + } + } + // if we made it here, + return true; +} + +void EntityEditFilters::removeFilter(EntityItemID entityID) { + QWriteLocker writeLock(&_lock); + FilterData filterData = _filterDataMap.value(entityID); + if (filterData.valid()) { + delete filterData.engine; + } + _filterDataMap.remove(entityID); +} + +void EntityEditFilters::addFilter(EntityItemID entityID, QString filterURL) { + + QUrl scriptURL(filterURL); + + // setting it to an empty string is same as removing + if (filterURL.size() == 0) { + removeFilter(entityID); + return; + } + + // The following should be abstracted out for use in Agent.cpp (and maybe later AvatarMixer.cpp) + if (scriptURL.scheme().isEmpty() || (scriptURL.scheme() == URL_SCHEME_FILE)) { + qWarning() << "Cannot load script from local filesystem, because assignment may be on a different computer."; + scriptRequestFinished(entityID); + return; + } + + // first remove any existing info for this entity + removeFilter(entityID); + + // reject all edits until we load the script + FilterData filterData; + filterData.rejectAll = true; + + _lock.lockForWrite(); + _filterDataMap.insert(entityID, filterData); + _lock.unlock(); + + auto scriptRequest = ResourceManager::createResourceRequest(this, scriptURL); + if (!scriptRequest) { + qWarning() << "Could not create ResourceRequest for Entity Edit filter script at" << scriptURL.toString(); + scriptRequestFinished(entityID); + return; + } + // Agent.cpp sets up a timeout here, but that is unnecessary, as ResourceRequest has its own. + connect(scriptRequest, &ResourceRequest::finished, this, [this, entityID]{ EntityEditFilters::scriptRequestFinished(entityID);} ); + // FIXME: handle atp rquests setup here. See Agent::requestScript() + qInfo() << "Requesting script at URL" << qPrintable(scriptRequest->getUrl().toString()); + scriptRequest->send(); + qDebug() << "script request sent for entity " << entityID; +} + +// Copied from ScriptEngine.cpp. We should make this a class method for reuse. +// Note: I've deliberately stopped short of using ScriptEngine instead of QScriptEngine, as that is out of project scope at this point. +static bool hasCorrectSyntax(const QScriptProgram& program) { + const auto syntaxCheck = QScriptEngine::checkSyntax(program.sourceCode()); + if (syntaxCheck.state() != QScriptSyntaxCheckResult::Valid) { + const auto error = syntaxCheck.errorMessage(); + const auto line = QString::number(syntaxCheck.errorLineNumber()); + const auto column = QString::number(syntaxCheck.errorColumnNumber()); + const auto message = QString("[SyntaxError] %1 in %2:%3(%4)").arg(error, program.fileName(), line, column); + qCritical() << qPrintable(message); + return false; + } + return true; +} +static bool hadUncaughtExceptions(QScriptEngine& engine, const QString& fileName) { + if (engine.hasUncaughtException()) { + const auto backtrace = engine.uncaughtExceptionBacktrace(); + const auto exception = engine.uncaughtException().toString(); + const auto line = QString::number(engine.uncaughtExceptionLineNumber()); + engine.clearExceptions(); + + static const QString SCRIPT_EXCEPTION_FORMAT = "[UncaughtException] %1 in %2:%3"; + auto message = QString(SCRIPT_EXCEPTION_FORMAT).arg(exception, fileName, line); + if (!backtrace.empty()) { + static const auto lineSeparator = "\n "; + message += QString("\n[Backtrace]%1%2").arg(lineSeparator, backtrace.join(lineSeparator)); + } + qCritical() << qPrintable(message); + return true; + } + return false; +} + +void EntityEditFilters::scriptRequestFinished(EntityItemID entityID) { + qDebug() << "script request completed for entity " << entityID; + auto scriptRequest = qobject_cast(sender()); + const QString urlString = scriptRequest->getUrl().toString(); + if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) { + auto scriptContents = scriptRequest->getData(); + qInfo() << "Downloaded script:" << scriptContents; + QScriptProgram program(scriptContents, urlString); + if (hasCorrectSyntax(program)) { + // create a QScriptEngine for this script + QScriptEngine* engine = new QScriptEngine(); + engine->evaluate(scriptContents); + if (!hadUncaughtExceptions(*engine, urlString)) { + // put the engine in the engine map (so we don't leak them, etc...) + FilterData filterData; + filterData.engine = engine; + filterData.rejectAll = false; + + // define the uncaughtException function + QScriptEngine& engineRef = *engine; + filterData.uncaughtExceptions = [this, &engineRef, urlString]() { return hadUncaughtExceptions(engineRef, urlString); }; + + // now get the filter function + auto global = engine->globalObject(); + auto entitiesObject = engine->newObject(); + entitiesObject.setProperty("ADD_FILTER_TYPE", EntityTree::FilterType::Add); + entitiesObject.setProperty("EDIT_FILTER_TYPE", EntityTree::FilterType::Edit); + entitiesObject.setProperty("PHYSICS_FILTER_TYPE", EntityTree::FilterType::Physics); + global.setProperty("Entities", entitiesObject); + filterData.filterFn = global.property("filter"); + if (!filterData.filterFn.isFunction()) { + qDebug() << "Filter function specified but not found. Will reject all edits for those without lock rights."; + delete engine; + filterData.rejectAll=true; + } + + + _lock.lockForWrite(); + _filterDataMap.insert(entityID, filterData); + _lock.unlock(); + + qDebug() << "script request filter processed for entity id " << entityID; + + emit filterAdded(entityID, true); + return; + } + } + } else if (scriptRequest) { + qCritical() << "Failed to download script at" << urlString; + // See HTTPResourceRequest::onRequestFinished for interpretation of codes. For example, a 404 is code 6 and 403 is 3. A timeout is 2. Go figure. + qCritical() << "ResourceRequest error was" << scriptRequest->getResult(); + } else { + qCritical() << "Failed to create script request."; + } + emit filterAdded(entityID, false); +} diff --git a/libraries/entities/src/EntityEditFilters.h b/libraries/entities/src/EntityEditFilters.h new file mode 100644 index 0000000000..6aeb347603 --- /dev/null +++ b/libraries/entities/src/EntityEditFilters.h @@ -0,0 +1,65 @@ +// +// EntityEditFilters.h +// libraries/entities/src +// +// Created by David Kelly on 2/7/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 +// +#ifndef hifi_EntityEditFilters_h +#define hifi_EntityEditFilters_h + +#include +#include +#include +#include +#include + +#include + +#include "EntityItemID.h" +#include "EntityItemProperties.h" +#include "EntityTree.h" + +class EntityEditFilters : public QObject, public Dependency { + Q_OBJECT +public: + struct FilterData { + QScriptValue filterFn; + std::function uncaughtExceptions; + QScriptEngine* engine; + bool rejectAll; + + FilterData(): engine(nullptr), rejectAll(false) {}; + bool valid() { return (rejectAll || (engine != nullptr && filterFn.isFunction() && uncaughtExceptions)); } + }; + + EntityEditFilters() {}; + EntityEditFilters(EntityTreePointer tree ): _tree(tree) {}; + + void addFilter(EntityItemID entityID, QString filterURL); + void removeFilter(EntityItemID entityID); + + bool filter(glm::vec3& position, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, + EntityTree::FilterType filterType, EntityItemID& entityID); + +signals: + void filterAdded(EntityItemID id, bool success); + +private slots: + void scriptRequestFinished(EntityItemID entityID); + +private: + QList getZonesByPosition(glm::vec3& position); + + EntityTreePointer _tree {}; + bool _rejectAll {false}; + QScriptValue _nullObjectForFilter{}; + + QReadWriteLock _lock; + QMap _filterDataMap; +}; + +#endif //hifi_EntityEditFilters_h diff --git a/libraries/entities/src/EntityItemID.cpp b/libraries/entities/src/EntityItemID.cpp index 1462a4ef88..5f07019db4 100644 --- a/libraries/entities/src/EntityItemID.cpp +++ b/libraries/entities/src/EntityItemID.cpp @@ -19,6 +19,7 @@ #include "RegisteredMetaTypes.h" #include "EntityItemID.h" +int entityItemIDTypeID = qRegisterMetaType(); EntityItemID::EntityItemID() : QUuid() { diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 21018d8afa..ea81df3801 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -332,6 +332,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_FLYING_ALLOWED, flyingAllowed); CHECK_PROPERTY_CHANGE(PROP_GHOSTING_ALLOWED, ghostingAllowed); + CHECK_PROPERTY_CHANGE(PROP_FILTER_URL, filterURL); CHECK_PROPERTY_CHANGE(PROP_CLIENT_ONLY, clientOnly); CHECK_PROPERTY_CHANGE(PROP_OWNING_AVATAR_ID, owningAvatarID); @@ -509,6 +510,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FLYING_ALLOWED, flyingAllowed); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_GHOSTING_ALLOWED, ghostingAllowed); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_FILTER_URL, filterURL); } // Web only @@ -751,6 +753,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(flyingAllowed, bool, setFlyingAllowed); COPY_PROPERTY_FROM_QSCRIPTVALUE(ghostingAllowed, bool, setGhostingAllowed); + COPY_PROPERTY_FROM_QSCRIPTVALUE(filterURL, QString, setFilterURL); COPY_PROPERTY_FROM_QSCRIPTVALUE(clientOnly, bool, setClientOnly); COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID); @@ -879,6 +882,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(flyingAllowed); COPY_PROPERTY_IF_CHANGED(ghostingAllowed); + COPY_PROPERTY_IF_CHANGED(filterURL); COPY_PROPERTY_IF_CHANGED(clientOnly); COPY_PROPERTY_IF_CHANGED(owningAvatarID); @@ -1063,6 +1067,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool); ADD_PROPERTY_TO_MAP(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool); + ADD_PROPERTY_TO_MAP(PROP_FILTER_URL, FilterURL, filterURL, QString); ADD_PROPERTY_TO_MAP(PROP_DPI, DPI, dpi, uint16_t); @@ -1311,6 +1316,7 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem APPEND_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, properties.getFlyingAllowed()); APPEND_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, properties.getGhostingAllowed()); + APPEND_ENTITY_PROPERTY(PROP_FILTER_URL, properties.getFilterURL()); } if (properties.getType() == EntityTypes::PolyVox) { @@ -1605,6 +1611,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FLYING_ALLOWED, bool, setFlyingAllowed); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_GHOSTING_ALLOWED, bool, setGhostingAllowed); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_FILTER_URL, QString, setFilterURL); } if (properties.getType() == EntityTypes::PolyVox) { @@ -1808,6 +1815,7 @@ void EntityItemProperties::markAllChanged() { _flyingAllowedChanged = true; _ghostingAllowedChanged = true; + _filterURLChanged = true; _clientOnlyChanged = true; _owningAvatarIDChanged = true; @@ -2150,7 +2158,9 @@ QList EntityItemProperties::listChangedProperties() { if (ghostingAllowedChanged()) { out += "ghostingAllowed"; } - + if (filterURLChanged()) { + out += "filterURL"; + } if (dpiChanged()) { out += "dpi"; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 1961feaf1d..419740e4ea 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -215,6 +215,7 @@ public: DEFINE_PROPERTY(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool, ZoneEntityItem::DEFAULT_FLYING_ALLOWED); DEFINE_PROPERTY(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool, ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED); + DEFINE_PROPERTY(PROP_FILTER_URL, FilterURL, filterURL, QString, ZoneEntityItem::DEFAULT_FILTER_URL); DEFINE_PROPERTY(PROP_CLIENT_ONLY, ClientOnly, clientOnly, bool, false); DEFINE_PROPERTY_REF(PROP_OWNING_AVATAR_ID, OwningAvatarID, owningAvatarID, QUuid, UNKNOWN_ENTITY_ID); @@ -458,6 +459,7 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, FlyingAllowed, flyingAllowed, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, GhostingAllowed, ghostingAllowed, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, FilterURL, filterURL, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, ClientOnly, clientOnly, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, OwningAvatarID, owningAvatarID, ""); diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index b77d3cc077..b3cfc143c2 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -185,6 +185,8 @@ enum EntityPropertyList { PROP_SERVER_SCRIPTS, + PROP_FILTER_URL, + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 427f6b4af0..d7471474a6 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -24,6 +24,7 @@ #include "EntitiesLogging.h" #include "RecurseOctreeToMapOperator.h" #include "LogHandler.h" +#include "EntityEditFilters.h" static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50; const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour @@ -923,55 +924,14 @@ void EntityTree::fixupTerseEditLogging(EntityItemProperties& properties, QList entityEditFilterHadUncaughtExceptions) { - _entityEditFilterEngine = engine; - _entityEditFilterHadUncaughtExceptions = entityEditFilterHadUncaughtExceptions; - auto global = _entityEditFilterEngine->globalObject(); - _entityEditFilterFunction = global.property("filter"); - if (!_entityEditFilterFunction.isFunction()) { - qCDebug(entities) << "Filter function specified but not found. Will reject all edits."; - _entityEditFilterEngine = nullptr; // So that we don't try to call it. See filterProperties. - } - auto entitiesObject = _entityEditFilterEngine->newObject(); - entitiesObject.setProperty("ADD_FILTER_TYPE", FilterType::Add); - entitiesObject.setProperty("EDIT_FILTER_TYPE", FilterType::Edit); - entitiesObject.setProperty("PHYSICS_FILTER_TYPE", FilterType::Physics); - global.setProperty("Entities", entitiesObject); - _hasEntityEditFilter = true; -} -bool EntityTree::filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType) { - if (!_entityEditFilterEngine) { - propertiesOut = propertiesIn; - wasChanged = false; // not changed - if (_hasEntityEditFilter) { - qCDebug(entities) << "Rejecting properties because filter has not been set."; - return false; - } - return true; // allowed - } - auto oldProperties = propertiesIn.getDesiredProperties(); - auto specifiedProperties = propertiesIn.getChangedProperties(); - propertiesIn.setDesiredProperties(specifiedProperties); - QScriptValue inputValues = propertiesIn.copyToScriptValue(_entityEditFilterEngine, false, true, true); - propertiesIn.setDesiredProperties(oldProperties); - - auto in = QJsonValue::fromVariant(inputValues.toVariant()); // grab json copy now, because the inputValues might be side effected by the filter. - QScriptValueList args; - args << inputValues; - args << filterType; - - QScriptValue result = _entityEditFilterFunction.call(_nullObjectForFilter, args); - if (_entityEditFilterHadUncaughtExceptions()) { - result = QScriptValue(); - } - - bool accepted = result.isObject(); // filters should return null or false to completely reject edit or add - if (accepted) { - propertiesOut.copyFromScriptValue(result, false); - // Javascript objects are == only if they are the same object. To compare arbitrary values, we need to use JSON. - auto out = QJsonValue::fromVariant(result.toVariant()); - wasChanged = in != out; +bool EntityTree::filterProperties(EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType) { + bool accepted = true; + auto entityEditFilters = DependencyManager::get(); + if (entityEditFilters) { + auto position = existingEntity ? existingEntity->getPosition() : propertiesIn.getPosition(); + auto entityID = existingEntity ? existingEntity->getEntityItemID() : EntityItemID(); + accepted = entityEditFilters->filter(position, propertiesIn, propertiesOut, wasChanged, filterType, entityID); } return accepted; @@ -1076,11 +1036,16 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c // an existing entity... handle appropriately if (validEditPacket) { + // search for the entity by EntityItemID + startLookup = usecTimestampNow(); + EntityItemPointer existingEntity = findEntityByEntityItemID(entityItemID); + endLookup = usecTimestampNow(); + startFilter = usecTimestampNow(); bool wasChanged = false; // Having (un)lock rights bypasses the filter, unless it's a physics result. FilterType filterType = isPhysics ? FilterType::Physics : (isAdd ? FilterType::Add : FilterType::Edit); - bool allowed = (!isPhysics && senderNode->isAllowedEditor()) || filterProperties(properties, properties, wasChanged, filterType); + bool allowed = (!isPhysics && senderNode->isAllowedEditor()) || filterProperties(existingEntity, properties, properties, wasChanged, filterType); if (!allowed) { auto timestamp = properties.getLastEdited(); properties = EntityItemProperties(); @@ -1093,10 +1058,6 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } endFilter = usecTimestampNow(); - // search for the entity by EntityItemID - startLookup = usecTimestampNow(); - EntityItemPointer existingEntity = findEntityByEntityItemID(entityItemID); - endLookup = usecTimestampNow(); if (existingEntity && !isAdd) { if (suppressDisallowedScript) { @@ -1767,3 +1728,4 @@ QStringList EntityTree::getJointNames(const QUuid& entityID) const { } return entity->getJointNames(); } + diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index 5dad282d3b..63f7bbfd66 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -25,6 +25,7 @@ typedef std::shared_ptr EntityTreePointer; #include "EntityTreeElement.h" #include "DeleteEntityOperator.h" +class EntityEditFilters; class Model; using ModelPointer = std::shared_ptr; using ModelWeakPointer = std::weak_ptr; @@ -271,9 +272,6 @@ public: void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID); - void initEntityEditFilterEngine(QScriptEngine* engine, std::function entityEditFilterHadUncaughtExceptions); - void setHasEntityFilter(bool hasFilter) { _hasEntityEditFilter = hasFilter; } - static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME; public slots: @@ -362,13 +360,8 @@ protected: float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME }; - bool filterProperties(EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType); + bool filterProperties(EntityItemPointer& existingEntity, EntityItemProperties& propertiesIn, EntityItemProperties& propertiesOut, bool& wasChanged, FilterType filterType); bool _hasEntityEditFilter{ false }; - QScriptEngine* _entityEditFilterEngine{}; - QScriptValue _entityEditFilterFunction{}; - QScriptValue _nullObjectForFilter{}; - std::function _entityEditFilterHadUncaughtExceptions; - QStringList _entityScriptSourceWhitelist; }; diff --git a/libraries/entities/src/ZoneEntityItem.cpp b/libraries/entities/src/ZoneEntityItem.cpp index 3e21497d63..37b3be99a3 100644 --- a/libraries/entities/src/ZoneEntityItem.cpp +++ b/libraries/entities/src/ZoneEntityItem.cpp @@ -19,6 +19,7 @@ #include "EntityTree.h" #include "EntityTreeElement.h" #include "ZoneEntityItem.h" +#include "EntityEditFilters.h" bool ZoneEntityItem::_zonesArePickable = false; bool ZoneEntityItem::_drawZoneBoundaries = false; @@ -28,7 +29,7 @@ const ShapeType ZoneEntityItem::DEFAULT_SHAPE_TYPE = SHAPE_TYPE_BOX; const QString ZoneEntityItem::DEFAULT_COMPOUND_SHAPE_URL = ""; const bool ZoneEntityItem::DEFAULT_FLYING_ALLOWED = true; const bool ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED = true; - +const QString ZoneEntityItem::DEFAULT_FILTER_URL = ""; EntityItemPointer ZoneEntityItem::factory(const EntityItemID& entityID, const EntityItemProperties& properties) { EntityItemPointer entity { new ZoneEntityItem(entityID) }; @@ -61,6 +62,7 @@ EntityItemProperties ZoneEntityItem::getProperties(EntityPropertyFlags desiredPr COPY_ENTITY_PROPERTY_TO_PROPERTIES(flyingAllowed, getFlyingAllowed); COPY_ENTITY_PROPERTY_TO_PROPERTIES(ghostingAllowed, getGhostingAllowed); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(filterURL, getFilterURL); return properties; } @@ -79,6 +81,7 @@ bool ZoneEntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(flyingAllowed, setFlyingAllowed); SET_ENTITY_PROPERTY_FROM_PROPERTIES(ghostingAllowed, setGhostingAllowed); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(filterURL, setFilterURL); bool somethingChangedInSkybox = _skyboxProperties.setProperties(properties); @@ -128,6 +131,7 @@ int ZoneEntityItem::readEntitySubclassDataFromBuffer(const unsigned char* data, READ_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, bool, setFlyingAllowed); READ_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, bool, setGhostingAllowed); + READ_ENTITY_PROPERTY(PROP_FILTER_URL, QString, setFilterURL); return bytesRead; } @@ -147,6 +151,7 @@ EntityPropertyFlags ZoneEntityItem::getEntityProperties(EncodeBitstreamParams& p requestedProperties += PROP_FLYING_ALLOWED; requestedProperties += PROP_GHOSTING_ALLOWED; + requestedProperties += PROP_FILTER_URL; return requestedProperties; } @@ -177,6 +182,7 @@ void ZoneEntityItem::appendSubclassData(OctreePacketData* packetData, EncodeBits APPEND_ENTITY_PROPERTY(PROP_FLYING_ALLOWED, getFlyingAllowed()); APPEND_ENTITY_PROPERTY(PROP_GHOSTING_ALLOWED, getGhostingAllowed()); + APPEND_ENTITY_PROPERTY(PROP_FILTER_URL, getFilterURL()); } void ZoneEntityItem::debugDump() const { @@ -215,3 +221,13 @@ bool ZoneEntityItem::findDetailedRayIntersection(const glm::vec3& origin, const return _zonesArePickable; } + +void ZoneEntityItem::setFilterURL(QString url) { + _filterURL = url; + if (DependencyManager::isSet()) { + auto entityEditFilters = DependencyManager::get(); + qCDebug(entities) << "adding filter " << url << "for zone" << getEntityItemID(); + entityEditFilters->addFilter(getEntityItemID(), url); + } +} + diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 3084d71f46..2bef95e452 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -74,6 +74,8 @@ public: void setFlyingAllowed(bool value) { _flyingAllowed = value; } bool getGhostingAllowed() const { return _ghostingAllowed; } void setGhostingAllowed(bool value) { _ghostingAllowed = value; } + QString getFilterURL() const { return _filterURL; } + void setFilterURL(const QString url); virtual bool supportsDetailedRayIntersection() const override { return true; } virtual bool findDetailedRayIntersection(const glm::vec3& origin, const glm::vec3& direction, @@ -87,6 +89,7 @@ public: static const QString DEFAULT_COMPOUND_SHAPE_URL; static const bool DEFAULT_FLYING_ALLOWED; static const bool DEFAULT_GHOSTING_ALLOWED; + static const QString DEFAULT_FILTER_URL; protected: KeyLightPropertyGroup _keyLightProperties; @@ -101,6 +104,7 @@ protected: bool _flyingAllowed { DEFAULT_FLYING_ALLOWED }; bool _ghostingAllowed { DEFAULT_GHOSTING_ALLOWED }; + QString _filterURL { DEFAULT_FILTER_URL }; static bool _drawZoneBoundaries; static bool _zonesArePickable; diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index b9bf9fea67..459382c5bf 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -577,7 +577,7 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t if (it != _nodeHash.end()) { SharedNodePointer& matchingNode = it->second; - + matchingNode->setPublicSocket(publicSocket); matchingNode->setLocalSocket(localSocket); matchingNode->setPermissions(permissions); @@ -717,14 +717,20 @@ SharedNodePointer LimitedNodeList::soloNodeOfType(NodeType_t nodeType) { }); } -void LimitedNodeList::getPacketStats(float& packetsPerSecond, float& bytesPerSecond) { - packetsPerSecond = (float) _numCollectedPackets / ((float) _packetStatTimer.elapsed() / 1000.0f); - bytesPerSecond = (float) _numCollectedBytes / ((float) _packetStatTimer.elapsed() / 1000.0f); +void LimitedNodeList::getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond) { + packetsInPerSecond = (float) getPacketReceiver().getInPacketCount() / ((float) _packetStatTimer.elapsed() / 1000.0f); + bytesInPerSecond = (float) getPacketReceiver().getInByteCount() / ((float) _packetStatTimer.elapsed() / 1000.0f); + + packetsOutPerSecond = (float) _numCollectedPackets / ((float) _packetStatTimer.elapsed() / 1000.0f); + bytesOutPerSecond = (float) _numCollectedBytes / ((float) _packetStatTimer.elapsed() / 1000.0f); } void LimitedNodeList::resetPacketStats() { + getPacketReceiver().resetCounters(); + _numCollectedPackets = 0; _numCollectedBytes = 0; + _packetStatTimer.restart(); } diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 0d54c843f6..5256e55397 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -161,7 +161,7 @@ public: unsigned int broadcastToNodes(std::unique_ptr packet, const NodeSet& destinationNodeTypes); SharedNodePointer soloNodeOfType(NodeType_t nodeType); - void getPacketStats(float &packetsPerSecond, float &bytesPerSecond); + void getPacketStats(float& packetsInPerSecond, float& bytesInPerSecond, float& packetsOutPerSecond, float& bytesOutPerSecond); void resetPacketStats(); std::unique_ptr constructPingPacket(PingType_t pingType = PingType::Agnostic); diff --git a/libraries/networking/src/ThreadedAssignment.cpp b/libraries/networking/src/ThreadedAssignment.cpp index c7b6b05c35..9db540cae2 100644 --- a/libraries/networking/src/ThreadedAssignment.cpp +++ b/libraries/networking/src/ThreadedAssignment.cpp @@ -45,9 +45,9 @@ void ThreadedAssignment::setFinished(bool isFinished) { if (_isFinished) { qCDebug(networking) << "ThreadedAssignment::setFinished(true) called - finishing up."; - + auto nodeList = DependencyManager::get(); - + auto& packetReceiver = nodeList->getPacketReceiver(); // we should de-register immediately for any of our packets @@ -55,7 +55,7 @@ void ThreadedAssignment::setFinished(bool isFinished) { // we should also tell the packet receiver to drop packets while we're cleaning up packetReceiver.setShouldDropPackets(true); - + // send a disconnect packet to the domain nodeList->getDomainHandler().disconnect(); @@ -92,12 +92,17 @@ void ThreadedAssignment::commonInit(const QString& targetName, NodeType_t nodeTy void ThreadedAssignment::addPacketStatsAndSendStatsPacket(QJsonObject statsObject) { auto nodeList = DependencyManager::get(); - float packetsPerSecond, bytesPerSecond; - nodeList->getPacketStats(packetsPerSecond, bytesPerSecond); + float packetsInPerSecond, bytesInPerSecond, packetsOutPerSecond, bytesOutPerSecond; + nodeList->getPacketStats(packetsInPerSecond, bytesInPerSecond, packetsOutPerSecond, bytesOutPerSecond); nodeList->resetPacketStats(); - statsObject["packets_per_second"] = packetsPerSecond; - statsObject["bytes_per_second"] = bytesPerSecond; + QJsonObject ioStats; + ioStats["inbound_bytes_per_s"] = bytesInPerSecond; + ioStats["inbound_packets_per_s"] = packetsInPerSecond; + ioStats["outbound_bytes_per_s"] = bytesOutPerSecond; + ioStats["outbound_packets_per_s"] = packetsOutPerSecond; + + statsObject["io_stats"] = ioStats; nodeList->sendStatsToDomainServer(statsObject); } diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 855499c0e7..ddbc30d020 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -49,7 +49,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return VERSION_ENTITIES_PHYSICS_PACKET; + return VERSION_ENTITIES_ZONE_FILTERS; case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::JsonFilter); case PacketType::AvatarIdentity: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index e198a486f7..de3d0369b5 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -204,6 +204,7 @@ const PacketVersion VERSION_ENTITIES_ARROW_ACTION = 64; const PacketVersion VERSION_ENTITIES_LAST_EDITED_BY = 65; const PacketVersion VERSION_ENTITIES_SERVER_SCRIPTS = 66; const PacketVersion VERSION_ENTITIES_PHYSICS_PACKET = 67; +const PacketVersion VERSION_ENTITIES_ZONE_FILTERS = 68; enum class EntityQueryPacketVersion: PacketVersion { JsonFilter = 18 diff --git a/libraries/plugins/src/plugins/PluginManager.cpp b/libraries/plugins/src/plugins/PluginManager.cpp index 75b0f1e3bc..8471dbc7e8 100644 --- a/libraries/plugins/src/plugins/PluginManager.cpp +++ b/libraries/plugins/src/plugins/PluginManager.cpp @@ -123,8 +123,6 @@ const CodecPluginList& PluginManager::getCodecPlugins() { static CodecPluginList codecPlugins; static std::once_flag once; std::call_once(once, [&] { - //codecPlugins = ::getCodecPlugins(); - // Now grab the dynamic plugins for (auto loader : getLoadedPlugins()) { CodecProvider* codecProvider = qobject_cast(loader->instance()); diff --git a/libraries/recording/src/recording/Deck.cpp b/libraries/recording/src/recording/Deck.cpp index 6f624db191..61eb86c91f 100644 --- a/libraries/recording/src/recording/Deck.cpp +++ b/libraries/recording/src/recording/Deck.cpp @@ -145,10 +145,8 @@ void Deck::processFrames() { } if (!nextClip) { - qCDebug(recordingLog) << "No more frames available"; // No more frames available, so handle the end of playback if (_loop) { - qCDebug(recordingLog) << "Looping enabled, seeking back to beginning"; // If we have looping enabled, start the playback over seek(0); // FIXME configure the recording scripting interface to reset the avatar basis on a loop diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 665e063a6e..adfffe2614 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1178,7 +1178,7 @@ void Model::updateClusterMatrices() { for (int j = 0; j < mesh.clusters.size(); j++) { const FBXCluster& cluster = mesh.clusters.at(j); auto jointMatrix = _rig->getJointTransform(cluster.jointIndex); -#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MACOS) +#if (GLM_ARCH & GLM_ARCH_SSE2) && !(defined Q_OS_MAC) glm::mat4 out, inverseBindMatrix = cluster.inverseBindMatrix; glm_mat4_mul((glm_vec4*)&jointMatrix, (glm_vec4*)&inverseBindMatrix, (glm_vec4*)&out); state.clusterMatrices[j] = out; diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 527a9cfc2b..f38d17fa2f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1188,8 +1188,8 @@ function MyController(hand) { return; } } else { - this.homeButtonTouched = false; - } + this.homeButtonTouched = false; + } } else { this.hideStylus(); } @@ -1987,9 +1987,11 @@ function MyController(hand) { this.clearEquipHaptics(); this.grabPointSphereOff(); - this.shouldScale = false; + this.shouldScale = false; - var worldControllerPosition = getControllerWorldLocation(this.handToController(), true).position; + var controllerLocation = getControllerWorldLocation(this.handToController(), true); + var worldControllerPosition = controllerLocation.position; + var worldControllerRotation = controllerLocation.orientation; // transform the position into room space var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); @@ -2007,7 +2009,12 @@ function MyController(hand) { this.grabRadius = Vec3.distance(this.currentObjectPosition, worldControllerPosition); this.grabRadialVelocity = 0.0; - // compute a constant based on the initial conditions which we use below to exagerate hand motion + // offset between controller vector at the grab radius and the entity position + var targetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); + targetPosition = Vec3.sum(targetPosition, worldControllerPosition); + this.offsetPosition = Vec3.subtract(this.currentObjectPosition, targetPosition); + + // compute a constant based on the initial conditions which we use below to exaggerate hand motion // onto the held object this.radiusScalar = Math.log(this.grabRadius + 1.0); if (this.radiusScalar < 1.0) { @@ -2124,6 +2131,7 @@ function MyController(hand) { var newTargetPosition = Vec3.multiply(this.grabRadius, Quat.getUp(worldControllerRotation)); newTargetPosition = Vec3.sum(newTargetPosition, worldControllerPosition); + newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition); var objectToAvatar = Vec3.subtract(this.currentObjectPosition, MyAvatar.position); var handControllerData = getEntityCustomData('handControllerKey', this.grabbedEntity, defaultMoveWithHeadData); diff --git a/scripts/system/html/entityProperties.html b/scripts/system/html/entityProperties.html index e563758782..b11127b26c 100644 --- a/scripts/system/html/entityProperties.html +++ b/scripts/system/html/entityProperties.html @@ -450,6 +450,11 @@ +
+
+ + +
diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 957cea4528..8879c0f34e 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -697,6 +697,7 @@ function loaded() { var elZoneFlyingAllowed = document.getElementById("property-zone-flying-allowed"); var elZoneGhostingAllowed = document.getElementById("property-zone-ghosting-allowed"); + var elZoneFilterURL = document.getElementById("property-zone-filter-url"); var elPolyVoxSections = document.querySelectorAll(".poly-vox-section"); allSections.push(elPolyVoxSections); @@ -1032,6 +1033,7 @@ function loaded() { elZoneFlyingAllowed.checked = properties.flyingAllowed; elZoneGhostingAllowed.checked = properties.ghostingAllowed; + elZoneFilterURL.value = properties.filterURL; showElements(document.getElementsByClassName('skybox-section'), elZoneBackgroundMode.value == 'skybox'); } else if (properties.type == "PolyVox") { @@ -1387,7 +1389,8 @@ function loaded() { elZoneFlyingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('flyingAllowed')); elZoneGhostingAllowed.addEventListener('change', createEmitCheckedPropertyUpdateFunction('ghostingAllowed')); - + elZoneFilterURL.addEventListener('change', createEmitTextPropertyUpdateFunction('filterURL')); + var voxelVolumeSizeChangeFunction = createEmitVec3PropertyUpdateFunction( 'voxelVolumeSize', elVoxelVolumeSizeX, elVoxelVolumeSizeY, elVoxelVolumeSizeZ); elVoxelVolumeSizeX.addEventListener('change', voxelVolumeSizeChangeFunction); diff --git a/scripts/system/html/users.html b/scripts/system/html/users.html index c18162ff8a..0e593cc799 100644 --- a/scripts/system/html/users.html +++ b/scripts/system/html/users.html @@ -24,19 +24,18 @@ } .top-bar { - width: 100%; height: 90px; background: linear-gradient(#2b2b2b, #1e1e1e); font-weight: bold; - } - - .top-bar .myContainer { + padding-left: 30px; + padding-right: 30px; display: flex; - justify-content: space-between; align-items: center; - margin-left: 30px; - margin-right: 30px; - height: 100%; + position: fixed; + width: 480px; + top: 0; + z-index: 1; + justify-content: space-between; } #refresh-button { @@ -46,6 +45,7 @@ .main { padding: 30px; + margin-top: 90px; } #user-info-div { @@ -219,7 +219,11 @@ } .dropdown-menu { - width: 280px; + width: 350px; + border: 0; + display: block; + float: none; + position: inherit; } .dropdown-menu li { @@ -231,52 +235,32 @@ padding: 0px; } - .dropdown-menu li:hover { + .dropdown-menu li.current { background: #dcdcdc; } - .dropdown-menu li h6 { + .dropdown-menu li h5 { font-weight: bold; } .dropdown-menu li p { - font-size: 11px; + font-size: 14px; }
-
-
Users Online
- -
+
Users Online
+

- +
  • Everyone (0)
  • @@ -291,7 +275,7 @@
- +