mirror of
https://github.com/overte-org/overte.git
synced 2025-04-08 05:52:38 +02:00
Merge branch 'master' of https://github.com/highfidelity/hifi into avatarMixerLoopImprovements
This commit is contained in:
commit
5d90b7b1b7
41 changed files with 873 additions and 387 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
237
libraries/entities/src/EntityEditFilters.cpp
Normal file
237
libraries/entities/src/EntityEditFilters.cpp
Normal 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);
|
||||
}
|
65
libraries/entities/src/EntityEditFilters.h
Normal file
65
libraries/entities/src/EntityEditFilters.h
Normal 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
|
|
@ -19,6 +19,7 @@
|
|||
#include "RegisteredMetaTypes.h"
|
||||
#include "EntityItemID.h"
|
||||
|
||||
int entityItemIDTypeID = qRegisterMetaType<EntityItemID>();
|
||||
|
||||
EntityItemID::EntityItemID() : QUuid()
|
||||
{
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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, "");
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">×</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",
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue