Merge branch 'master' of https://github.com/highfidelity/hifi into avatarMixerLoopImprovements

This commit is contained in:
Brad Hefta-Gaub 2017-02-15 16:15:57 -08:00
commit 5d90b7b1b7
41 changed files with 873 additions and 387 deletions

View file

@ -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<QString, std::shared_ptr<CodecPlugin>> AudioMixer::_availableCodecs{ };
QStringList AudioMixer::_codecPreferenceOrder{};
QHash<QString, AABox> AudioMixer::_audioZones;
QVector<AudioMixer::ZoneSettings> AudioMixer::_zoneSettings;
QVector<AudioMixer::ReverbSettings> 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<NodeList>();
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<ReceivedMessage> message, SharedNodePointer sendingNode) {
getOrCreateClientData(sendingNode.data());
DependencyManager::get<NodeList>()->updateNodeWithDataFromPacket(message, sendingNode);
}
void AudioMixer::queueAudioPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node) {
if (message->getType() == PacketType::SilentAudioFrame) {
_numSilentPackets++;
}
void AudioMixer::handleSilentAudioPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
_numSilentPackets++;
getOrCreateClientData(sendingNode.data());
DependencyManager::get<NodeList>()->updateNodeWithDataFromPacket(message, sendingNode);
getOrCreateClientData(node.data())->queuePacket(message, node);
}
void AudioMixer::handleMuteEnvironmentPacket(QSharedPointer<ReceivedMessage> 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<ReceivedMessage> 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<QString, CodecPluginPointer> AudioMixer::negotiateCodec(std::vector<QString> codecs) {
QString selectedCodecName;
CodecPluginPointer selectedCodec;
QStringList codecPreferenceList = _codecPreferenceOrder.split(",");
// read the codecs requested (by the client)
int minPreference = std::numeric_limits<int>::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<ReceivedMessage> packet,
}
}
void AudioMixer::handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->getOrCreateLinkedData(senderNode);
if (senderNode->getLinkedData()) {
AudioMixerClientData* nodeData = dynamic_cast<AudioMixerClientData*>(senderNode->getLinkedData());
if (nodeData != nullptr) {
bool isRequesting;
message->readPrimitive(&isRequesting);
nodeData->setRequestsDomainListData(isRequesting);
}
}
}
void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
sendingNode->parseIgnoreRequestMessage(packet);
}
void AudioMixer::handlePerAvatarGainSetDataPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
auto clientData = dynamic_cast<AudioMixerClientData*>(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<ReceivedMessage> packet, SharedNodePointer sendingNode) {
sendingNode->parseIgnoreRadiusRequestMessage(packet);
}
void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) {
auto injectorClientData = qobject_cast<AudioMixerClientData*>(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;
}

View file

@ -49,6 +49,7 @@ public:
static const QHash<QString, AABox>& getAudioZones() { return _audioZones; }
static const QVector<ZoneSettings>& getZoneSettings() { return _zoneSettings; }
static const QVector<ReverbSettings>& getReverbSettings() { return _zoneReverbSettings; }
static const std::pair<QString, CodecPluginPointer> negotiateCodec(std::vector<QString> codecs);
public slots:
void run() override;
@ -56,20 +57,14 @@ public slots:
private slots:
// packet handlers
void handleAudioPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleSilentAudioPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleMuteEnvironmentPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleNegotiateAudioFormat(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
void handleNodeKilled(SharedNodePointer killedNode);
void handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleNodeMuteRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handlePerAvatarGainSetDataPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleNodeKilled(SharedNodePointer killedNode);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void start();
void queueAudioPacket(QSharedPointer<ReceivedMessage> 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<QString, CodecPluginPointer> _availableCodecs;
static QStringList _codecPreferenceOrder;
static QHash<QString, AABox> _audioZones;
static QVector<ZoneSettings> _zoneSettings;
static QVector<ReverbSettings> _zoneReverbSettings;
};
#endif // hifi_AudioMixer_h

View file

@ -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<ReceivedMessage> 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<QString> codecs;
for (auto i = 0; i < numberOfCodecs; i++) {
codecs.push_back(message.readString());
}
const std::pair<QString, CodecPluginPointer> 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<ReceivedMessage> message, const SharedNodePointer& node) {
node->parseIgnoreRequestMessage(message);
}
void AudioMixerClientData::parseRadiusIgnoreRequest(QSharedPointer<ReceivedMessage> 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));
}
}

View file

@ -12,6 +12,8 @@
#ifndef hifi_AudioMixerClientData_h
#define hifi_AudioMixerClientData_h
#include <queue>
#include <QtCore/QJsonObject>
#include <AABox.h>
@ -34,12 +36,15 @@ public:
using SharedStreamPointer = std::shared_ptr<PositionalAudioStream>;
using AudioStreamMap = std::unordered_map<QUuid, SharedStreamPointer>;
void queuePacket(QSharedPointer<ReceivedMessage> 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<ReceivedMessage> message, const SharedNodePointer& node);
void parseRadiusIgnoreRequest(QSharedPointer<ReceivedMessage> 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<QSharedPointer<ReceivedMessage>> {
QWeakPointer<Node> 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);

View file

@ -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;

View file

@ -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);

View file

@ -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();

View file

@ -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<std::unique_ptr<AudioMixerSlaveThread>> _slaves;
@ -82,6 +87,8 @@ private:
Mutex _mutex;
ConditionVariable _slaveCondition;
ConditionVariable _poolCondition;
void (AudioMixerSlave::*_function)(const SharedNodePointer& node);
std::function<void(AudioMixerSlave&)> _configure;
int _numThreads { 0 };
int _numStarted { 0 }; // guarded by _mutex
int _numFinished { 0 }; // guarded by _mutex

View file

@ -15,6 +15,7 @@
#include <SimpleEntitySimulation.h>
#include <ResourceCache.h>
#include <ScriptCache.h>
#include <EntityEditFilters.h>
#include "EntityServer.h"
#include "EntityServerConsts.h"
@ -71,6 +72,7 @@ OctreePointer EntityServer::createTree() {
DependencyManager::registerInheritance<SpatialParentFinder, AssignmentParentFinder>();
DependencyManager::set<AssignmentParentFinder>(tree);
DependencyManager::set<EntityEditFilters>(std::static_pointer_cast<EntityTree>(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<EntityTree>(_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<EntityEditFilters>();
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<ResourceRequest*>(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<EntityTree>(_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) {

View file

@ -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<ReceivedMessage> message, SharedNodePointer senderNode);
void scriptRequestFinished();
private:
SimpleEntitySimulationPointer _entitySimulation;
@ -77,9 +77,6 @@ private:
QReadWriteLock _viewerSendingStatsLock;
QMap<QUuid, QMap<QUuid, ViewerSendingStats>> _viewerSendingStats;
QString _entityEditFilter{};
QScriptEngine _entityEditFilterEngine{};
};
#endif // hifi_EntityServer_h

View file

@ -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)

View file

@ -29,7 +29,7 @@
#include <NLPacketList.h>
#include <NumericalConstants.h>
#include <SettingHandle.h>
#include <AvatarData.h> //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<QVariantMap*>(getSettingsMap()[DESCRIPTORS].data());
}
@ -721,6 +728,13 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
}
}
}
// if we are here, then we kicked them, so send the KillAvatar message to everyone
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true);
packet->write(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(QSharedPointer<Re
// we've changed permissions, time to store them to disk and emit our signal to say they have changed
packPermissions();
} else {
emit updateNodePermissions();
}
// we emit this no matter what -- though if this isn't a new permission probably 2 people are racing to kick and this
// person lost the race. No matter, just be sure this is called as otherwise it takes like 10s for the person being banned
// to go away
emit updateNodePermissions();
} else {
qWarning() << "Node kick request received for unknown node. Refusing to process.";

View file

@ -7,6 +7,7 @@ QPlainTextEdit {
color: #333333;
background-color: #FFFFFF;
border: none;
selection-background-color: #7b91b5;
}
QLineEdit {
@ -32,6 +33,26 @@ QPushButton#searchButton {
border-bottom-left-radius: 9px;
}
QPushButton#searchNextButton {
font-family: Helvetica, Arial, sans-serif;
text-align: center;
background-color: #CCCCCC;
color: #3d3d3d;
border-width: 0;
border-radius: 9px;
font-size: 11px;
}
QPushButton#searchPrevButton {
font-family: Helvetica, Arial, sans-serif;
text-align: center;
background-color: #CCCCCC;
color: #3d3d3d;
border-width: 0;
border-radius: 9px;
font-size: 11px;
}
QPushButton#revealLogButton {
font-family: Helvetica, Arial, sans-serif;
background: url(styles/txt-file.svg);

View file

@ -116,7 +116,7 @@ void CauterizedModel::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;
@ -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;

View file

@ -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;

View file

@ -14,6 +14,7 @@
#include <QDir>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QTextCursor>
#include <QPushButton>
#include <QSyntaxHighlighter>
@ -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);
}

View file

@ -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 };

View file

@ -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;

View file

@ -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 <QUrl>
#include <ResourceManager.h>
#include "EntityEditFilters.h"
QList<EntityItemID> EntityEditFilters::getZonesByPosition(glm::vec3& position) {
QList<EntityItemID> zones;
QList<EntityItemID> 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<ZoneEntityItem>(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<ResourceRequest*>(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);
}

View file

@ -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 <QObject>
#include <QMap>
#include <QScriptValue>
#include <QScriptEngine>
#include <glm/glm.hpp>
#include <functional>
#include "EntityItemID.h"
#include "EntityItemProperties.h"
#include "EntityTree.h"
class EntityEditFilters : public QObject, public Dependency {
Q_OBJECT
public:
struct FilterData {
QScriptValue filterFn;
std::function<bool()> 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<EntityItemID> getZonesByPosition(glm::vec3& position);
EntityTreePointer _tree {};
bool _rejectAll {false};
QScriptValue _nullObjectForFilter{};
QReadWriteLock _lock;
QMap<EntityItemID, FilterData> _filterDataMap;
};
#endif //hifi_EntityEditFilters_h

View file

@ -19,6 +19,7 @@
#include "RegisteredMetaTypes.h"
#include "EntityItemID.h"
int entityItemIDTypeID = qRegisterMetaType<EntityItemID>();
EntityItemID::EntityItemID() : QUuid()
{

View file

@ -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<QString> EntityItemProperties::listChangedProperties() {
if (ghostingAllowedChanged()) {
out += "ghostingAllowed";
}
if (filterURLChanged()) {
out += "filterURL";
}
if (dpiChanged()) {
out += "dpi";
}

View file

@ -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, "");

View file

@ -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,

View file

@ -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<Q
}
}
void EntityTree::initEntityEditFilterEngine(QScriptEngine* engine, std::function<bool()> 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<EntityEditFilters>();
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();
}

View file

@ -25,6 +25,7 @@ typedef std::shared_ptr<EntityTree> EntityTreePointer;
#include "EntityTreeElement.h"
#include "DeleteEntityOperator.h"
class EntityEditFilters;
class Model;
using ModelPointer = std::shared_ptr<Model>;
using ModelWeakPointer = std::weak_ptr<Model>;
@ -271,9 +272,6 @@ public:
void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID);
void initEntityEditFilterEngine(QScriptEngine* engine, std::function<bool()> 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<bool()> _entityEditFilterHadUncaughtExceptions;
QStringList _entityScriptSourceWhitelist;
};

View file

@ -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<EntityEditFilters>()) {
auto entityEditFilters = DependencyManager::get<EntityEditFilters>();
qCDebug(entities) << "adding filter " << url << "for zone" << getEntityItemID();
entityEditFilters->addFilter(getEntityItemID(), url);
}
}

View file

@ -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;

View file

@ -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();
}

View file

@ -161,7 +161,7 @@ public:
unsigned int broadcastToNodes(std::unique_ptr<NLPacket> 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<NLPacket> constructPingPacket(PingType_t pingType = PingType::Agnostic);

View file

@ -45,9 +45,9 @@ void ThreadedAssignment::setFinished(bool isFinished) {
if (_isFinished) {
qCDebug(networking) << "ThreadedAssignment::setFinished(true) called - finishing up.";
auto nodeList = DependencyManager::get<NodeList>();
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<NodeList>();
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);
}

View file

@ -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<PacketVersion>(EntityQueryPacketVersion::JsonFilter);
case PacketType::AvatarIdentity:

View file

@ -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

View file

@ -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<CodecProvider*>(loader->instance());

View file

@ -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

View file

@ -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;

View file

@ -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);

View file

@ -450,6 +450,11 @@
<input type="checkbox" id="property-zone-ghosting-allowed">
<label for="property-zone-ghosting-allowed">Ghosting allowed</label>
</div>
<hr class="zone-group zone-section">
<div class="zone-group zone-section property url ">
<label for="property-zone-filter-url">Filter URL</label>
<input type="text" id="property-zone-filter-url">
</div>
<div class="sub-section-header zone-group zone-section keylight-section">
<label>Key Light</label>
</div>

View file

@ -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);

View file

@ -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;
}
</style>
</head>
<body>
<div class="top-bar">
<div class="myContainer">
<div>Users Online</div>
<img id="refresh-button" onclick="pollUsers()" src="https://hifi-content.s3.amazonaws.com/faye/tablet-dev/refresh-icon.svg"></img>
</div>
<div>Users Online</div>
<img id="refresh-button" onclick="pollUsers()" src="https://hifi-content.s3.amazonaws.com/faye/tablet-dev/refresh-icon.svg"></img>
</div>
<div class="main">
<div id="user-info-div">
<h4></h4>
<div class="dropdown">
<button id="visibility-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Online
<span class="glyphicon glyphicon-menu-down"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="visibility-toggle">
<li class="visibility-option" data-visibility="all">
<h6>Online</h6>
<p>You will be shown online to everyone else. Anybody will be able to find you from the users online list and jump to your current location.</p>
</li>
<li role="separator" class="divider"></li>
<li class="visibility-option" data-visibility="friends">
<h6>Available to Friends Only</h6>
<p>You will be shown online only to users you have added as friends. Other users may still interact with you in the same domain, but they won't be able to find you from the users online list.</p>
</li>
<li role="separator" class="divider"></li>
<li class="visibility-option" data-visibility="none">
<h6>Appear Offline</h6>
<p>No one will be able to find you from the users online list. However, this does not prevent other users in the same domain from interacting with you. For a complete "Do not disturb" mode, you may want to go to your own private domain and set allow entering to no one.</p>
</li>
</ul>
</div>
<button id="visibility-toggle" data-toggle="modal" data-target="#myModal2" aria-haspopup="true" aria-expanded="false">
Online
<span class="glyphicon glyphicon-menu-down"></span>
</button>
</div>
<ul class="tabs">
<li tab-id="tab-1" class="current">Everyone (0)</li>
@ -291,7 +275,7 @@
</div>
</div>
<!-- Modal -->
<!-- Modal for jump to-->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
@ -310,6 +294,39 @@
</div>
</div>
<!-- Modal for visibility-->
<div class="modal fade" id="myModal2" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">Change your visibility setting</h4>
</div>
<div class="modal-body">
<ul class="dropdown-menu" aria-labelledby="visibility-toggle">
<li class="visibility-option" data-visibility="all">
<h5>Online</h5>
<p>You will be shown online to everyone else. Anybody will be able to find you from the users online list and jump to your current location.</p>
</li>
<li role="separator" class="divider"></li>
<li class="visibility-option" data-visibility="friends">
<h5>Available to Friends Only</h5>
<p>You will be shown online only to users you have added as friends. Other users may still interact with you in the same domain, but they won't be able to find you from the users online list.</p>
</li>
<li role="separator" class="divider"></li>
<li class="visibility-option" data-visibility="none">
<h5>Appear Offline</h5>
<p>No one will be able to find you from the users online list. However, this does not prevent other users in the same domain from interacting with you. For a complete "Do not disturb" mode, you may want to go to your own private domain and set allow entering to no one.</p>
</li>
</ul>
</div>
<div class="modal-footer">
<input type="button" data-dismiss="modal" value="OK">
</div>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script>
@ -321,30 +338,53 @@
function displayUsers(data, element) {
element.empty();
for (var i = 0; i < data.users.length; i++) {
// Don't display users who aren't in a domain
if (typeof data.users[i].location.root.name === "undefined") {
// Display users who aren't in a domain differently
if (typeof data.users[i].location.root === "undefined" || typeof data.users[i].location.root.name === "undefined") {
console.log(data.users[i].username + "is online but not in a domain");
$("#dev-div").append("<p>" + data.users[i].username + "is online but not in a domain</p>");
$("<li></li>", {
"data-toggle": "modal",
"data-target": "#myModal",
"data-username": data.users[i].username,
"data-placename": "",
text: data.users[i].username
}).appendTo(element);
} else {
$("#dev-div").append("<li>" + data.users[i].username + " @ " + data.users[i].location.root.name + "</li>");
if (data.users[i].username === myUsername) {
$("#user-info-div h4").text(data.users[i].username + " @ " + data.users[i].location.root.name);
} else {
console.log(data.users[i].username + " @ " + data.users[i].location.root.name);
// Create a list item and put user info in data-* attributes, also make it trigger the jump to confirmation modal
$("<li></li>", {
"data-toggle": "modal",
"data-target": "#myModal",
"data-username": data.users[i].username,
"data-placename": data.users[i].location.root.name,
text: data.users[i].username + " @ " + data.users[i].location.root.name
}).appendTo(element);
}
console.log(data.users[i].username + " @ " + data.users[i].location.root.name);
// Create a list item and put user info in data-* attributes, also make it trigger the jump to confirmation modal
$("<li></li>", {
"data-toggle": "modal",
"data-target": "#myModal",
"data-username": data.users[i].username,
"data-placename": data.users[i].location.root.name,
text: data.users[i].username + " @ " + data.users[i].location.root.name
}).appendTo(element);
}
}
}
function isOtherUsers(user) {
return user.username != myUsername;
}
function processData(data, type) {
// Filter out yourself from the users list
data.users = data.users.filter(isOtherUsers);
// Sort array alphabetically by username
data.users.sort(function(a, b) {
var nameA = a.username.toLowerCase();
var nameB = b.username.toLowerCase();
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
return 0;
});
var num = data.users.length;
if (type === "everyone") {
$(".tabs li:nth-child(1)").text("Everyone (" + num + ")");
@ -418,7 +458,11 @@
var placename = li.data("placename");
// Write info to the modal
var modal = $(this);
modal.find(".modal-title").text("Jump to " + username + " @ " + placename);
if (placename === "") {
modal.find(".modal-title").text("Jump to " + username);
} else {
modal.find(".modal-title").text("Jump to " + username + " @ " + placename);
}
$("#jump-to-confirm-button").data("username", username);
})
@ -434,8 +478,10 @@
// Click listener for toggling who can see me
$(".visibility-option").click(function() {
$(".visibility-option").removeClass("current");
$(this).addClass("current");
myVisibility = $(this).data("visibility");
var newButtonText = $(this).find("h6").text();
var newButtonText = $(this).find("h5").text();
$("#visibility-toggle").html(newButtonText + "<span class='glyphicon glyphicon-menu-down'></span>");
var visibilityObject = {
"type": "toggle-visibility",

View file

@ -35,7 +35,7 @@
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
var button = tablet.addButton({
icon: "icons/tablet-icons/people-i.svg",
icon: "icons/tablet-icons/users-i.svg",
text: "USERS",
sortOrder: 11
});