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

This commit is contained in:
samcake 2017-02-01 10:12:47 -08:00
commit edcba1b7c5
899 changed files with 64094 additions and 10557 deletions

6
.editorconfig Normal file
View file

@ -0,0 +1,6 @@
root = true
# 4-space indentation
[*]
indent_style = space
indent_size = 4

4
.gitignore vendored
View file

@ -4,6 +4,10 @@ CMakeFiles/
CMakeScripts/
cmake_install.cmake
build*/
release*/
debug*/
gprof*/
valgrind*/
ext/
Makefile
*.user

View file

@ -3,4 +3,4 @@ Please read the [general build guide](BUILD.md) for information on dependencies
###Qt5 Dependencies
Should you choose not to install Qt5 via a package manager that handles dependencies for you, you may be missing some Qt5 dependencies. On Ubuntu, for example, the following additional packages are required:
libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev
libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev

View file

@ -9,6 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "Agent.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QEventLoop>
#include <QtCore/QStandardPaths>
@ -46,14 +48,12 @@
#include "RecordingScriptingInterface.h"
#include "AbstractAudioInterface.h"
#include "Agent.h"
#include "AvatarAudioTimer.h"
static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10;
Agent::Agent(ReceivedMessage& message) :
ThreadedAssignment(message),
_entityEditSender(),
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES) {
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
@ -68,7 +68,7 @@ Agent::Agent(ReceivedMessage& message) :
DependencyManager::set<recording::Recorder>();
DependencyManager::set<RecordingScriptingInterface>();
DependencyManager::set<ScriptCache>();
DependencyManager::set<ScriptEngines>();
DependencyManager::set<ScriptEngines>(ScriptEngine::AGENT_SCRIPT);
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
@ -143,7 +143,7 @@ void Agent::handleAudioPacket(QSharedPointer<ReceivedMessage> message) {
_receivedAudioStream.clearBuffer();
}
const QString AGENT_LOGGING_NAME = "agent";
static const QString AGENT_LOGGING_NAME = "agent";
void Agent::run() {
@ -266,6 +266,9 @@ void Agent::handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message) {
}
void Agent::selectAudioFormat(const QString& selectedCodecName) {
if (_selectedCodecName == selectedCodecName) {
return;
}
_selectedCodecName = selectedCodecName;
qDebug() << "Selected Codec:" << _selectedCodecName;
@ -321,7 +324,7 @@ void Agent::scriptRequestFinished() {
}
void Agent::executeScript() {
_scriptEngine = std::unique_ptr<ScriptEngine>(new ScriptEngine(_scriptContents, _payload));
_scriptEngine = std::unique_ptr<ScriptEngine>(new ScriptEngine(ScriptEngine::AGENT_SCRIPT, _scriptContents, _payload));
_scriptEngine->setParent(this); // be the parent of the script engine so it gets moved when we do
// setup an Avatar for the script to use
@ -351,7 +354,15 @@ void Agent::executeScript() {
Transform audioTransform;
audioTransform.setTranslation(scriptedAvatar->getPosition());
audioTransform.setRotation(scriptedAvatar->getOrientation());
AbstractAudioInterface::emitAudioPacket(audio.data(), audio.size(), audioSequenceNumber, audioTransform, PacketType::MicrophoneAudioNoEcho);
QByteArray encodedBuffer;
if (_encoder) {
_encoder->encode(audio, encodedBuffer);
} else {
encodedBuffer = audio;
}
AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber,
audioTransform, scriptedAvatar->getPosition(), glm::vec3(0),
PacketType::MicrophoneAudioNoEcho, _selectedCodecName);
});
auto avatarHashMap = DependencyManager::set<AvatarHashMap>();
@ -374,6 +385,9 @@ void Agent::executeScript() {
_scriptEngine->registerGlobalObject("EntityViewer", &_entityViewer);
auto recordingInterface = DependencyManager::get<RecordingScriptingInterface>();
_scriptEngine->registerGlobalObject("Recording", recordingInterface.data());
// we need to make sure that init has been called for our EntityScriptingInterface
// so that it actually has a jurisdiction listener when we ask it for it next
entityScriptingInterface->init();
@ -424,8 +438,9 @@ void Agent::setIsListeningToAudioStream(bool isListeningToAudioStream) {
},
[&](const SharedNodePointer& node) {
qDebug() << "sending KillAvatar message to Audio Mixers";
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID, true);
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true);
packet->write(getSessionUUID().toRfc4122());
packet->writePrimitive(KillAvatarReason::NoReason);
nodeList->sendPacket(std::move(packet), *node);
});
@ -475,8 +490,9 @@ void Agent::setIsAvatar(bool isAvatar) {
},
[&](const SharedNodePointer& node) {
qDebug() << "sending KillAvatar message to Avatar and Audio Mixers";
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID, true);
auto packet = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason), true);
packet->write(getSessionUUID().toRfc4122());
packet->writePrimitive(KillAvatarReason::NoReason);
nodeList->sendPacket(std::move(packet), *node);
});
}
@ -495,7 +511,8 @@ void Agent::processAgentAvatar() {
if (!_scriptEngine->isFinished() && _isAvatar) {
auto scriptedAvatar = DependencyManager::get<ScriptableAvatar>();
QByteArray avatarByteArray = scriptedAvatar->toByteArray(true, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO);
AvatarData::AvatarDataDetail dataDetail = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO) ? AvatarData::SendAllData : AvatarData::CullSmallData;
QByteArray avatarByteArray = scriptedAvatar->toByteArray(dataDetail, 0, scriptedAvatar->getLastSentJointData());
scriptedAvatar->doneEncoding(true);
static AvatarDataSequenceNumber sequenceNumber = 0;
@ -580,6 +597,8 @@ void Agent::processAgentAvatarAudio() {
audioPacket->writePrimitive(scriptedAvatar->getPosition());
glm::quat headOrientation = scriptedAvatar->getHeadOrientation();
audioPacket->writePrimitive(headOrientation);
audioPacket->writePrimitive(scriptedAvatar->getPosition());
audioPacket->writePrimitive(glm::vec3(0));
} else if (nextSoundOutput) {
// write the codec
@ -592,6 +611,8 @@ void Agent::processAgentAvatarAudio() {
audioPacket->writePrimitive(scriptedAvatar->getPosition());
glm::quat headOrientation = scriptedAvatar->getHeadOrientation();
audioPacket->writePrimitive(headOrientation);
audioPacket->writePrimitive(scriptedAvatar->getPosition());
audioPacket->writePrimitive(glm::vec3(0));
QByteArray encodedBuffer;
if (_flushEncoder) {

View file

@ -12,7 +12,6 @@
#include <assert.h>
#include <QProcess>
#include <QSettings>
#include <QSharedMemory>
#include <QThread>
#include <QTimer>
@ -38,6 +37,8 @@
#include "AssignmentClient.h"
#include "AssignmentClientLogging.h"
#include "avatars/ScriptableAvatar.h"
#include <Trace.h>
#include <StatTracker.h>
const QString ASSIGNMENT_CLIENT_TARGET_NAME = "assignment-client";
const long long ASSIGNMENT_REQUEST_INTERVAL_MSECS = 1 * 1000;
@ -49,8 +50,8 @@ AssignmentClient::AssignmentClient(Assignment::Type requestAssignmentType, QStri
{
LogUtils::init();
QSettings::setDefaultFormat(QSettings::IniFormat);
DependencyManager::set<tracing::Tracer>();
DependencyManager::set<StatTracker>();
DependencyManager::set<AccountManager>();
auto scriptableAvatar = DependencyManager::set<ScriptableAvatar>();
@ -175,9 +176,6 @@ AssignmentClient::~AssignmentClient() {
void AssignmentClient::aboutToQuit() {
stopAssignmentClient();
// clear the log handler so that Qt doesn't call the destructor on LogHandler
qInstallMessageHandler(0);
}
void AssignmentClient::setUpStatusToMonitor() {

View file

@ -12,7 +12,6 @@
#include <QCommandLineParser>
#include <QThread>
#include <BuildInfo.h>
#include <LogHandler.h>
#include <SharedUtil.h>
#include <HifiConfigVariantMap.h>
@ -40,14 +39,6 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
ShutdownEventListener::getInstance();
# endif
setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION);
setOrganizationDomain("highfidelity.io");
setApplicationName("assignment-client");
setApplicationVersion(BuildInfo::VERSION);
// use the verbose message handler in Logging
qInstallMessageHandler(LogHandler::verboseMessageHandler);
// parse command-line
QCommandLineParser parser;
parser.setApplicationDescription("High Fidelity Assignment Client");

View file

@ -126,9 +126,6 @@ void AssignmentClientMonitor::stopChildProcesses() {
void AssignmentClientMonitor::aboutToQuit() {
stopChildProcesses();
// clear the log handler so that Qt doesn't call the destructor on LogHandler
qInstallMessageHandler(0);
}
void AssignmentClientMonitor::spawnChildClient() {

View file

@ -12,12 +12,13 @@
#include <udt/PacketHeaders.h>
#include "Agent.h"
#include "assets/AssetServer.h"
#include "AssignmentFactory.h"
#include "audio/AudioMixer.h"
#include "avatars/AvatarMixer.h"
#include "entities/EntityServer.h"
#include "assets/AssetServer.h"
#include "messages/MessagesMixer.h"
#include "scripts/EntityScriptServer.h"
ThreadedAssignment* AssignmentFactory::unpackAssignment(ReceivedMessage& message) {
@ -39,7 +40,9 @@ ThreadedAssignment* AssignmentFactory::unpackAssignment(ReceivedMessage& message
return new AssetServer(message);
case Assignment::MessagesMixerType:
return new MessagesMixer(message);
case Assignment::EntityScriptServerType:
return new EntityScriptServer(message);
default:
return NULL;
return nullptr;
}
}

View file

@ -15,7 +15,6 @@
// this should send a signal every 10ms, with pretty good precision. Hardcoding
// to 10ms since that's what you'd want for audio.
void AvatarAudioTimer::start() {
qDebug() << __FUNCTION__;
auto startTime = usecTimestampNow();
quint64 frameCounter = 0;
const int TARGET_INTERVAL_USEC = 10000; // 10ms

View file

@ -12,6 +12,8 @@
#include "AssetServer.h"
#include <thread>
#include <QtCore/QCoreApplication>
#include <QtCore/QCryptographicHash>
#include <QtCore/QDateTime>
@ -21,15 +23,55 @@
#include <QtCore/QJsonDocument>
#include <QtCore/QString>
#include <SharedUtil.h>
#include <ServerPathUtils.h>
#include "NetworkLogging.h"
#include "NodeType.h"
#include "SendAssetTask.h"
#include "UploadAssetTask.h"
#include <ClientServerUtils.h>
static const uint8_t MIN_CORES_FOR_MULTICORE = 4;
static const uint8_t CPU_AFFINITY_COUNT_HIGH = 2;
static const uint8_t CPU_AFFINITY_COUNT_LOW = 1;
#ifdef Q_OS_WIN
static const int INTERFACE_RUNNING_CHECK_FREQUENCY_MS = 1000;
#endif
const QString ASSET_SERVER_LOGGING_TARGET_NAME = "asset-server";
bool interfaceRunning() {
bool result = false;
#ifdef Q_OS_WIN
QSharedMemory sharedMemory { getInterfaceSharedMemoryName() };
result = sharedMemory.attach(QSharedMemory::ReadOnly);
if (result) {
sharedMemory.detach();
}
#endif
return result;
}
void updateConsumedCores() {
static bool wasInterfaceRunning = false;
bool isInterfaceRunning = interfaceRunning();
// If state is unchanged, return early
if (isInterfaceRunning == wasInterfaceRunning) {
return;
}
wasInterfaceRunning = isInterfaceRunning;
auto coreCount = std::thread::hardware_concurrency();
if (isInterfaceRunning) {
coreCount = coreCount > MIN_CORES_FOR_MULTICORE ? CPU_AFFINITY_COUNT_HIGH : CPU_AFFINITY_COUNT_LOW;
}
qDebug() << "Setting max consumed cores to " << coreCount;
setMaxCores(coreCount);
}
AssetServer::AssetServer(ReceivedMessage& message) :
ThreadedAssignment(message),
_taskPool(this)
@ -45,6 +87,20 @@ AssetServer::AssetServer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::AssetGetInfo, this, "handleAssetGetInfo");
packetReceiver.registerListener(PacketType::AssetUpload, this, "handleAssetUpload");
packetReceiver.registerListener(PacketType::AssetMappingOperation, this, "handleAssetMappingOperation");
#ifdef Q_OS_WIN
updateConsumedCores();
QTimer* timer = new QTimer(this);
auto timerConnection = connect(timer, &QTimer::timeout, [] {
updateConsumedCores();
});
connect(qApp, &QCoreApplication::aboutToQuit, [this, timerConnection] {
disconnect(timerConnection);
});
timer->setInterval(INTERFACE_RUNNING_CHECK_FREQUENCY_MS);
timer->setTimerType(Qt::CoarseTimer);
timer->start();
#endif
}
void AssetServer::run() {
@ -137,7 +193,7 @@ void AssetServer::completeSetup() {
cleanupUnmappedFiles();
}
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
} else {
qCritical() << "Asset Server assignment will not continue because mapping file could not be loaded.";
setFinished(true);

View file

@ -21,6 +21,7 @@
#include <udt/Packet.h>
#include "AssetUtils.h"
#include "ClientServerUtils.h"
SendAssetTask::SendAssetTask(QSharedPointer<ReceivedMessage> message, const SharedNodePointer& sendToNode, const QDir& resourcesDir) :
QRunnable(),

View file

@ -18,6 +18,8 @@
#include <NodeList.h>
#include <NLPacketList.h>
#include "ClientServerUtils.h"
UploadAssetTask::UploadAssetTask(QSharedPointer<ReceivedMessage> receivedMessage, SharedNodePointer senderNode,
const QDir& resourcesDir) :

File diff suppressed because it is too large Load diff

View file

@ -18,106 +18,116 @@
#include <ThreadedAssignment.h>
#include <UUIDHasher.h>
#include "AudioMixerStats.h"
#include "AudioMixerSlavePool.h"
class PositionalAudioStream;
class AvatarAudioStream;
class AudioHRTF;
class AudioMixerClientData;
const int SAMPLE_PHASE_DELAY_AT_90 = 20;
const int READ_DATAGRAMS_STATS_WINDOW_SECONDS = 30;
/// Handles assignments of type AudioMixer - mixing streams of audio and re-distributing to various clients.
class AudioMixer : public ThreadedAssignment {
Q_OBJECT
public:
AudioMixer(ReceivedMessage& message);
public slots:
/// threaded run of assignment
void run() override;
void sendStatsPacket() override;
static int getStaticJitterFrames() { return _numStaticJitterFrames; }
private slots:
void broadcastMixes();
void handleNodeAudioPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleMuteEnvironmentPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleNegotiateAudioFormat(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
void handleNodeKilled(SharedNodePointer killedNode);
void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void removeHRTFsForFinishedInjector(const QUuid& streamID);
private:
AudioMixerClientData* getOrCreateClientData(Node* node);
void domainSettingsRequestComplete();
/// adds one stream to the mix for a listening node
void addStreamToMixForListeningNodeWithStream(AudioMixerClientData& listenerNodeData,
const PositionalAudioStream& streamToAdd,
const QUuid& sourceNodeID,
const AvatarAudioStream& listeningNodeStream);
float gainForSource(const PositionalAudioStream& streamToAdd, const AvatarAudioStream& listeningNodeStream,
const glm::vec3& relativePosition, bool isEcho);
float azimuthForSource(const PositionalAudioStream& streamToAdd, const AvatarAudioStream& listeningNodeStream,
const glm::vec3& relativePosition);
/// prepares and sends a mix to one Node
bool prepareMixForListeningNode(Node* node);
/// Send Audio Environment packet for a single node
void sendAudioEnvironmentPacket(SharedNodePointer node);
void perSecondActions();
QString percentageForMixStats(int counter);
bool shouldMute(float quietestFrame);
void parseSettingsObject(const QJsonObject& settingsObject);
float _trailingSleepRatio;
float _minAudibilityThreshold;
float _performanceThrottlingRatio;
float _attenuationPerDoublingInDistance;
float _noiseMutingThreshold;
int _numStatFrames { 0 };
int _sumStreams { 0 };
int _sumListeners { 0 };
int _hrtfRenders { 0 };
int _hrtfSilentRenders { 0 };
int _hrtfStruggleRenders { 0 };
int _manualStereoMixes { 0 };
int _manualEchoMixes { 0 };
int _totalMixes { 0 };
QString _codecPreferenceOrder;
float _mixedSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
int16_t _clampedSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
QHash<QString, AABox> _audioZones;
struct ZonesSettings {
struct ZoneSettings {
QString source;
QString listener;
float coefficient;
};
QVector<ZonesSettings> _zonesSettings;
struct ReverbSettings {
QString zone;
float reverbTime;
float wetLevel;
};
QVector<ReverbSettings> _zoneReverbSettings;
static int getStaticJitterFrames() { return _numStaticJitterFrames; }
static bool shouldMute(float quietestFrame) { return quietestFrame > _noiseMutingThreshold; }
static float getAttenuationPerDoublingInDistance() { return _attenuationPerDoublingInDistance; }
static const QHash<QString, AABox>& getAudioZones() { return _audioZones; }
static const QVector<ZoneSettings>& getZoneSettings() { return _zoneSettings; }
static const QVector<ReverbSettings>& getReverbSettings() { return _zoneReverbSettings; }
public slots:
void run() override;
void sendStatsPacket() override;
private slots:
// packet handlers
void handleNodeAudioPacket(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 start();
void removeHRTFsForFinishedInjector(const QUuid& streamID);
private:
// mixing helpers
std::chrono::microseconds timeFrame(p_high_resolution_clock::time_point& timestamp);
void throttle(std::chrono::microseconds frameDuration, int frame);
// pop a frame from any streams on the node
// returns the number of available streams
int prepareFrame(const SharedNodePointer& node, unsigned int frame);
AudioMixerClientData* getOrCreateClientData(Node* node);
QString percentageForMixStats(int counter);
void parseSettingsObject(const QJsonObject& settingsObject);
float _trailingMixRatio { 0.0f };
float _throttlingRatio { 0.0f };
int _numStatFrames { 0 };
AudioMixerStats _stats;
QString _codecPreferenceOrder;
AudioMixerSlavePool _slavePool;
class Timer {
public:
class Timing{
public:
Timing(uint64_t& sum);
~Timing();
private:
p_high_resolution_clock::time_point _timing;
uint64_t& _sum;
};
Timing timer() { return Timing(_sum); }
void get(uint64_t& timing, uint64_t& trailing);
private:
static const int TIMER_TRAILING_SECONDS = 10;
uint64_t _sum { 0 };
uint64_t _trailing { 0 };
uint64_t _history[TIMER_TRAILING_SECONDS] {};
int _index { 0 };
};
Timer _ticTiming;
Timer _sleepTiming;
Timer _frameTiming;
Timer _prepareTiming;
Timer _mixTiming;
Timer _eventsTiming;
static int _numStaticJitterFrames; // -1 denotes dynamic jitter buffering
static bool _enableFilter;
static float _noiseMutingThreshold;
static float _attenuationPerDoublingInDistance;
static QHash<QString, AABox> _audioZones;
static QVector<ZoneSettings> _zoneSettings;
static QVector<ReverbSettings> _zoneReverbSettings;
};
#endif // hifi_AudioMixer_h

View file

@ -365,10 +365,6 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() {
}
void AudioMixerClientData::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) {
qDebug() << __FUNCTION__ <<
"sendingNode:" << *node <<
"currentCodec:" << currentCodec <<
"receivedCodec:" << recievedCodec;
sendSelectAudioFormat(node, currentCodec);
}

View file

@ -86,6 +86,14 @@ public:
bool shouldFlushEncoder() { return _shouldFlushEncoder; }
QString getCodecName() { return _selectedCodecName; }
bool shouldMuteClient() { return _shouldMuteClient; }
void setShouldMuteClient(bool shouldMuteClient) { _shouldMuteClient = shouldMuteClient; }
glm::vec3 getPosition() { return getAvatarAudioStream() ? getAvatarAudioStream()->getPosition() : glm::vec3(0); }
glm::vec3 getAvatarBoundingBoxCorner() { return getAvatarAudioStream() ? getAvatarAudioStream()->getAvatarBoundingBoxCorner() : glm::vec3(0); }
glm::vec3 getAvatarBoundingBoxScale() { return getAvatarAudioStream() ? getAvatarAudioStream()->getAvatarBoundingBoxScale() : glm::vec3(0); }
bool getRequestsDomainListData() { return _requestsDomainListData; }
void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; }
signals:
void injectorStreamFinished(const QUuid& streamIdentifier);
@ -114,6 +122,9 @@ private:
Decoder* _decoder{ nullptr }; // for mic stream
bool _shouldFlushEncoder { false };
bool _shouldMuteClient { false };
bool _requestsDomainListData { false };
};
#endif // hifi_AudioMixerClientData_h

View file

@ -0,0 +1,557 @@
//
// AudioMixerSlave.cpp
// assignment-client/src/audio
//
// Created by Zach Pomerantz on 11/22/16.
// Copyright 2016 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 <algorithm>
#include <glm/glm.hpp>
#include <glm/gtx/norm.hpp>
#include <glm/gtx/vector_angle.hpp>
#include <LogHandler.h>
#include <NetworkAccessManager.h>
#include <NodeList.h>
#include <Node.h>
#include <OctreeConstants.h>
#include <plugins/PluginManager.h>
#include <plugins/CodecPlugin.h>
#include <udt/PacketHeaders.h>
#include <SharedUtil.h>
#include <StDev.h>
#include <UUID.h>
#include "AudioRingBuffer.h"
#include "AudioMixer.h"
#include "AudioMixerClientData.h"
#include "AvatarAudioStream.h"
#include "InjectedAudioStream.h"
#include "AudioHelpers.h"
#include "AudioMixerSlave.h"
using AudioStreamMap = AudioMixerClientData::AudioStreamMap;
// packet helpers
std::unique_ptr<NLPacket> createAudioPacket(PacketType type, int size, quint16 sequence, QString codec);
void sendMixPacket(const SharedNodePointer& node, AudioMixerClientData& data, QByteArray& buffer);
void sendSilentPacket(const SharedNodePointer& node, AudioMixerClientData& data);
void sendMutePacket(const SharedNodePointer& node, AudioMixerClientData&);
void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& data);
// mix helpers
bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node);
float gainForSource(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
const glm::vec3& relativePosition, bool isEcho);
float azimuthForSource(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
const glm::vec3& relativePosition);
void AudioMixerSlave::configure(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) {
_begin = begin;
_end = end;
_frame = frame;
_throttlingRatio = throttlingRatio;
}
void AudioMixerSlave::mix(const SharedNodePointer& node) {
// check that the node is valid
AudioMixerClientData* data = (AudioMixerClientData*)node->getLinkedData();
if (data == nullptr) {
return;
}
// check that the stream is valid
auto avatarStream = data->getAvatarAudioStream();
if (avatarStream == nullptr) {
return;
}
// send mute packet, if necessary
if (AudioMixer::shouldMute(avatarStream->getQuietestFrameLoudness()) || data->shouldMuteClient()) {
sendMutePacket(node, *data);
}
// send audio packets, if necessary
if (node->getType() == NodeType::Agent && node->getActiveSocket()) {
++stats.sumListeners;
// mix the audio
bool mixHasAudio = prepareMix(node);
// send audio packet
if (mixHasAudio || data->shouldFlushEncoder()) {
QByteArray encodedBuffer;
if (mixHasAudio) {
// encode the audio
QByteArray decodedBuffer(reinterpret_cast<char*>(_bufferSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO);
data->encode(decodedBuffer, encodedBuffer);
} else {
// time to flush (resets shouldFlush until the next encode)
data->encodeFrameOfZeros(encodedBuffer);
}
sendMixPacket(node, *data, encodedBuffer);
} else {
sendSilentPacket(node, *data);
}
// send environment packet
sendEnvironmentPacket(node, *data);
// send stats packet (about every second)
const unsigned int NUM_FRAMES_PER_SEC = (int)ceil(AudioConstants::NETWORK_FRAMES_PER_SEC);
if (data->shouldSendStats(_frame % NUM_FRAMES_PER_SEC)) {
data->sendAudioStreamStatsPackets(node);
}
}
}
bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) {
AvatarAudioStream* listenerAudioStream = static_cast<AudioMixerClientData*>(listener->getLinkedData())->getAvatarAudioStream();
AudioMixerClientData* listenerData = static_cast<AudioMixerClientData*>(listener->getLinkedData());
// zero out the mix for this listener
memset(_mixSamples, 0, sizeof(_mixSamples));
bool isThrottling = _throttlingRatio > 0.0f;
std::vector<std::pair<float, SharedNodePointer>> throttledNodes;
typedef void (AudioMixerSlave::*MixFunctor)(
AudioMixerClientData&, const QUuid&, const AvatarAudioStream&, const PositionalAudioStream&);
auto allStreams = [&](const SharedNodePointer& node, MixFunctor mixFunctor) {
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(node->getLinkedData());
for (auto& streamPair : nodeData->getAudioStreams()) {
auto nodeStream = streamPair.second;
(this->*mixFunctor)(*listenerData, node->getUUID(), *listenerAudioStream, *nodeStream);
}
};
std::for_each(_begin, _end, [&](const SharedNodePointer& node) {
if (*node == *listener) {
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(node->getLinkedData());
// only mix the echo, if requested
for (auto& streamPair : nodeData->getAudioStreams()) {
auto nodeStream = streamPair.second;
if (nodeStream->shouldLoopbackForNode()) {
mixStream(*listenerData, node->getUUID(), *listenerAudioStream, *nodeStream);
}
}
} else if (!shouldIgnoreNode(listener, node)) {
if (!isThrottling) {
allStreams(node, &AudioMixerSlave::mixStream);
} else {
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(node->getLinkedData());
// compute the node's max relative volume
float nodeVolume;
for (auto& streamPair : nodeData->getAudioStreams()) {
auto nodeStream = streamPair.second;
float distance = glm::length(nodeStream->getPosition() - listenerAudioStream->getPosition());
nodeVolume = std::max(nodeStream->getLastPopOutputTrailingLoudness() / distance, nodeVolume);
}
// max-heapify the nodes by relative volume
throttledNodes.push_back(std::make_pair(nodeVolume, node));
if (!throttledNodes.empty()) {
std::push_heap(throttledNodes.begin(), throttledNodes.end());
}
}
}
});
if (isThrottling) {
// pop the loudest nodes off the heap and mix their streams
int numToRetain = (int)(std::distance(_begin, _end) * (1 - _throttlingRatio));
for (int i = 0; i < numToRetain; i++) {
if (throttledNodes.empty()) {
break;
}
std::pop_heap(throttledNodes.begin(), throttledNodes.end());
auto& node = throttledNodes.back().second;
allStreams(node, &AudioMixerSlave::mixStream);
throttledNodes.pop_back();
}
// throttle the remaining nodes' streams
for (const std::pair<float, SharedNodePointer>& nodePair : throttledNodes) {
auto& node = nodePair.second;
allStreams(node, &AudioMixerSlave::throttleStream);
}
}
// use the per listener AudioLimiter to render the mixed data...
listenerData->audioLimiter.render(_mixSamples, _bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
// check for silent audio after the peak limiter has converted the samples
bool hasAudio = false;
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; ++i) {
if (_bufferSamples[i] != 0) {
hasAudio = true;
break;
}
}
return hasAudio;
}
void AudioMixerSlave::throttleStream(AudioMixerClientData& listenerNodeData, const QUuid& sourceNodeID,
const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd) {
addStream(listenerNodeData, sourceNodeID, listeningNodeStream, streamToAdd, true);
}
void AudioMixerSlave::mixStream(AudioMixerClientData& listenerNodeData, const QUuid& sourceNodeID,
const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd) {
addStream(listenerNodeData, sourceNodeID, listeningNodeStream, streamToAdd, false);
}
void AudioMixerSlave::addStream(AudioMixerClientData& listenerNodeData, const QUuid& sourceNodeID,
const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
bool throttle) {
++stats.totalMixes;
// to reduce artifacts we call the HRTF functor for every source, even if throttled or silent
// this ensures the correct tail from last mixed block and the correct spatialization of next first block
// check if this is a server echo of a source back to itself
bool isEcho = (&streamToAdd == &listeningNodeStream);
glm::vec3 relativePosition = streamToAdd.getPosition() - listeningNodeStream.getPosition();
float distance = glm::max(glm::length(relativePosition), EPSILON);
float gain = gainForSource(listeningNodeStream, streamToAdd, relativePosition, isEcho);
float azimuth = isEcho ? 0.0f : azimuthForSource(listeningNodeStream, listeningNodeStream, relativePosition);
static const int HRTF_DATASET_INDEX = 1;
if (!streamToAdd.lastPopSucceeded()) {
bool forceSilentBlock = true;
if (!streamToAdd.getLastPopOutput().isNull()) {
bool isInjector = dynamic_cast<const InjectedAudioStream*>(&streamToAdd);
// in an injector, just go silent - the injector has likely ended
// in other inputs (microphone, &c.), repeat with fade to avoid the harsh jump to silence
if (!isInjector) {
// calculate its fade factor, which depends on how many times it's already been repeated.
float fadeFactor = calculateRepeatedFrameFadeFactor(streamToAdd.getConsecutiveNotMixedCount() - 1);
if (fadeFactor > 0.0f) {
// apply the fadeFactor to the gain
gain *= fadeFactor;
forceSilentBlock = false;
}
}
}
if (forceSilentBlock) {
// call renderSilent with a forced silent block to reduce artifacts
// (this is not done for stereo streams since they do not go through the HRTF)
if (!streamToAdd.isStereo() && !isEcho) {
// get the existing listener-source HRTF object, or create a new one
auto& hrtf = listenerNodeData.hrtfForStream(sourceNodeID, streamToAdd.getStreamIdentifier());
static int16_t silentMonoBlock[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL] = {};
hrtf.renderSilent(silentMonoBlock, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
++stats.hrtfSilentRenders;
}
return;
}
}
// grab the stream from the ring buffer
AudioRingBuffer::ConstIterator streamPopOutput = streamToAdd.getLastPopOutput();
// stereo sources are not passed through HRTF
if (streamToAdd.isStereo()) {
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; ++i) {
_mixSamples[i] += float(streamPopOutput[i] * gain / AudioConstants::MAX_SAMPLE_VALUE);
}
++stats.manualStereoMixes;
return;
}
// echo sources are not passed through HRTF
if (isEcho) {
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i += 2) {
auto monoSample = float(streamPopOutput[i / 2] * gain / AudioConstants::MAX_SAMPLE_VALUE);
_mixSamples[i] += monoSample;
_mixSamples[i + 1] += monoSample;
}
++stats.manualEchoMixes;
return;
}
// get the existing listener-source HRTF object, or create a new one
auto& hrtf = listenerNodeData.hrtfForStream(sourceNodeID, streamToAdd.getStreamIdentifier());
streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
if (streamToAdd.getLastPopOutputLoudness() == 0.0f) {
// call renderSilent to reduce artifacts
hrtf.renderSilent(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
++stats.hrtfSilentRenders;
return;
}
if (throttle) {
// call renderSilent with actual frame data and a gain of 0.0f to reduce artifacts
hrtf.renderSilent(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, 0.0f,
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
++stats.hrtfThrottleRenders;
return;
}
hrtf.render(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
++stats.hrtfRenders;
}
std::unique_ptr<NLPacket> createAudioPacket(PacketType type, int size, quint16 sequence, QString codec) {
auto audioPacket = NLPacket::create(type, size);
audioPacket->writePrimitive(sequence);
audioPacket->writeString(codec);
return audioPacket;
}
void sendMixPacket(const SharedNodePointer& node, AudioMixerClientData& data, QByteArray& buffer) {
static const int MIX_PACKET_SIZE =
sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE + AudioConstants::NETWORK_FRAME_BYTES_STEREO;
quint16 sequence = data.getOutgoingSequenceNumber();
QString codec = data.getCodecName();
auto mixPacket = createAudioPacket(PacketType::MixedAudio, MIX_PACKET_SIZE, sequence, codec);
// pack samples
mixPacket->write(buffer.constData(), buffer.size());
// send packet
DependencyManager::get<NodeList>()->sendPacket(std::move(mixPacket), *node);
data.incrementOutgoingMixedAudioSequenceNumber();
}
void sendSilentPacket(const SharedNodePointer& node, AudioMixerClientData& data) {
static const int SILENT_PACKET_SIZE =
sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE + sizeof(quint16);
quint16 sequence = data.getOutgoingSequenceNumber();
QString codec = data.getCodecName();
auto mixPacket = createAudioPacket(PacketType::SilentAudioFrame, SILENT_PACKET_SIZE, sequence, codec);
// pack number of samples
mixPacket->writePrimitive(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
// send packet
DependencyManager::get<NodeList>()->sendPacket(std::move(mixPacket), *node);
data.incrementOutgoingMixedAudioSequenceNumber();
}
void sendMutePacket(const SharedNodePointer& node, AudioMixerClientData& data) {
auto mutePacket = NLPacket::create(PacketType::NoisyMute, 0);
DependencyManager::get<NodeList>()->sendPacket(std::move(mutePacket), *node);
// probably now we just reset the flag, once should do it (?)
data.setShouldMuteClient(false);
}
void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& data) {
bool hasReverb = false;
float reverbTime, wetLevel;
auto& reverbSettings = AudioMixer::getReverbSettings();
auto& audioZones = AudioMixer::getAudioZones();
AvatarAudioStream* stream = data.getAvatarAudioStream();
glm::vec3 streamPosition = stream->getPosition();
// find reverb properties
for (int i = 0; i < reverbSettings.size(); ++i) {
AABox box = audioZones[reverbSettings[i].zone];
if (box.contains(streamPosition)) {
hasReverb = true;
reverbTime = reverbSettings[i].reverbTime;
wetLevel = reverbSettings[i].wetLevel;
break;
}
}
// check if data changed
bool dataChanged = (stream->hasReverb() != hasReverb) ||
(stream->hasReverb() && (stream->getRevebTime() != reverbTime || stream->getWetLevel() != wetLevel));
if (dataChanged) {
// update stream
if (hasReverb) {
stream->setReverb(reverbTime, wetLevel);
} else {
stream->clearReverb();
}
}
// send packet at change or every so often
float CHANCE_OF_SEND = 0.01f;
bool sendData = dataChanged || (randFloat() < CHANCE_OF_SEND);
if (sendData) {
// size the packet
unsigned char bitset = 0;
int packetSize = sizeof(bitset);
if (hasReverb) {
packetSize += sizeof(reverbTime) + sizeof(wetLevel);
}
// write the packet
auto envPacket = NLPacket::create(PacketType::AudioEnvironment, packetSize);
if (hasReverb) {
setAtBit(bitset, HAS_REVERB_BIT);
}
envPacket->writePrimitive(bitset);
if (hasReverb) {
envPacket->writePrimitive(reverbTime);
envPacket->writePrimitive(wetLevel);
}
// send the packet
DependencyManager::get<NodeList>()->sendPacket(std::move(envPacket), *node);
}
}
bool shouldIgnoreNode(const SharedNodePointer& listener, const SharedNodePointer& node) {
AudioMixerClientData* listenerData = static_cast<AudioMixerClientData*>(listener->getLinkedData());
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(node->getLinkedData());
// when this is true, the AudioMixer will send Audio data to a client about avatars that have ignored them
bool getsAnyIgnored = listenerData->getRequestsDomainListData() && listener->getCanKick();
bool ignore = true;
if (nodeData &&
// make sure that it isn't being ignored by our listening node
(!listener->isIgnoringNodeWithID(node->getUUID()) || (nodeData->getRequestsDomainListData() && node->getCanKick())) &&
// and that it isn't ignoring our listening node
(!node->isIgnoringNodeWithID(listener->getUUID()) || getsAnyIgnored)) {
// is either node enabling the space bubble / ignore radius?
if ((listener->isIgnoreRadiusEnabled() || node->isIgnoreRadiusEnabled())) {
// define the minimum bubble size
static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f);
// set up the bounding box for the listener
AABox listenerBox(listenerData->getAvatarBoundingBoxCorner(), listenerData->getAvatarBoundingBoxScale());
if (glm::any(glm::lessThan(listenerData->getAvatarBoundingBoxScale(), minBubbleSize))) {
listenerBox.setScaleStayCentered(minBubbleSize);
}
// set up the bounding box for the node
AABox nodeBox(nodeData->getAvatarBoundingBoxCorner(), nodeData->getAvatarBoundingBoxScale());
// Clamp the size of the bounding box to a minimum scale
if (glm::any(glm::lessThan(nodeData->getAvatarBoundingBoxScale(), minBubbleSize))) {
nodeBox.setScaleStayCentered(minBubbleSize);
}
// quadruple the scale of both bounding boxes
listenerBox.embiggen(4.0f);
nodeBox.embiggen(4.0f);
// perform the collision check between the two bounding boxes
ignore = listenerBox.touches(nodeBox);
} else {
ignore = false;
}
}
return ignore;
}
float gainForSource(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
const glm::vec3& relativePosition, bool isEcho) {
float gain = 1.0f;
float distanceBetween = glm::length(relativePosition);
if (distanceBetween < EPSILON) {
distanceBetween = EPSILON;
}
if (streamToAdd.getType() == PositionalAudioStream::Injector) {
gain *= reinterpret_cast<const InjectedAudioStream*>(&streamToAdd)->getAttenuationRatio();
}
if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) {
// source is another avatar, apply fixed off-axis attenuation to make them quieter as they turn away from listener
glm::vec3 rotatedListenerPosition = glm::inverse(streamToAdd.getOrientation()) * relativePosition;
float angleOfDelivery = glm::angle(glm::vec3(0.0f, 0.0f, -1.0f),
glm::normalize(rotatedListenerPosition));
const float MAX_OFF_AXIS_ATTENUATION = 0.2f;
const float OFF_AXIS_ATTENUATION_FORMULA_STEP = (1 - MAX_OFF_AXIS_ATTENUATION) / 2.0f;
float offAxisCoefficient = MAX_OFF_AXIS_ATTENUATION +
(OFF_AXIS_ATTENUATION_FORMULA_STEP * (angleOfDelivery / PI_OVER_TWO));
// multiply the current attenuation coefficient by the calculated off axis coefficient
gain *= offAxisCoefficient;
}
float attenuationPerDoublingInDistance = AudioMixer::getAttenuationPerDoublingInDistance();
auto& zoneSettings = AudioMixer::getZoneSettings();
auto& audioZones = AudioMixer::getAudioZones();
for (int i = 0; i < zoneSettings.length(); ++i) {
if (audioZones[zoneSettings[i].source].contains(streamToAdd.getPosition()) &&
audioZones[zoneSettings[i].listener].contains(listeningNodeStream.getPosition())) {
attenuationPerDoublingInDistance = zoneSettings[i].coefficient;
break;
}
}
const float ATTENUATION_BEGINS_AT_DISTANCE = 1.0f;
if (distanceBetween >= ATTENUATION_BEGINS_AT_DISTANCE) {
// translate the zone setting to gain per log2(distance)
float g = 1.0f - attenuationPerDoublingInDistance;
g = (g < EPSILON) ? EPSILON : g;
g = (g > 1.0f) ? 1.0f : g;
// calculate the distance coefficient using the distance to this node
float distanceCoefficient = fastExp2f(fastLog2f(g) * fastLog2f(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE));
// multiply the current attenuation coefficient by the distance coefficient
gain *= distanceCoefficient;
}
return gain;
}
float azimuthForSource(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
const glm::vec3& relativePosition) {
glm::quat inverseOrientation = glm::inverse(listeningNodeStream.getOrientation());
// Compute sample delay for the two ears to create phase panning
glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition;
// project the rotated source position vector onto the XZ plane
rotatedSourcePosition.y = 0.0f;
const float SOURCE_DISTANCE_THRESHOLD = 1e-30f;
if (glm::length2(rotatedSourcePosition) > SOURCE_DISTANCE_THRESHOLD) {
// produce an oriented angle about the y-axis
return glm::orientedAngle(glm::vec3(0.0f, 0.0f, -1.0f), glm::normalize(rotatedSourcePosition), glm::vec3(0.0f, -1.0f, 0.0f));
} else {
// there is no distance between listener and source - return no azimuth
return 0;
}
}

View file

@ -0,0 +1,63 @@
//
// AudioMixerSlave.h
// assignment-client/src/audio
//
// Created by Zach Pomerantz on 11/22/16.
// Copyright 2016 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_AudioMixerSlave_h
#define hifi_AudioMixerSlave_h
#include <AABox.h>
#include <AudioHRTF.h>
#include <AudioRingBuffer.h>
#include <ThreadedAssignment.h>
#include <UUIDHasher.h>
#include <NodeList.h>
#include "AudioMixerStats.h"
class PositionalAudioStream;
class AvatarAudioStream;
class AudioHRTF;
class AudioMixerClientData;
class AudioMixerSlave {
public:
using ConstIter = NodeList::const_iterator;
void configure(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio);
// mix and broadcast non-ignored streams to the node
// returns true if a mixed packet was sent to the node
void mix(const SharedNodePointer& node);
AudioMixerStats stats;
private:
// create mix, returns true if mix has audio
bool prepareMix(const SharedNodePointer& listener);
void throttleStream(AudioMixerClientData& listenerData, const QUuid& streamerID,
const AvatarAudioStream& listenerStream, const PositionalAudioStream& streamer);
void mixStream(AudioMixerClientData& listenerData, const QUuid& streamerID,
const AvatarAudioStream& listenerStream, const PositionalAudioStream& streamer);
void addStream(AudioMixerClientData& listenerData, const QUuid& streamerID,
const AvatarAudioStream& listenerStream, const PositionalAudioStream& streamer,
bool throttle);
// mixing buffers
float _mixSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
int16_t _bufferSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
// frame state
ConstIter _begin;
ConstIter _end;
unsigned int _frame { 0 };
float _throttlingRatio { 0.0f };
};
#endif // hifi_AudioMixerSlave_h

View file

@ -0,0 +1,188 @@
//
// AudioMixerSlavePool.cpp
// assignment-client/src/audio
//
// Created by Zach Pomerantz on 11/16/2016.
// Copyright 2016 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 <assert.h>
#include <algorithm>
#include "AudioMixerSlavePool.h"
void AudioMixerSlaveThread::run() {
while (true) {
wait();
// iterate over all available nodes
SharedNodePointer node;
while (try_pop(node)) {
mix(node);
}
bool stopping = _stop;
notify(stopping);
if (stopping) {
return;
}
}
}
void AudioMixerSlaveThread::wait() {
{
Lock lock(_pool._mutex);
_pool._slaveCondition.wait(lock, [&] {
assert(_pool._numStarted <= _pool._numThreads);
return _pool._numStarted != _pool._numThreads;
});
++_pool._numStarted;
}
configure(_pool._begin, _pool._end, _pool._frame, _pool._throttlingRatio);
}
void AudioMixerSlaveThread::notify(bool stopping) {
{
Lock lock(_pool._mutex);
assert(_pool._numFinished < _pool._numThreads);
++_pool._numFinished;
if (stopping) {
++_pool._numStopped;
}
}
_pool._poolCondition.notify_one();
}
bool AudioMixerSlaveThread::try_pop(SharedNodePointer& node) {
return _pool._queue.try_pop(node);
}
#ifdef AUDIO_SINGLE_THREADED
static AudioMixerSlave slave;
#endif
void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) {
_begin = begin;
_end = end;
_frame = frame;
_throttlingRatio = throttlingRatio;
#ifdef AUDIO_SINGLE_THREADED
slave.configure(_begin, _end, frame, throttlingRatio);
std::for_each(begin, end, [&](const SharedNodePointer& node) {
slave.mix(node);
});
#else
// fill the queue
std::for_each(_begin, _end, [&](const SharedNodePointer& node) {
_queue.emplace(node);
});
{
Lock lock(_mutex);
// mix
_numStarted = _numFinished = 0;
_slaveCondition.notify_all();
// wait
_poolCondition.wait(lock, [&] {
assert(_numFinished <= _numThreads);
return _numFinished == _numThreads;
});
assert(_numStarted == _numThreads);
}
assert(_queue.empty());
#endif
}
void AudioMixerSlavePool::each(std::function<void(AudioMixerSlave& slave)> functor) {
#ifdef AUDIO_SINGLE_THREADED
functor(slave);
#else
for (auto& slave : _slaves) {
functor(*slave.get());
}
#endif
}
void AudioMixerSlavePool::setNumThreads(int numThreads) {
// clamp to allowed size
{
int maxThreads = QThread::idealThreadCount();
if (maxThreads == -1) {
// idealThreadCount returns -1 if cores cannot be detected
static const int MAX_THREADS_IF_UNKNOWN = 4;
maxThreads = MAX_THREADS_IF_UNKNOWN;
}
int clampedThreads = std::min(std::max(1, numThreads), maxThreads);
if (clampedThreads != numThreads) {
qWarning("%s: clamped to %d (was %d)", __FUNCTION__, clampedThreads, numThreads);
numThreads = clampedThreads;
}
}
resize(numThreads);
}
void AudioMixerSlavePool::resize(int numThreads) {
assert(_numThreads == (int)_slaves.size());
#ifdef AUDIO_SINGLE_THREADED
qDebug("%s: running single threaded", __FUNCTION__, numThreads);
#else
qDebug("%s: set %d threads (was %d)", __FUNCTION__, numThreads, _numThreads);
Lock lock(_mutex);
if (numThreads > _numThreads) {
// start new slaves
for (int i = 0; i < numThreads - _numThreads; ++i) {
auto slave = new AudioMixerSlaveThread(*this);
slave->start();
_slaves.emplace_back(slave);
}
} else if (numThreads < _numThreads) {
auto extraBegin = _slaves.begin() + numThreads;
// mark slaves to stop...
auto slave = extraBegin;
while (slave != _slaves.end()) {
(*slave)->_stop = true;
++slave;
}
// ...cycle them until they do stop...
_numStopped = 0;
while (_numStopped != (_numThreads - numThreads)) {
_numStarted = _numFinished = _numStopped;
_slaveCondition.notify_all();
_poolCondition.wait(lock, [&] {
assert(_numFinished <= _numThreads);
return _numFinished == _numThreads;
});
}
// ...wait for threads to finish...
slave = extraBegin;
while (slave != _slaves.end()) {
QThread* thread = reinterpret_cast<QThread*>(slave->get());
static const int MAX_THREAD_WAIT_TIME = 10;
thread->wait(MAX_THREAD_WAIT_TIME);
++slave;
}
// ...and erase them
_slaves.erase(extraBegin, _slaves.end());
}
_numThreads = _numStarted = _numFinished = numThreads;
assert(_numThreads == (int)_slaves.size());
#endif
}

View file

@ -0,0 +1,98 @@
//
// AudioMixerSlavePool.h
// assignment-client/src/audio
//
// Created by Zach Pomerantz on 11/16/2016.
// Copyright 2016 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_AudioMixerSlavePool_h
#define hifi_AudioMixerSlavePool_h
#include <condition_variable>
#include <mutex>
#include <vector>
#include <tbb/concurrent_queue.h>
#include <QThread>
#include "AudioMixerSlave.h"
class AudioMixerSlavePool;
class AudioMixerSlaveThread : public QThread, public AudioMixerSlave {
Q_OBJECT
using ConstIter = NodeList::const_iterator;
using Mutex = std::mutex;
using Lock = std::unique_lock<Mutex>;
public:
AudioMixerSlaveThread(AudioMixerSlavePool& pool) : _pool(pool) {}
void run() override final;
private:
friend class AudioMixerSlavePool;
void wait();
void notify(bool stopping);
bool try_pop(SharedNodePointer& node);
AudioMixerSlavePool& _pool;
bool _stop { false };
};
// Slave pool for audio mixers
// AudioMixerSlavePool is not thread-safe! It should be instantiated and used from a single thread.
class AudioMixerSlavePool {
using Queue = tbb::concurrent_queue<SharedNodePointer>;
using Mutex = std::mutex;
using Lock = std::unique_lock<Mutex>;
using ConditionVariable = std::condition_variable;
public:
using ConstIter = NodeList::const_iterator;
AudioMixerSlavePool(int numThreads = QThread::idealThreadCount()) { setNumThreads(numThreads); }
~AudioMixerSlavePool() { resize(0); }
// mix on slave threads
void mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio);
// iterate over all slaves
void each(std::function<void(AudioMixerSlave& slave)> functor);
void setNumThreads(int numThreads);
int numThreads() { return _numThreads; }
private:
void resize(int numThreads);
std::vector<std::unique_ptr<AudioMixerSlaveThread>> _slaves;
friend void AudioMixerSlaveThread::wait();
friend void AudioMixerSlaveThread::notify(bool stopping);
friend bool AudioMixerSlaveThread::try_pop(SharedNodePointer& node);
// synchronization state
Mutex _mutex;
ConditionVariable _slaveCondition;
ConditionVariable _poolCondition;
int _numThreads { 0 };
int _numStarted { 0 }; // guarded by _mutex
int _numFinished { 0 }; // guarded by _mutex
int _numStopped { 0 }; // guarded by _mutex
// frame state
Queue _queue;
unsigned int _frame { 0 };
float _throttlingRatio { 0.0f };
ConstIter _begin;
ConstIter _end;
};
#endif // hifi_AudioMixerSlavePool_h

View file

@ -0,0 +1,34 @@
//
// AudioMixerStats.cpp
// assignment-client/src/audio
//
// Created by Zach Pomerantz on 11/22/16.
// Copyright 2016 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 "AudioMixerStats.h"
void AudioMixerStats::reset() {
sumStreams = 0;
sumListeners = 0;
totalMixes = 0;
hrtfRenders = 0;
hrtfSilentRenders = 0;
hrtfThrottleRenders = 0;
manualStereoMixes = 0;
manualEchoMixes = 0;
}
void AudioMixerStats::accumulate(const AudioMixerStats& otherStats) {
sumStreams += otherStats.sumStreams;
sumListeners += otherStats.sumListeners;
totalMixes += otherStats.totalMixes;
hrtfRenders += otherStats.hrtfRenders;
hrtfSilentRenders += otherStats.hrtfSilentRenders;
hrtfThrottleRenders += otherStats.hrtfThrottleRenders;
manualStereoMixes += otherStats.manualStereoMixes;
manualEchoMixes += otherStats.manualEchoMixes;
}

View file

@ -0,0 +1,32 @@
//
// AudioMixerStats.h
// assignment-client/src/audio
//
// Created by Zach Pomerantz on 11/22/16.
// Copyright 2016 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_AudioMixerStats_h
#define hifi_AudioMixerStats_h
struct AudioMixerStats {
int sumStreams { 0 };
int sumListeners { 0 };
int totalMixes { 0 };
int hrtfRenders { 0 };
int hrtfSilentRenders { 0 };
int hrtfThrottleRenders { 0 };
int manualStereoMixes { 0 };
int manualEchoMixes { 0 };
void reset();
void accumulate(const AudioMixerStats& otherStats);
};
#endif // hifi_AudioMixerStats_h

View file

@ -16,9 +16,11 @@
#include <QtCore/QCoreApplication>
#include <QtCore/QDateTime>
#include <QtCore/QJsonObject>
#include <QtCore/QRegularExpression>
#include <QtCore/QTimer>
#include <QtCore/QThread>
#include <AABox.h>
#include <LogHandler.h>
#include <NodeList.h>
#include <udt/PacketHeaders.h>
@ -26,12 +28,12 @@
#include <UUID.h>
#include <TryLocker.h>
#include "AvatarMixerClientData.h"
#include "AvatarMixer.h"
const QString AVATAR_MIXER_LOGGING_NAME = "avatar-mixer";
const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 60;
// FIXME - what we'd actually like to do is send to users at ~50% of their present rate down to 30hz. Assume 90 for now.
const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45;
const unsigned int AVATAR_DATA_SEND_INTERVAL_MSECS = (1.0f / (float) AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND) * 1000;
AvatarMixer::AvatarMixer(ReceivedMessage& message) :
@ -42,10 +44,13 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
connect(DependencyManager::get<NodeList>().data(), &NodeList::nodeKilled, this, &AvatarMixer::nodeKilled);
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListener(PacketType::ViewFrustum, this, "handleViewFrustumPacket");
packetReceiver.registerListener(PacketType::AvatarData, this, "handleAvatarDataPacket");
packetReceiver.registerListener(PacketType::AvatarIdentity, this, "handleAvatarIdentityPacket");
packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket");
packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
auto nodeList = DependencyManager::get<NodeList>();
connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch);
@ -64,10 +69,26 @@ AvatarMixer::~AvatarMixer() {
// assuming 60 htz update rate.
const float IDENTITY_SEND_PROBABILITY = 1.0f / 187.0f;
void AvatarMixer::sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode) {
QByteArray individualData = nodeData->getAvatar().identityByteArray();
auto identityPacket = NLPacket::create(PacketType::AvatarIdentity, individualData.size());
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122());
identityPacket->write(individualData);
DependencyManager::get<NodeList>()->sendPacket(std::move(identityPacket), *destinationNode);
++_sumIdentityPackets;
}
// NOTE: some additional optimizations to consider.
// 1) use the view frustum to cull those avatars that are out of view. Since avatar data doesn't need to be present
// if the avatar is not in view or in the keyhole.
void AvatarMixer::broadcastAvatarData() {
_broadcastRate.increment();
int idleTime = AVATAR_DATA_SEND_INTERVAL_MSECS;
if (_lastFrameTimestamp.time_since_epoch().count() > 0) {
@ -88,6 +109,13 @@ void AvatarMixer::broadcastAvatarData() {
const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES;
const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO;
// only send extra avatar data (avatars out of view, ignored) every Nth AvatarData frame
// Extra avatar data will be sent (AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND/EXTRA_AVATAR_DATA_FRAME_RATIO) times
// per second.
// This value should be a power of two for performance purposes, as the mixer performs a modulo operation every frame
// to determine whether the extra data should be sent.
const int EXTRA_AVATAR_DATA_FRAME_RATIO = 16;
// NOTE: The following code calculates the _performanceThrottlingRatio based on how much the avatar-mixer was
// able to sleep. This will eventually be used to ask for an additional avatar-mixer to help out. Currently the value
// is unused as it is assumed this should not be hit before the avatar-mixer hits the desired bandwidth limit per client.
@ -156,6 +184,7 @@ void AvatarMixer::broadcastAvatarData() {
return;
}
++_sumListeners;
nodeData->resetInViewStats();
AvatarData& avatar = nodeData->getAvatar();
glm::vec3 myPosition = avatar.getClientGlobalPosition();
@ -184,6 +213,15 @@ void AvatarMixer::broadcastAvatarData() {
// use the data rate specifically for avatar data for FRD adjustment checks
float avatarDataRateLastSecond = nodeData->getOutboundAvatarDataKbps();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that are not in the view frustrum
bool getsOutOfView = nodeData->getRequestsDomainListData();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that they've ignored
bool getsIgnoredByMe = getsOutOfView;
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them
bool getsAnyIgnored = getsIgnoredByMe && node->getCanKick();
// Check if it is time to adjust what we send this client based on the observed
// bandwidth to this node. We do this once a second, which is also the window for
// the bandwidth reported by node->getOutboundBandwidth();
@ -224,17 +262,82 @@ void AvatarMixer::broadcastAvatarData() {
// setup a PacketList for the avatarPackets
auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData);
if (nodeData->getAvatarSessionDisplayNameMustChange()) {
const QString& existingBaseDisplayName = nodeData->getBaseDisplayName();
if (--_sessionDisplayNames[existingBaseDisplayName].second <= 0) {
_sessionDisplayNames.remove(existingBaseDisplayName);
}
QString baseName = avatar.getDisplayName().trimmed();
const QRegularExpression curses{ "fuck|shit|damn|cock|cunt" }; // POC. We may eventually want something much more elaborate (subscription?).
baseName = baseName.replace(curses, "*"); // Replace rather than remove, so that people have a clue that the person's a jerk.
const QRegularExpression trailingDigits{ "\\s*_\\d+$" }; // whitespace "_123"
baseName = baseName.remove(trailingDigits);
if (baseName.isEmpty()) {
baseName = "anonymous";
}
QPair<int, int>& soFar = _sessionDisplayNames[baseName]; // Inserts and answers 0, 0 if not already present, which is what we want.
int& highWater = soFar.first;
nodeData->setBaseDisplayName(baseName);
QString sessionDisplayName = (highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName;
avatar.setSessionDisplayName(sessionDisplayName);
highWater++;
soFar.second++; // refcount
nodeData->flagIdentityChange();
nodeData->setAvatarSessionDisplayNameMustChange(false);
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name. Others will find out below.
qDebug() << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID();
}
// this is an AGENT we have received head data from
// send back a packet with other active node data to this node
nodeList->eachMatchingNode(
[&](const SharedNodePointer& otherNode)->bool {
// make sure we have data for this avatar, that it isn't the same node,
// and isn't an avatar that the viewing node has ignored
// or that has ignored the viewing node
if (!otherNode->getLinkedData()
|| otherNode->getUUID() == node->getUUID()
|| node->isIgnoringNodeWithID(otherNode->getUUID())) {
|| (node->isIgnoringNodeWithID(otherNode->getUUID()) && !getsIgnoredByMe)
|| (otherNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) {
return false;
} else {
AvatarMixerClientData* otherData = reinterpret_cast<AvatarMixerClientData*>(otherNode->getLinkedData());
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
// Check to see if the space bubble is enabled
if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) {
// Define the minimum bubble size
static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f);
// Define the scale of the box for the current node
glm::vec3 nodeBoxScale = (nodeData->getPosition() - nodeData->getGlobalBoundingBoxCorner()) * 2.0f;
// Define the scale of the box for the current other node
glm::vec3 otherNodeBoxScale = (otherData->getPosition() - otherData->getGlobalBoundingBoxCorner()) * 2.0f;
// Set up the bounding box for the current node
AABox nodeBox(nodeData->getGlobalBoundingBoxCorner(), nodeBoxScale);
// Clamp the size of the bounding box to a minimum scale
if (glm::any(glm::lessThan(nodeBoxScale, minBubbleSize))) {
nodeBox.setScaleStayCentered(minBubbleSize);
}
// Set up the bounding box for the current other node
AABox otherNodeBox(otherData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
// Clamp the size of the bounding box to a minimum scale
if (glm::any(glm::lessThan(otherNodeBoxScale, minBubbleSize))) {
otherNodeBox.setScaleStayCentered(minBubbleSize);
}
// Quadruple the scale of both bounding boxes
nodeBox.embiggen(4.0f);
otherNodeBox.embiggen(4.0f);
// Perform the collision check between the two bounding boxes
if (nodeBox.touches(otherNodeBox)) {
nodeData->ignoreOther(node, otherNode);
return getsAnyIgnored;
}
}
// Not close enough to ignore
nodeData->removeFromRadiusIgnoringSet(node, otherNode->getUUID());
return true;
}
},
@ -254,18 +357,7 @@ void AvatarMixer::broadcastAvatarData() {
&& (forceSend
|| otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp
|| distribution(generator) < IDENTITY_SEND_PROBABILITY)) {
QByteArray individualData = otherNodeData->getAvatar().identityByteArray();
auto identityPacket = NLPacket::create(PacketType::AvatarIdentity, individualData.size());
individualData.replace(0, NUM_BYTES_RFC4122_UUID, otherNode->getUUID().toRfc4122());
identityPacket->write(individualData);
nodeList->sendPacket(std::move(identityPacket), *node);
++_sumIdentityPackets;
sendIdentityPacket(otherNodeData, node);
}
AvatarData& otherAvatar = otherNodeData->getAvatar();
@ -280,6 +372,7 @@ void AvatarMixer::broadcastAvatarData() {
maxAvatarDistanceThisFrame = std::max(maxAvatarDistanceThisFrame, distanceToAvatar);
if (distanceToAvatar != 0.0f
&& !getsOutOfView
&& distribution(generator) > (nodeData->getFullRateDistance() / distanceToAvatar)) {
return;
}
@ -311,12 +404,36 @@ void AvatarMixer::broadcastAvatarData() {
nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(),
otherNodeData->getLastReceivedSequenceNumber());
// determine if avatar is in view, to determine how much data to include...
glm::vec3 otherNodeBoxScale = (otherNodeData->getPosition() - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
bool isInView = nodeData->otherAvatarInView(otherNodeBox);
// this throttles the extra data to only be sent every Nth message
if (!isInView && getsOutOfView && (lastSeqToReceiver % EXTRA_AVATAR_DATA_FRAME_RATIO > 0)) {
return;
}
// start a new segment in the PacketList for this avatar
avatarPacketList->startSegment();
AvatarData::AvatarDataDetail detail;
if (!isInView && !getsOutOfView) {
detail = AvatarData::MinimumData;
nodeData->incrementAvatarOutOfView();
} else {
detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO
? AvatarData::SendAllData : AvatarData::CullSmallData;
nodeData->incrementAvatarInView();
}
numAvatarDataBytes += avatarPacketList->write(otherNode->getUUID().toRfc4122());
numAvatarDataBytes +=
avatarPacketList->write(otherAvatar.toByteArray(false, distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO));
auto lastEncodeForOther = nodeData->getLastOtherAvatarEncodeTime(otherNode->getUUID());
QVector<JointData>& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID());
bool distanceAdjust = true;
glm::vec3 viewerPosition = nodeData->getPosition();
auto bytes = otherAvatar.toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, distanceAdjust, viewerPosition, &lastSentJointsForOther);
numAvatarDataBytes += avatarPacketList->write(bytes);
avatarPacketList->endSegment();
});
@ -345,6 +462,9 @@ void AvatarMixer::broadcastAvatarData() {
// We're done encoding this version of the otherAvatars. Update their "lastSent" joint-states so
// that we can notice differences, next time around.
//
// FIXME - this seems suspicious, the code seems to consider all avatars, but not all avatars will
// have had their joints sent, so actually we should consider the time since they actually were sent????
nodeList->eachMatchingNode(
[&](const SharedNodePointer& otherNode)->bool {
if (!otherNode->getLinkedData()) {
@ -369,6 +489,18 @@ void AvatarMixer::broadcastAvatarData() {
});
_lastFrameTimestamp = p_high_resolution_clock::now();
#ifdef WANT_DEBUG
auto sinceLastDebug = p_high_resolution_clock::now() - _lastDebugMessage;
auto sinceLastDebugUsecs = std::chrono::duration_cast<std::chrono::microseconds>(sinceLastDebug).count();
quint64 DEBUG_INTERVAL = USECS_PER_SECOND * 5;
if (sinceLastDebugUsecs > DEBUG_INTERVAL) {
qDebug() << "broadcast rate:" << _broadcastRate.rate() << "hz";
_lastDebugMessage = p_high_resolution_clock::now();
}
#endif
}
void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
@ -376,10 +508,21 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
&& killedNode->getLinkedData()) {
auto nodeList = DependencyManager::get<NodeList>();
{ // decrement sessionDisplayNames table and possibly remove
QMutexLocker nodeDataLocker(&killedNode->getLinkedData()->getMutex());
AvatarMixerClientData* nodeData = dynamic_cast<AvatarMixerClientData*>(killedNode->getLinkedData());
const QString& baseDisplayName = nodeData->getBaseDisplayName();
// No sense guarding against very rare case of a node with no entry, as this will work without the guard and do one less lookup in the common case.
if (--_sessionDisplayNames[baseDisplayName].second <= 0) {
_sessionDisplayNames.remove(baseDisplayName);
}
}
// this was an avatar we were sending to other people
// send a kill packet for it to our other nodes
auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID);
auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
killPacket->write(killedNode->getUUID().toRfc4122());
killPacket->writePrimitive(KillAvatarReason::AvatarDisconnected);
nodeList->broadcastToNodes(std::move(killPacket), NodeSet() << NodeType::Agent);
@ -407,6 +550,32 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
}
}
void AvatarMixer::handleViewFrustumPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->getOrCreateLinkedData(senderNode);
if (senderNode->getLinkedData()) {
AvatarMixerClientData* nodeData = dynamic_cast<AvatarMixerClientData*>(senderNode->getLinkedData());
if (nodeData != nullptr) {
nodeData->readViewFrustumPacket(message->getMessage());
}
}
}
void AvatarMixer::handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->getOrCreateLinkedData(senderNode);
if (senderNode->getLinkedData()) {
AvatarMixerClientData* nodeData = dynamic_cast<AvatarMixerClientData*>(senderNode->getLinkedData());
if (nodeData != nullptr) {
bool isRequesting;
message->readPrimitive(&isRequesting);
nodeData->setRequestsDomainListData(isRequesting);
}
}
}
void AvatarMixer::handleAvatarDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->updateNodeWithDataFromPacket(message, senderNode);
@ -427,6 +596,7 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
if (avatar.processAvatarIdentity(identity)) {
QMutexLocker nodeDataLocker(&nodeData->getMutex());
nodeData->flagIdentityChange();
nodeData->setAvatarSessionDisplayNameMustChange(true);
}
}
}
@ -440,6 +610,10 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage>
senderNode->parseIgnoreRequestMessage(message);
}
void AvatarMixer::handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
sendingNode->parseIgnoreRadiusRequestMessage(packet);
}
void AvatarMixer::sendStatsPacket() {
QJsonObject statsObject;
statsObject["average_listeners_last_second"] = (float) _sumListeners / (float) _numStatFrames;
@ -448,6 +622,7 @@ void AvatarMixer::sendStatsPacket() {
statsObject["trailing_sleep_percentage"] = _trailingSleepRatio * 100;
statsObject["performance_throttling_ratio"] = _performanceThrottlingRatio;
statsObject["broadcast_loop_rate"] = _broadcastRate.rate();
QJsonObject avatarsObject;
@ -500,6 +675,7 @@ void AvatarMixer::run() {
// setup the timer that will be fired on the broadcast thread
_broadcastTimer = new QTimer;
_broadcastTimer->setTimerType(Qt::PreciseTimer);
_broadcastTimer->setInterval(AVATAR_DATA_SEND_INTERVAL_MSECS);
_broadcastTimer->moveToThread(&_broadcastThread);
@ -510,14 +686,21 @@ void AvatarMixer::run() {
void AvatarMixer::domainSettingsRequestComplete() {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
nodeList->linkedDataCreateCallback = [] (Node* node) {
node->setLinkedData(std::unique_ptr<AvatarMixerClientData> { new AvatarMixerClientData });
};
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
// parse the settings to pull out the values we need
parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject());
float domainMinimumScale = _domainMinimumScale;
float domainMaximumScale = _domainMaximumScale;
nodeList->linkedDataCreateCallback = [domainMinimumScale, domainMaximumScale] (Node* node) {
auto clientData = std::unique_ptr<AvatarMixerClientData> { new AvatarMixerClientData(node->getUUID()) };
clientData->getAvatar().setDomainMinimumScale(domainMinimumScale);
clientData->getAvatar().setDomainMaximumScale(domainMaximumScale);
node->setLinkedData(std::move(clientData));
};
// start the broadcastThread
_broadcastThread.start();
@ -541,7 +724,7 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
const QString AVATAR_MIXER_SETTINGS_KEY = "avatar_mixer";
const QString NODE_SEND_BANDWIDTH_KEY = "max_node_send_bandwidth";
const float DEFAULT_NODE_SEND_BANDWIDTH = 1.0f;
const float DEFAULT_NODE_SEND_BANDWIDTH = 5.0f;
QJsonValue nodeBandwidthValue = domainSettings[AVATAR_MIXER_SETTINGS_KEY].toObject()[NODE_SEND_BANDWIDTH_KEY];
if (!nodeBandwidthValue.isDouble()) {
qDebug() << NODE_SEND_BANDWIDTH_KEY << "is not a double - will continue with default value";
@ -549,4 +732,22 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
_maxKbpsPerNode = nodeBandwidthValue.toDouble(DEFAULT_NODE_SEND_BANDWIDTH) * KILO_PER_MEGA;
qDebug() << "The maximum send bandwidth per node is" << _maxKbpsPerNode << "kbps.";
const QString AVATARS_SETTINGS_KEY = "avatars";
static const QString MIN_SCALE_OPTION = "min_avatar_scale";
float settingMinScale = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MIN_SCALE_OPTION].toDouble(MIN_AVATAR_SCALE);
_domainMinimumScale = glm::clamp(settingMinScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE);
static const QString MAX_SCALE_OPTION = "max_avatar_scale";
float settingMaxScale = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MAX_SCALE_OPTION].toDouble(MAX_AVATAR_SCALE);
_domainMaximumScale = glm::clamp(settingMaxScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE);
// make sure that the domain owner didn't flip min and max
if (_domainMinimumScale > _domainMaximumScale) {
std::swap(_domainMinimumScale, _domainMaximumScale);
}
qDebug() << "This domain requires a minimum avatar scale of" << _domainMinimumScale
<< "and a maximum avatar scale of" << _domainMaximumScale;
}

View file

@ -15,9 +15,11 @@
#ifndef hifi_AvatarMixer_h
#define hifi_AvatarMixer_h
#include <shared/RateCounter.h>
#include <PortableHighResolutionClock.h>
#include <ThreadedAssignment.h>
#include "AvatarMixerClientData.h"
/// Handles assignments of type AvatarMixer - distribution of avatar data to various clients
class AvatarMixer : public ThreadedAssignment {
@ -34,10 +36,13 @@ public slots:
void sendStatsPacket() override;
private slots:
void handleViewFrustumPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleAvatarDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message);
void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void domainSettingsRequestComplete();
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
@ -45,6 +50,7 @@ private slots:
private:
void broadcastAvatarData();
void parseDomainServerSettings(const QJsonObject& domainSettings);
void sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
QThread _broadcastThread;
@ -59,7 +65,14 @@ private:
float _maxKbpsPerNode = 0.0f;
float _domainMinimumScale { MIN_AVATAR_SCALE };
float _domainMaximumScale { MAX_AVATAR_SCALE };
QTimer* _broadcastTimer = nullptr;
RateCounter<> _broadcastRate;
p_high_resolution_clock::time_point _lastDebugMessage;
QHash<QString, QPair<int, int>> _sessionDisplayNames;
};
#endif // hifi_AvatarMixer_h

View file

@ -11,6 +11,9 @@
#include <udt/PacketHeaders.h>
#include <DependencyManager.h>
#include <NodeList.h>
#include "AvatarMixerClientData.h"
int AvatarMixerClientData::parseData(ReceivedMessage& message) {
@ -39,6 +42,38 @@ uint16_t AvatarMixerClientData::getLastBroadcastSequenceNumber(const QUuid& node
}
}
void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointer other) {
if (!isRadiusIgnoring(other->getUUID())) {
addToRadiusIgnoringSet(other->getUUID());
auto killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
killPacket->write(other->getUUID().toRfc4122());
if (self->isIgnoreRadiusEnabled()) {
killPacket->writePrimitive(KillAvatarReason::TheirAvatarEnteredYourBubble);
} else {
killPacket->writePrimitive(KillAvatarReason::YourAvatarEnteredTheirBubble);
}
DependencyManager::get<NodeList>()->sendUnreliablePacket(*killPacket, *self);
_hasReceivedFirstPacketsFrom.erase(other->getUUID());
}
}
void AvatarMixerClientData::removeFromRadiusIgnoringSet(SharedNodePointer self, const QUuid& other) {
if (isRadiusIgnoring(other)) {
_radiusIgnoredOthers.erase(other);
auto exitingSpaceBubblePacket = NLPacket::create(PacketType::ExitingSpaceBubble, NUM_BYTES_RFC4122_UUID);
exitingSpaceBubblePacket->write(other.toRfc4122());
DependencyManager::get<NodeList>()->sendUnreliablePacket(*exitingSpaceBubblePacket, *self);
}
}
void AvatarMixerClientData::readViewFrustumPacket(const QByteArray& message) {
_currentViewFrustum.fromByteArray(message);
}
bool AvatarMixerClientData::otherAvatarInView(const AABox& otherAvatarBox) {
return _currentViewFrustum.boxIntersectsKeyhole(otherAvatarBox);
}
void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const {
jsonObject["display_name"] = _avatar->getDisplayName();
jsonObject["full_rate_distance"] = _fullRateDistance;
@ -52,4 +87,6 @@ void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const {
jsonObject[INBOUND_AVATAR_DATA_STATS_KEY] = _avatar->getAverageBytesReceivedPerSecond() / (float) BYTES_PER_KILOBIT;
jsonObject["av_data_receive_rate"] = _avatar->getReceiveRate();
jsonObject["recent_other_av_in_view"] = _recentOtherAvatarsInView;
jsonObject["recent_other_av_out_of_view"] = _recentOtherAvatarsOutOfView;
}

View file

@ -27,6 +27,7 @@
#include <PortableHighResolutionClock.h>
#include <SimpleMovingAverage.h>
#include <UUIDHasher.h>
#include <ViewFrustum.h>
const QString OUTBOUND_AVATAR_DATA_STATS_KEY = "outbound_av_data_kbps";
const QString INBOUND_AVATAR_DATA_STATS_KEY = "inbound_av_data_kbps";
@ -34,6 +35,8 @@ const QString INBOUND_AVATAR_DATA_STATS_KEY = "inbound_av_data_kbps";
class AvatarMixerClientData : public NodeData {
Q_OBJECT
public:
AvatarMixerClientData(const QUuid& nodeID = QUuid()) : NodeData(nodeID) { _currentViewFrustum.invalidate(); }
virtual ~AvatarMixerClientData() {}
using HRCTime = p_high_resolution_clock::time_point;
int parseData(ReceivedMessage& message) override;
@ -50,6 +53,8 @@ public:
HRCTime getIdentityChangeTimestamp() const { return _identityChangeTimestamp; }
void flagIdentityChange() { _identityChangeTimestamp = p_high_resolution_clock::now(); }
bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; }
void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; }
void setFullRateDistance(float fullRateDistance) { _fullRateDistance = fullRateDistance; }
float getFullRateDistance() const { return _fullRateDistance; }
@ -79,6 +84,42 @@ public:
{ return _avgOtherAvatarDataRate.getAverageSampleValuePerSecond() / (float) BYTES_PER_KILOBIT; }
void loadJSONStats(QJsonObject& jsonObject) const;
glm::vec3 getPosition() { return _avatar ? _avatar->getPosition() : glm::vec3(0); }
glm::vec3 getGlobalBoundingBoxCorner() { return _avatar ? _avatar->getGlobalBoundingBoxCorner() : glm::vec3(0); }
bool isRadiusIgnoring(const QUuid& other) { return _radiusIgnoredOthers.find(other) != _radiusIgnoredOthers.end(); }
void addToRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.insert(other); }
void removeFromRadiusIgnoringSet(SharedNodePointer self, const QUuid& other);
void ignoreOther(SharedNodePointer self, SharedNodePointer other);
void readViewFrustumPacket(const QByteArray& message);
bool otherAvatarInView(const AABox& otherAvatarBox);
void resetInViewStats() { _recentOtherAvatarsInView = _recentOtherAvatarsOutOfView = 0; }
void incrementAvatarInView() { _recentOtherAvatarsInView++; }
void incrementAvatarOutOfView() { _recentOtherAvatarsOutOfView++; }
const QString& getBaseDisplayName() { return _baseDisplayName; }
void setBaseDisplayName(const QString& baseDisplayName) { _baseDisplayName = baseDisplayName; }
bool getRequestsDomainListData() { return _requestsDomainListData; }
void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; }
quint64 getLastOtherAvatarEncodeTime(QUuid otherAvatar) {
quint64 result = 0;
if (_lastOtherAvatarEncodeTime.find(otherAvatar) != _lastOtherAvatarEncodeTime.end()) {
result = _lastOtherAvatarEncodeTime[otherAvatar];
}
_lastOtherAvatarEncodeTime[otherAvatar] = usecTimestampNow();
return result;
}
QVector<JointData>& getLastOtherAvatarSentJoints(QUuid otherAvatar) {
_lastOtherAvatarSentJoints[otherAvatar].resize(_avatar->getJointCount());
return _lastOtherAvatarSentJoints[otherAvatar];
}
private:
AvatarSharedPointer _avatar { new AvatarData() };
@ -86,7 +127,13 @@ private:
std::unordered_map<QUuid, uint16_t> _lastBroadcastSequenceNumbers;
std::unordered_set<QUuid> _hasReceivedFirstPacketsFrom;
// this is a map of the last time we encoded an "other" avatar for
// sending to "this" node
std::unordered_map<QUuid, quint64> _lastOtherAvatarEncodeTime;
std::unordered_map<QUuid, QVector<JointData>> _lastOtherAvatarSentJoints;
HRCTime _identityChangeTimestamp;
bool _avatarSessionDisplayNameMustChange{ false };
float _fullRateDistance = FLT_MAX;
float _maxAvatarDistance = FLT_MAX;
@ -99,6 +146,13 @@ private:
int _numOutOfOrderSends = 0;
SimpleMovingAverage _avgOtherAvatarDataRate;
std::unordered_set<QUuid> _radiusIgnoredOthers;
ViewFrustum _currentViewFrustum;
int _recentOtherAvatarsInView { 0 };
int _recentOtherAvatarsOutOfView { 0 };
QString _baseDisplayName{}; // The santized key used in determinging unique sessionDisplayName, so that we can remove from dictionary.
bool _requestsDomainListData { false };
};
#endif // hifi_AvatarMixerClientData_h

View file

@ -14,6 +14,13 @@
#include <GLMHelpers.h>
#include "ScriptableAvatar.h"
QByteArray ScriptableAvatar::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector<JointData>& lastSentJointData,
bool distanceAdjust, glm::vec3 viewerPosition, QVector<JointData>* sentJointDataOut) {
_globalPosition = getPosition();
return AvatarData::toByteArray(dataDetail, lastSentTime, lastSentJointData, distanceAdjust, viewerPosition, sentJointDataOut);
}
// hold and priority unused but kept so that client side JS can run.
void ScriptableAvatar::startAnimation(const QString& url, float fps, float priority,
bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints) {
@ -89,7 +96,7 @@ void ScriptableAvatar::update(float deltatime) {
int mapping = _bind->getGeometry().getJointIndex(name);
if (mapping != -1 && !_maskedJoints.contains(name)) {
// Eventually, this should probably deal with post rotations and translations, too.
poses[mapping].rot = modelJoints[mapping].preRotation *
poses[mapping].rot() = modelJoints[mapping].preRotation *
safeMix(floorFrame.rotations.at(i), ceilFrame.rotations.at(i), frameFraction);;
}
}
@ -97,8 +104,8 @@ void ScriptableAvatar::update(float deltatime) {
for (int i = 0; i < nJoints; i++) {
JointData& data = _jointData[i];
AnimPose& pose = poses[i];
if (data.rotation != pose.rot) {
data.rotation = pose.rot;
if (data.rotation != pose.rot()) {
data.rotation = pose.rot();
data.rotationSet = true;
}
}

View file

@ -17,7 +17,7 @@
#include <AvatarData.h>
#include <ScriptEngine.h>
class ScriptableAvatar : public AvatarData, public Dependency{
class ScriptableAvatar : public AvatarData, public Dependency {
Q_OBJECT
public:
@ -27,6 +27,10 @@ public:
Q_INVOKABLE void stopAnimation();
Q_INVOKABLE AnimationDetails getAnimationDetails();
virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override;
virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector<JointData>& lastSentJointData,
bool distanceAdjust = false, glm::vec3 viewerPosition = glm::vec3(0), QVector<JointData>* sentJointDataOut = nullptr) override;
private slots:
void update(float deltatime);
@ -39,4 +43,4 @@ private:
std::shared_ptr<AnimSkeleton> _animSkeleton;
};
#endif // hifi_ScriptableAvatar_h
#endif // hifi_ScriptableAvatar_h

View file

@ -11,6 +11,8 @@
#include "AssignmentParentFinder.h"
#include <AvatarHashMap.h>
SpatiallyNestableWeakPointer AssignmentParentFinder::find(QUuid parentID, bool& success, SpatialParentTree* entityTree) const {
SpatiallyNestableWeakPointer parent;
@ -25,10 +27,21 @@ SpatiallyNestableWeakPointer AssignmentParentFinder::find(QUuid parentID, bool&
} else {
parent = _tree->findEntityByEntityItemID(parentID);
}
if (parent.expired()) {
success = false;
} else {
if (!parent.expired()) {
success = true;
return parent;
}
// search avatars
if (DependencyManager::isSet<AvatarHashMap>()) {
auto avatarHashMap = DependencyManager::get<AvatarHashMap>();
parent = avatarHashMap->getAvatarBySessionID(parentID);
if (!parent.expired()) {
success = true;
return parent;
}
}
success = false;
return parent;
}

View file

@ -9,9 +9,12 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtCore/QEventLoop>
#include <QTimer>
#include <EntityTree.h>
#include <SimpleEntitySimulation.h>
#include <ResourceCache.h>
#include <ScriptCache.h>
#include "EntityServer.h"
#include "EntityServerConsts.h"
@ -26,6 +29,10 @@ EntityServer::EntityServer(ReceivedMessage& message) :
OctreeServer(message),
_entitySimulation(NULL)
{
ResourceManager::init();
DependencyManager::set<ResourceCacheSharedItems>();
DependencyManager::set<ScriptCache>();
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListenerForTypes({ PacketType::EntityAdd, PacketType::EntityEdit, PacketType::EntityErase },
this, "handleEntityPacket");
@ -278,6 +285,103 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
tree->setWantEditLogging(wantEditLogging);
tree->setWantTerseEditLogging(wantTerseEditLogging);
QString entityScriptSourceWhitelist;
if (readOptionString("entityScriptSourceWhitelist", settingsSectionObject, entityScriptSourceWhitelist)) {
tree->setEntityScriptSourceWhitelist(entityScriptSourceWhitelist);
} 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";
}
}
// 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 EntityServer::scriptRequestFinished() {
qDebug() << "script request completed";
auto scriptRequest = qobject_cast<ResourceRequest*>(sender());
const QString urlString = scriptRequest->getUrl().toString();
if (scriptRequest && scriptRequest->getResult() == ResourceRequest::Success) {
auto scriptContents = scriptRequest->getData();
qInfo() << "Downloaded script:" << scriptContents;
QScriptProgram program(scriptContents, urlString);
if (hasCorrectSyntax(program)) {
_entityEditFilterEngine.evaluate(scriptContents);
if (!hadUncaughtExceptions(_entityEditFilterEngine, urlString)) {
std::static_pointer_cast<EntityTree>(_tree)->initEntityEditFilterEngine(&_entityEditFilterEngine, [this]() {
return hadUncaughtExceptions(_entityEditFilterEngine, _entityEditFilter);
});
scriptRequest->deleteLater();
qDebug() << "script request filter processed";
return;
}
}
} else if (scriptRequest) {
qCritical() << "Failed to download script at" << urlString;
// See HTTPResourceRequest::onRequestFinished for interpretation of codes. For example, a 404 is code 6 and 403 is 3. A timeout is 2. Go figure.
qCritical() << "ResourceRequest error was" << scriptRequest->getResult();
} else {
qCritical() << "Failed to create script request.";
}
// Hard stop of the assignment client on failure. We don't want anyone to think they have a filter in place when they don't.
// Alas, only indications will be the above logging with assignment client restarting repeatedly, and clients will not see any entities.
qDebug() << "script request failure causing stop";
stop();
}
void EntityServer::nodeAdded(SharedNodePointer node) {

View file

@ -69,6 +69,7 @@ protected:
private slots:
void handleEntityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void scriptRequestFinished();
private:
SimpleEntitySimulationPointer _entitySimulation;
@ -76,6 +77,9 @@ private:
QReadWriteLock _viewerSendingStatsLock;
QMap<QUuid, QMap<QUuid, ViewerSendingStats>> _viewerSendingStats;
QString _entityEditFilter{};
QScriptEngine _entityEditFilterEngine{};
};
#endif // hifi_EntityServer_h

View file

@ -9,19 +9,28 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtCore/QDebug>
#include <LogHandler.h>
#include <SharedUtil.h>
#include "AssignmentClientApp.h"
#include <BuildInfo.h>
int main(int argc, char* argv[]) {
disableQtBearerPoll(); // Fixes wifi ping spikes
QCoreApplication::setApplicationName(BuildInfo::ASSIGNMENT_CLIENT_NAME);
QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION);
QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN);
QCoreApplication::setApplicationVersion(BuildInfo::VERSION);
qInstallMessageHandler(LogHandler::verboseMessageHandler);
qInfo() << "Starting.";
AssignmentClientApp app(argc, argv);
int acReturn = app.exec();
qDebug() << "assignment-client process" << app.applicationPid() << "exiting with status code" << acReturn;
qInfo() << "Quitting.";
return acReturn;
}

View file

@ -44,8 +44,7 @@ void MessagesMixer::handleMessages(QSharedPointer<ReceivedMessage> receivedMessa
nodeList->eachMatchingNode(
[&](const SharedNodePointer& node)->bool {
return node->getType() == NodeType::Agent && node->getActiveSocket() &&
_channelSubscribers[channel].contains(node->getUUID());
return node->getActiveSocket() && _channelSubscribers[channel].contains(node->getUUID());
},
[&](const SharedNodePointer& node) {
auto packetList = MessagesClient::encodeMessagesPacket(channel, message, senderID);
@ -83,5 +82,6 @@ void MessagesMixer::sendStatsPacket() {
void MessagesMixer::run() {
ThreadedAssignment::commonInit(MESSAGES_MIXER_LOGGING_NAME, NodeType::MessagesMixer);
DependencyManager::get<NodeList>()->addNodeTypeToInterestSet(NodeType::Agent);
}
auto nodeList = DependencyManager::get<NodeList>();
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
}

View file

@ -316,8 +316,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
int truePacketsSent = 0;
int trueBytesSent = 0;
int packetsSentThisInterval = 0;
bool isFullScene = ((!viewFrustumChanged) && nodeData->getViewFrustumJustStoppedChanging())
|| nodeData->hasLodChanged();
bool isFullScene = nodeData->haveJSONParametersChanged() ||
(nodeData->getUsesFrustum()
&& ((!viewFrustumChanged && nodeData->getViewFrustumJustStoppedChanging()) || nodeData->hasLodChanged()));
bool somethingToSend = true; // assume we have something
@ -432,7 +433,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
boundaryLevelAdjust, octreeSizeScale,
nodeData->getLastTimeBagEmpty(),
isFullScene, &nodeData->stats, _myServer->getJurisdiction(),
&nodeData->extraEncodeData);
&nodeData->extraEncodeData,
nodeData->getUsesFrustum(),
nodeData);
nodeData->copyCurrentViewFrustum(params.viewFrustum);
if (viewFrustumChanged) {
nodeData->copyLastKnownViewFrustum(params.lastViewFrustum);

View file

@ -660,6 +660,7 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
quint64 averageUpdateTime = _tree->getAverageUpdateTime();
quint64 averageCreateTime = _tree->getAverageCreateTime();
quint64 averageLoggingTime = _tree->getAverageLoggingTime();
quint64 averageFilterTime = _tree->getAverageFilterTime();
int FLOAT_PRECISION = 3;
@ -699,6 +700,8 @@ bool OctreeServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
.arg(locale.toString((uint)averageCreateTime).rightJustified(COLUMN_WIDTH, ' '));
statsString += QString(" Average Logging Time: %1 usecs\r\n")
.arg(locale.toString((uint)averageLoggingTime).rightJustified(COLUMN_WIDTH, ' '));
statsString += QString(" Average Filter Time: %1 usecs\r\n")
.arg(locale.toString((uint)averageFilterTime).rightJustified(COLUMN_WIDTH, ' '));
int senderNumber = 0;
@ -1136,8 +1139,8 @@ void OctreeServer::domainSettingsRequestComplete() {
auto nodeList = DependencyManager::get<NodeList>();
// we need to ask the DS about agents so we can ping/reply with them
nodeList->addNodeTypeToInterestSet(NodeType::Agent);
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListener(getMyQueryMessageType(), this, "handleOctreeQueryPacket");
packetReceiver.registerListener(PacketType::OctreeDataNack, this, "handleOctreeDataNackPacket");

View file

@ -0,0 +1,372 @@
//
// EntityScriptServer.cpp
// assignment-client/src/scripts
//
// Created by Clément Brisset on 1/5/17.
// Copyright 2013 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 "EntityScriptServer.h"
#include <AudioConstants.h>
#include <AudioInjectorManager.h>
#include <EntityScriptingInterface.h>
#include <MessagesClient.h>
#include <plugins/CodecPlugin.h>
#include <plugins/PluginManager.h>
#include <ResourceManager.h>
#include <ScriptCache.h>
#include <ScriptEngines.h>
#include <SoundCache.h>
#include <UUID.h>
#include <WebSocketServerClass.h>
#include "ClientServerUtils.h"
#include "../entities/AssignmentParentFinder.h"
int EntityScriptServer::_entitiesScriptEngineCount = 0;
EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssignment(message) {
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
ResourceManager::init();
DependencyManager::registerInheritance<SpatialParentFinder, AssignmentParentFinder>();
DependencyManager::set<ResourceCacheSharedItems>();
DependencyManager::set<SoundCache>();
DependencyManager::set<AudioInjectorManager>();
DependencyManager::set<ScriptCache>();
DependencyManager::set<ScriptEngines>(ScriptEngine::ENTITY_SERVER_SCRIPT);
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase },
this, "handleOctreePacket");
packetReceiver.registerListener(PacketType::Jurisdiction, this, "handleJurisdictionPacket");
packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat");
auto avatarHashMap = DependencyManager::set<AvatarHashMap>();
packetReceiver.registerListener(PacketType::BulkAvatarData, avatarHashMap.data(), "processAvatarDataPacket");
packetReceiver.registerListener(PacketType::KillAvatar, avatarHashMap.data(), "processKillAvatar");
packetReceiver.registerListener(PacketType::AvatarIdentity, avatarHashMap.data(), "processAvatarIdentityPacket");
packetReceiver.registerListener(PacketType::ReloadEntityServerScript, this, "handleReloadEntityServerScriptPacket");
packetReceiver.registerListener(PacketType::EntityScriptGetStatus, this, "handleEntityScriptGetStatusPacket");
}
static const QString ENTITY_SCRIPT_SERVER_LOGGING_NAME = "entity-script-server";
void EntityScriptServer::handleReloadEntityServerScriptPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
// These are temporary checks until we can ensure that nodes eventually disconnect if the Domain Server stops telling them
// about each other.
if (senderNode->getCanRez() || senderNode->getCanRezTmp()) {
auto entityID = QUuid::fromRfc4122(message->read(NUM_BYTES_RFC4122_UUID));
if (_entityViewer.getTree() && !_shuttingDown) {
qDebug() << "Reloading: " << entityID;
_entitiesScriptEngine->unloadEntityScript(entityID);
checkAndCallPreload(entityID, true);
}
}
}
void EntityScriptServer::handleEntityScriptGetStatusPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
// These are temporary checks until we can ensure that nodes eventually disconnect if the Domain Server stops telling them
// about each other.
if (senderNode->getCanRez() || senderNode->getCanRezTmp()) {
MessageID messageID;
message->readPrimitive(&messageID);
auto entityID = QUuid::fromRfc4122(message->read(NUM_BYTES_RFC4122_UUID));
auto replyPacketList = NLPacketList::create(PacketType::EntityScriptGetStatusReply, QByteArray(), true, true);
replyPacketList->writePrimitive(messageID);
EntityScriptDetails details;
if (_entitiesScriptEngine->getEntityScriptDetails(entityID, details)) {
replyPacketList->writePrimitive(true);
replyPacketList->writePrimitive(details.status);
replyPacketList->writeString(details.errorInfo);
} else {
replyPacketList->writePrimitive(false);
}
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacketList(std::move(replyPacketList), *senderNode);
}
}
void EntityScriptServer::run() {
// make sure we request our script once the agent connects to the domain
auto nodeList = DependencyManager::get<NodeList>();
ThreadedAssignment::commonInit(ENTITY_SCRIPT_SERVER_LOGGING_NAME, NodeType::EntityScriptServer);
// Setup MessagesClient
auto messagesClient = DependencyManager::set<MessagesClient>();
QThread* messagesThread = new QThread;
messagesThread->setObjectName("Messages Client Thread");
messagesClient->moveToThread(messagesThread);
connect(messagesThread, &QThread::started, messagesClient.data(), &MessagesClient::init);
messagesThread->start();
// make sure we hear about connected nodes so we can grab an ATP script if a request is pending
connect(nodeList.data(), &LimitedNodeList::nodeActivated, this, &EntityScriptServer::nodeActivated);
connect(nodeList.data(), &LimitedNodeList::nodeKilled, this, &EntityScriptServer::nodeKilled);
nodeList->addSetOfNodeTypesToNodeInterestSet({
NodeType::Agent, NodeType::AudioMixer, NodeType::AvatarMixer,
NodeType::EntityServer, NodeType::MessagesMixer, NodeType::AssetServer
});
// Setup Script Engine
resetEntitiesScriptEngine();
// we need to make sure that init has been called for our EntityScriptingInterface
// so that it actually has a jurisdiction listener when we ask it for it next
auto entityScriptingInterface = DependencyManager::get<EntityScriptingInterface>();
entityScriptingInterface->init();
_entityViewer.setJurisdictionListener(entityScriptingInterface->getJurisdictionListener());
_entityViewer.init();
// setup the JSON filter that asks for entities with a non-default serverScripts property
QJsonObject queryJSONParameters;
static const QString SERVER_SCRIPTS_PROPERTY = "serverScripts";
queryJSONParameters[SERVER_SCRIPTS_PROPERTY] = EntityQueryFilterSymbol::NonDefault;
// setup the JSON parameters so that OctreeQuery does not use a frustum and uses our JSON filter
_entityViewer.getOctreeQuery().setUsesFrustum(false);
_entityViewer.getOctreeQuery().setJSONParameters(queryJSONParameters);
entityScriptingInterface->setEntityTree(_entityViewer.getTree());
DependencyManager::set<AssignmentParentFinder>(_entityViewer.getTree());
auto tree = _entityViewer.getTree().get();
connect(tree, &EntityTree::deletingEntity, this, &EntityScriptServer::deletingEntity, Qt::QueuedConnection);
connect(tree, &EntityTree::addingEntity, this, &EntityScriptServer::addingEntity, Qt::QueuedConnection);
connect(tree, &EntityTree::entityServerScriptChanging, this, &EntityScriptServer::entityServerScriptChanging, Qt::QueuedConnection);
}
void EntityScriptServer::nodeActivated(SharedNodePointer activatedNode) {
if (activatedNode->getType() == NodeType::AudioMixer) {
negotiateAudioFormat();
}
}
void EntityScriptServer::negotiateAudioFormat() {
auto nodeList = DependencyManager::get<NodeList>();
auto negotiateFormatPacket = NLPacket::create(PacketType::NegotiateAudioFormat);
auto codecPlugins = PluginManager::getInstance()->getCodecPlugins();
quint8 numberOfCodecs = (quint8)codecPlugins.size();
negotiateFormatPacket->writePrimitive(numberOfCodecs);
for (auto& plugin : codecPlugins) {
auto codecName = plugin->getName();
negotiateFormatPacket->writeString(codecName);
}
// grab our audio mixer from the NodeList, if it exists
SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer);
if (audioMixer) {
// send off this mute packet
nodeList->sendPacket(std::move(negotiateFormatPacket), *audioMixer);
}
}
void EntityScriptServer::handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message) {
QString selectedCodecName = message->readString();
selectAudioFormat(selectedCodecName);
}
void EntityScriptServer::selectAudioFormat(const QString& selectedCodecName) {
_selectedCodecName = selectedCodecName;
qDebug() << "Selected Codec:" << _selectedCodecName;
// release any old codec encoder/decoder first...
if (_codec && _encoder) {
_codec->releaseEncoder(_encoder);
_encoder = nullptr;
_codec = nullptr;
}
auto codecPlugins = PluginManager::getInstance()->getCodecPlugins();
for (auto& plugin : codecPlugins) {
if (_selectedCodecName == plugin->getName()) {
_codec = plugin;
_encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO);
qDebug() << "Selected Codec Plugin:" << _codec.get();
break;
}
}
}
void EntityScriptServer::resetEntitiesScriptEngine() {
auto engineName = QString("Entities %1").arg(++_entitiesScriptEngineCount);
auto newEngine = QSharedPointer<ScriptEngine>(new ScriptEngine(ScriptEngine::ENTITY_SERVER_SCRIPT, NO_SCRIPT, engineName));
auto webSocketServerConstructorValue = newEngine->newFunction(WebSocketServerClass::constructor);
newEngine->globalObject().setProperty("WebSocketServer", webSocketServerConstructorValue);
newEngine->registerGlobalObject("SoundCache", DependencyManager::get<SoundCache>().data());
// connect this script engines printedMessage signal to the global ScriptEngines these various messages
auto scriptEngines = DependencyManager::get<ScriptEngines>().data();
connect(newEngine.data(), &ScriptEngine::printedMessage, scriptEngines, &ScriptEngines::onPrintedMessage);
connect(newEngine.data(), &ScriptEngine::errorMessage, scriptEngines, &ScriptEngines::onErrorMessage);
connect(newEngine.data(), &ScriptEngine::warningMessage, scriptEngines, &ScriptEngines::onWarningMessage);
connect(newEngine.data(), &ScriptEngine::infoMessage, scriptEngines, &ScriptEngines::onInfoMessage);
connect(newEngine.data(), &ScriptEngine::update, this, [this] {
_entityViewer.queryOctree();
});
newEngine->runInThread();
DependencyManager::get<EntityScriptingInterface>()->setEntitiesScriptEngine(newEngine.data());
_entitiesScriptEngine.swap(newEngine);
}
void EntityScriptServer::clear() {
// unload and stop the engine
if (_entitiesScriptEngine) {
// do this here (instead of in deleter) to avoid marshalling unload signals back to this thread
_entitiesScriptEngine->unloadAllEntityScripts();
_entitiesScriptEngine->stop();
}
// reset the engine
if (!_shuttingDown) {
resetEntitiesScriptEngine();
}
_entityViewer.clear();
}
void EntityScriptServer::shutdownScriptEngine() {
if (_entitiesScriptEngine) {
_entitiesScriptEngine->disconnectNonEssentialSignals(); // disconnect all slots/signals from the script engine, except essential
}
_shuttingDown = true;
clear(); // always clear() on shutdown
}
void EntityScriptServer::addingEntity(const EntityItemID& entityID) {
checkAndCallPreload(entityID);
}
void EntityScriptServer::deletingEntity(const EntityItemID& entityID) {
if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) {
_entitiesScriptEngine->unloadEntityScript(entityID);
}
}
void EntityScriptServer::entityServerScriptChanging(const EntityItemID& entityID, const bool reload) {
if (_entityViewer.getTree() && !_shuttingDown) {
_entitiesScriptEngine->unloadEntityScript(entityID);
checkAndCallPreload(entityID, reload);
}
}
void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, const bool reload) {
if (_entityViewer.getTree() && !_shuttingDown && _entitiesScriptEngine) {
EntityItemPointer entity = _entityViewer.getTree()->findEntityByEntityItemID(entityID);
EntityScriptDetails details;
bool notRunning = !_entitiesScriptEngine->getEntityScriptDetails(entityID, details);
if (entity && (reload || notRunning || details.scriptText != entity->getServerScripts())) {
QString scriptUrl = entity->getServerScripts();
if (!scriptUrl.isEmpty()) {
scriptUrl = ResourceManager::normalizeURL(scriptUrl);
qDebug() << "Loading entity server script" << scriptUrl << "for" << entityID;
ScriptEngine::loadEntityScript(_entitiesScriptEngine, entityID, scriptUrl, reload);
}
}
}
}
void EntityScriptServer::nodeKilled(SharedNodePointer killedNode) {
if (!_shuttingDown && killedNode->getType() == NodeType::EntityServer) {
if (_entitiesScriptEngine) {
_entitiesScriptEngine->unloadAllEntityScripts();
_entitiesScriptEngine->stop();
}
resetEntitiesScriptEngine();
_entityViewer.clear();
}
}
void EntityScriptServer::sendStatsPacket() {
}
void EntityScriptServer::handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
auto packetType = message->getType();
if (packetType == PacketType::OctreeStats) {
int statsMessageLength = OctreeHeadlessViewer::parseOctreeStats(message, senderNode);
if (message->getSize() > statsMessageLength) {
// pull out the piggybacked packet and create a new QSharedPointer<NLPacket> for it
int piggyBackedSizeWithHeader = message->getSize() - statsMessageLength;
auto buffer = std::unique_ptr<char[]>(new char[piggyBackedSizeWithHeader]);
memcpy(buffer.get(), message->getRawMessage() + statsMessageLength, piggyBackedSizeWithHeader);
auto newPacket = NLPacket::fromReceivedPacket(std::move(buffer), piggyBackedSizeWithHeader, message->getSenderSockAddr());
message = QSharedPointer<ReceivedMessage>::create(*newPacket);
} else {
return; // bail since no piggyback data
}
packetType = message->getType();
} // fall through to piggyback message
if (packetType == PacketType::EntityData) {
_entityViewer.processDatagram(*message, senderNode);
} else if (packetType == PacketType::EntityErase) {
_entityViewer.processEraseMessage(*message, senderNode);
}
}
void EntityScriptServer::handleJurisdictionPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
NodeType_t nodeType;
message->peekPrimitive(&nodeType);
// PacketType_JURISDICTION, first byte is the node type...
if (nodeType == NodeType::EntityServer) {
DependencyManager::get<EntityScriptingInterface>()->getJurisdictionListener()->
queueReceivedPacket(message, senderNode);
}
}
void EntityScriptServer::aboutToFinish() {
shutdownScriptEngine();
// our entity tree is going to go away so tell that to the EntityScriptingInterface
DependencyManager::get<EntityScriptingInterface>()->setEntityTree(nullptr);
ResourceManager::cleanup();
// cleanup the AudioInjectorManager (and any still running injectors)
DependencyManager::destroy<AudioInjectorManager>();
DependencyManager::destroy<ScriptEngines>();
// cleanup codec & encoder
if (_codec && _encoder) {
_codec->releaseEncoder(_encoder);
_encoder = nullptr;
}
}

View file

@ -0,0 +1,70 @@
//
// EntityScriptServer.h
// assignment-client/src/scripts
//
// Created by Clément Brisset on 1/5/17.
// Copyright 2013 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_EntityScriptServer_h
#define hifi_EntityScriptServer_h
#include <QtCore/QObject>
#include <EntityEditPacketSender.h>
#include <EntityTreeHeadlessViewer.h>
#include <plugins/CodecPlugin.h>
#include <ScriptEngine.h>
#include <ThreadedAssignment.h>
class EntityScriptServer : public ThreadedAssignment {
Q_OBJECT
public:
EntityScriptServer(ReceivedMessage& message);
virtual void aboutToFinish() override;
public slots:
void run() override;
void nodeActivated(SharedNodePointer activatedNode);
void nodeKilled(SharedNodePointer killedNode);
void sendStatsPacket() override;
private slots:
void handleOctreePacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleJurisdictionPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleSelectedAudioFormat(QSharedPointer<ReceivedMessage> message);
void handleReloadEntityServerScriptPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleEntityScriptGetStatusPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
private:
void negotiateAudioFormat();
void selectAudioFormat(const QString& selectedCodecName);
void resetEntitiesScriptEngine();
void clear();
void shutdownScriptEngine();
void addingEntity(const EntityItemID& entityID);
void deletingEntity(const EntityItemID& entityID);
void entityServerScriptChanging(const EntityItemID& entityID, const bool reload);
void checkAndCallPreload(const EntityItemID& entityID, const bool reload = false);
bool _shuttingDown { false };
static int _entitiesScriptEngineCount;
QSharedPointer<ScriptEngine> _entitiesScriptEngine;
EntityEditPacketSender _entityEditSender;
EntityTreeHeadlessViewer _entityViewer;
QString _selectedCodecName;
CodecPluginPointer _codec;
Encoder* _encoder { nullptr };
};
#endif // hifi_EntityScriptServer_h

View file

@ -0,0 +1,20 @@
set(EXTERNAL_NAME GifCreator)
include(ExternalProject)
ExternalProject_Add(
${EXTERNAL_NAME}
URL https://hifi-public.s3.amazonaws.com/dependencies/GifCreator.zip
URL_MD5 8ac8ef5196f47c658dce784df5ecdb70
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD 1
)
# Hide this external target (for ide users)
set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")
ExternalProject_Get_Property(${EXTERNAL_NAME} INSTALL_DIR)
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${INSTALL_DIR}/src/${EXTERNAL_NAME} CACHE PATH "List of GifCreator include directories")

View file

@ -0,0 +1,32 @@
include(ExternalProject)
include(SelectLibraryConfigurations)
set(EXTERNAL_NAME LibOVRPlatform)
string(TOUPPER ${EXTERNAL_NAME} EXTERNAL_NAME_UPPER)
if (WIN32)
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://hifi-public.s3.amazonaws.com/dependencies/OVRPlatformSDK_v1.10.0.zip
URL_MD5 e6c8264af16d904e6506acd5172fa0a9
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""
LOG_DOWNLOAD 1
)
ExternalProject_Get_Property(${EXTERNAL_NAME} SOURCE_DIR)
if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${SOURCE_DIR}/Windows/LibOVRPlatform64_1.lib CACHE TYPE INTERNAL)
else()
set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${SOURCE_DIR}/Windows/LibOVRPlatform32_1.lib CACHE TYPE INTERNAL)
endif()
set(${EXTERNAL_NAME_UPPER}_INCLUDE_DIRS ${SOURCE_DIR}/Include CACHE TYPE INTERNAL)
endif ()
# Hide this external target (for ide users)
set_target_properties(${EXTERNAL_NAME} PROPERTIES FOLDER "hidden/externals")

View file

@ -44,7 +44,7 @@ elseif (WIN32)
set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/quazip5d.lib CACHE FILEPATH "Location of QuaZip release library")
else ()
set(${EXTERNAL_NAME_UPPER}_LIBRARY_RELEASE ${INSTALL_DIR}/lib/libquazip5.so CACHE FILEPATH "Location of QuaZip release library")
set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/libquazip5d.so CACHE FILEPATH "Location of QuaZip release library")
set(${EXTERNAL_NAME_UPPER}_LIBRARY_DEBUG ${INSTALL_DIR}/lib/libquazip5.so CACHE FILEPATH "Location of QuaZip release library")
endif ()
include(SelectLibraryConfigurations)
@ -52,4 +52,4 @@ select_library_configurations(${EXTERNAL_NAME_UPPER})
# Force selected libraries into the cache
set(${EXTERNAL_NAME_UPPER}_LIBRARY ${${EXTERNAL_NAME_UPPER}_LIBRARY} CACHE FILEPATH "Location of QuaZip libraries")
set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES} CACHE FILEPATH "Location of QuaZip libraries")
set(${EXTERNAL_NAME_UPPER}_LIBRARIES ${${EXTERNAL_NAME_UPPER}_LIBRARIES} CACHE FILEPATH "Location of QuaZip libraries")

View file

@ -6,8 +6,8 @@ if (WIN32)
include(ExternalProject)
ExternalProject_Add(
${EXTERNAL_NAME}
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi3.zip
URL_MD5 1a2433f80a788a54c70f505ff4f43ac1
URL http://hifi-public.s3.amazonaws.com/dependencies/qtaudio_wasapi7.zip
URL_MD5 bc2861e50852dd590cdc773a14a041a7
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ""

View file

@ -16,6 +16,7 @@ if (HIFI_MEMORY_DEBUGGING)
if (UNIX)
SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -U_FORTIFY_SOURCE -fno-stack-protector -fno-omit-frame-pointer")
SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libasan -static-libstdc++ -fsanitize=address")
SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libasan -static-libstdc++ -fsanitize=address")
endif (UNIX)
endif ()
endmacro(SETUP_MEMORY_DEBUGGER)

View file

@ -43,26 +43,24 @@ macro(PACKAGE_LIBRARIES_FOR_DEPLOYMENT)
)
set(QTAUDIO_PATH $<TARGET_FILE_DIR:${TARGET_NAME}>/audio)
set(QTAUDIO_WIN7_PATH $<TARGET_FILE_DIR:${TARGET_NAME}>/audioWin7/audio)
set(QTAUDIO_WIN8_PATH $<TARGET_FILE_DIR:${TARGET_NAME}>/audioWin8/audio)
if (DEPLOY_PACKAGE)
# copy qtaudio_wasapi.dll alongside qtaudio_windows.dll, and let the installer resolve
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.dll ${QTAUDIO_PATH} && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.pdb ${QTAUDIO_PATH} )
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.dll ${QTAUDIO_PATH} && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.pdb ${QTAUDIO_PATH} )
)
elseif (${CMAKE_SYSTEM_VERSION} VERSION_LESS 6.2)
# continue using qtaudio_windows.dll on Windows 7
else ()
# replace qtaudio_windows.dll with qtaudio_wasapi.dll on Windows 8/8.1/10
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E remove ${QTAUDIO_PATH}/qtaudio_windows.dll && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.dll ${QTAUDIO_PATH} && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.pdb ${QTAUDIO_PATH} )
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E remove ${QTAUDIO_PATH}/qtaudio_windowsd.dll && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.dll ${QTAUDIO_PATH} && ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.pdb ${QTAUDIO_PATH} )
)
endif ()
# copy qtaudio_wasapi.dll and qtaudio_windows.dll in the correct directories for runtime selection
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory ${QTAUDIO_WIN7_PATH}
COMMAND ${CMAKE_COMMAND} -E make_directory ${QTAUDIO_WIN8_PATH}
# copy release DLLs
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E copy ${QTAUDIO_PATH}/qtaudio_windows.dll ${QTAUDIO_WIN7_PATH} )
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windows.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapi.dll ${QTAUDIO_WIN8_PATH} )
# copy debug DLLs
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E copy ${QTAUDIO_PATH}/qtaudio_windowsd.dll ${QTAUDIO_WIN7_PATH} )
COMMAND if exist ${QTAUDIO_PATH}/qtaudio_windowsd.dll ( ${CMAKE_COMMAND} -E copy ${WASAPI_DLL_PATH}/qtaudio_wasapid.dll ${QTAUDIO_WIN8_PATH} )
# remove directory
COMMAND ${CMAKE_COMMAND} -E remove_directory ${QTAUDIO_PATH}
)
endif ()
endmacro()

View file

@ -139,7 +139,8 @@ macro(SET_PACKAGING_PARAMETERS)
set(CLIENT_DESKTOP_SHORTCUT_REG_KEY "ClientDesktopShortcut")
set(CONSOLE_DESKTOP_SHORTCUT_REG_KEY "ConsoleDesktopShortcut")
set(CONSOLE_STARTUP_REG_KEY "ConsoleStartupShortcut")
set(LAUNCH_NOW_REG_KEY "LaunchAfterInstall")
set(CLIENT_LAUNCH_NOW_REG_KEY "ClientLaunchAfterInstall")
set(SERVER_LAUNCH_NOW_REG_KEY "ServerLaunchAfterInstall")
endif ()
# setup component categories for installer

View file

@ -0,0 +1,17 @@
#
# Created by Brad Hefta-Gaub on 2016/12/7
# Copyright 2016 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
#
macro(TARGET_KINECT)
# Kinect SDK data reader is only available on these platforms
if (WIN32)
#add_dependency_external_projects(kinect)
find_package(Kinect REQUIRED)
target_include_directories(${TARGET_NAME} PRIVATE ${KINECT_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} ${KINECT_LIBRARIES})
add_definitions(-DHAVE_KINECT)
endif(WIN32)
endmacro()

View file

@ -0,0 +1,26 @@
#
# FindGifCreator.cmake
#
# Try to find GifCreator include path.
# Once done this will define
#
# GIFCREATOR_INCLUDE_DIRS
#
# Created on 11/15/2016 by Zach Fox
# Copyright 2016 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
#
# setup hints for GifCreator search
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
hifi_library_search_hints("GIFCREATOR")
# locate header
find_path(GIFCREATOR_INCLUDE_DIRS "GifCreator/GifCreator.h" HINTS ${GIFCREATOR_SEARCH_DIRS})
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(GIFCREATOR DEFAULT_MSG GIFCREATOR_INCLUDE_DIRS)
mark_as_advanced(GIFCREATOR_INCLUDE_DIRS GIFCREATOR_SEARCH_DIRS)

View file

@ -0,0 +1,59 @@
#
# FindKinect.cmake
#
# Try to find the Perception Kinect SDK
#
# You must provide a KINECT_ROOT_DIR which contains lib and include directories
#
# Once done this will define
#
# KINECT_FOUND - system found Kinect SDK
# KINECT_INCLUDE_DIRS - the Kinect SDK include directory
# KINECT_LIBRARIES - Link this to use Kinect
#
# Created by Brad Hefta-Gaub on 2016/12/7
# Copyright 2016 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("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
hifi_library_search_hints("kinect")
find_path(KINECT_INCLUDE_DIRS Kinect.h PATH_SUFFIXES inc HINTS $ENV{KINECT_ROOT_DIR})
if (WIN32)
if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
set(ARCH_DIR "x64")
else()
set(ARCH_DIR "x86")
endif()
find_library(
KINECT_LIBRARY_RELEASE Kinect20
PATH_SUFFIXES "Lib/${ARCH_DIR}" "lib"
HINTS ${KINECT_SEARCH_DIRS}
PATH $ENV{KINECT_ROOT_DIR})
set(KINECT_LIBRARIES ${KINECT_LIBRARY})
# DLL not needed yet??
#find_path(KINECT_DLL_PATH Kinect20.Face.dll PATH_SUFFIXES "bin" HINTS ${KINECT_SEARCH_DIRS})
endif ()
include(SelectLibraryConfigurations)
select_library_configurations(KINECT)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(KINECT DEFAULT_MSG KINECT_INCLUDE_DIRS KINECT_LIBRARY)
# DLLs not needed yet
#if (WIN32)
# add_paths_to_fixup_libs(${KINECT_DLL_PATH})
#endif ()
mark_as_advanced(KINECT_INCLUDE_DIRS KINECT_LIBRARIES KINECT_SEARCH_DIRS)

View file

@ -0,0 +1,44 @@
#
# FindLibOVRPlatform.cmake
#
# Try to find the LibOVRPlatform library to use the Oculus Platform SDK
#
# You must provide a LIBOVRPLATFORM_ROOT_DIR which contains Windows and Include directories
#
# Once done this will define
#
# LIBOVRPLATFORM_FOUND - system found Oculus Platform SDK
# LIBOVRPLATFORM_INCLUDE_DIRS - the Oculus Platform include directory
# LIBOVRPLATFORM_LIBRARIES - Link this to use Oculus Platform
#
# Created on December 16, 2016 by Stephen Birarda
# Copyright 2016 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
#
if (WIN32)
# setup hints for LIBOVRPLATFORM search
include("${MACRO_DIR}/HifiLibrarySearchHints.cmake")
hifi_library_search_hints("LibOVRPlatform")
find_path(LIBOVRPLATFORM_INCLUDE_DIRS OVR_Platform.h PATH_SUFFIXES Include HINTS ${LIBOVRPLATFORM_SEARCH_DIRS})
if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
set(_LIB_NAME LibOVRPlatform64_1.lib)
else()
set(_LIB_NAME LibOVRPlatform32_1.lib)
endif()
find_library(LIBOVRPLATFORM_LIBRARY_RELEASE NAMES ${_LIB_NAME} PATH_SUFFIXES Windows HINTS ${LIBOVRPLATFORM_SEARCH_DIRS})
include(SelectLibraryConfigurations)
select_library_configurations(LIBOVRPLATFORM)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(LIBOVRPLATFORM DEFAULT_MSG LIBOVRPLATFORM_INCLUDE_DIRS LIBOVRPLATFORM_LIBRARIES)
mark_as_advanced(LIBOVRPLATFORM_INCLUDE_DIRS LIBOVRPLATFORM_LIBRARIES LIBOVRPLATFORM_SEARCH_DIRS)
endif ()

View file

@ -1,6 +1,6 @@
//
// BuildInfo.h.in
// cmake/macros
// cmake/templates
//
// Created by Stephen Birarda on 1/14/16.
// Copyright 2015 High Fidelity, Inc.
@ -11,10 +11,19 @@
#define USE_STABLE_GLOBAL_SERVICES @USE_STABLE_GLOBAL_SERVICES@
#include <QString>
namespace BuildInfo {
// WARNING: This file has been auto-generated.
// Check cmake/templates/BuildInfo.h.in if you want to modify it.
const QString INTERFACE_NAME = "Interface";
const QString ASSIGNMENT_CLIENT_NAME = "assignment-client";
const QString DOMAIN_SERVER_NAME = "domain-server";
const QString AC_CLIENT_SERVER_NAME = "ac-client";
const QString MODIFIED_ORGANIZATION = "@BUILD_ORGANIZATION@";
const QString ORGANIZATION_DOMAIN = "highfidelity.io";
const QString VERSION = "@BUILD_VERSION@";
const QString BUILD_BRANCH = "@BUILD_BRANCH@";
const QString BUILD_GLOBAL_SERVICES = "@BUILD_GLOBAL_SERVICES@";
}

View file

@ -38,7 +38,8 @@ set(POST_INSTALL_OPTIONS_REG_GROUP "@POST_INSTALL_OPTIONS_REG_GROUP@")
set(CLIENT_DESKTOP_SHORTCUT_REG_KEY "@CLIENT_DESKTOP_SHORTCUT_REG_KEY@")
set(CONSOLE_DESKTOP_SHORTCUT_REG_KEY "@CONSOLE_DESKTOP_SHORTCUT_REG_KEY@")
set(CONSOLE_STARTUP_REG_KEY "@CONSOLE_STARTUP_REG_KEY@")
set(LAUNCH_NOW_REG_KEY "@LAUNCH_NOW_REG_KEY@")
set(SERVER_LAUNCH_NOW_REG_KEY "@SERVER_LAUNCH_NOW_REG_KEY@")
set(CLIENT_LAUNCH_NOW_REG_KEY "@CLIENT_LAUNCH_NOW_REG_KEY@")
set(INSTALLER_HEADER_IMAGE "@INSTALLER_HEADER_IMAGE@")
set(UNINSTALLER_HEADER_IMAGE "@UNINSTALLER_HEADER_IMAGE@")
set(ADD_REMOVE_ICON_PATH "@ADD_REMOVE_ICON_PATH@")

View file

@ -135,10 +135,6 @@ Var AR_RegFlags
SectionSetFlags ${${SecName}} $AR_SecFlags
"default_${SecName}:"
; The client is always selected by default
${If} ${SecName} == @CLIENT_COMPONENT_NAME@
SectionSetFlags ${${SecName}} 17
${EndIf}
!insertmacro LoadSectionSelectedIntoVar ${SecName} ${SecName}_selected
!macroend
@ -368,7 +364,8 @@ Var PostInstallDialog
Var DesktopClientCheckbox
Var DesktopServerCheckbox
Var ServerStartupCheckbox
Var LaunchNowCheckbox
Var LaunchServerNowCheckbox
Var LaunchClientNowCheckbox
Var CurrentOffset
Var OffsetUnits
Var CopyFromProductionCheckbox
@ -431,17 +428,24 @@ Function PostInstallOptionsPage
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ after install"
${Else}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install"
Pop $LaunchServerNowCheckbox
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $LaunchServerNowCheckbox @SERVER_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
IntOp $CurrentOffset $CurrentOffset + 15
${EndIf}
Pop $LaunchNowCheckbox
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install"
Pop $LaunchClientNowCheckbox
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $LaunchNowCheckbox @LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
${EndIf}
${If} @PR_BUILD@ == 1
; a PR build defaults all install options expect LaunchNowCheckbox and the settings copy to unchecked
; a PR build defaults all install options expect LaunchServerNowCheckbox, LaunchClientNowCheckbox and the settings copy to unchecked
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${NSD_SetState} $DesktopClientCheckbox ${BST_UNCHECKED}
${EndIf}
@ -471,7 +475,8 @@ FunctionEnd
Var DesktopClientState
Var DesktopServerState
Var ServerStartupState
Var LaunchNowState
Var LaunchServerNowState
Var LaunchClientNowState
Var CopyFromProductionState
Function ReadPostInstallOptions
@ -493,8 +498,15 @@ Function ReadPostInstallOptions
${NSD_GetState} $CopyFromProductionCheckbox $CopyFromProductionState
${EndIf}
; check if we need to launch an application post-install
${NSD_GetState} $LaunchNowCheckbox $LaunchNowState
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
; check if we need to launch the server post-install
${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState
${EndIf}
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
; check if we need to launch the client post-install
${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState
${EndIf}
FunctionEnd
Function HandlePostInstallOptions
@ -565,20 +577,31 @@ Function HandlePostInstallOptions
${EndIf}
${EndIf}
${If} $LaunchNowState == ${BST_CHECKED}
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ YES
${If} $LaunchServerNowState == ${BST_CHECKED}
!insertmacro WritePostInstallOption @SERVER_LAUNCH_NOW_REG_KEY@ YES
; both launches use the explorer trick in case the user has elevated permissions for the installer
; it won't be possible to use this approach if either application should be launched with a command line param
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${If} $LaunchClientNowState == ${BST_CHECKED}
!insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ YES
; create shortcut with ARGUMENTS
CreateShortCut "$TEMP\SandboxShortcut.lnk" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@" "-- --launchInterface"
Exec '"$WINDIR\explorer.exe" "$TEMP\SandboxShortcut.lnk"'
${Else}
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"'
!insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ NO
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@CONSOLE_INSTALL_SUBDIR@\@CONSOLE_WIN_EXEC_NAME@"'
${EndIf}
${Else}
!insertmacro WritePostInstallOption @LAUNCH_NOW_REG_KEY@ NO
!insertmacro WritePostInstallOption @SERVER_LAUNCH_NOW_REG_KEY@ NO
; launch uses the explorer trick in case the user has elevated permissions for the installer
${If} $LaunchClientNowState == ${BST_CHECKED}
!insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ YES
Exec '"$WINDIR\explorer.exe" "$INSTDIR\@INTERFACE_WIN_EXEC_NAME@"'
${Else}
!insertmacro WritePostInstallOption @CLIENT_LAUNCH_NOW_REG_KEY@ NO
${EndIf}
${EndIf}
FunctionEnd
@ -607,17 +630,6 @@ Section "-Core installation"
Delete "$INSTDIR\version"
Delete "$INSTDIR\xinput1_3.dll"
; The installer includes two different Qt audio plugins.
; On Windows 8 and above, only qtaudio_wasapi.dll should be installed.
; On Windows 7 and below, only qtaudio_windows.dll should be installed.
${If} ${AtLeastWin8}
Delete "$INSTDIR\audio\qtaudio_windows.dll"
Delete "$INSTDIR\audio\qtaudio_windows.pdb"
${Else}
Delete "$INSTDIR\audio\qtaudio_wasapi.dll"
Delete "$INSTDIR\audio\qtaudio_wasapi.pdb"
${EndIf}
; Delete old desktop shortcuts before they were renamed during Sandbox rename
Delete "$DESKTOP\@PRE_SANDBOX_INTERFACE_SHORTCUT_NAME@.lnk"
Delete "$DESKTOP\@PRE_SANDBOX_CONSOLE_SHORTCUT_NAME@.lnk"

View file

@ -40,7 +40,7 @@
{
"name": "local_port",
"label": "Local UDP Port",
"help": "This is the local port your domain-server binds to for UDP connections.<br/>Depending on your router, this may need to be changed to run multiple full automatic networking domain-servers in the same network.",
"help": "This is the local port your domain-server binds to for UDP connections.<br/>Depending on your router, this may need to be changed to unique values for each domain-server in order to run multiple full automatic networking domain-servers in the same network. You can use the value 0 to have the domain-server select a random port, which will help in preventing port collisions.",
"default": "40102",
"type": "int",
"advanced": true
@ -372,6 +372,13 @@
"help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.",
"value-hidden": true
},
{
"name": "verify_http_password",
"label": "Verify HTTP Password",
"type": "password",
"help": "Must match the password entered above for change to be saved.",
"value-hidden": true
},
{
"name": "maximum_user_capacity",
"label": "Maximum User Capacity",
@ -388,6 +395,23 @@
"default": "",
"advanced": false
},
{
"name": "ac_subnet_whitelist",
"label": "Assignment Client IP address Whitelist",
"type": "table",
"can_add_new_rows": true,
"help": "The IP addresses or subnets of ACs that can connect to this server. You can specify an IP address or a subnet in CIDR notation ('A.B.C.D/E', Example: '10.0.0.0/24'). Local ACs (localhost) are always permitted and do not need to be added here.",
"numbered": false,
"advanced": true,
"columns": [
{
"name": "ip",
"label": "IP Address",
"type": "ip",
"can_set": true
}
]
},
{
"name": "standard_permissions",
"type": "table",
@ -667,6 +691,79 @@
}
]
},
{
"name": "permissions",
"type": "table",
"caption": "Permissions for Specific Users",
"can_add_new_rows": true,
"groups": [
{
"label": "User",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level or group permissions that might otherwise apply to that user.</p>'>?</a>",
"span": 7
}
],
"columns": [
{
"name": "permissions_id",
"label": ""
},
{
"name": "id_can_connect",
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez",
"label": "Rez",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez_tmp",
"label": "Rez Temporary",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_write_to_asset_server",
"label": "Write Assets",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_connect_past_max_capacity",
"label": "Ignore Max Capacity",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_kick",
"label": "Kick Users",
"type": "checkbox",
"editable": true,
"default": false
}
]
},
{
"name": "ip_permissions",
"type": "table",
@ -740,18 +837,89 @@
]
},
{
"name": "permissions",
"name": "mac_permissions",
"type": "table",
"caption": "Permissions for Specific Users",
"caption": "Permissions for Users with MAC Addresses",
"can_add_new_rows": true,
"groups": [
{
"label": "User",
"label": "MAC Address",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level or group permissions that might otherwise apply to that user.</p>'>?</a>",
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide MAC Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether users with specific MACs can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether users from specific MACs can change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether users with specific MACs can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether users with specific MACs can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether users with specific MACs can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether users with specific MACs can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific MAC will supersede any parameter-level permissions that might otherwise apply to that user (from groups or standard permissions above). MAC address permissions are overriden if the user has their own row in the users section.</p>'>?</a>",
"span": 7
}
],
"columns": [
{
"name": "permissions_id",
"label": ""
},
{
"name": "id_can_connect",
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez",
"label": "Rez",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez_tmp",
"label": "Rez Temporary",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_write_to_asset_server",
"label": "Write Assets",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_connect_past_max_capacity",
"label": "Ignore Max Capacity",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_kick",
"label": "Kick Users",
"type": "checkbox",
"editable": true,
"default": false
}
]
},
{
"name": "machine_fingerprint_permissions",
"type": "table",
"caption": "Permissions for Users with Machine Fingerprints",
"can_add_new_rows": true,
"groups": [
{
"label": "Machine Fingerprint",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide Machine Fingerprint Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether users with specific Machine Fingerprints can connect to the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether users from specific Machine Fingerprints can change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether users with specific Machine Fingerprints can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether users with specific Machine Fingerprints can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether users with specific Machine Fingerprints can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether users with specific Machine Fingerprints can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific Machine Fingerprint will supersede any parameter-level permissions that might otherwise apply to that user (from groups or standard permissions above). Machine Fingerprint address permissions are overriden if the user has their own row in the users section.</p>'>?</a>",
"span": 7
}
],
@ -866,6 +1034,52 @@
}
]
},
{
"name": "avatars",
"label": "Avatars",
"assignment-types": [1, 2],
"settings": [
{
"name": "min_avatar_scale",
"type": "double",
"label": "Minimum Avatar Scale",
"help": "Limits the scale of avatars in your domain. Must be at least 0.005.",
"placeholder": 0.25,
"default": 0.25
},
{
"name": "max_avatar_scale",
"type": "double",
"label": "Maximum Avatar Scale",
"help": "Limits the scale of avatars in your domain. Cannot be greater than 1000.",
"placeholder": 3.0,
"default": 3.0
}
]
},
{
"name": "audio_threading",
"label": "Audio Threading",
"assignment-types": [0],
"settings": [
{
"name": "auto_threads",
"label": "Automatically determine thread count",
"type": "checkbox",
"help": "Allow system to determine number of threads (recommended)",
"default": false,
"advanced": true
},
{
"name": "num_threads",
"label": "Number of Threads",
"help": "Threads to spin up for audio mixing (if not automatically set)",
"placeholder": "1",
"default": "1",
"advanced": true
}
]
},
{
"name": "audio_env",
"label": "Audio Environment",
@ -882,9 +1096,9 @@
{
"name": "noise_muting_threshold",
"label": "Noise Muting Threshold",
"help": "Loudness value for noise background between 0 and 1.0 (0: mute everyone, 1.0: never mute)",
"placeholder": "0.003",
"default": "0.003",
"help": "Loudness value for noise background between 0 and 1.0 (0: mute everyone, 1.0: never mute). 0.003 is a typical setting to mute loud people.",
"placeholder": "1.0",
"default": "1.0",
"advanced": false
},
{
@ -1075,6 +1289,22 @@
"default": "3600",
"advanced": true
},
{
"name": "entityScriptSourceWhitelist",
"label": "Entity Scripts Allowed from:",
"help": "Comma separated list of URLs (with optional paths) that entity scripts are allowed from. If someone attempts to create and entity or edit an entity to have a different domain, it will be rejected. If left blank, any domain is allowed.",
"placeholder": "",
"default": "",
"advanced": true
},
{
"name": "entityEditFilter",
"label": "Filter Entity Edits",
"help": "Check all entity edits against this filter function.",
"placeholder": "url whose content is like: function filter(properties) { return properties; }",
"default": "",
"advanced": true
},
{
"name": "persistFilePath",
"label": "Entities File Path",
@ -1255,8 +1485,8 @@
"type": "double",
"label": "Per-Node Bandwidth",
"help": "Desired maximum send bandwidth (in Megabits per second) to each node",
"placeholder": 1.0,
"default": 1.0,
"placeholder": 5.0,
"default": 5.0,
"advanced": true
}
]

View file

@ -125,6 +125,10 @@ tr.new-row {
background-color: #dff0d8;
}
tr.invalid-input {
background-color: #f2dede;
}
.graphable-stat {
text-align: center;
color: #5286BC;

View file

@ -12,7 +12,7 @@
<div id="setup-sidebar" class="hidden-xs" data-spy="affix" data-offset-top="55" data-clampedwidth="#setup-sidebar-col">
<script id="list-group-template" type="text/template">
<% _.each(descriptions, function(group){ %>
<% panelID = group.name ? group.name : group.label %>
<% panelID = group.name ? group.name : group.html_id %>
<li>
<a href="#<%- panelID %>" class="list-group-item">
<span class="badge"></span>

View file

@ -38,14 +38,15 @@ var Settings = {
DOMAIN_ID_SELECTOR: '[name="metaverse.id"]',
ACCESS_TOKEN_SELECTOR: '[name="metaverse.access_token"]',
PLACES_TABLE_ID: 'places-table',
FORM_ID: 'settings-form'
FORM_ID: 'settings-form',
INVALID_ROW_CLASS: 'invalid-input'
};
var viewHelpers = {
getFormGroup: function(keypath, setting, values, isAdvanced) {
form_group = "<div class='form-group " +
(isAdvanced ? Settings.ADVANCED_CLASS : "") + " " +
(setting.deprecated ? Settings.DEPRECATED_CLASS : "" ) + "' " +
(setting.deprecated ? Settings.DEPRECATED_CLASS : "" ) + "' " +
"data-keypath='" + keypath + "'>";
setting_value = _(values).valueForKeyPath(keypath);
@ -215,8 +216,8 @@ $(document).ready(function(){
sibling = sibling.next();
}
if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click();
// for tables with categories we add the entry and setup the new row on enter
if (sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).length) {
sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click();
// set focus to the first input in the new row
@ -891,39 +892,152 @@ function reloadSettings(callback) {
});
}
function validateInputs() {
// check if any new values are bad
var tables = $('table');
var inputsValid = true;
var tables = $('table');
// clear any current invalid rows
$('tr.' + Settings.INVALID_ROW_CLASS).removeClass(Settings.INVALID_ROW_CLASS);
function markParentRowInvalid(rowChild) {
$(rowChild).closest('tr').addClass(Settings.INVALID_ROW_CLASS);
}
_.each(tables, function(table) {
var inputs = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ':not([data-category]) input[data-changed="true"]');
var empty = false;
_.each(inputs, function(input){
var inputVal = $(input).val();
if (inputVal.length === 0) {
empty = true
markParentRowInvalid(input);
return;
}
});
if (empty) {
showErrorMessage("Error", "Empty field(s)");
inputsValid = false;
return
}
// validate keys specificially for spaces and equality to an existing key
var newKeys = $(table).find('tr.' + Settings.NEW_ROW_CLASS + ' td.key');
var keyWithSpaces = false;
var duplicateKey = false;
_.each(newKeys, function(keyCell) {
var keyVal = $(keyCell).children('input').val();
if (keyVal.indexOf(' ') !== -1) {
keyWithSpaces = true;
markParentRowInvalid(keyCell);
return;
}
// make sure we don't have duplicate keys in the table
var otherKeys = $(table).find('td.key').not(keyCell);
_.each(otherKeys, function(otherKeyCell) {
var keyInput = $(otherKeyCell).children('input');
if (keyInput.length) {
if ($(keyInput).val() == keyVal) {
duplicateKey = true;
}
} else if ($(otherKeyCell).html() == keyVal) {
duplicateKey = true;
}
if (duplicateKey) {
markParentRowInvalid(keyCell);
return;
}
});
});
if (keyWithSpaces) {
showErrorMessage("Error", "Key contains spaces");
inputsValid = false;
return
}
if (duplicateKey) {
showErrorMessage("Error", "Two keys cannot be identical");
inputsValid = false;
return;
}
});
return inputsValid;
}
var SETTINGS_ERROR_MESSAGE = "There was a problem saving domain settings. Please try again!";
function saveSettings() {
// disable any inputs not changed
$("input:not([data-changed])").each(function(){
$(this).prop('disabled', true);
});
// grab a JSON representation of the form via form2js
var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true);
if (validateInputs()) {
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
// check if we've set the basic http password - if so convert it to base64
if (formJSON["security"]) {
var password = formJSON["security"]["http_password"];
if (password && password.length > 0) {
formJSON["security"]["http_password"] = sha256_digest(password);
// disable any inputs not changed
$("input:not([data-changed])").each(function(){
$(this).prop('disabled', true);
});
// grab a JSON representation of the form via form2js
var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true);
// check if we've set the basic http password - if so convert it to base64
if (formJSON["security"]) {
var password = formJSON["security"]["http_password"];
if (password && password.length > 0) {
formJSON["security"]["http_password"] = sha256_digest(password);
}
}
// verify that the password and confirmation match before saving
var canPost = true;
if (formJSON["security"]) {
var password = formJSON["security"]["http_password"];
var verify_password = formJSON["security"]["verify_http_password"];
if (password && password.length > 0) {
if (password != verify_password) {
bootbox.alert({"message": "Passwords must match!", "title":"Password Error"});
canPost = false;
} else {
formJSON["security"]["http_password"] = sha256_digest(password);
delete formJSON["security"]["verify_http_password"];
}
}
}
console.log("----- SAVING ------");
console.log(formJSON);
// re-enable all inputs
$("input").each(function(){
$(this).prop('disabled', false);
});
// remove focus from the button
$(this).blur();
if (canPost) {
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
postSettings(formJSON);
}
}
console.log("----- SAVING ------");
console.log(formJSON);
// re-enable all inputs
$("input").each(function(){
$(this).prop('disabled', false);
});
// remove focus from the button
$(this).blur();
// POST the form JSON to the domain-server settings.json endpoint so the settings are saved
postSettings(formJSON);
}
$('body').on('click', '.save-button', function(e){
@ -1100,8 +1214,9 @@ function makeTable(setting, keypath, setting_value) {
if (setting.can_add_new_categories) {
html += makeTableCategoryInput(setting, numVisibleColumns);
}
if (setting.can_add_new_rows || setting.can_add_new_categories) {
html += makeTableInputs(setting, {}, "");
html += makeTableHiddenInputs(setting, {}, "");
}
}
html += "</table>"
@ -1127,7 +1242,7 @@ function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns,
return html;
}
function makeTableInputs(setting, initialValues, categoryValue) {
function makeTableHiddenInputs(setting, initialValues, categoryValue) {
var html = "<tr class='inputs'" + (setting.can_add_new_categories && !categoryValue ? " hidden" : "") + " " +
(categoryValue ? ("data-category='" + categoryValue + "'") : "") + " " +
(setting.categorize_by_key ? ("data-keep-field='" + setting.categorize_by_key + "'") : "") + ">";
@ -1138,7 +1253,7 @@ function makeTableInputs(setting, initialValues, categoryValue) {
if (setting.key) {
html += "<td class='key' name='" + setting.key.name + "'>\
<input type='text' class='form-control' placeholder='" + (_.has(setting.key, 'placeholder') ? setting.key.placeholder : "") + "' value=''>\
<input type='text' style='display: none;' class='form-control' placeholder='" + (_.has(setting.key, 'placeholder') ? setting.key.placeholder : "") + "' value=''>\
</td>"
}
@ -1147,14 +1262,14 @@ function makeTableInputs(setting, initialValues, categoryValue) {
if (col.type === "checkbox") {
html +=
"<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>" +
"<input type='checkbox' class='form-control table-checkbox' " +
"<input type='checkbox' style='display: none;' class='form-control table-checkbox' " +
"name='" + col.name + "'" + (defaultValue ? " checked" : "") + "/>" +
"</td>";
} else {
html +=
"<td " + (col.hidden ? "style='display: none;'" : "") + " class='" + Settings.DATA_COL_CLASS + "' " +
"name='" + col.name + "'>" +
"<input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "' " +
"<input type='text' style='display: none;' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "' " +
"value='" + (defaultValue || "") + "' data-default='" + (defaultValue || "") + "'" +
(col.readonly ? " readonly" : "") + ">" +
"</td>";
@ -1234,49 +1349,17 @@ function addTableRow(row) {
var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS);
var input_clone = row.clone();
if (!isArray) {
// Check key spaces
var key = row.children(".key").children("input").val()
if (key.indexOf(' ') !== -1) {
showErrorMessage("Error", "Key contains spaces")
return
}
// Check keys with the same name
var equals = false;
_.each(columns.children(".key"), function(element) {
if ($(element).text() === key) {
equals = true
return
}
})
if (equals) {
showErrorMessage("Error", "Two keys cannot be identical")
return
}
// show the key input
var keyInput = row.children(".key").children("input");
}
// Check empty fields
var empty = false;
_.each(row.children('.' + Settings.DATA_COL_CLASS + ' input'), function(element) {
if ($(element).val().length === 0) {
empty = true
return
}
})
if (empty) {
showErrorMessage("Error", "Empty field(s)")
return
}
var input_clone = row.clone()
// Change input row to data row
var table = row.parents("table")
var setting_name = table.attr("name")
var full_name = setting_name + "." + key
row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS)
row.removeClass("inputs")
var table = row.parents("table");
var setting_name = table.attr("name");
row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS);
_.each(row.children(), function(element) {
if ($(element).hasClass("numbered")) {
@ -1298,56 +1381,43 @@ function addTableRow(row) {
anchor.addClass(Settings.DEL_ROW_SPAN_CLASSES)
} else if ($(element).hasClass("key")) {
var input = $(element).children("input")
$(element).html(input.val())
input.remove()
input.show();
} else if ($(element).hasClass(Settings.DATA_COL_CLASS)) {
// Hide inputs
var input = $(element).find("input")
var isCheckbox = false;
var isTime = false;
if (input.hasClass("table-checkbox")) {
input = $(input).parent();
isCheckbox = true;
} else if (input.hasClass("table-time")) {
input = $(input).parent();
isTime = true;
}
// show inputs
var input = $(element).find("input");
input.show();
var val = input.val();
if (isCheckbox) {
// don't hide the checkbox
val = $(input).find("input").is(':checked');
} else if (isTime) {
// don't hide the time
} else {
input.attr("type", "hidden")
}
var isCheckbox = input.hasClass("table-checkbox");
if (isArray) {
var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length
var key = $(element).attr('name')
var key = $(element).attr('name');
// are there multiple columns or just one?
// with multiple we have an array of Objects, with one we have an array of whatever the value type is
var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length
if (isCheckbox) {
$(input).find("input").attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
} else {
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
}
} else {
input.attr("name", full_name + "." + $(element).attr("name"))
// because the name of the setting in question requires the key
// setup a hook to change the HTML name of the element whenever the key changes
var colName = $(element).attr("name");
keyInput.on('change', function(){
input.attr("name", setting_name + "." + $(this).val() + "." + colName);
});
}
if (isCheckbox) {
$(input).find("input").attr("data-changed", "true");
} else {
input.attr("data-changed", "true");
$(element).append(val);
}
} else {
console.log("Unknown table element")
console.log("Unknown table element");
}
});
@ -1377,7 +1447,12 @@ function deleteTableRow($row) {
$row.empty();
if (!isArray) {
$row.html("<input type='hidden' class='form-control' name='" + $row.attr('name') + "' data-changed='true' value=''>");
if ($row.attr('name')) {
$row.html("<input type='hidden' class='form-control' name='" + $row.attr('name') + "' data-changed='true' value=''>");
} else {
// for rows that didn't have a key, simply remove the row
$row.remove();
}
} else {
if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) {
// This is the last row of the category, so delete the header

View file

@ -46,10 +46,9 @@ QUuid DomainGatekeeper::assignmentUUIDForPendingAssignment(const QUuid& tempUUID
}
}
const NodeSet STATICALLY_ASSIGNED_NODES = NodeSet() << NodeType::AudioMixer
<< NodeType::AvatarMixer << NodeType::EntityServer
<< NodeType::AssetServer
<< NodeType::MessagesMixer;
const NodeSet STATICALLY_ASSIGNED_NODES = NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer
<< NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer
<< NodeType::EntityScriptServer;
void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessage> message) {
if (message->getSize() == 0) {
@ -72,7 +71,7 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
}
static const NodeSet VALID_NODE_TYPES {
NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::AssetServer, NodeType::EntityServer, NodeType::Agent, NodeType::MessagesMixer
NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::AssetServer, NodeType::EntityServer, NodeType::Agent, NodeType::MessagesMixer, NodeType::EntityScriptServer
};
if (!VALID_NODE_TYPES.contains(nodeConnection.nodeType)) {
@ -107,20 +106,34 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
if (node) {
// set the sending sock addr and node interest set on this node
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
nodeData->setSendingSockAddr(message->getSenderSockAddr());
nodeData->setNodeInterestSet(nodeConnection.interestList.toSet());
// guard against patched agents asking to hear about other agents
auto safeInterestSet = nodeConnection.interestList.toSet();
if (nodeConnection.nodeType == NodeType::Agent) {
safeInterestSet.remove(NodeType::Agent);
}
nodeData->setNodeInterestSet(safeInterestSet);
nodeData->setPlaceName(nodeConnection.placeName);
qDebug() << "Allowed connection from node" << uuidStringWithoutCurlyBraces(node->getUUID())
<< "on" << message->getSenderSockAddr() << "with MAC" << nodeConnection.hardwareAddress
<< "and machine fingerprint" << nodeConnection.machineFingerprint;
// signal that we just connected a node so the DomainServer can get it a list
// and broadcast its presence right away
emit connectedNode(node);
} else {
qDebug() << "Refusing connection from node at" << message->getSenderSockAddr();
qDebug() << "Refusing connection from node at" << message->getSenderSockAddr()
<< "with hardware address" << nodeConnection.hardwareAddress
<< "and machine fingerprint" << nodeConnection.machineFingerprint;
}
}
NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress) {
NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress,
const QString& hardwareAddress, const QUuid& machineFingerprint) {
NodePermissions userPerms;
userPerms.setAll(false);
@ -137,8 +150,19 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: unverified or no username for" << userPerms.getID() << ", so:" << userPerms;
#endif
if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) {
// this user comes from a MAC we have in our permissions table, apply those permissions
userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress);
if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) {
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms;
#endif
} else if (_server->_settingsManager.hasPermissionsForMachineFingerprint(machineFingerprint)) {
userPerms = _server->_settingsManager.getPermissionsForMachineFingerprint(machineFingerprint);
#ifdef WANT_DEBUG
qDebug(() << "| user-permissions: specific Machine Fingerprint matches, so: " << userPerms;
#endif
} else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) {
// this user comes from an IP we have in our permissions table, apply those permissions
userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress);
@ -151,6 +175,13 @@ NodePermissions DomainGatekeeper::setPermissionsForUser(bool isLocalUser, QStrin
userPerms = _server->_settingsManager.getPermissionsForName(verifiedUsername);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: specific user matches, so:" << userPerms;
#endif
} else if (!hardwareAddress.isEmpty() && _server->_settingsManager.hasPermissionsForMAC(hardwareAddress)) {
// this user comes from a MAC we have in our permissions table, apply those permissions
userPerms = _server->_settingsManager.getPermissionsForMAC(hardwareAddress);
#ifdef WANT_DEBUG
qDebug() << "| user-permissions: specific MAC matches, so:" << userPerms;
#endif
} else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) {
// this user comes from an IP we have in our permissions table, apply those permissions
@ -248,7 +279,16 @@ void DomainGatekeeper::updateNodePermissions() {
// or the public socket if we haven't activated a socket for the node yet
HifiSockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket();
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress());
QString hardwareAddress;
QUuid machineFingerprint;
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
if (nodeData) {
hardwareAddress = nodeData->getHardwareAddress();
machineFingerprint = nodeData->getMachineFingerprint();
}
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress(), hardwareAddress, machineFingerprint);
}
node->setPermissions(userPerms);
@ -295,12 +335,15 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
// add the new node
SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection);
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(newNode->getLinkedData());
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(newNode->getLinkedData());
// set assignment related data on the linked data for this node
nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID());
nodeData->setWalletUUID(it->second.getWalletUUID());
nodeData->setNodeVersion(it->second.getNodeVersion());
nodeData->setHardwareAddress(nodeConnection.hardwareAddress);
nodeData->setMachineFingerprint(nodeConnection.machineFingerprint);
nodeData->setWasAssigned(true);
// cleanup the PendingAssignedNodeData for this assignment now that it's connecting
@ -362,7 +405,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
}
}
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress());
userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress(),
nodeConnection.hardwareAddress, nodeConnection.machineFingerprint);
if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) {
sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.",
@ -413,11 +457,17 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
newNode->setPermissions(userPerms);
// grab the linked data for our new node so we can set the username
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(newNode->getLinkedData());
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(newNode->getLinkedData());
// if we have a username from the connect request, set it on the DomainServerNodeData
nodeData->setUsername(username);
// set the hardware address passed in the connect request
nodeData->setHardwareAddress(nodeConnection.hardwareAddress);
// set the machine fingerprint passed in the connect request
nodeData->setMachineFingerprint(nodeConnection.machineFingerprint);
// also add an interpolation to DomainServerNodeData so that servers can get username in stats
nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY,
uuidStringWithoutCurlyBraces(newNode->getUUID()), username);

View file

@ -107,7 +107,8 @@ private:
QSet<QString> _domainOwnerFriends; // keep track of friends of the domain owner
QSet<QString> _inFlightGroupMembershipsRequests; // keep track of which we've already asked for
NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress);
NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress,
const QString& hardwareAddress, const QUuid& machineFingerprint);
void getGroupMemberships(const QString& username);
// void getIsGroupMember(const QString& username, const QUuid groupID);

View file

@ -43,6 +43,8 @@
#include "DomainServerNodeData.h"
#include "NodeConnectionData.h"
#include <Trace.h>
#include <StatTracker.h>
int const DomainServer::EXIT_CODE_REBOOT = 234923;
@ -72,21 +74,14 @@ DomainServer::DomainServer(int argc, char* argv[]) :
_iceServerPort(ICE_SERVER_DEFAULT_PORT)
{
parseCommandLine();
qInstallMessageHandler(LogHandler::verboseMessageHandler);
DependencyManager::set<tracing::Tracer>();
DependencyManager::set<StatTracker>();
LogUtils::init();
Setting::init();
connect(this, &QCoreApplication::aboutToQuit, this, &DomainServer::aboutToQuit);
setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION);
setOrganizationDomain("highfidelity.io");
setApplicationName("domain-server");
setApplicationVersion(BuildInfo::VERSION);
QSettings::setDefaultFormat(QSettings::IniFormat);
qDebug() << "Setting up domain-server";
qDebug() << "[VERSION] Build sequence:" << qPrintable(applicationVersion());
qDebug() << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION;
qDebug() << "[VERSION] VERSION:" << BuildInfo::VERSION;
@ -112,7 +107,7 @@ DomainServer::DomainServer(int argc, char* argv[]) :
qRegisterMetaType<DomainServerWebSessionData>("DomainServerWebSessionData");
qRegisterMetaTypeStreamOperators<DomainServerWebSessionData>("DomainServerWebSessionData");
// make sure we hear about newly connected nodes from our gatekeeper
connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode);
@ -161,6 +156,42 @@ DomainServer::DomainServer(int argc, char* argv[]) :
qDebug() << "domain-server is running";
static const QString AC_SUBNET_WHITELIST_SETTING_PATH = "security.ac_subnet_whitelist";
static const Subnet LOCALHOST { QHostAddress("127.0.0.1"), 32 };
_acSubnetWhitelist = { LOCALHOST };
auto whitelist = _settingsManager.valueOrDefaultValueForKeyPath(AC_SUBNET_WHITELIST_SETTING_PATH).toStringList();
for (auto& subnet : whitelist) {
auto netmaskParts = subnet.trimmed().split("/");
if (netmaskParts.size() > 2) {
qDebug() << "Ignoring subnet in whitelist, malformed: " << subnet;
continue;
}
// The default netmask is 32 if one has not been specified, which will
// match only the ip provided.
int netmask = 32;
if (netmaskParts.size() == 2) {
bool ok;
netmask = netmaskParts[1].toInt(&ok);
if (!ok) {
qDebug() << "Ignoring subnet in whitelist, bad netmask: " << subnet;
continue;
}
}
auto ip = QHostAddress(netmaskParts[0]);
if (!ip.isNull()) {
qDebug() << "Adding AC whitelist subnet: " << subnet << " -> " << (ip.toString() + "/" + QString::number(netmask));
_acSubnetWhitelist.push_back({ ip , netmask });
} else {
qDebug() << "Ignoring subnet in whitelist, invalid ip portion: " << subnet;
}
}
}
void DomainServer::parseCommandLine() {
@ -211,6 +242,7 @@ void DomainServer::parseCommandLine() {
}
DomainServer::~DomainServer() {
qInfo() << "Domain Server is shutting down.";
// destroy the LimitedNodeList before the DomainServer QCoreApplication is down
DependencyManager::destroy<LimitedNodeList>();
}
@ -223,12 +255,6 @@ void DomainServer::queuedQuit(QString quitMessage, int exitCode) {
QCoreApplication::exit(exitCode);
}
void DomainServer::aboutToQuit() {
// clear the log handler so that Qt doesn't call the destructor on LogHandler
qInstallMessageHandler(0);
}
void DomainServer::restart() {
qDebug() << "domain-server is restarting.";
@ -255,7 +281,7 @@ bool DomainServer::optionallyReadX509KeyAndCertificate() {
QString keyPassphraseString = QProcessEnvironment::systemEnvironment().value(X509_KEY_PASSPHRASE_ENV);
qDebug() << "Reading certificate file at" << certPath << "for HTTPS.";
qDebug() << "Reading key file at" << keyPath << "for HTTPS.";
qDebug() << "Reading key file at" << keyPath << "for HTTPS.";
QFile certFile(certPath);
certFile.open(QIODevice::ReadOnly);
@ -502,11 +528,12 @@ void DomainServer::setupNodeListAndAssignments() {
packetReceiver.registerListener(PacketType::DomainServerPathQuery, this, "processPathQueryPacket");
packetReceiver.registerListener(PacketType::NodeJsonStats, this, "processNodeJSONStatsPacket");
packetReceiver.registerListener(PacketType::DomainDisconnectRequest, this, "processNodeDisconnectRequestPacket");
// NodeList won't be available to the settings manager when it is created, so call registerListener here
packetReceiver.registerListener(PacketType::DomainSettingsRequest, &_settingsManager, "processSettingsRequestPacket");
packetReceiver.registerListener(PacketType::NodeKickRequest, &_settingsManager, "processNodeKickRequestPacket");
packetReceiver.registerListener(PacketType::UsernameFromIDRequest, &_settingsManager, "processUsernameFromIDRequestPacket");
// register the gatekeeper for the packets it needs to receive
packetReceiver.registerListener(PacketType::DomainConnectRequest, &_gatekeeper, "processConnectRequestPacket");
packetReceiver.registerListener(PacketType::ICEPing, &_gatekeeper, "processICEPingPacket");
@ -515,7 +542,7 @@ void DomainServer::setupNodeListAndAssignments() {
packetReceiver.registerListener(PacketType::ICEServerHeartbeatDenied, this, "processICEServerHeartbeatDenialPacket");
packetReceiver.registerListener(PacketType::ICEServerHeartbeatACK, this, "processICEServerHeartbeatACK");
// add whatever static assignments that have been parsed to the queue
addStaticAssignmentsToQueue();
@ -781,21 +808,19 @@ void DomainServer::populateDefaultStaticAssignmentsExcludingTypes(const QSet<Ass
for (Assignment::Type defaultedType = Assignment::AudioMixerType;
defaultedType != Assignment::AllTypes;
defaultedType = static_cast<Assignment::Type>(static_cast<int>(defaultedType) + 1)) {
if (!excludedTypes.contains(defaultedType)
&& defaultedType != Assignment::UNUSED_1
&& defaultedType != Assignment::AgentType) {
if (!excludedTypes.contains(defaultedType) && defaultedType != Assignment::AgentType) {
if (defaultedType == Assignment::AssetServerType) {
// Make sure the asset-server is enabled before adding it here.
// Initially we do not assign it by default so we can test it in HF domains first
static const QString ASSET_SERVER_ENABLED_KEYPATH = "asset_server.enabled";
if (!_settingsManager.valueOrDefaultValueForKeyPath(ASSET_SERVER_ENABLED_KEYPATH).toBool()) {
// skip to the next iteration if asset-server isn't enabled
continue;
}
}
// type has not been set from a command line or config file config, use the default
// by clearing whatever exists and writing a single default assignment with no payload
Assignment* newAssignment = new Assignment(Assignment::CreateCommand, (Assignment::Type) defaultedType);
@ -812,10 +837,17 @@ void DomainServer::processListRequestPacket(QSharedPointer<ReceivedMessage> mess
// update this node's sockets in case they have changed
sendingNode->setPublicSocket(nodeRequestData.publicSockAddr);
sendingNode->setLocalSocket(nodeRequestData.localSockAddr);
// update the NodeInterestSet in case there have been any changes
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(sendingNode->getLinkedData());
nodeData->setNodeInterestSet(nodeRequestData.interestList.toSet());
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(sendingNode->getLinkedData());
// guard against patched agents asking to hear about other agents
auto safeInterestSet = nodeRequestData.interestList.toSet();
if (sendingNode->getType() == NodeType::Agent) {
safeInterestSet.remove(NodeType::Agent);
}
nodeData->setNodeInterestSet(safeInterestSet);
// update the connecting hostname in case it has changed
nodeData->setPlaceName(nodeRequestData.placeName);
@ -823,6 +855,44 @@ void DomainServer::processListRequestPacket(QSharedPointer<ReceivedMessage> mess
sendDomainListToNode(sendingNode, message->getSenderSockAddr());
}
bool DomainServer::isInInterestSet(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB) {
auto nodeAData = static_cast<DomainServerNodeData*>(nodeA->getLinkedData());
auto nodeBData = static_cast<DomainServerNodeData*>(nodeB->getLinkedData());
// if we have no linked data for node A then B can't possibly be in the interest set
if (!nodeAData) {
return false;
}
// first check if the general interest set A contains the type for B
if (nodeAData->getNodeInterestSet().contains(nodeB->getType())) {
// given that there is a match in the general interest set, do any special checks
// (1/19/17) Agents only need to connect to Entity Script Servers to perform administrative tasks
// related to entity server scripts. Only agents with rez permissions should be doing that, so
// if the agent does not have those permissions, we do not want them and the server to incur the
// overhead of connecting to one another. Additionally we exclude agents that do not care about the
// Entity Script Server and won't attempt to connect to it.
bool isAgentWithoutRights = nodeA->getType() == NodeType::Agent
&& nodeB->getType() == NodeType::EntityScriptServer
&& !nodeA->getCanRez() && !nodeA->getCanRezTmp();
if (isAgentWithoutRights) {
return false;
}
bool isScriptServerForIneffectiveAgent =
(nodeA->getType() == NodeType::EntityScriptServer && nodeB->getType() == NodeType::Agent)
&& ((nodeBData && !nodeBData->getNodeInterestSet().contains(NodeType::EntityScriptServer))
|| (!nodeB->getCanRez() && !nodeB->getCanRezTmp()));
return !isScriptServerForIneffectiveAgent;
} else {
return false;
}
}
unsigned int DomainServer::countConnectedUsers() {
unsigned int result = 0;
auto nodeList = DependencyManager::get<LimitedNodeList>();
@ -894,14 +964,14 @@ void DomainServer::handleConnectedNode(SharedNodePointer newNode) {
void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr &senderSockAddr) {
const int NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES = NUM_BYTES_RFC4122_UUID + NUM_BYTES_RFC4122_UUID + 2;
// setup the extended header for the domain list packets
// this data is at the beginning of each of the domain list packets
QByteArray extendedHeader(NUM_DOMAIN_LIST_EXTENDED_HEADER_BYTES, 0);
QDataStream extendedHeaderStream(&extendedHeader, QIODevice::WriteOnly);
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
extendedHeaderStream << limitedNodeList->getSessionUUID();
extendedHeaderStream << node->getUUID();
extendedHeaderStream << node->getPermissions();
@ -911,7 +981,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
// always send the node their own UUID back
QDataStream domainListStream(domainListPackets.get());
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
// store the nodeInterestSet on this DomainServerNodeData, in case it has changed
auto& nodeInterestSet = nodeData->getNodeInterestSet();
@ -921,9 +991,8 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
// DTLSServerSession* dtlsSession = _isUsingDTLS ? _dtlsSessions[senderSockAddr] : NULL;
if (nodeData->isAuthenticated()) {
// if this authenticated node has any interest types, send back those nodes as well
limitedNodeList->eachNode([&](const SharedNodePointer& otherNode){
if (otherNode->getUUID() != node->getUUID() && nodeInterestSet.contains(otherNode->getType())) {
limitedNodeList->eachNode([&](const SharedNodePointer& otherNode) {
if (otherNode->getUUID() != node->getUUID() && isInInterestSet(node, otherNode)) {
// since we're about to add a node to the packet we start a segment
domainListPackets->startSegment();
@ -939,7 +1008,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
});
}
}
// send an empty list to the node, in case there were no other nodes
domainListPackets->closeCurrentPacket(true);
@ -948,8 +1017,8 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
}
QUuid DomainServer::connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB) {
DomainServerNodeData* nodeAData = dynamic_cast<DomainServerNodeData*>(nodeA->getLinkedData());
DomainServerNodeData* nodeBData = dynamic_cast<DomainServerNodeData*>(nodeB->getLinkedData());
DomainServerNodeData* nodeAData = static_cast<DomainServerNodeData*>(nodeA->getLinkedData());
DomainServerNodeData* nodeBData = static_cast<DomainServerNodeData*>(nodeB->getLinkedData());
if (nodeAData && nodeBData) {
QUuid& secretUUID = nodeAData->getSessionSecretHash()[nodeB->getUUID()];
@ -959,7 +1028,7 @@ QUuid DomainServer::connectionSecretForNodes(const SharedNodePointer& nodeA, con
secretUUID = QUuid::createUuid();
// set it on the other Node's sessionSecretHash
reinterpret_cast<DomainServerNodeData*>(nodeBData)->getSessionSecretHash().insert(nodeA->getUUID(), secretUUID);
static_cast<DomainServerNodeData*>(nodeBData)->getSessionSecretHash().insert(nodeA->getUUID(), secretUUID);
}
return secretUUID;
@ -985,8 +1054,7 @@ void DomainServer::broadcastNewNode(const SharedNodePointer& addedNode) {
[&](const SharedNodePointer& node)->bool {
if (node->getLinkedData() && node->getActiveSocket() && node != addedNode) {
// is the added Node in this node's interest list?
DomainServerNodeData* nodeData = dynamic_cast<DomainServerNodeData*>(node->getLinkedData());
return nodeData->getNodeInterestSet().contains(addedNode->getType());
return isInInterestSet(node, addedNode);
} else {
return false;
}
@ -1009,6 +1077,21 @@ void DomainServer::processRequestAssignmentPacket(QSharedPointer<ReceivedMessage
// construct the requested assignment from the packet data
Assignment requestAssignment(*message);
auto senderAddr = message->getSenderSockAddr().getAddress();
auto isHostAddressInSubnet = [&senderAddr](const Subnet& mask) -> bool {
return senderAddr.isInSubnet(mask);
};
auto it = find_if(_acSubnetWhitelist.begin(), _acSubnetWhitelist.end(), isHostAddressInSubnet);
if (it == _acSubnetWhitelist.end()) {
static QString repeatedMessage = LogHandler::getInstance().addRepeatedMessageRegex(
"Received an assignment connect request from a disallowed ip address: [^ ]+");
qDebug() << "Received an assignment connect request from a disallowed ip address:"
<< senderAddr.toString();
return;
}
// Suppress these for Assignment::AgentType to once per 5 seconds
static QElapsedTimer noisyMessageTimer;
static bool wasNoisyTimerStarted = false;
@ -1074,7 +1157,7 @@ void DomainServer::processRequestAssignmentPacket(QSharedPointer<ReceivedMessage
void DomainServer::setupPendingAssignmentCredits() {
// enumerate the NodeList to find the assigned nodes
DependencyManager::get<LimitedNodeList>()->eachNode([&](const SharedNodePointer& node){
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
if (!nodeData->getAssignmentUUID().isNull() && !nodeData->getWalletUUID().isNull()) {
// check if we have a non-finalized transaction for this node to add this amount to
@ -1460,7 +1543,7 @@ void DomainServer::sendHeartbeatToIceServer() {
}
void DomainServer::processNodeJSONStatsPacket(QSharedPointer<ReceivedMessage> packetList, SharedNodePointer sendingNode) {
auto nodeData = dynamic_cast<DomainServerNodeData*>(sendingNode->getLinkedData());
auto nodeData = static_cast<DomainServerNodeData*>(sendingNode->getLinkedData());
if (nodeData) {
nodeData->updateJSONStats(packetList->getMessage());
}
@ -1506,7 +1589,7 @@ QJsonObject DomainServer::jsonObjectForNode(const SharedNodePointer& node) {
nodeJson[JSON_KEY_UPTIME] = QString::number(double(QDateTime::currentMSecsSinceEpoch() - node->getWakeTimestamp()) / 1000.0);
// if the node has pool information, add it
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
// add the node username, if it exists
nodeJson[JSON_KEY_USERNAME] = nodeData->getUsername();
@ -1574,23 +1657,23 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
if (connection->requestOperation() == QNetworkAccessManager::GetOperation
&& assignmentRegex.indexIn(url.path()) != -1) {
QUuid nodeUUID = QUuid(assignmentRegex.cap(1));
auto matchingNode = nodeList->nodeWithUUID(nodeUUID);
// don't handle if we don't have a matching node
if (!matchingNode) {
return false;
}
auto nodeData = dynamic_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
auto nodeData = static_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
// don't handle if we don't have node data for this node
if (!nodeData) {
return false;
}
SharedAssignmentPointer matchingAssignment = _allAssignments.value(nodeData->getAssignmentUUID());
// check if we have an assignment that matches this temp UUID, and it is a scripted assignment
if (matchingAssignment && matchingAssignment->getType() == Assignment::AgentType) {
// we have a matching assignment and it is for the right type, have the HTTP manager handle it
@ -1605,7 +1688,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
return true;
}
// request not handled
return false;
}
@ -1637,7 +1720,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
// enumerate the NodeList to find the assigned nodes
nodeList->eachNode([this, &assignedNodesJSON](const SharedNodePointer& node){
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
if (!nodeData->getAssignmentUUID().isNull()) {
// add the node using the UUID as the key
@ -1725,7 +1808,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
if (matchingNode) {
// create a QJsonDocument with the stats QJsonObject
QJsonObject statsObject =
reinterpret_cast<DomainServerNodeData*>(matchingNode->getLinkedData())->getStatsJSONObject();
static_cast<DomainServerNodeData*>(matchingNode->getLinkedData())->getStatsJSONObject();
// add the node type to the JSON data for output purposes
statsObject["node_type"] = NodeType::getNodeTypeName(matchingNode->getType()).toLower().replace(' ', '-');
@ -2197,7 +2280,7 @@ void DomainServer::addStaticAssignmentsToQueue() {
// if the domain-server has just restarted,
// check if there are static assignments that we need to throw into the assignment queue
auto sharedAssignments = _allAssignments.values();
// sort the assignments to put the server/mixer assignments first
qSort(sharedAssignments.begin(), sharedAssignments.end(), [](SharedAssignmentPointer a, SharedAssignmentPointer b){
if (a->getType() == b->getType()) {
@ -2208,9 +2291,9 @@ void DomainServer::addStaticAssignmentsToQueue() {
return a->getType() != Assignment::AgentType;
}
});
auto staticAssignment = sharedAssignments.begin();
while (staticAssignment != sharedAssignments.end()) {
// add any of the un-matched static assignments to the queue
@ -2321,7 +2404,6 @@ void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMes
}
void DomainServer::handleKillNode(SharedNodePointer nodeToKill) {
auto nodeType = nodeToKill->getType();
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
const QUuid& nodeUUID = nodeToKill->getUUID();
@ -2333,10 +2415,9 @@ void DomainServer::handleKillNode(SharedNodePointer nodeToKill) {
removedNodePacket->write(nodeUUID.toRfc4122());
// broadcast out the DomainServerRemovedNode message
limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool {
limitedNodeList->eachMatchingNode([this, &nodeToKill](const SharedNodePointer& otherNode) -> bool {
// only send the removed node packet to nodes that care about the type of node this was
auto nodeLinkedData = dynamic_cast<DomainServerNodeData*>(otherNode->getLinkedData());
return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType);
return isInInterestSet(otherNode, nodeToKill);
}, [&limitedNodeList](const SharedNodePointer& otherNode){
limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode);
});

View file

@ -36,6 +36,9 @@
typedef QSharedPointer<Assignment> SharedAssignmentPointer;
typedef QMultiHash<QUuid, WalletTransaction*> TransactionHash;
using Subnet = QPair<QHostAddress, int>;
using SubnetList = std::vector<Subnet>;
class DomainServer : public QCoreApplication, public HTTPSRequestHandler {
Q_OBJECT
public:
@ -72,8 +75,6 @@ public slots:
void processICEServerHeartbeatACK(QSharedPointer<ReceivedMessage> message);
private slots:
void aboutToQuit();
void setupPendingAssignmentCredits();
void sendPendingTransactionsToServer();
@ -131,6 +132,8 @@ private:
void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr);
bool isInInterestSet(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB);
QUuid connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB);
void broadcastNewNode(const SharedNodePointer& node);
@ -150,18 +153,16 @@ private:
bool isAuthenticatedRequest(HTTPConnection* connection, const QUrl& url);
void handleTokenRequestFinished();
QNetworkReply* profileRequestGivenTokenReply(QNetworkReply* tokenReply);
void handleProfileRequestFinished();
Headers setupCookieHeadersFromProfileReply(QNetworkReply* profileReply);
void loadExistingSessionsFromSettings();
QJsonObject jsonForSocket(const HifiSockAddr& socket);
QJsonObject jsonObjectForNode(const SharedNodePointer& node);
void setupGroupCacheRefresh();
SubnetList _acSubnetWhitelist;
DomainGatekeeper _gatekeeper;
HTTPManager _httpManager;

View file

@ -53,7 +53,13 @@ public:
void setNodeVersion(const QString& nodeVersion) { _nodeVersion = nodeVersion; }
const QString& getNodeVersion() { return _nodeVersion; }
void setHardwareAddress(const QString& hardwareAddress) { _hardwareAddress = hardwareAddress; }
const QString& getHardwareAddress() { return _hardwareAddress; }
void setMachineFingerprint(const QUuid& machineFingerprint) { _machineFingerprint = machineFingerprint; }
const QUuid& getMachineFingerprint() { return _machineFingerprint; }
void addOverrideForKey(const QString& key, const QString& value, const QString& overrideValue);
void removeOverrideForKey(const QString& key, const QString& value);
@ -81,6 +87,8 @@ private:
bool _isAuthenticated = true;
NodeSet _nodeInterestSet;
QString _nodeVersion;
QString _hardwareAddress;
QUuid _machineFingerprint;
QString _placeName;

View file

@ -9,6 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "DomainServerSettingsManager.h"
#include <algorithm>
#include <QtCore/QCoreApplication>
@ -16,20 +18,20 @@
#include <QtCore/QFile>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
#include <QtCore/QSettings>
#include <QtCore/QStandardPaths>
#include <QtCore/QUrl>
#include <QtCore/QUrlQuery>
#include <AccountManager.h>
#include <QTimeZone>
#include <AccountManager.h>
#include <Assignment.h>
#include <HifiConfigVariantMap.h>
#include <HTTPConnection.h>
#include <NLPacketList.h>
#include <NumericalConstants.h>
#include <SettingHandle.h>
#include "DomainServerSettingsManager.h"
#include "DomainServerNodeData.h"
const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json";
@ -41,6 +43,8 @@ const QString DESCRIPTION_COLUMNS_KEY = "columns";
const QString SETTINGS_VIEWPOINT_KEY = "viewpoint";
static Setting::Handle<double> JSON_SETTING_VERSION("json-settings/version", 0.0);
DomainServerSettingsManager::DomainServerSettingsManager() :
_descriptionArray(),
_configMap()
@ -101,9 +105,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
// What settings version were we before and what are we using now?
// Do we need to do any re-mapping?
QSettings appSettings;
const QString JSON_SETTINGS_VERSION_KEY = "json-settings/version";
double oldVersion = appSettings.value(JSON_SETTINGS_VERSION_KEY, 0.0).toDouble();
double oldVersion = JSON_SETTING_VERSION.get();
if (oldVersion != _descriptionVersion) {
const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users";
@ -299,7 +301,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
unpackPermissions();
// write the current description version to our settings
appSettings.setValue(JSON_SETTINGS_VERSION_KEY, _descriptionVersion);
JSON_SETTING_VERSION.set(_descriptionVersion);
}
QVariantMap& DomainServerSettingsManager::getDescriptorsMap() {
@ -439,6 +441,12 @@ void DomainServerSettingsManager::packPermissions() {
// save settings for IP addresses
packPermissionsForMap("permissions", _ipPermissions, IP_PERMISSIONS_KEYPATH);
// save settings for MAC addresses
packPermissionsForMap("permissions", _macPermissions, MAC_PERMISSIONS_KEYPATH);
// save settings for Machine Fingerprint
packPermissionsForMap("permissions", _machineFingerprintPermissions, MACHINE_FINGERPRINT_PERMISSIONS_KEYPATH);
// save settings for groups
packPermissionsForMap("permissions", _groupPermissions, GROUP_PERMISSIONS_KEYPATH);
@ -506,6 +514,29 @@ void DomainServerSettingsManager::unpackPermissions() {
}
});
needPack |= unpackPermissionsForKeypath(MAC_PERMISSIONS_KEYPATH, &_macPermissions,
[&](NodePermissionsPointer perms){
// make sure that this permission row is for a non-empty hardware
if (perms->getKey().first.isEmpty()) {
_macPermissions.remove(perms->getKey());
// we removed a row from the MAC permissions, we'll need a re-pack
needPack = true;
}
});
needPack |= unpackPermissionsForKeypath(MACHINE_FINGERPRINT_PERMISSIONS_KEYPATH, &_machineFingerprintPermissions,
[&](NodePermissionsPointer perms){
// make sure that this permission row has valid machine fingerprint
if (QUuid(perms->getKey().first) == QUuid()) {
_machineFingerprintPermissions.remove(perms->getKey());
// we removed a row, so we'll need a re-pack
needPack = true;
}
});
needPack |= unpackPermissionsForKeypath(GROUP_PERMISSIONS_KEYPATH, &_groupPermissions,
[&](NodePermissionsPointer perms){
@ -558,7 +589,10 @@ void DomainServerSettingsManager::unpackPermissions() {
qDebug() << "--------------- permissions ---------------------";
QList<QHash<NodePermissionsKey, NodePermissionsPointer>> permissionsSets;
permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get()
<< _groupPermissions.get() << _groupForbiddens.get() << _ipPermissions.get();
<< _groupPermissions.get() << _groupForbiddens.get()
<< _ipPermissions.get() << _macPermissions.get()
<< _machineFingerprintPermissions.get();
foreach (auto permissionSet, permissionsSets) {
QHashIterator<NodePermissionsKey, NodePermissionsPointer> i(permissionSet);
while (i.hasNext()) {
@ -634,7 +668,6 @@ bool DomainServerSettingsManager::ensurePermissionsForGroupRanks() {
return changed;
}
void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
// before we do any processing on this packet make sure it comes from a node that is allowed to kick
if (sendingNode->getCanKick()) {
@ -654,19 +687,25 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
auto verifiedUsername = matchingNode->getPermissions().getVerifiedUserName();
bool hadExistingPermissions = false;
bool newPermissions = false;
if (!verifiedUsername.isEmpty()) {
// if we have a verified user name for this user, we apply the kick to the username
// check if there were already permissions
hadExistingPermissions = havePermissionsForName(verifiedUsername);
bool hadPermissions = havePermissionsForName(verifiedUsername);
// grab or create permissions for the given username
destinationPermissions = _agentPermissions[matchingNode->getPermissions().getKey()];
auto userPermissions = _agentPermissions[matchingNode->getPermissions().getKey()];
newPermissions = !hadPermissions || userPermissions->can(NodePermissions::Permission::canConnectToDomain);
// ensure that the connect permission is clear
userPermissions->clear(NodePermissions::Permission::canConnectToDomain);
} else {
// otherwise we apply the kick to the IP from active socket for this node
// (falling back to the public socket if not yet active)
// otherwise we apply the kick to the IP from active socket for this node and the MAC address
// remove connect permissions for the IP (falling back to the public socket if not yet active)
auto& kickAddress = matchingNode->getActiveSocket()
? matchingNode->getActiveSocket()->getAddress()
: matchingNode->getPublicSocket().getAddress();
@ -674,32 +713,54 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid());
// check if there were already permissions for the IP
hadExistingPermissions = hasPermissionsForIP(kickAddress);
bool hadIPPermissions = hasPermissionsForIP(kickAddress);
// grab or create permissions for the given IP address
destinationPermissions = _ipPermissions[ipAddressKey];
auto ipPermissions = _ipPermissions[ipAddressKey];
if (!hadIPPermissions || ipPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
newPermissions = true;
ipPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
// potentially remove connect permissions for the MAC address and machine fingerprint
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
if (nodeData) {
// mac address first
NodePermissionsKey macAddressKey(nodeData->getHardwareAddress(), 0);
bool hadMACPermissions = hasPermissionsForMAC(nodeData->getHardwareAddress());
auto macPermissions = _macPermissions[macAddressKey];
if (!hadMACPermissions || macPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
newPermissions = true;
macPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
// now for machine fingerprint
NodePermissionsKey machineFingerprintKey(nodeData->getMachineFingerprint().toString(), 0);
bool hadFingerprintPermissions = hasPermissionsForMachineFingerprint(nodeData->getMachineFingerprint());
auto fingerprintPermissions = _machineFingerprintPermissions[machineFingerprintKey];
if (!hadFingerprintPermissions || fingerprintPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
newPermissions = true;
fingerprintPermissions->clear(NodePermissions::Permission::canConnectToDomain);
}
}
}
// make sure we didn't already have existing permissions that disallowed connect
if (!hadExistingPermissions
|| destinationPermissions->can(NodePermissions::Permission::canConnectToDomain)) {
if (newPermissions) {
qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID())
<< "after kick request";
// ensure that the connect permission is clear
destinationPermissions->clear(NodePermissions::Permission::canConnectToDomain);
<< "after kick request from" << uuidStringWithoutCurlyBraces(sendingNode->getUUID());
// we've changed permissions, time to store them to disk and emit our signal to say they have changed
packPermissions();
emit updateNodePermissions();
} else {
qWarning() << "Received kick request for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID())
<< "that already did not have permission to connect";
// in this case, though we don't expect the node to be connected to the domain, it is
// emit updateNodePermissions so that the DomainGatekeeper kicks it out
emit updateNodePermissions();
}
@ -717,6 +778,57 @@ void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer<Re
}
}
// This function processes the "Get Username from ID" request.
void DomainServerSettingsManager::processUsernameFromIDRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
// From the packet, pull the UUID we're identifying
QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
if (!nodeUUID.isNull()) {
// First, make sure we actually have a node with this UUID
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
auto matchingNode = limitedNodeList->nodeWithUUID(nodeUUID);
// If we do have a matching node...
if (matchingNode) {
// Setup the packet
auto usernameFromIDReplyPacket = NLPacket::create(PacketType::UsernameFromIDReply);
QString verifiedUsername;
QUuid machineFingerprint;
// Write the UUID to the packet
usernameFromIDReplyPacket->write(nodeUUID.toRfc4122());
// Check if the sending node has permission to kick (is an admin)
// OR if the message is from a node whose UUID matches the one in the packet
if (sendingNode->getCanKick() || nodeUUID == sendingNode->getUUID()) {
// It's time to figure out the username
verifiedUsername = matchingNode->getPermissions().getVerifiedUserName();
usernameFromIDReplyPacket->writeString(verifiedUsername);
// now put in the machine fingerprint
DomainServerNodeData* nodeData = static_cast<DomainServerNodeData*>(matchingNode->getLinkedData());
machineFingerprint = nodeData ? nodeData->getMachineFingerprint() : QUuid();
usernameFromIDReplyPacket->write(machineFingerprint.toRfc4122());
} else {
usernameFromIDReplyPacket->writeString(verifiedUsername);
usernameFromIDReplyPacket->write(machineFingerprint.toRfc4122());
}
// Write whether or not the user is an admin
bool isAdmin = matchingNode->getCanKick();
usernameFromIDReplyPacket->writePrimitive(isAdmin);
qDebug() << "Sending username" << verifiedUsername << "and machine fingerprint" << machineFingerprint << "associated with node" << nodeUUID << ". Node admin status: " << isAdmin;
// Ship it!
limitedNodeList->sendPacket(std::move(usernameFromIDReplyPacket), *sendingNode);
} else {
qWarning() << "Node username request received for unknown node. Refusing to process.";
}
} else {
qWarning() << "Node username request received for invalid node ID. Refusing to process.";
}
}
QStringList DomainServerSettingsManager::getAllNames() const {
QStringList result;
foreach (auto key, _agentPermissions.keys()) {
@ -754,6 +866,26 @@ NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddr
return nullPermissions;
}
NodePermissions DomainServerSettingsManager::getPermissionsForMAC(const QString& macAddress) const {
NodePermissionsKey macKey = NodePermissionsKey(macAddress, 0);
if (_macPermissions.contains(macKey)) {
return *(_macPermissions[macKey].get());
}
NodePermissions nullPermissions;
nullPermissions.setAll(false);
return nullPermissions;
}
NodePermissions DomainServerSettingsManager::getPermissionsForMachineFingerprint(const QUuid& machineFingerprint) const {
NodePermissionsKey fingerprintKey = NodePermissionsKey(machineFingerprint.toString(), 0);
if (_machineFingerprintPermissions.contains(fingerprintKey)) {
return *(_machineFingerprintPermissions[fingerprintKey].get());
}
NodePermissions nullPermissions;
nullPermissions.setAll(false);
return nullPermissions;
}
NodePermissions DomainServerSettingsManager::getPermissionsForGroup(const QString& groupName, QUuid rankID) const {
NodePermissionsKey groupRankKey = NodePermissionsKey(groupName, rankID);
if (_groupPermissions.contains(groupRankKey)) {
@ -1077,6 +1209,9 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson
}
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) {
static const QString SECURITY_ROOT_KEY = "security";
static const QString AC_SUBNET_WHITELIST_KEY = "ac_subnet_whitelist";
auto& settingsVariant = _configMap.getConfig();
bool needRestart = false;
@ -1127,7 +1262,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
if (rootKey != "security") {
if (rootKey != SECURITY_ROOT_KEY) {
needRestart = true;
}
} else {
@ -1143,7 +1278,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
QJsonValue settingValue = rootValue.toObject()[settingKey];
updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
if (rootKey != "security") {
if (rootKey != SECURITY_ROOT_KEY || settingKey == AC_SUBNET_WHITELIST_KEY) {
needRestart = true;
}
} else {

View file

@ -14,6 +14,7 @@
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtNetwork/QNetworkReply>
#include <HifiConfigVariantMap.h>
#include <HTTPManager.h>
@ -21,6 +22,8 @@
#include <ReceivedMessage.h>
#include "NodePermissions.h"
#include <Node.h>
const QString SETTINGS_PATHS_KEY = "paths";
const QString SETTINGS_PATH = "/settings";
@ -28,6 +31,8 @@ const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json";
const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions";
const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions";
const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions";
const QString MAC_PERMISSIONS_KEYPATH = "security.mac_permissions";
const QString MACHINE_FINGERPRINT_PERMISSIONS_KEYPATH = "security.machine_fingerprint_permissions";
const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions";
const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens";
@ -62,6 +67,14 @@ public:
bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), 0); }
NodePermissions getPermissionsForIP(const QHostAddress& address) const;
// these give access to permissions for specific MACs from the domain-server settings page
bool hasPermissionsForMAC(const QString& macAddress) const { return _macPermissions.contains(macAddress, 0); }
NodePermissions getPermissionsForMAC(const QString& macAddress) const;
// these give access to permissions for specific machine fingerprints from the domain-server settings page
bool hasPermissionsForMachineFingerprint(const QUuid& machineFingerprint) { return _machineFingerprintPermissions.contains(machineFingerprint.toString(), 0); }
NodePermissions getPermissionsForMachineFingerprint(const QUuid& machineFingerprint) const;
// these give access to permissions for specific groups from the domain-server settings page
bool havePermissionsForGroup(const QString& groupName, QUuid rankID) const {
return _groupPermissions.contains(groupName, rankID);
@ -105,6 +118,7 @@ public slots:
private slots:
void processSettingsRequestPacket(QSharedPointer<ReceivedMessage> message);
void processNodeKickRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
void processUsernameFromIDRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
private:
QStringList _argumentList;
@ -142,6 +156,8 @@ private:
NodePermissionsMap _agentPermissions; // specific account-names
NodePermissionsMap _ipPermissions; // permissions granted by node IP address
NodePermissionsMap _macPermissions; // permissions granted by node MAC address
NodePermissionsMap _machineFingerprintPermissions; // permissions granted by Machine Fingerprint
NodePermissionsMap _groupPermissions; // permissions granted by membership to specific groups
NodePermissionsMap _groupForbiddens; // permissions denied due to membership in a specific group

View file

@ -29,6 +29,12 @@ NodeConnectionData NodeConnectionData::fromDataStream(QDataStream& dataStream, c
// NOTE: QDataStream::readBytes() - The buffer is allocated using new []. Destroy it with the delete [] operator.
delete[] rawBytes;
// read the hardware address sent by the client
dataStream >> newHeader.hardwareAddress;
// now the machine fingerprint
dataStream >> newHeader.machineFingerprint;
}
dataStream >> newHeader.nodeType

View file

@ -28,6 +28,8 @@ public:
HifiSockAddr senderSockAddr;
QList<NodeType_t> interestList;
QString placeName;
QString hardwareAddress;
QUuid machineFingerprint;
QByteArray protocolVersion;
};

View file

@ -15,20 +15,27 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QtCore/QCoreApplication>
#include <LogHandler.h>
#include <SharedUtil.h>
#include <BuildInfo.h>
#include "DomainServer.h"
int main(int argc, char* argv[]) {
disableQtBearerPoll(); // Fixes wifi ping spikes
QCoreApplication::setApplicationName(BuildInfo::DOMAIN_SERVER_NAME);
QCoreApplication::setOrganizationName(BuildInfo::MODIFIED_ORGANIZATION);
QCoreApplication::setOrganizationDomain(BuildInfo::ORGANIZATION_DOMAIN);
QCoreApplication::setApplicationVersion(BuildInfo::VERSION);
#ifndef WIN32
setvbuf(stdout, NULL, _IOLBF, 0);
#endif
qInstallMessageHandler(LogHandler::verboseMessageHandler);
qInfo() << "Starting.";
int currentExitCode = 0;
// use a do-while to handle domain-server restart
@ -37,7 +44,7 @@ int main(int argc, char* argv[]) {
currentExitCode = domainServer.exec();
} while (currentExitCode == DomainServer::EXIT_CODE_REBOOT);
qInfo() << "Quitting.";
return currentExitCode;
}

View file

@ -19,8 +19,9 @@ int main(int argc, char* argv[]) {
#ifndef WIN32
setvbuf(stdout, NULL, _IOLBF, 0);
#endif
qInstallMessageHandler(LogHandler::verboseMessageHandler);
qInfo() << "Starting.";
IceServer iceServer(argc, argv);
return iceServer.exec();

View file

@ -166,6 +166,7 @@ if (WIN32)
add_dependency_external_projects(OpenVR)
add_dependency_external_projects(neuron)
add_dependency_external_projects(wasapi)
add_dependency_external_projects(steamworks)
endif()
# disable /OPT:REF and /OPT:ICF for the Debug builds
@ -175,10 +176,6 @@ if (WIN32)
set_property(TARGET ${TARGET_NAME} APPEND_STRING PROPERTY LINK_FLAGS_DEBUG "/OPT:NOREF /OPT:NOICF")
endif()
if (NOT ANDROID)
set(NON_ANDROID_LIBRARIES steamworks-wrapper)
endif ()
# link required hifi libraries
link_hifi_libraries(
shared octree gpu gl gpu-gl procedural model render
@ -351,3 +348,7 @@ if (ANDROID)
qt_create_apk()
endif ()
add_dependency_external_projects(GifCreator)
find_package(GifCreator REQUIRED)
target_include_directories(${TARGET_NAME} PUBLIC ${GIFCREATOR_INCLUDE_DIRS})

View file

@ -33,6 +33,7 @@ Item {
propagateComposedEvents: true
acceptedButtons: "AllButtons"
onClicked: {
menu.visible = false;
menu.done();
mouse.accepted = false;
}

View file

@ -32,6 +32,7 @@ Item {
MouseArea {
anchors.fill: parent
onClicked: {
menu.visible = false;
root.triggered();
menu.done();
}

View file

@ -0,0 +1,7 @@
{
"name": "Kinect to Standard",
"channels": [
{ "from": "Kinect.LeftHand", "to": "Standard.LeftHand" },
{ "from": "Kinect.RightHand", "to": "Standard.RightHand" }
]
}

View file

@ -3,6 +3,8 @@
"channels": [
{ "from": "OculusTouch.A", "to": "Standard.RightPrimaryThumb", "peek": true },
{ "from": "OculusTouch.X", "to": "Standard.LeftPrimaryThumb", "peek": true },
{ "from": "OculusTouch.B", "to": "Standard.RightSecondaryThumb", "peek": true},
{ "from": "OculusTouch.Y", "to": "Standard.LeftSecondaryThumb", "peek": true},
{ "from": "OculusTouch.A", "to": "Standard.A" },
{ "from": "OculusTouch.B", "to": "Standard.B" },

View file

@ -1,50 +1,12 @@
{
"name": "Standard to Action",
"when": "Application.NavigationFocused",
"name": "Standard to UI Navigation Action",
"channels": [
{ "from": "Standard.DU", "to": "Actions.UiNavVertical" },
{ "from": "Standard.DD", "to": "Actions.UiNavVertical", "filters": "invert" },
{ "from": "Standard.DL", "to": "Actions.UiNavLateral", "filters": "invert" },
{ "from": "Standard.DR", "to": "Actions.UiNavLateral" },
{ "from": "Standard.LB", "to": "Actions.UiNavGroup","filters": "invert" },
{ "from": "Standard.RB", "to": "Actions.UiNavGroup" },
{ "from": [ "Standard.A", "Standard.X" ], "to": "Actions.UiNavSelect" },
{ "from": [ "Standard.B", "Standard.Y" ], "to": "Actions.UiNavBack" },
{ "from": [ "Standard.RTClick", "Standard.LTClick" ], "to": "Actions.UiNavSelect" },
{
"from": "Standard.LX", "to": "Actions.UiNavLateral",
"filters": [
{ "type": "deadZone", "min": 0.95 },
"constrainToInteger",
{ "type": "pulse", "interval": 0.4 }
]
},
{
"from": "Standard.LY", "to": "Actions.UiNavVertical",
"filters": [
"invert",
{ "type": "deadZone", "min": 0.95 },
"constrainToInteger",
{ "type": "pulse", "interval": 0.4 }
]
},
{
"from": "Standard.RX", "to": "Actions.UiNavLateral",
"filters": [
{ "type": "deadZone", "min": 0.95 },
"constrainToInteger",
{ "type": "pulse", "interval": 0.4 }
]
},
{
"from": "Standard.RY", "to": "Actions.UiNavVertical",
"filters": [
"invert",
{ "type": "deadZone", "min": 0.95 },
"constrainToInteger",
{ "type": "pulse", "interval": 0.4 }
]
}
{ "from": "Standard.RB", "to": "Actions.UiNavGroup" }
]
}

View file

@ -4,12 +4,12 @@
{ "from": "GamePad.LY", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateZ" },
{ "from": "GamePad.LX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.TranslateX" },
{ "from": "GamePad.LT", "to": "Standard.LTClick",
{ "from": "GamePad.LT", "to": "Standard.LTClick",
"peek": true,
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]
},
{ "from": "GamePad.LT", "to": "Standard.LT" },
{ "from": "GamePad.LB", "to": "Standard.LB" },
{ "from": "GamePad.LT", "to": "Standard.LT" },
{ "from": "GamePad.LB", "to": "Standard.LB" },
{ "from": "GamePad.LS", "to": "Standard.LS" },
@ -25,37 +25,40 @@
]
},
{ "from": "GamePad.RX", "to": "Actions.Yaw" },
{ "from": "GamePad.RX", "filters": { "type": "deadZone", "min": 0.05 }, "to": "Actions.Yaw" },
{ "from": "GamePad.RY",
"to": "Actions.VERTICAL_UP",
"filters":
{ "from": "GamePad.RY",
"to": "Actions.VERTICAL_UP",
"filters":
[
{ "type": "deadZone", "min": 0.95 },
"invert"
]
},
},
{ "from": "GamePad.RT", "to": "Standard.RTClick",
{ "from": "GamePad.RT", "to": "Standard.RTClick",
"peek": true,
"filters": [ { "type": "hysteresis", "min": 0.85, "max": 0.9 } ]
},
{ "from": "GamePad.RT", "to": "Standard.RT" },
{ "from": "GamePad.RB", "to": "Standard.RB" },
{ "from": "GamePad.RT", "to": "Standard.RT" },
{ "from": "GamePad.RB", "to": "Standard.RB" },
{ "from": "GamePad.RS", "to": "Standard.RS" },
{ "from": "GamePad.Start", "to": "Actions.CycleCamera" },
{ "from": "GamePad.Back", "to": "Actions.ContextMenu" },
{ "from": "GamePad.Start", "to": "Standard.Start" },
{ "from": "GamePad.Back", "to": "Actions.CycleCamera" },
{ "from": "GamePad.DU", "to": "Standard.DU" },
{ "from": "GamePad.DD", "to": "Standard.DD" },
{ "from": "GamePad.DD", "to": "Standard.DD" },
{ "from": "GamePad.DL", "to": "Standard.DL" },
{ "from": "GamePad.DR", "to": "Standard.DR" },
{ "from": "GamePad.DR", "to": "Standard.DR" },
{ "from": [ "GamePad.Y" ], "to": "Standard.RightPrimaryThumb", "peek": true },
{ "from": "GamePad.A", "to": "Standard.A" },
{ "from": "GamePad.B", "to": "Standard.B" },
{ "from": "GamePad.A", "to": "Standard.A" },
{ "from": "GamePad.B", "to": "Standard.B" },
{ "from": "GamePad.X", "to": "Standard.X" },
{ "from": "GamePad.Y", "to": "Standard.Y" }
{ "from": "GamePad.Y", "to": "Standard.Y" },
{ "from": [ "Standard.A", "Standard.X" ], "to": "Actions.UiNavSelect" },
{ "from": [ "Standard.B", "Standard.Y" ], "to": "Actions.UiNavBack" }
]
}

View file

@ -47,26 +47,70 @@
}
</style>
<script>
var handControllerImageURL = null;
function showKbm() {
document.getElementById("main_image").setAttribute("src", "img/controls-help-keyboard.png");
}
function showViveControllers() {
document.getElementById("main_image").setAttribute("src", "img/controls-help-vive.png");
function showHandControllers() {
document.getElementById("main_image").setAttribute("src", handControllerImageURL);
}
function showXboxController() {
function showGamepad() {
document.getElementById("main_image").setAttribute("src", "img/controls-help-gamepad.png");
}
function load() {
console.log("In help.html: ", window.location.href);
parts = window.location.href.split("?");
if (parts.length > 0) {
var defaultTab = parts[1];
if (defaultTab == "xbox") {
showXboxController();
} else if (defaultTab == "vive") {
showViveControllers();
// This is not meant to be a complete or hardened query string parser - it only
// needs to handle the values we send in and have control over.
//
// queryString is a string of the form "key1=value1&key2=value2&key3&key4=value4"
function parseQueryString(queryString) {
var params = {};
var paramsParts = queryString.split("&");
for (var i = 0; i < paramsParts.length; ++i) {
var paramKeyValue = paramsParts[i].split("=");
if (paramKeyValue.length == 1) {
params[paramKeyValue[0]] = undefined;
} else if (paramKeyValue.length == 2) {
params[paramKeyValue[0]] = paramKeyValue[1];
} else {
console.error("Error parsing param keyvalue: ", paramParts);
}
}
return params;
}
function load() {
var parts = window.location.href.split("?");
var params = {};
if (parts.length > 0) {
params = parseQueryString(parts[1]);
}
switch (params.handControllerName) {
case "oculus":
handControllerImageURL = "img/controls-help-oculus.png";
break;
case "vive":
default:
handControllerImageURL = "img/controls-help-vive.png";
}
switch (params.defaultTab) {
case "gamepad":
showGamepad();
break;
case "handControllers":
showHandControllers();
break;
case "kbm":
default:
showKbm();
}
}
</script>
</head>
@ -75,8 +119,8 @@
<div id="image_area">
<img id="main_image" src="img/controls-help-keyboard.png" width="1024px" height="720px"></img>
<a href="#" id="kbm_button" onmousedown="showKbm()"></a>
<a href="#" id="hand_controllers_button" onmousedown="showViveControllers()"></a>
<a href="#" id="game_controller_button" onmousedown="showXboxController()"></a>
<a href="#" id="hand_controllers_button" onmousedown="showHandControllers()"></a>
<a href="#" id="game_controller_button" onmousedown="showGamepad()"></a>
</div>
</body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

View file

@ -17,7 +17,10 @@
var KEYBOARD_HEIGHT = 200;
function shouldRaiseKeyboard() {
if (document.activeElement.nodeName === "INPUT" || document.activeElement.nodeName === "TEXTAREA") {
var nodeName = document.activeElement.nodeName;
var nodeType = document.activeElement.type;
if (nodeName === "INPUT" && (nodeType === "text" || nodeType === "number" || nodeType === "password")
|| document.activeElement.nodeName === "TEXTAREA") {
return true;
} else {
// check for contenteditable attribute

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="70mm"
height="70mm"
viewBox="0 0 248.0315 248.03149"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="circle.svg">
<defs
id="defs4" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="79.162615"
inkscape:cy="188.36943"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2560"
inkscape:window-height="1377"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-804.33071)">
<circle
style="fill:#4b504f;fill-opacity:1;stroke:#feffff;stroke-width:0.22076035;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4140"
cx="124.46446"
cy="928.42853"
r="121.41819" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 50 49.999998"
xml:space="preserve"
id="svg4239"
inkscape:version="0.91 r13725"
sodipodi:docname="edit-icon.svg"
width="50"
height="50"><metadata
id="metadata4336"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs4334" /><sodipodi:namedview
pagecolor="#615f71"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1663"
inkscape:window-height="1023"
id="namedview4332"
showgrid="false"
inkscape:zoom="4.717641"
inkscape:cx="27.234437"
inkscape:cy="40.361616"
inkscape:window-x="412"
inkscape:window-y="188"
inkscape:window-maximized="0"
inkscape:current-layer="svg4239" /><style
type="text/css"
id="style4241">
.st0{fill:#414042;}
.st1{fill:#FFFFFF;}
.st2{fill:#1E1E1E;}
.st3{fill:#333333;}
</style><g
id="Layer_3"
transform="matrix(2.0055009,0,0,2.0055009,-26.481547,-324.23978)" /><path
style="fill:#ffffff;fill-opacity:1"
inkscape:connector-curvature="0"
id="path4268"
d="m 17.615466,29.206401 c -2.807702,2.807702 -5.615403,5.615403 -8.4231037,8.423104 -0.601651,-0.60165 -1.403851,-1.40385 -2.005501,-2.005501 2.807701,-2.807701 5.6154017,-5.615402 8.4231037,-8.423104 l -2.406601,-2.206051 c -3.2088017,3.208802 -6.4176027,6.417603 -9.8269547,9.626405 -0.60165,0.60165 -1.00275,1.40385 -1.2033,2.406601 -0.401101,2.607151 -0.802201,5.214302 -1.20330104,8.022004 0.20055004,0 0.40110004,0 0.60165004,0 2.607151,-0.601651 5.013753,-1.002751 7.620904,-1.604401 0.4011,-0.20055 1.0027497,-0.4011 1.4038497,-0.802201 3.409352,-3.409351 6.818703,-6.818703 10.228055,-10.228054 l -3.208801,-3.208802 z"
class="st3" /><path
style="fill:#ffffff;fill-opacity:1"
inkscape:connector-curvature="0"
id="path4270"
d="m 30.851772,7.3464388 c 1.00275,-1.00275 2.206051,-2.206051 3.409351,-3.409351 0.4011,-0.4011 1.002751,-0.4011 1.403851,0 2.005501,2.005501 4.011002,4.011002 6.016502,6.016503 0.401101,0.4011002 0.401101,1.0027502 0,1.6044002 -1.2033,1.203303 -2.406601,2.406603 -3.409351,3.409354 -2.406601,-2.406603 -4.813202,-5.0137542 -7.420353,-7.6209062 z"
class="st3" /><path
style="fill:#ffffff;fill-opacity:1"
inkscape:connector-curvature="0"
id="path4272"
d="m 33.659473,25.9976 c -0.60165,0.20055 -0.802201,0 -1.203301,-0.4011 -0.8022,-0.802201 -1.6044,-1.604401 -2.406601,-2.406601 2.005501,-2.005501 3.810452,-3.810452 5.815953,-5.815953 0.20055,-0.20055 0.4011,-0.4011 0.4011,-0.4011 -2.607151,-2.607151 -5.013752,-5.013754 -7.620903,-7.8214562 -0.200551,0.20055 -0.200551,0.4011 -0.401101,0.601651 -2.005501,2.0055002 -3.810451,3.8104532 -5.815952,5.8159542 -3.609902,-3.609903 -7.019253,-7.0192552 -10.629155,-10.6291572 -0.20055,-0.20055 -0.60165,-0.60165 -0.8022,-0.8022 -2.0055007,-1.403851 -4.4121017,-1.203301 -6.0165027,0.60165 -1.604401,1.804951 -1.403851,4.412102 0.20055,6.0165032 5.2143017,5.214304 10.6291547,10.629157 15.8434567,15.843459 1.804951,1.804951 3.409352,3.409352 5.214302,5.214303 0.200551,0.20055 0.401101,0.4011 0.200551,0.8022 -0.200551,0.8022 -0.401101,1.604401 -0.401101,2.406601 -0.4011,5.013752 2.807702,9.425854 7.821454,10.829705 2.005501,0.60165 4.011002,0.4011 6.217053,-0.4011 -0.20055,-0.20055 -0.4011,-0.4011 -0.601651,-0.4011 -1.40385,-1.403851 -3.008251,-3.008252 -4.412102,-4.412102 -1.00275,-1.002751 -1.2033,-2.807702 -0.20055,-3.810452 0.802201,-0.802201 1.604401,-1.604401 2.406601,-2.406601 1.203301,-1.002751 2.607152,-1.002751 3.609902,0 0.20055,0.20055 0.4011,0.4011 0.60165,0.60165 1.403851,1.403851 3.008252,3.008251 4.412102,4.412102 0,0 0.20055,0 0.20055,0 0.200551,-0.8022 0.401101,-1.604401 0.401101,-2.607151 C 47.697979,29.607502 40.678726,23.992099 33.659473,25.9976 Z M 8.1896113,9.1513898 c -0.8022,0 -1.40385,-0.60165 -1.40385,-1.40385 0,-0.802201 0.60165,-1.604401 1.40385,-1.604401 0.802201,0 1.604401,0.8022 1.604401,1.403851 0,0.8022 -0.8022,1.6044 -1.604401,1.6044 z M 29.046821,13.764044 c 0.60165,0.601651 1.2033,1.403851 2.005501,2.005501 -1.403851,1.403851 -2.807702,2.807702 -4.211552,4.211552 -0.601651,-0.60165 -1.403851,-1.40385 -2.005501,-2.005501 1.40385,-1.40385 2.807701,-2.807701 4.211552,-4.211552 z"
class="st3" /></svg>

After

Width:  |  Height:  |  Size: 5 KiB

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<g>
<path class="st0" d="M23.2,24.1c-0.8,0.9-1.5,1.8-2.2,2.6c-0.1,0.2-0.1,0.5-0.1,0.7c0.1,1.7,0.2,3.4,0.2,5.1
c0,0.8-0.4,1.2-1.1,1.3c-0.7,0.1-1.3-0.4-1.4-1.1c-0.2-2.2-0.3-4.3-0.5-6.5c0-0.3,0.1-0.7,0.4-1c1.1-1.5,2.3-3,3.4-4.5
c0.6-0.7,1.6-1.6,2.6-1.6c0.3,0,1.1,0,1.4,0c0.8-0.1,1.3,0.1,1.9,0.9c1,1.2,1.5,2.3,2.4,3.6c0.7,1.1,1.4,1.6,2.9,1.9
c1.1,0.2,2.2,0.5,3.3,0.8c0.3,0.1,0.6,0.2,0.8,0.3c0.5,0.3,0.7,0.8,0.6,1.3c-0.1,0.5-0.5,0.7-1,0.8c-0.4,0-0.9,0-1.3-0.1
c-1.4-0.3-2.7-0.6-4.1-0.9c-0.8-0.2-1.5-0.6-2.1-1.1c-0.3-0.3-0.6-0.5-0.9-0.8c0,0.3,0,0.5,0,0.7c0,1.2,0,2.4,0,3.6
c0,0.4-0.3,12.6-0.1,16.8c0,0.5-0.1,1-0.2,1.5c-0.2,0.7-0.6,1-1.4,1.1c-0.8,0-1.4-0.3-1.7-1c-0.2-0.5-0.3-1.1-0.4-1.6
c-0.4-4.6-0.9-12.9-1.1-13.8c-0.1-0.8-0.2-1.1-0.3-2.1c-0.1-0.5-0.1-0.9-0.1-1.3C23.3,27.9,23.2,26.1,23.2,24.1z"/>
<path class="st0" d="M28.2,14.6c0,1.4-1.1,2.6-2.6,2.6l0,0c-1.4,0-2.6-1.1-2.6-2.6v-1.6c0-1.4,1.1-2.6,2.6-2.6l0,0
c1.4,0,2.6,1.1,2.6,2.6V14.6z"/>
</g>
<path class="st0" d="M8.4,38.9c2.8,3.2,6.4,5.5,10.5,6.7c0.6,0.2,1.3,0.1,1.7-0.3c0.4-0.3,0.6-0.6,0.7-1c0.2-0.5,0.1-1.1-0.2-1.5
c-0.3-0.5-0.7-0.8-1.2-1c-1.6-0.5-3.2-1.2-4.6-2.1c-1.5-0.9-2.8-2.1-4-3.4c-0.4-0.4-0.9-0.7-1.5-0.7c-0.5,0-1,0.2-1.3,0.5
c-0.4,0.4-0.6,0.8-0.7,1.4C7.8,38,8,38.5,8.4,38.9z"/>
<path class="st0" d="M43.7,36.8c0.3-0.4,0.4-1,0.3-1.5c-0.1-0.5-0.4-1-0.8-1.3c-0.3-0.2-0.7-0.3-1.1-0.3c-0.7,0-1.3,0.3-1.7,0.9
c-1.1,1.6-2.4,3-4,4.2c-1.2,0.9-2.5,1.7-3.9,2.3c-0.5,0.2-0.9,0.6-1.1,1.1c-0.2,0.5-0.2,1,0,1.5c0.4,1,1.6,1.5,2.6,1
c1.7-0.7,3.3-1.7,4.8-2.8C40.7,40.4,42.4,38.7,43.7,36.8z"/>
<path class="st0" d="M5.1,33.2c0.5,0.4,1.2,0.4,1.8,0.2c0.5-0.2,0.9-0.6,1.1-1.1c0.2-0.5,0.2-1,0-1.5c-0.1-0.4-0.4-0.7-0.7-0.9
c-0.3-0.2-0.7-0.3-1.1-0.3c-0.2,0-0.5,0-0.7,0.1c-1,0.4-1.5,1.6-1.1,2.6C4.5,32.7,4.7,33,5.1,33.2z"/>
<path class="st0" d="M45.4,27.3c-0.2,0-0.3-0.1-0.5-0.1c-0.9,0-1.7,0.6-1.9,1.5c-0.1,0.5-0.1,1.1,0.2,1.5c0.3,0.5,0.7,0.8,1.2,0.9
c0.2,0,0.3,0.1,0.5,0.1c0.9,0,1.7-0.6,1.9-1.5c0.1-0.5,0.1-1.1-0.2-1.5C46.4,27.8,45.9,27.5,45.4,27.3z"/>
<path class="st0" d="M8.6,12c-0.3-0.2-0.7-0.3-1-0.3c-0.3,0-0.7,0.1-1,0.3c-0.3,0.2-0.6,0.4-0.7,0.7c-2,3.5-3.1,7.4-3.1,11.4
c0,0.2,0,0.4,0,0.6c0,0.5,0.2,1,0.6,1.4c0.4,0.4,0.9,0.6,1.4,0.6v0.4l0.1-0.4c0.5,0,1-0.2,1.4-0.6c0.4-0.4,0.6-0.9,0.5-1.4
c0-0.2,0-0.4,0-0.5c0-3.3,0.9-6.6,2.6-9.4C9.9,13.8,9.6,12.6,8.6,12z"/>
<path class="st0" d="M39.3,11.4c-0.1,0.5,0.1,1.1,0.4,1.5c1.1,1.4,2,3,2.6,4.6c0.6,1.6,1,3.2,1.2,4.9c0,0.5,0.3,1,0.6,1.3
c0.4,0.4,1,0.6,1.5,0.5c0.5,0,1-0.3,1.4-0.7c0.3-0.4,0.5-0.9,0.5-1.5c-0.4-4.2-2-8.2-4.6-11.6c-0.4-0.5-1-0.8-1.6-0.8
c-0.4,0-0.9,0.1-1.2,0.4C39.7,10.4,39.4,10.8,39.3,11.4z"/>
<path class="st0" d="M12.2,6.3c-0.5,0-0.9,0.2-1.3,0.5c-0.4,0.3-0.7,0.8-0.7,1.4c-0.1,0.5,0.1,1.1,0.4,1.5c0.7,0.8,2,1,2.8,0.3
c0.4-0.3,0.7-0.8,0.7-1.3c0.1-0.5-0.1-1.1-0.4-1.5C13.4,6.6,12.8,6.3,12.2,6.3z"/>
<path class="st0" d="M37.2,5.2c-0.3-0.2-0.7-0.3-1.1-0.3c-0.7,0-1.3,0.3-1.7,0.9c-0.3,0.4-0.4,1-0.3,1.5c0.1,0.5,0.4,1,0.9,1.3
C36,9.2,37.3,8.9,37.9,8C38.4,7.1,38.2,5.8,37.2,5.2z"/>
<path class="st0" d="M16.5,4c-0.2,0.5-0.3,1-0.1,1.5c0.4,1,1.5,1.6,2.6,1.2c3.3-1.2,6.8-1.4,10.2-0.6c0.6,0.1,1.2,0,1.7-0.4
c0.4-0.3,0.6-0.7,0.7-1.1c0.1-0.5,0-1.1-0.3-1.5c-0.3-0.5-0.7-0.8-1.3-0.9c-1.6-0.4-3.3-0.5-4.9-0.5c-2.6,0-5.1,0.4-7.5,1.3
C17.1,3.2,16.7,3.6,16.5,4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g>
<path class="st0" d="M20.7,29.7c-2.2,2.2-4.4,4.4-6.7,6.7c-0.5-0.5-1.1-1.1-1.6-1.6c2.2-2.2,4.4-4.4,6.7-6.7l-1.8-1.8
c-2.6,2.5-5.1,5.1-7.7,7.6c-0.5,0.5-0.9,1.1-1,1.8C8.3,37.8,8,39.8,7.7,42c0.2,0,0.4,0,0.5,0c2-0.4,4-0.8,5.9-1.2
c0.4-0.1,0.8-0.3,1.1-0.6c2.7-2.6,5.3-5.3,8-8L20.7,29.7z"/>
<path class="st0" d="M31.1,11c0.8-0.8,1.8-1.8,2.7-2.7C34.2,8,34.6,8,34.9,8.4c1.6,1.6,3.1,3.1,4.7,4.7c0.4,0.4,0.4,0.8,0,1.2
c-0.9,0.9-1.8,1.8-2.7,2.7C35,15,33.1,13,31.1,11z"/>
<path class="st0" d="M33,25.9c-0.4,0.1-0.6,0-0.9-0.2c-0.6-0.6-1.3-1.3-1.9-1.9c1.5-1.5,3.1-3.1,4.6-4.6c0.1-0.1,0.3-0.3,0.3-0.3
c-2-2-3.9-4-5.9-6.1c-0.1,0.2-0.2,0.3-0.4,0.5c-1.5,1.5-3,3-4.6,4.6c-2.8-2.8-5.6-5.6-8.4-8.4c-0.2-0.2-0.4-0.4-0.6-0.6
c-1.5-1.2-3.5-1-4.8,0.4c-1.2,1.4-1.1,3.5,0.2,4.8c4.2,4.2,8.3,8.3,12.5,12.5c1.4,1.4,2.7,2.7,4.1,4.1c0.2,0.2,0.2,0.4,0.2,0.7
c-0.2,0.6-0.3,1.2-0.3,1.9c-0.3,4,2.3,7.5,6.1,8.5c1.6,0.4,3.2,0.3,4.8-0.3c-0.1-0.2-0.3-0.2-0.4-0.4c-1.2-1.2-2.3-2.3-3.5-3.5
c-0.8-0.9-0.9-2.1-0.1-3c0.6-0.7,1.3-1.3,2-2c0.9-0.8,2-0.8,2.9,0c0.2,0.2,0.3,0.3,0.5,0.5c1.2,1.2,2.3,2.3,3.5,3.5
c0.1,0,0.1,0,0.2,0c0.1-0.7,0.3-1.3,0.3-2C43.9,28.7,38.5,24.3,33,25.9z M12.9,12.6c-0.6,0-1.2-0.5-1.2-1.2s0.5-1.2,1.2-1.2
c0.6,0,1.2,0.6,1.2,1.2C14.1,12,13.6,12.6,12.9,12.6z M29.3,16.3c0.5,0.5,1,1.1,1.6,1.6c-1.1,1.1-2.2,2.2-3.3,3.3
c-0.5-0.5-1.1-1.1-1.6-1.6C27.1,18.5,28.2,17.4,29.3,16.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<path class="st0" d="M47.2,41.3l-9.1-9.1c-0.8-0.8-1.9-1.1-3-1l-2.4-2.4c1.8-2.6,2.8-5.7,2.8-9c0-8.9-7.2-16.1-16.1-16.1
S3.3,11,3.3,19.8c0,8.9,7.2,16.1,16.1,16.1c4.1,0,7.8-1.5,10.6-4l2.2,2.2c-0.2,1.1,0.1,2.2,1,3l9.1,9.1c1.4,1.4,3.6,1.4,4.9,0
C48.5,44.9,48.5,42.7,47.2,41.3z M19.4,32.2c-6.8,0-12.3-5.5-12.3-12.3c0-6.8,5.5-12.3,12.3-12.3s12.3,5.5,12.3,12.3
C31.8,26.6,26.2,32.2,19.4,32.2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 832 B

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#FFFFFF;stroke:#FFFFFF;stroke-width:0.25;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<path class="st0" d="M11.3,36.7c-0.2-0.3-0.4-0.6-0.6-0.8C6.3,29,5.8,21.8,10,14.8c4.3-7.2,11-10.4,19.3-9.3
c7.9,1.1,13.4,5.8,15.8,13.4c2.4,7.8,0.4,14.6-5.5,20.1c-3.8,3.6-8.4,5.1-13.5,5.1c-7.3,0-14.6-0.1-21.9-0.1c-0.2,0-0.5,0-1,0
C6,41.5,8.6,39.1,11.3,36.7z M10.9,41c0.3,0,0.6,0.1,0.8,0.1c4.9,0,9.9,0,14.8,0C35.6,41,42.8,34,43,25.1
c0.2-9.3-7.3-16.9-16.6-16.7c-6.9,0.1-12,3.5-14.9,9.7c-2.9,6.4-1.8,12.5,2.8,17.8c0.3,0.4,0.7,0.7,1,1.1
C13.9,38.3,12.5,39.6,10.9,41z"/>
<path class="st1" d="M22.5,20.9c0-0.7,0.1-1.4,0.3-2c0.2-0.6,0.6-1.1,1-1.6c0.4-0.4,0.9-0.8,1.6-1c0.6-0.2,1.3-0.4,2-0.4
c0.6,0,1.2,0.1,1.7,0.3c0.5,0.2,1,0.4,1.4,0.8c0.4,0.3,0.7,0.8,1,1.3c0.2,0.5,0.3,1.1,0.3,1.8c0,0.5-0.1,0.9-0.2,1.2
c-0.1,0.3-0.2,0.6-0.4,0.9c-0.2,0.3-0.4,0.5-0.6,0.7c-0.2,0.2-0.4,0.4-0.7,0.6c-0.3,0.2-0.5,0.4-0.7,0.6c-0.2,0.2-0.4,0.4-0.6,0.7
c-0.2,0.3-0.3,0.5-0.4,0.9c-0.1,0.3-0.1,0.8-0.1,1.2H26c0-0.6,0-1.1,0.1-1.5c0.1-0.4,0.2-0.8,0.3-1.1c0.1-0.3,0.3-0.6,0.5-0.8
c0.2-0.2,0.4-0.5,0.7-0.7c0.2-0.2,0.4-0.4,0.6-0.5c0.2-0.2,0.4-0.3,0.5-0.5c0.2-0.2,0.3-0.4,0.4-0.7c0.1-0.2,0.1-0.5,0.1-0.9
c0-0.4-0.1-0.8-0.2-1.1c-0.1-0.3-0.3-0.5-0.5-0.7c-0.2-0.2-0.4-0.3-0.7-0.4c-0.2-0.1-0.4-0.1-0.6-0.1c-0.8,0-1.5,0.3-1.9,0.8
c-0.4,0.6-0.6,1.3-0.6,2.2H22.5z"/>
<path class="st0" d="M27.6,29.8c-0.1,0-0.3-0.1-0.4-0.1c-0.8,0-1.4,0.5-1.6,1.2c-0.1,0.4,0,0.9,0.2,1.3c0.2,0.4,0.6,0.7,1,0.8
c0.1,0,0.3,0.1,0.4,0.1c0.8,0,1.4-0.5,1.6-1.2c0.1-0.4,0.1-0.9-0.2-1.3C28.4,30.1,28,29.9,27.6,29.8z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<path class="st0" d="M27.1,42.3c-1.4,0.3-2.7,0.5-4.1,0.5c-9.3,0-16.8-7.5-16.8-16.8S13.8,9.2,23,9.2S39.8,16.7,39.8,26
c0,0.2,0,0.3,0,0.5c0.7-0.7,1.4-1,2.4-1.5C41.7,14.9,33.3,6.8,23,6.8C12.4,6.8,3.8,15.4,3.8,26S12.4,45.2,23,45.2
c1.7,0,3.4-0.3,5.1-0.7C27.8,43.6,27.5,43,27.1,42.3z"/>
<path class="st0" d="M32.6,20.7L32.6,20.7c-0.4-0.4-0.7-0.4-1.2-0.3c0,0-5.9,0.7-8.4,0.7h-0.1c-2.5,0-8.6-0.9-8.6-0.9
c-0.4-0.1-0.9,0-1.2,0.4L13,21c-0.1,0.3-0.1,0.6-0.1,1c0.1,0.3,0.3,0.6,0.6,0.7c1,0.4,4.9,1.8,5.9,2.1c0.1,0,0.6,0.1,0.6,0.9
c0,0.9-0.3,4.6-0.7,6.4c-0.4,1.8-1.2,4-1.2,4c-0.1,0.6,0.1,1.3,0.7,1.5l0.7,0.3c0.3,0.1,0.6,0.1,0.9,0c0.3-0.1,0.4-0.4,0.6-0.7
l2.1-6.4l1.9,6.5c0.1,0.3,0.3,0.6,0.6,0.7c0.1,0.1,0.3,0.1,0.4,0.1c0.1,0,0.3,0,0.4-0.1l0.7-0.3c0.6-0.1,0.9-0.7,0.7-1.3
c0,0-0.6-2.4-1-4.3c-0.3-1.2-0.4-3-0.4-4.3c0-0.9-0.1-1.6-0.1-2.2c0-0.3,0.1-0.6,0.6-0.7c0.1,0,5.5-1.9,5.5-1.9
c0.4-0.1,0.6-0.4,0.7-0.9C32.9,21.5,32.9,21,32.6,20.7z"/>
<circle class="st0" cx="23" cy="17" r="2.5"/>
<g>
<path class="st0" d="M43.1,43L43.1,43L43.1,43z"/>
<path class="st0" d="M41,35.6l4.6-4.6c0.7-0.7,0.7-1.9,0-2.6c-0.7-0.7-1.9-0.7-2.6,0L38.5,33l-4.6-4.6c-0.7-0.7-1.9-0.7-2.6,0
s-0.7,1.9,0,2.6l4.6,4.6l-4.6,4.6c-0.7,0.7-0.7,1.9,0,2.6s1.9,0.7,2.6,0l4.6-4.6l4.6,4.6c0.7,0.7,1.9,0.7,2.6,0
c0.7-0.7,0.7-1.9,0-2.6L41,35.6z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 200.1" style="enable-background:new 0 0 50 200.1;" xml:space="preserve">
<style type="text/css">
.st0{fill:#414042;}
.st1{fill:#FFFFFF;}
.st2{fill:#1E1E1E;}
.st3{fill:#333333;}
</style>
<g id="Layer_2">
<g>
<g>
<path class="st0" d="M50.1,146.1c0,2.2-1.8,4-4,4h-42c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V146.1z"/>
</g>
</g>
<g>
<g>
<path class="st0" d="M50,196.1c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V196.1z"/>
</g>
</g>
<g>
<g>
<path class="st1" d="M50,46c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4V4c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V46z"/>
</g>
</g>
<g>
<path class="st2" d="M50,96.1c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V96.1z"/>
</g>
</g>
<path class="st1" d="M24.7,81.2c-6.2,0-11.2-5-11.2-11.2c0-6.2,5-11.2,11.2-11.2c6.2,0,11.2,5,11.2,11.2
C35.9,76.2,30.9,81.2,24.7,81.2z M24.7,60.3c-5.4,0-9.8,4.4-9.8,9.8c0,5.4,4.4,9.8,9.8,9.8c5.4,0,9.8-4.4,9.8-9.8
C34.5,64.6,30.1,60.3,24.7,60.3z"/>
<g>
<path class="st3" d="M7.7,42.5v-6.4h1.2v6.4H7.7z"/>
<path class="st3" d="M14.7,41.8c-0.5,0.5-1.1,0.8-1.7,0.8c-0.4,0-0.8-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-0.9-0.7c-0.3-0.3-0.5-0.7-0.6-1
s-0.2-0.8-0.2-1.2c0-0.4,0.1-0.9,0.2-1.2c0.2-0.4,0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7c0.4-0.2,0.8-0.3,1.2-0.3
c0.6,0,1.1,0.1,1.5,0.4s0.7,0.6,0.9,1l-0.9,0.7c-0.2-0.3-0.4-0.6-0.6-0.7s-0.6-0.3-0.9-0.3c-0.3,0-0.5,0.1-0.7,0.2
c-0.2,0.1-0.4,0.3-0.5,0.5c-0.2,0.2-0.3,0.4-0.4,0.7c-0.1,0.3-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7
c0.2,0.2,0.4,0.3,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.6,0,1.1-0.3,1.6-0.9v-0.5h-1.3v-0.9h2.3v3.3h-1V41.8z"/>
<path class="st3" d="M18.3,38.4v4.1h-1.2v-6.4h1l3.3,4.2v-4.2h1.2v6.4h-1L18.3,38.4z"/>
<path class="st3" d="M26.8,42.5c-0.5,0-0.9-0.1-1.2-0.3s-0.7-0.4-1-0.7c-0.3-0.3-0.5-0.6-0.6-1c-0.1-0.4-0.2-0.8-0.2-1.2
c0-0.4,0.1-0.8,0.2-1.2s0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.2-0.3c0.5,0,0.9,0.1,1.2,0.3s0.7,0.4,1,0.7
c0.3,0.3,0.5,0.7,0.6,1c0.1,0.4,0.2,0.8,0.2,1.2c0,0.4-0.1,0.8-0.2,1.2c-0.2,0.4-0.4,0.7-0.6,1c-0.3,0.3-0.6,0.5-1,0.7
C27.7,42.4,27.2,42.5,26.8,42.5z M25,39.3c0,0.3,0,0.5,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5s0.5,0.2,0.8,0.2
c0.3,0,0.5-0.1,0.8-0.2s0.4-0.3,0.6-0.5c0.1-0.2,0.3-0.4,0.3-0.7s0.1-0.5,0.1-0.8c0-0.3,0-0.5-0.1-0.8c-0.1-0.3-0.2-0.5-0.4-0.7
c-0.2-0.2-0.3-0.4-0.6-0.5c-0.2-0.1-0.5-0.2-0.7-0.2c-0.3,0-0.5,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.2,0.2-0.3,0.4-0.3,0.7
C25.1,38.8,25,39,25,39.3z"/>
<path class="st3" d="M31,42.5v-6.4h2.8c0.3,0,0.6,0.1,0.8,0.2s0.5,0.3,0.6,0.5c0.2,0.2,0.3,0.4,0.4,0.7c0.1,0.3,0.2,0.5,0.2,0.8
c0,0.4-0.1,0.8-0.3,1.1c-0.2,0.3-0.5,0.6-0.8,0.7l1.5,2.4h-1.4l-1.3-2.1h-1.2v2.1H31z M32.3,39.3h1.6c0.1,0,0.2,0,0.3-0.1
c0.1-0.1,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3s0.1-0.3,0.1-0.4c0-0.1,0-0.3-0.1-0.4s-0.1-0.2-0.2-0.3c-0.1-0.1-0.2-0.2-0.3-0.2
c-0.1-0.1-0.2-0.1-0.3-0.1h-1.5V39.3z"/>
<path class="st3" d="M41.8,41.4v1.1h-4.4v-6.4h4.4v1.1h-3.1v1.5h2.7v1h-2.7v1.7H41.8z"/>
</g>
<path class="st1" d="M27.1,79.5c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
c1,0,2-0.2,3-0.4C27.5,80.4,27.3,80,27.1,79.5z"/>
<path class="st1" d="M31.2,66.5L31.2,66.5c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,66.9,31.4,66.7,31.2,66.5z"/>
<circle class="st1" cx="24.7" cy="64" r="1.7"/>
<g>
<path class="st1" d="M7.7,92.3V86h1.2v6.4H7.7z"/>
<path class="st1" d="M14.7,91.6c-0.5,0.5-1.1,0.8-1.7,0.8c-0.4,0-0.8-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-0.9-0.7c-0.3-0.3-0.5-0.7-0.6-1
s-0.2-0.8-0.2-1.2c0-0.4,0.1-0.9,0.2-1.2c0.2-0.4,0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7c0.4-0.2,0.8-0.3,1.2-0.3
c0.6,0,1.1,0.1,1.5,0.4s0.7,0.6,0.9,1L14.6,88c-0.2-0.3-0.4-0.6-0.6-0.7S13.4,87,13.1,87c-0.3,0-0.5,0.1-0.7,0.2
c-0.2,0.1-0.4,0.3-0.5,0.5c-0.2,0.2-0.3,0.4-0.4,0.7c-0.1,0.3-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7
c0.2,0.2,0.4,0.3,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.6,0,1.1-0.3,1.6-0.9V90h-1.3v-0.9h2.3v3.3h-1V91.6z"/>
<path class="st1" d="M18.3,88.2v4.1h-1.2V86h1l3.3,4.2V86h1.2v6.4h-1L18.3,88.2z"/>
<path class="st1" d="M26.8,92.4c-0.5,0-0.9-0.1-1.2-0.3s-0.7-0.4-1-0.7c-0.3-0.3-0.5-0.6-0.6-1c-0.1-0.4-0.2-0.8-0.2-1.2
c0-0.4,0.1-0.8,0.2-1.2s0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.2-0.3c0.5,0,0.9,0.1,1.2,0.3s0.7,0.4,1,0.7
c0.3,0.3,0.5,0.7,0.6,1c0.1,0.4,0.2,0.8,0.2,1.2c0,0.4-0.1,0.8-0.2,1.2c-0.2,0.4-0.4,0.7-0.6,1c-0.3,0.3-0.6,0.5-1,0.7
C27.7,92.3,27.2,92.4,26.8,92.4z M25,89.1c0,0.3,0,0.5,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5s0.5,0.2,0.8,0.2
c0.3,0,0.5-0.1,0.8-0.2s0.4-0.3,0.6-0.5c0.1-0.2,0.3-0.4,0.3-0.7s0.1-0.5,0.1-0.8c0-0.3,0-0.5-0.1-0.8c-0.1-0.3-0.2-0.5-0.4-0.7
c-0.2-0.2-0.3-0.4-0.6-0.5c-0.2-0.1-0.5-0.2-0.7-0.2c-0.3,0-0.5,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.2,0.2-0.3,0.4-0.3,0.7
C25.1,88.6,25,88.9,25,89.1z"/>
<path class="st1" d="M31,92.3V86h2.8c0.3,0,0.6,0.1,0.8,0.2s0.5,0.3,0.6,0.5c0.2,0.2,0.3,0.4,0.4,0.7c0.1,0.3,0.2,0.5,0.2,0.8
c0,0.4-0.1,0.8-0.3,1.1c-0.2,0.3-0.5,0.6-0.8,0.7l1.5,2.4h-1.4l-1.3-2.1h-1.2v2.1H31z M32.3,89.1h1.6c0.1,0,0.2,0,0.3-0.1
c0.1-0.1,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3s0.1-0.3,0.1-0.4c0-0.1,0-0.3-0.1-0.4s-0.1-0.2-0.2-0.3c-0.1-0.1-0.2-0.2-0.3-0.2
C34,87.1,33.9,87,33.8,87h-1.5V89.1z"/>
<path class="st1" d="M41.8,91.3v1.1h-4.4V86h4.4V87h-3.1v1.5h2.7v1h-2.7v1.7H41.8z"/>
</g>
<path class="st3" d="M27.1,29.7c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9C35.6,13.7,30.7,9,24.7,9c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
c1,0,2-0.2,3-0.4C27.5,30.5,27.3,30.1,27.1,29.7z"/>
<path class="st3" d="M31.2,16.6L31.2,16.6c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,17.1,31.4,16.8,31.2,16.6z"/>
<circle class="st3" cx="24.7" cy="14.1" r="1.7"/>
<g>
<polygon class="st3" points="36.4,30.1 36.4,30.1 36.4,30.1 "/>
<path class="st3" d="M35.2,25.8l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
c0.4-0.4,0.4-1.1,0-1.5L35.2,25.8z"/>
</g>
<path class="st1" d="M27.3,129.6c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
c1,0,2-0.2,3-0.4C27.6,130.5,27.4,130.1,27.3,129.6z"/>
<path class="st1" d="M31.3,116.6L31.3,116.6c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
c0.3-0.1,0.4-0.3,0.5-0.6C31.6,117,31.5,116.8,31.3,116.6z"/>
<circle class="st1" cx="24.9" cy="114.1" r="1.7"/>
<g>
<path class="st1" d="M7.8,142.5v-6.4H9v6.4H7.8z"/>
<path class="st1" d="M14.9,141.7c-0.5,0.5-1.1,0.8-1.7,0.8c-0.4,0-0.8-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-0.9-0.7
c-0.3-0.3-0.5-0.7-0.6-1s-0.2-0.8-0.2-1.2c0-0.4,0.1-0.9,0.2-1.2c0.2-0.4,0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7
c0.4-0.2,0.8-0.3,1.2-0.3c0.6,0,1.1,0.1,1.5,0.4s0.7,0.6,0.9,1l-0.9,0.7c-0.2-0.3-0.4-0.6-0.6-0.7s-0.6-0.3-0.9-0.3
c-0.3,0-0.5,0.1-0.7,0.2c-0.2,0.1-0.4,0.3-0.5,0.5c-0.2,0.2-0.3,0.4-0.4,0.7c-0.1,0.3-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8
c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.4,0.3,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.6,0,1.1-0.3,1.6-0.9v-0.5h-1.3v-0.9h2.3v3.3h-1V141.7
z"/>
<path class="st1" d="M18.4,138.4v4.1h-1.2v-6.4h1l3.3,4.2v-4.2h1.2v6.4h-1L18.4,138.4z"/>
<path class="st1" d="M26.9,142.5c-0.5,0-0.9-0.1-1.2-0.3s-0.7-0.4-1-0.7c-0.3-0.3-0.5-0.6-0.6-1c-0.1-0.4-0.2-0.8-0.2-1.2
c0-0.4,0.1-0.8,0.2-1.2s0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.2-0.3c0.5,0,0.9,0.1,1.2,0.3s0.7,0.4,1,0.7
c0.3,0.3,0.5,0.7,0.6,1c0.1,0.4,0.2,0.8,0.2,1.2c0,0.4-0.1,0.8-0.2,1.2c-0.2,0.4-0.4,0.7-0.6,1c-0.3,0.3-0.6,0.5-1,0.7
C27.8,142.4,27.4,142.5,26.9,142.5z M25.2,139.3c0,0.3,0,0.5,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5
s0.5,0.2,0.8,0.2c0.3,0,0.5-0.1,0.8-0.2s0.4-0.3,0.6-0.5c0.1-0.2,0.3-0.4,0.3-0.7s0.1-0.5,0.1-0.8c0-0.3,0-0.5-0.1-0.8
c-0.1-0.3-0.2-0.5-0.4-0.7c-0.2-0.2-0.3-0.4-0.6-0.5c-0.2-0.1-0.5-0.2-0.7-0.2c-0.3,0-0.5,0.1-0.8,0.2s-0.4,0.3-0.6,0.5
c-0.2,0.2-0.3,0.4-0.3,0.7C25.2,138.7,25.2,139,25.2,139.3z"/>
<path class="st1" d="M31.2,142.5v-6.4H34c0.3,0,0.6,0.1,0.8,0.2s0.5,0.3,0.6,0.5c0.2,0.2,0.3,0.4,0.4,0.7c0.1,0.3,0.2,0.5,0.2,0.8
c0,0.4-0.1,0.8-0.3,1.1c-0.2,0.3-0.5,0.6-0.8,0.7l1.5,2.4H35l-1.3-2.1h-1.2v2.1H31.2z M32.4,139.2H34c0.1,0,0.2,0,0.3-0.1
c0.1-0.1,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3s0.1-0.3,0.1-0.4c0-0.1,0-0.3-0.1-0.4s-0.1-0.2-0.2-0.3c-0.1-0.1-0.2-0.2-0.3-0.2
c-0.1-0.1-0.2-0.1-0.3-0.1h-1.5V139.2z"/>
<path class="st1" d="M41.9,141.4v1.1h-4.4v-6.4h4.4v1.1h-3.1v1.5h2.7v1h-2.7v1.7H41.9z"/>
</g>
<g>
<polygon class="st1" points="36.5,130.1 36.5,130.1 36.5,130.1 "/>
<path class="st1" d="M35.3,125.8l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
c0.4-0.4,0.4-1.1,0-1.5L35.3,125.8z"/>
</g>
<g>
<path class="st1" d="M7.7,192.7v-6.4h1.2v6.4H7.7z"/>
<path class="st1" d="M14.7,192c-0.5,0.5-1.1,0.8-1.7,0.8c-0.4,0-0.8-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-0.9-0.7c-0.3-0.3-0.5-0.7-0.6-1
s-0.2-0.8-0.2-1.2c0-0.4,0.1-0.9,0.2-1.2c0.2-0.4,0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7c0.4-0.2,0.8-0.3,1.2-0.3
c0.6,0,1.1,0.1,1.5,0.4s0.7,0.6,0.9,1l-0.9,0.7c-0.2-0.3-0.4-0.6-0.6-0.7s-0.6-0.3-0.9-0.3c-0.3,0-0.5,0.1-0.7,0.2
c-0.2,0.1-0.4,0.3-0.5,0.5c-0.2,0.2-0.3,0.4-0.4,0.7c-0.1,0.3-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7
c0.2,0.2,0.4,0.3,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.6,0,1.1-0.3,1.6-0.9v-0.5h-1.3v-0.9h2.3v3.3h-1V192z"/>
<path class="st1" d="M18.3,188.6v4.1h-1.2v-6.4h1l3.3,4.2v-4.2h1.2v6.4h-1L18.3,188.6z"/>
<path class="st1" d="M26.8,192.7c-0.5,0-0.9-0.1-1.2-0.3s-0.7-0.4-1-0.7c-0.3-0.3-0.5-0.6-0.6-1c-0.1-0.4-0.2-0.8-0.2-1.2
c0-0.4,0.1-0.8,0.2-1.2s0.4-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.2-0.3c0.5,0,0.9,0.1,1.2,0.3s0.7,0.4,1,0.7
c0.3,0.3,0.5,0.7,0.6,1c0.1,0.4,0.2,0.8,0.2,1.2c0,0.4-0.1,0.8-0.2,1.2c-0.2,0.4-0.4,0.7-0.6,1c-0.3,0.3-0.6,0.5-1,0.7
C27.7,192.6,27.2,192.7,26.8,192.7z M25,189.5c0,0.3,0,0.5,0.1,0.8c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5
s0.5,0.2,0.8,0.2c0.3,0,0.5-0.1,0.8-0.2s0.4-0.3,0.6-0.5c0.1-0.2,0.3-0.4,0.3-0.7s0.1-0.5,0.1-0.8c0-0.3,0-0.5-0.1-0.8
c-0.1-0.3-0.2-0.5-0.4-0.7c-0.2-0.2-0.3-0.4-0.6-0.5c-0.2-0.1-0.5-0.2-0.7-0.2c-0.3,0-0.5,0.1-0.8,0.2s-0.4,0.3-0.6,0.5
c-0.2,0.2-0.3,0.4-0.3,0.7C25.1,189,25,189.2,25,189.5z"/>
<path class="st1" d="M31,192.7v-6.4h2.8c0.3,0,0.6,0.1,0.8,0.2s0.5,0.3,0.6,0.5c0.2,0.2,0.3,0.4,0.4,0.7c0.1,0.3,0.2,0.5,0.2,0.8
c0,0.4-0.1,0.8-0.3,1.1c-0.2,0.3-0.5,0.6-0.8,0.7l1.5,2.4h-1.4l-1.3-2.1h-1.2v2.1H31z M32.3,189.5h1.6c0.1,0,0.2,0,0.3-0.1
c0.1-0.1,0.2-0.1,0.3-0.2c0.1-0.1,0.1-0.2,0.2-0.3s0.1-0.3,0.1-0.4c0-0.1,0-0.3-0.1-0.4s-0.1-0.2-0.2-0.3c-0.1-0.1-0.2-0.2-0.3-0.2
c-0.1-0.1-0.2-0.1-0.3-0.1h-1.5V189.5z"/>
<path class="st1" d="M41.8,191.6v1.1h-4.4v-6.4h4.4v1.1h-3.1v1.5h2.7v1h-2.7v1.7H41.8z"/>
</g>
<path class="st1" d="M27.1,179.9c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
c1,0,2-0.2,3-0.4C27.5,180.7,27.3,180.3,27.1,179.9z"/>
<path class="st1" d="M31.2,166.8L31.2,166.8c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,167.3,31.4,167,31.2,166.8z"/>
<circle class="st1" cx="24.7" cy="164.3" r="1.7"/>
<g>
<polygon class="st1" points="36.4,180.3 36.4,180.3 36.4,180.3 "/>
<path class="st1" d="M35.2,176l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
c0.4-0.4,0.4-1.1,0-1.5L35.2,176z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 200.1" style="enable-background:new 0 0 50 200.1;" xml:space="preserve">
<style type="text/css">
.st0{fill:#414042;}
.st1{fill:#FFFFFF;}
.st2{fill:#1E1E1E;}
.st3{fill:#333333;}
</style>
<g id="Layer_2">
<g>
<g>
<path class="st0" d="M50.1,146.1c0,2.2-1.8,4-4,4h-42c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V146.1z"/>
</g>
</g>
<g>
<g>
<path class="st0" d="M50,196.1c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V196.1z"/>
</g>
</g>
<g>
<g>
<path class="st1" d="M50,46c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4V4c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V46z"/>
</g>
</g>
<g>
<path class="st2" d="M50,96.1c0,2.2-1.8,4-4,4H4c-2.2,0-4-1.8-4-4v-42c0-2.2,1.8-4,4-4h42c2.2,0,4,1.8,4,4V96.1z"/>
</g>
</g>
<path class="st1" d="M24.7,81.2c-6.2,0-11.2-5-11.2-11.2c0-6.2,5-11.2,11.2-11.2c6.2,0,11.2,5,11.2,11.2
C35.9,76.2,30.9,81.2,24.7,81.2z M24.7,60.3c-5.4,0-9.8,4.4-9.8,9.8c0,5.4,4.4,9.8,9.8,9.8c5.4,0,9.8-4.4,9.8-9.8
C34.5,64.6,30.1,60.3,24.7,60.3z"/>
<path class="st1" d="M27.1,79.5c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
c1,0,2-0.2,3-0.4C27.5,80.4,27.3,80,27.1,79.5z"/>
<path class="st1" d="M31.2,66.5L31.2,66.5c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,66.9,31.4,66.7,31.2,66.5z"/>
<circle class="st1" cx="24.7" cy="64" r="1.7"/>
<path class="st3" d="M27.1,29.7c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9C35.6,13.7,30.7,9,24.7,9c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
c1,0,2-0.2,3-0.4C27.5,30.5,27.3,30.1,27.1,29.7z"/>
<path class="st3" d="M31.2,16.6L31.2,16.6c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,17.1,31.4,16.8,31.2,16.6z"/>
<circle class="st3" cx="24.7" cy="14.1" r="1.7"/>
<g>
<polygon class="st3" points="36.4,30.1 36.4,30.1 36.4,30.1 "/>
<path class="st3" d="M35.2,25.8l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
c0.4-0.4,0.4-1.1,0-1.5L35.2,25.8z"/>
</g>
<path class="st1" d="M27.3,129.6c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
c1,0,2-0.2,3-0.4C27.6,130.5,27.4,130.1,27.3,129.6z"/>
<path class="st1" d="M31.3,116.6L31.3,116.6c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
c0.3-0.1,0.4-0.3,0.5-0.6C31.6,117,31.5,116.8,31.3,116.6z"/>
<circle class="st1" cx="24.9" cy="114.1" r="1.7"/>
<g>
<polygon class="st1" points="36.5,130.1 36.5,130.1 36.5,130.1 "/>
<path class="st1" d="M35.3,125.8l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
c0.4-0.4,0.4-1.1,0-1.5L35.3,125.8z"/>
</g>
<path class="st1" d="M27.1,179.9c-0.8,0.2-1.6,0.3-2.4,0.3c-5.4,0-9.8-4.4-9.8-9.8c0-5.4,4.4-9.8,9.8-9.8c5.4,0,9.8,4.4,9.8,9.8
c0,0.1,0,0.2,0,0.3c0.4-0.4,0.8-0.6,1.4-0.9c-0.3-5.9-5.2-10.6-11.2-10.6c-6.2,0-11.2,5-11.2,11.2c0,6.2,5,11.2,11.2,11.2
c1,0,2-0.2,3-0.4C27.5,180.7,27.3,180.3,27.1,179.9z"/>
<path class="st1" d="M31.2,166.8L31.2,166.8c-0.3-0.3-0.5-0.3-0.8-0.2c0,0-4,0.5-5.7,0.5c0,0-0.1,0-0.1,0c-1.7,0-5.8-0.6-5.8-0.6
c-0.3-0.1-0.6,0-0.8,0.3l-0.1,0.2c-0.1,0.2-0.1,0.4-0.1,0.7c0.1,0.2,0.2,0.4,0.4,0.5c0.7,0.3,3.3,1.2,4,1.4c0.1,0,0.4,0.1,0.4,0.6
c0,0.6-0.2,3.1-0.5,4.3c-0.3,1.2-0.8,2.7-0.8,2.7c-0.1,0.4,0.1,0.9,0.5,1l0.5,0.2c0.2,0.1,0.4,0.1,0.6,0c0.2-0.1,0.3-0.3,0.4-0.5
l1.4-4.3l1.3,4.4c0.1,0.2,0.2,0.4,0.4,0.5c0.1,0.1,0.2,0.1,0.3,0.1c0.1,0,0.2,0,0.3-0.1l0.5-0.2c0.4-0.1,0.6-0.5,0.5-0.9
c0,0-0.4-1.6-0.7-2.9c-0.2-0.8-0.3-2-0.3-2.9c0-0.6-0.1-1.1-0.1-1.5c0-0.2,0.1-0.4,0.4-0.5c0.1,0,3.7-1.3,3.7-1.3
c0.3-0.1,0.4-0.3,0.5-0.6C31.4,167.3,31.4,167,31.2,166.8z"/>
<circle class="st1" cx="24.7" cy="164.3" r="1.7"/>
<g>
<polygon class="st1" points="36.4,180.3 36.4,180.3 36.4,180.3 "/>
<path class="st1" d="M35.2,176l2.7-2.7c0.4-0.4,0.4-1.1,0-1.5c-0.4-0.4-1.1-0.4-1.5,0l-2.7,2.7l-2.7-2.7c-0.4-0.4-1.1-0.4-1.5,0
c-0.4,0.4-0.4,1.1,0,1.5l2.7,2.7l-2.7,2.7c-0.4,0.4-0.4,1.1,0,1.5c0.4,0.4,1.1,0.4,1.5,0l2.7-2.7l2.7,2.7c0.4,0.4,1.1,0.4,1.5,0
c0.4-0.4,0.4-1.1,0-1.5L35.2,176z"/>
</g>
<g>
<path class="st1" d="M14.7,92.3v-6.4h1.2v3.1l2.8-3.1H20l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H14.7z"/>
<path class="st1" d="M20.9,92.3v-6.4h1.2v6.4H20.9z"/>
<path class="st1" d="M23.3,89.1c0-0.4,0.1-0.8,0.2-1.2c0.1-0.4,0.3-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.3-0.3
c0.6,0,1.1,0.1,1.5,0.4c0.4,0.3,0.7,0.6,0.9,1l-1,0.7c-0.1-0.2-0.2-0.3-0.3-0.5c-0.1-0.1-0.2-0.2-0.4-0.3c-0.1-0.1-0.3-0.1-0.4-0.2
c-0.1,0-0.3,0-0.4,0c-0.3,0-0.6,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.1,0.2-0.3,0.4-0.3,0.7s-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8
c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.1,0,0.3,0,0.4-0.1c0.1,0,0.3-0.1,0.4-0.2
c0.1-0.1,0.3-0.2,0.4-0.3c0.1-0.1,0.2-0.3,0.3-0.4l1,0.6c-0.1,0.2-0.2,0.5-0.4,0.6c-0.2,0.2-0.4,0.3-0.6,0.5
c-0.2,0.1-0.5,0.2-0.7,0.3c-0.3,0.1-0.5,0.1-0.8,0.1c-0.4,0-0.9-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-1-0.8c-0.3-0.3-0.5-0.7-0.6-1.1
C23.4,89.9,23.3,89.5,23.3,89.1z"/>
<path class="st1" d="M30,92.3v-6.4h1.2v3.1l2.8-3.1h1.3l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H30z"/>
</g>
<g>
<path class="st3" d="M14.7,42.6v-6.4h1.2v3.1l2.8-3.1H20l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H14.7z"/>
<path class="st3" d="M20.9,42.6v-6.4h1.2v6.4H20.9z"/>
<path class="st3" d="M23.3,39.4c0-0.4,0.1-0.8,0.2-1.2c0.1-0.4,0.3-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7s0.8-0.3,1.3-0.3
c0.6,0,1.1,0.1,1.5,0.4c0.4,0.3,0.7,0.6,0.9,1l-1,0.7c-0.1-0.2-0.2-0.3-0.3-0.5c-0.1-0.1-0.2-0.2-0.4-0.3c-0.1-0.1-0.3-0.1-0.4-0.2
c-0.1,0-0.3,0-0.4,0c-0.3,0-0.6,0.1-0.8,0.2S25.2,37.8,25,38c-0.1,0.2-0.3,0.4-0.3,0.7s-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8
c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.1,0,0.3,0,0.4-0.1c0.1,0,0.3-0.1,0.4-0.2
c0.1-0.1,0.3-0.2,0.4-0.3c0.1-0.1,0.2-0.3,0.3-0.4l1,0.6c-0.1,0.2-0.2,0.5-0.4,0.6c-0.2,0.2-0.4,0.3-0.6,0.5
c-0.2,0.1-0.5,0.2-0.7,0.3c-0.3,0.1-0.5,0.1-0.8,0.1c-0.4,0-0.9-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-1-0.8c-0.3-0.3-0.5-0.7-0.6-1.1
C23.4,40.2,23.3,39.8,23.3,39.4z"/>
<path class="st3" d="M30,42.6v-6.4h1.2v3.1l2.8-3.1h1.3l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H30z"/>
</g>
<g>
<path class="st1" d="M14.8,142.4v-6.4H16v3.1l2.8-3.1h1.3l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H14.8z"/>
<path class="st1" d="M21,142.4v-6.4h1.2v6.4H21z"/>
<path class="st1" d="M23.4,139.2c0-0.4,0.1-0.8,0.2-1.2c0.1-0.4,0.3-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7c0.4-0.2,0.8-0.3,1.3-0.3
c0.6,0,1.1,0.1,1.5,0.4c0.4,0.3,0.7,0.6,0.9,1l-1,0.7c-0.1-0.2-0.2-0.3-0.3-0.5c-0.1-0.1-0.2-0.2-0.4-0.3c-0.1-0.1-0.3-0.1-0.4-0.2
c-0.1,0-0.3,0-0.4,0c-0.3,0-0.6,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.1,0.2-0.3,0.4-0.3,0.7s-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8
c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.1,0,0.3,0,0.4-0.1c0.1,0,0.3-0.1,0.4-0.2
s0.3-0.2,0.4-0.3c0.1-0.1,0.2-0.3,0.3-0.4l1,0.6c-0.1,0.2-0.2,0.5-0.4,0.6c-0.2,0.2-0.4,0.3-0.6,0.5c-0.2,0.1-0.5,0.2-0.7,0.3
c-0.3,0.1-0.5,0.1-0.8,0.1c-0.4,0-0.9-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-1-0.8c-0.3-0.3-0.5-0.7-0.6-1.1
C23.5,140,23.4,139.6,23.4,139.2z"/>
<path class="st1" d="M30.1,142.4v-6.4h1.2v3.1l2.8-3.1h1.3l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H30.1z"/>
</g>
<g>
<path class="st1" d="M14.7,192.7v-6.4h1.2v3.1l2.8-3.1H20l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H14.7z"/>
<path class="st1" d="M20.9,192.7v-6.4h1.2v6.4H20.9z"/>
<path class="st1" d="M23.3,189.4c0-0.4,0.1-0.8,0.2-1.2c0.1-0.4,0.3-0.7,0.6-1c0.3-0.3,0.6-0.5,1-0.7c0.4-0.2,0.8-0.3,1.3-0.3
c0.6,0,1.1,0.1,1.5,0.4s0.7,0.6,0.9,1l-1,0.7c-0.1-0.2-0.2-0.3-0.3-0.5c-0.1-0.1-0.2-0.2-0.4-0.3s-0.3-0.1-0.4-0.2
c-0.1,0-0.3,0-0.4,0c-0.3,0-0.6,0.1-0.8,0.2s-0.4,0.3-0.6,0.5c-0.1,0.2-0.3,0.4-0.3,0.7s-0.1,0.5-0.1,0.8c0,0.3,0,0.6,0.1,0.8
c0.1,0.3,0.2,0.5,0.4,0.7c0.2,0.2,0.3,0.4,0.6,0.5c0.2,0.1,0.5,0.2,0.7,0.2c0.1,0,0.3,0,0.4-0.1c0.1,0,0.3-0.1,0.4-0.2
c0.1-0.1,0.3-0.2,0.4-0.3c0.1-0.1,0.2-0.3,0.3-0.4l1,0.6c-0.1,0.2-0.2,0.5-0.4,0.6c-0.2,0.2-0.4,0.3-0.6,0.5
c-0.2,0.1-0.5,0.2-0.7,0.3c-0.3,0.1-0.5,0.1-0.8,0.1c-0.4,0-0.9-0.1-1.2-0.3c-0.4-0.2-0.7-0.4-1-0.8c-0.3-0.3-0.5-0.7-0.6-1.1
C23.4,190.2,23.3,189.8,23.3,189.4z"/>
<path class="st1" d="M29.9,192.7v-6.4h1.2v3.1l2.8-3.1h1.3l-2.4,2.8l2.6,3.6h-1.3l-2-2.9l-0.9,0.9v1.9H29.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<g>
<path class="st0" d="M45.4,13.7c-1.6-0.1-3.2,0-4.8,0c-7.5,0-18,0-25.7,0c-0.6-1.8-1-3.5-1.7-5.2C13,7.9,12.2,7.1,11.5,7
C9.4,6.7,7.3,6.7,5.2,6.7c-1.1,0-1.9,0.5-1.9,1.7c0,1.2,0.8,1.7,1.9,1.7c1.3,0,2.7,0,4,0.1c0.5,0.1,1.2,0.6,1.4,1.1
c0.9,2.6,1.7,5.2,2.5,7.8c1.3,4.3,1.8,5.5,3.1,10.2c0.6,2.3,1.2,2.8,2.2,3.3c1.1,0.4,2.1,0.4,2.1,0.4h1.8c4.6,0,12.2,0,16.8,0
c1.1,0,2.1-0.1,2.6-1.4c1.9-5.1,3.8-10.2,5.7-15.3C47.8,14.7,47.1,13.8,45.4,13.7z M38.9,28.7c-0.1,0.3-0.8,0.7-1.2,0.7
c-4.6,0-12.2,0-16.8,0c-0.4,0-1.1-0.3-1.2-0.7c-1.3-3.8-2.4-7.6-3.7-11.5h27.1C41.8,21.2,40.4,24.9,38.9,28.7z"/>
<path class="st0" d="M17.2,37.3L17.2,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.3-0.9,2.3c0,1,0.3,1.8,0.8,2.4
c0.6,0.6,1.4,0.9,2.2,0.9c1.8,0,3.2-1.4,3.2-3.2c0-0.9-0.3-1.7-0.9-2.3C18.8,37.6,18,37.3,17.2,37.3z"/>
<path class="st0" d="M35.1,37.3L35.1,37.3c-1,0-1.7,0.2-2.2,0.7c-0.6,0.6-0.9,1.4-0.9,2.4c0,1,0.3,1.8,0.8,2.4
c0.6,0.6,1.3,0.9,2.2,0.9c1.8,0,3.2-1.5,3.2-3.3c0-0.9-0.3-1.6-0.9-2.2C36.8,37.6,36,37.3,35.1,37.3z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<path class="st0" d="M37,18.5H14.1c-0.9,0-1.7-0.7-1.7-1.7c0-0.9,0.8-1.7,1.7-1.7H37c0.9,0,1.7,0.7,1.7,1.7
C38.6,17.8,37.9,18.5,37,18.5z"/>
<path class="st0" d="M37,28H14.1c-0.9,0-1.7-0.8-1.7-1.7c0-0.9,0.8-1.7,1.7-1.7H37c0.9,0,1.7,0.8,1.7,1.7C38.6,27.3,37.9,28,37,28z
"/>
<path class="st0" d="M37,37.4H14.1c-0.9,0-1.7-0.8-1.7-1.7s0.8-1.7,1.7-1.7H37c0.9,0,1.7,0.8,1.7,1.7S37.9,37.4,37,37.4z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 835 B

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<path class="st0" d="M28.9,17.1v-0.5c0-2-1.7-3.6-3.7-3.6h0c-2,0-3.7,1.6-3.7,3.6v6.9L28.9,17.1z"/>
<path class="st0" d="M21.5,29.2v0.2c0,2,1.6,3.6,3.7,3.6h0c2,0,3.7-1.6,3.7-3.6v-6.6L21.5,29.2z"/>
<path class="st0" d="M39.1,16.8L13.6,39.1c-0.7,0.6-1.8,0.5-2.4-0.2l-0.2-0.2c-0.6-0.7-0.5-1.8,0.2-2.4l25.4-22.4
c0.7-0.6,1.8-0.5,2.4,0.2l0.2,0.2C39.8,15.1,39.7,16.1,39.1,16.8z"/>
<path class="st0" d="M23.4,40.2l0,3.4h-4.3c-1,0-1.8,0.8-1.8,1.8c0,1,0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8
c0-1-0.8-1.8-1.8-1.8h-4.4l0-3.4c5.2-0.8,9.2-5,9.2-10.1c0-0.1,0-5.1,0-5.3c0-1-0.9-1.7-1.8-1.7c-1,0-1.7,0.8-1.7,1.8
c0,0.3,0,4.8,0,5.2c0,3.7-3.4,6.7-7.5,6.7c-3.6,0-6.7-2.3-7.3-5.4L15,34C16.4,37.2,19.6,39.7,23.4,40.2z"/>
<path class="st0" d="M17.7,24.9c0-1-0.7-1.8-1.6-1.8c-1-0.1-1.8,0.7-1.9,1.6c0,0.2,0,4.2,0,5.3l3.5-3.1
C17.7,25.9,17.7,25,17.7,24.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<path class="st0" d="M31.4,14.1l2.2-2.2c-2.1-2.5-5.3-4.1-8.8-4.1c-3.4,0-6.4,1.5-8.5,3.8c0.7,0.7,1.5,1.5,2.2,2.2
c1.6-1.7,3.8-2.9,6.3-2.9C27.5,10.9,29.9,12.1,31.4,14.1z"/>
<path class="st0" d="M36.5,9l2.2-2.2C35.3,3,30.3,0.6,24.8,0.6c-5.3,0-10.2,2.3-13.6,5.9c0.7,0.7,1.5,1.5,2.2,2.2
c2.9-3,6.9-5,11.4-5C29.5,3.7,33.6,5.8,36.5,9z"/>
<path class="st0" d="M28.5,22.7v-4.4c0-2-1.6-3.6-3.7-3.6h0c-2,0-3.7,1.6-3.7,3.6v4.4H28.5z"/>
<path class="st0" d="M21.1,26.5v4.3c0,2,1.7,3.6,3.7,3.6h0c2,0,3.7-1.6,3.7-3.6v-4.3H21.1z"/>
<path class="st0" d="M36,31.5c0-0.1,0-5.1,0-5.3c0-1-0.9-1.7-1.8-1.7c-1,0-1.7,0.8-1.7,1.8c0,0.3,0,4.8,0,5.2
c0,3.7-3.4,6.7-7.5,6.7c-4.1,0-7.5-3-7.5-6.7c0-0.4,0-4.9,0-5.3c0-1-0.7-1.8-1.6-1.8C14.9,24.3,14,25,14,26c0,0.3,0,5.4,0,5.5
c0,5.1,4,9.3,9.2,10.1l0,3.4h-4.3c-1,0-1.8,0.8-1.8,1.8c0,1,0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8c0-1-0.8-1.8-1.8-1.8h-4.4
l0-3.4C32,40.7,36,36.6,36,31.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<circle class="st0" cx="25.6" cy="14.8" r="8.1"/>
<path class="st0" d="M31.4,27h-11c-4.6,0-8.2,3.9-8.2,8.5v4.3c3.8,2.8,8.1,4.5,13.1,4.5c5.6,0,10.6-2.1,14.4-5.6v-3.2
C39.6,30.9,35.9,27,31.4,27z"/>
<circle class="st0" cx="41.6" cy="17.1" r="3.5"/>
<path class="st0" d="M43.9,23.9h-4.1c-0.9,0-1.7,0.4-2.3,1c1.2,0.6,2.3,1.6,3.1,2.5c1,1.2,1.5,2.7,1.7,4.3c0.3,0.9,0.4,1.8,0.2,2.8
v0.6c1.6-2.2,3.3-6,4-8.1v-0.3C46.5,25.7,45.3,23.9,43.9,23.9z"/>
<circle class="st0" cx="9.4" cy="18" r="3.5"/>
<path class="st0" d="M8.5,35.7c-0.1-0.7-0.1-1.4,0-2.1l0.1-0.2c0-0.2,0-0.4,0-0.6c0-0.2,0-0.3,0.1-0.5c0-0.2,0-0.3,0-0.5
c0.2-2.1,1.6-4.4,3.2-5.7c0.4-0.4,0.9-0.6,1.4-0.9c-0.5-0.5-1.3-0.7-2-0.7H7c-1.4,0-2.6,1.8-2.4,2.7v0.3
C5.1,29.8,6.8,33.5,8.5,35.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<g>
<path class="st0" d="M29.1,45H12c-3.5,0-6.4-1.8-8.4-5.1c-1.4-2.4-1.9-4.7-1.9-4.8l-0.3-1.6h10.4V4.8h35.8v27
c0.1,0.8,0.5,6.6-2.4,10c-1.4,1.6-3.3,2.4-5.6,2.4c-4.8,0-7.2-2.6-8.3-4.7c-0.6-1.2-1-2.3-1.1-3.2H5c0.3,0.7,0.6,1.5,1.1,2.3
c1.5,2.4,3.5,3.6,5.9,3.6h17.1c0.8,0,1.4,0.6,1.4,1.4C30.5,44.3,29.9,45,29.1,45z M14.6,33.5h18l0.1,1.3c0,0,0.1,1.7,1,3.4
c1.2,2.1,3.1,3.2,5.8,3.2c1.5,0,2.7-0.5,3.5-1.4c1.2-1.3,1.6-3.3,1.7-4.8c0.2-1.7,0-3.1,0-3.1l0-0.1l0-0.1V7.6H14.6V33.5z"/>
<path class="st0" d="M39.6,14.3H19.7c-0.8,0-1.5-0.7-1.5-1.5s0.7-1.5,1.5-1.5h19.9c0.8,0,1.5,0.7,1.5,1.5S40.4,14.3,39.6,14.3z"/>
<path class="st0" d="M39.6,21.3H19.7c-0.8,0-1.5-0.7-1.5-1.6s0.7-1.6,1.5-1.6h19.9c0.8,0,1.5,0.7,1.5,1.6S40.4,21.3,39.6,21.3z"/>
<path class="st0" d="M39.6,28.1H19.7c-0.8,0-1.5-0.7-1.5-1.6s0.7-1.6,1.5-1.6h19.9c0.8,0,1.5,0.7,1.5,1.6S40.4,28.1,39.6,28.1z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<g>
<path class="st0" d="M38.5,16.3h-3.7v-1.6c0-2.4-1.6-3-4-3H18.7c-2.4,0-3.6,0.5-3.6,3v1.6h-3.6c-2.4,0-4.5,1-4.5,3.5V36
c0,2.4,1.9,4.6,4.5,4.6h27.1c2.4,0,4.2-2.7,4.2-5.2V19.7C42.8,17.3,41,16.3,38.5,16.3z M25.2,37c-5.5,0-9.9-4.5-9.9-9.9
c0-5.5,4.5-9.9,9.9-9.9c5.5,0,9.9,4.5,9.9,9.9S30.8,37,25.2,37z"/>
<circle class="st0" cx="25.2" cy="27" r="5.9"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 804 B

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<path class="st0" d="M42.2,6.2H8.1c-3.5,0-6.4,3.1-6.4,6.8v18c0,3.8,2.9,6.8,6.4,6.8h15.3v3.5h-8.9c-0.8,0-1.5,0.9-1.5,1.8
c0,0.9,0.7,1.8,1.5,1.8h21.9c0.8,0,1.5-0.9,1.5-1.8c0-0.9-0.7-1.8-1.5-1.8h-9.6v-3.4c-0.3,0-0.6,0-0.9,0h16.3
c3.5,0,6.4-3.1,6.4-6.8V13C48.6,9.2,45.7,6.2,42.2,6.2z M45.2,31c0,1.7-1.3,3.1-2.9,3.1H8.1c-1.6,0-2.9-1.4-2.9-3.1V13
c0-1.7,1.3-3.1,2.9-3.1h34.2c1.6,0,2.9,1.4,2.9,3.1V31z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 842 B

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<path class="st0" d="M25.4,28.7c1.7,0,2.8,1.8,3.8,3.6c0.5,0.8,1.6,2.6,2,2.6h7.5c4.2,0,7.6-3.8,7.6-8.4v-5.4
c0-4.6-3.4-8.4-7.6-8.4H11.9c-4.2,0-7.6,3.8-7.6,8.4v5.4c0,4.6,3.4,8.4,7.6,8.4h7.3c0.6,0,1.7-1.6,2.4-2.7
C22.6,30.5,23.7,28.7,25.4,28.7C25.3,28.7,25.4,28.7,25.4,28.7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 717 B

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 50 50" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_2">
</g>
<g id="Layer_1">
<g>
<path class="st0" d="M22.6,14.8c-0.1-0.9-0.1-1.8,0.1-2.6h-8.6c-0.9,0-1.7,0.8-1.7,1.8s0.8,1.8,1.7,1.8h8.7
C22.7,15.4,22.7,15.1,22.6,14.8z"/>
<path class="st0" d="M26.5,22.3c-0.6-0.5-1.1-1-1.6-1.6H14.1c-0.9,0-1.7,0.8-1.7,1.8c0,1,0.8,1.8,1.7,1.8h8.2
c0.3-0.2,0.6-0.3,0.9-0.4c1.2-0.8,2.5-1.2,3.9-1.3C26.9,22.6,26.7,22.5,26.5,22.3z"/>
<path class="st0" d="M17.9,30.3c0.1-0.3,0.1-0.5,0.2-0.7h-4c-0.9,0-1.7,0.8-1.7,1.8c0,1,0.8,1.8,1.7,1.8h3.2
C17.3,32.2,17.5,31.2,17.9,30.3z"/>
<path class="st0" d="M5.9,15.7H4.5c-0.9,0-1.7-0.8-1.7-1.8s0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8S6.8,15.7,5.9,15.7z"/>
<path class="st0" d="M5.9,24.4H4.5c-0.9,0-1.7-0.8-1.7-1.8c0-1,0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8
C7.6,23.6,6.8,24.4,5.9,24.4z"/>
<path class="st0" d="M5.9,33.2H4.5c-0.9,0-1.7-0.8-1.7-1.8s0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8S6.8,33.2,5.9,33.2z"/>
<path class="st0" d="M17.3,38.3h-3.2c-0.9,0-1.7,0.8-1.7,1.8s0.8,1.8,1.7,1.8h3.2V38.3z"/>
<path class="st0" d="M5.9,42H4.5c-0.9,0-1.7-0.8-1.7-1.8c0-1,0.8-1.8,1.7-1.8h1.4c0.9,0,1.7,0.8,1.7,1.8C7.6,41.1,6.8,42,5.9,42z"
/>
<circle class="st0" cx="34.5" cy="14.3" r="7.8"/>
<path class="st0" d="M23.9,44.1c10.4,0,19.5-4.4,23.3-13.4c-1.3-2.7-3.9-4.7-7.1-4.7H29.5c-4.4,0-8.2,3.8-8.2,8.3v9.7
C22.2,44,23,44.1,23.9,44.1z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<path class="st0" d="M24.6,12.5l1.6-1.6C24.7,9.2,22.4,8,19.9,8c-2.4,0-4.6,1-6.1,2.7c0.5,0.5,1,1,1.6,1.6c1.1-1.2,2.7-2,4.5-2
C21.8,10.2,23.5,11.1,24.6,12.5z"/>
<path class="st0" d="M28.3,8.9l1.6-1.6c-2.4-2.7-6-4.4-9.9-4.4c-3.8,0-7.3,1.6-9.7,4.2c0.5,0.5,1,1,1.6,1.6c2-2.2,4.9-3.5,8.1-3.5
C23.2,5.1,26.2,6.6,28.3,8.9z"/>
<path class="st0" d="M22.5,18.7v-3.1c0-1.5-1.2-2.6-2.6-2.6h0c-1.5,0-2.6,1.1-2.6,2.6v3.1H22.5z"/>
<path class="st0" d="M17.3,21.4v3c0,1.5,1.2,2.6,2.7,2.6h0c1.5,0,2.6-1.2,2.6-2.6v-3H17.3z"/>
<path class="st0" d="M27.9,24.9c0,0,0-3.6,0-3.8c0-0.7-0.6-1.2-1.3-1.2c-0.7,0-1.2,0.6-1.2,1.3c0,0.2,0,3.4,0,3.7
c0,2.6-2.4,4.8-5.3,4.8c-2.9,0-5.3-2.1-5.3-4.8c0-0.3,0-3.5,0-3.8c0-0.7-0.5-1.3-1.2-1.3c-0.7,0-1.3,0.5-1.3,1.2c0,0.2,0,3.9,0,3.9
c0,3.6,2.9,6.6,6.6,7.2l0,2.4h-3.1c-0.7,0-1.3,0.6-1.3,1.3c0,0.7,0.6,1.3,1.3,1.3h8.8c0.7,0,1.3-0.6,1.3-1.3c0-0.7-0.6-1.3-1.3-1.3
h-3.2l0-2.4C25,31.6,27.9,28.6,27.9,24.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<path class="st0" d="M17.3,18.4v6c0,1.5,1.2,2.6,2.7,2.6h0c1.5,0,2.6-1.2,2.6-2.6v-6H17.3z"/>
<path class="st0" d="M27.9,24.9c0,0,0-3.6,0-3.8c0-0.7-0.6-1.2-1.3-1.2c-0.7,0-1.2,0.6-1.2,1.3c0,0.2,0,3.4,0,3.7
c0,2.6-2.4,4.8-5.3,4.8c-2.9,0-5.3-2.1-5.3-4.8c0-0.3,0-3.5,0-3.8c0-0.7-0.5-1.3-1.2-1.3c-0.7,0-1.3,0.5-1.3,1.2c0,0.2,0,3.9,0,3.9
c0,3.6,2.9,6.6,6.6,7.2l0,2.4h-3.1c-0.7,0-1.3,0.6-1.3,1.3c0,0.7,0.6,1.3,1.3,1.3h8.8c0.7,0,1.3-0.6,1.3-1.3c0-0.7-0.6-1.3-1.3-1.3
h-3.2l0-2.4C25,31.6,27.9,28.6,27.9,24.9z"/>
<g>
<polygon class="st0" points="23.4,15.8 23.4,15.8 23.4,15.8 "/>
<path class="st0" d="M21.9,10.5l3.4-3.4c0.5-0.5,0.5-1.4,0-1.9c-0.5-0.5-1.4-0.5-1.9,0L20,8.6l-3.4-3.4c-0.5-0.5-1.4-0.5-1.9,0
s-0.5,1.4,0,1.9l3.4,3.4l-3.4,3.4c-0.5,0.5-0.5,1.4,0,1.9c0.5,0.5,1.4,0.5,1.9,0l3.4-3.4l3.4,3.4c0.5,0.5,1.4,0.5,1.9,0
c0.5-0.5,0.5-1.4,0-1.9L21.9,10.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Some files were not shown because too many files have changed in this diff Show more