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

This commit is contained in:
VRCat\VRKitten 2017-07-26 11:54:56 -06:00
commit a1f2e786af
792 changed files with 30817 additions and 13010 deletions
BUILD_LINUX.mdCMakeGraphvizOptions.cmakeCMakeLists.txtCONTRIBUTING.md
assignment-client
cmake
domain-server
gvr-interface/src
ice-server
interface

View file

@ -1,7 +1,96 @@
# Linux build guide
Please read the [general build guide](BUILD.md) for information on dependencies required for all platforms. Only Linux specific instructions are found in this file.
### Qt5 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 libssl-dev
## Ubuntu 16.04 specific build guide
### Prepare environment
Install qt:
```bash
wget http://debian.highfidelity.com/pool/h/hi/hifi-qt5.6.1_5.6.1_amd64.deb
sudo dpkg -i hifi-qt5.6.1_5.6.1_amd64.deb
```
Install build dependencies:
```bash
sudo apt-get install libasound2 libxmu-dev libxi-dev freeglut3-dev libasound2-dev libjack0 libjack-dev libxrandr-dev libudev-dev libssl-dev
```
To compile interface in a server you must install:
```bash
sudo apt -y install libpulse0 libnss3 libnspr4 libfontconfig1 libxcursor1 libxcomposite1 libxtst6 libxslt1.1
```
Install build tools:
```bash
sudo apt install cmake
```
### Get code and checkout the tag you need
Clone this repository:
```bash
git clone https://github.com/highfidelity/hifi.git
```
To compile a RELEASE version checkout the tag you need getting a list of all tags:
```bash
git fetch -a
git tags
```
Then checkout last tag with:
```bash
git checkout tags/RELEASE-6819
```
Or go to the highfidelity download page (https://highfidelity.com/download) to get the release version. For example, if there is a BETA 6731 type:
```bash
git checkout tags/RELEASE-6731
```
### Compiling
Create the build directory:
```bash
mkdir -p hifi/build
cd hifi/build
```
Prepare makefiles:
```bash
cmake -DQT_CMAKE_PREFIX_PATH=/usr/local/Qt5.6.1/5.6/gcc_64/lib/cmake ..
```
Start compilation and get a cup of coffee:
```bash
make domain-server assignment-client interface
```
In a server does not make sense to compile interface
### Running the software
Running domain server:
```bash
./domain-server/domain-server
```
Running assignment client:
```bash
./assignment-client/assignment-client -n 6
```
Running interface:
```bash
./interface/interface
```
Go to localhost in running interface.

View file

@ -1 +1,2 @@
set(GRAPHVIZ_EXTERNAL_LIBS FALSE)
set(GRAPHVIZ_EXTERNAL_LIBS FALSE)
set(GRAPHVIZ_IGNORE_TARGETS "shared;networking")

View file

@ -1,4 +1,8 @@
cmake_minimum_required(VERSION 3.2)
if (WIN32)
cmake_minimum_required(VERSION 3.7)
else()
cmake_minimum_required(VERSION 3.2)
endif()
if (USE_ANDROID_TOOLCHAIN)
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cmake/android/android.toolchain.cmake")
@ -33,6 +37,10 @@ set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")
find_package( Threads )
if (WIN32)
if (NOT "${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
message( FATAL_ERROR "Only 64 bit builds supported." )
endif()
add_definitions(-DNOMINMAX -D_CRT_SECURE_NO_WARNINGS)
if (NOT WINDOW_SDK_PATH)
@ -41,16 +49,13 @@ if (WIN32)
# sets path for Microsoft SDKs
# if you get build error about missing 'glu32' this path is likely wrong
if (MSVC10)
set(WINDOW_SDK_PATH "C:\\Program Files\\Microsoft SDKs\\Windows\\v7.1 " CACHE PATH "Windows SDK PATH")
elseif (MSVC12)
if ("${CMAKE_SIZEOF_VOID_P}" EQUAL "8")
set(WINDOW_SDK_FOLDER "x64")
else()
set(WINDOW_SDK_FOLDER "x86")
endif()
if (MSVC_VERSION GREATER_EQUAL 1910) # VS 2017
set(WINDOW_SDK_PATH "C:/Program Files (x86)/Windows Kits/10/Lib/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}/x64" CACHE PATH "Windows SDK PATH")
elseif (MSVC_VERSION GREATER_EQUAL 1800) # VS 2013
set(WINDOW_SDK_PATH "C:\\Program Files (x86)\\Windows Kits\\8.1\\Lib\\winv6.3\\um\\${WINDOW_SDK_FOLDER}" CACHE PATH "Windows SDK PATH")
endif ()
else()
message( FATAL_ERROR "Visual Studio 2013 or higher required." )
endif()
if (DEBUG_DISCOVERED_SDK_PATH)
message(STATUS "The discovered Windows SDK path is ${WINDOW_SDK_PATH}")
@ -103,7 +108,7 @@ else ()
endif ()
if (APPLE)
set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++0x")
set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++11")
set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libc++")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --stdlib=libc++")
endif ()

View file

@ -16,7 +16,7 @@ Contributing
git checkout -b new_branch_name
```
4. Code
* Follow the [coding standard](https://wiki.highfidelity.com/wiki/Coding_Standards)
* Follow the [coding standard](https://docs.highfidelity.com/build-guide/coding-standards)
5. Commit
* Use [well formed commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
6. Update your branch

View file

@ -7,11 +7,13 @@ if (APPLE)
set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks")
endif ()
setup_memory_debugger()
# link in the shared libraries
link_hifi_libraries(
audio avatars octree gpu model fbx entities
networking animation recording shared script-engine embedded-webserver
controllers physics plugins
physics plugins
)
if (WIN32)

View file

@ -23,6 +23,8 @@
#include <AvatarHashMap.h>
#include <AudioInjectorManager.h>
#include <AssetClient.h>
#include <DebugDraw.h>
#include <LocationScriptingInterface.h>
#include <MessagesClient.h>
#include <NetworkAccessManager.h>
#include <NodeList.h>
@ -49,19 +51,19 @@
#include "RecordingScriptingInterface.h"
#include "AbstractAudioInterface.h"
#include "AvatarAudioTimer.h"
static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10;
Agent::Agent(ReceivedMessage& message) :
ThreadedAssignment(message),
_receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES, RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES),
_audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO)
_audioGate(AudioConstants::SAMPLE_RATE, AudioConstants::MONO),
_avatarAudioTimer(this)
{
_entityEditSender.setPacketsPerSecond(DEFAULT_ENTITY_PPS_PER_SCRIPT);
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
ResourceManager::init();
DependencyManager::set<ResourceManager>();
DependencyManager::registerInheritance<SpatialParentFinder, AssignmentParentFinder>();
@ -80,6 +82,9 @@ Agent::Agent(ReceivedMessage& message) :
DependencyManager::set<RecordingScriptingInterface>();
DependencyManager::set<UsersScriptingInterface>();
// Needed to ensure the creation of the DebugDraw instance on the main thread
DebugDraw::getInstance();
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
@ -91,6 +96,14 @@ Agent::Agent(ReceivedMessage& message) :
this, "handleOctreePacket");
packetReceiver.registerListener(PacketType::Jurisdiction, this, "handleJurisdictionPacket");
packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat");
// 100Hz timer for audio
const int TARGET_INTERVAL_MSEC = 10; // 10ms
connect(&_avatarAudioTimer, &QTimer::timeout, this, &Agent::processAgentAvatarAudio);
_avatarAudioTimer.setSingleShot(false);
_avatarAudioTimer.setInterval(TARGET_INTERVAL_MSEC);
_avatarAudioTimer.setTimerType(Qt::PreciseTimer);
}
void Agent::playAvatarSound(SharedSoundPointer sound) {
@ -198,7 +211,7 @@ void Agent::requestScript() {
return;
}
auto request = ResourceManager::createResourceRequest(this, scriptURL);
auto request = DependencyManager::get<ResourceManager>()->createResourceRequest(this, scriptURL);
if (!request) {
qWarning() << "Could not create ResourceRequest for Agent script at" << scriptURL.toString();
@ -453,6 +466,9 @@ void Agent::executeScript() {
_scriptEngine->registerGlobalObject("EntityViewer", &_entityViewer);
_scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter,
LocationScriptingInterface::locationSetter);
auto recordingInterface = DependencyManager::get<RecordingScriptingInterface>();
_scriptEngine->registerGlobalObject("Recording", recordingInterface.data());
@ -467,14 +483,7 @@ void Agent::executeScript() {
DependencyManager::set<AssignmentParentFinder>(_entityViewer.getTree());
// 100Hz timer for audio
AvatarAudioTimer* audioTimerWorker = new AvatarAudioTimer();
audioTimerWorker->moveToThread(&_avatarAudioTimerThread);
connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAudio);
connect(this, &Agent::startAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::start);
connect(this, &Agent::stopAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::stop);
connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater);
_avatarAudioTimerThread.start();
QMetaObject::invokeMethod(&_avatarAudioTimer, "start");
// Agents should run at 45hz
static const int AVATAR_DATA_HZ = 45;
@ -553,7 +562,7 @@ void Agent::setIsAvatar(bool isAvatar) {
_avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); // FIXME - we shouldn't really need to constantly send identity packets
// tell the avatarAudioTimer to start ticking
emit startAvatarAudioTimer();
QMetaObject::invokeMethod(&_avatarAudioTimer, "start");
}
@ -582,7 +591,7 @@ void Agent::setIsAvatar(bool isAvatar) {
nodeList->sendPacket(std::move(packet), *node);
});
}
emit stopAvatarAudioTimer();
QMetaObject::invokeMethod(&_avatarAudioTimer, "stop");
}
}
@ -600,6 +609,24 @@ void Agent::processAgentAvatar() {
AvatarData::AvatarDataDetail dataDetail = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO) ? AvatarData::SendAllData : AvatarData::CullSmallData;
QByteArray avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail);
int maximumByteArraySize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber);
if (avatarByteArray.size() > maximumByteArraySize) {
qWarning() << " scriptedAvatar->toByteArrayStateful() resulted in very large buffer:" << avatarByteArray.size() << "... attempt to drop facial data";
avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail, true);
if (avatarByteArray.size() > maximumByteArraySize) {
qWarning() << " scriptedAvatar->toByteArrayStateful() without facial data resulted in very large buffer:" << avatarByteArray.size() << "... reduce to MinimumData";
avatarByteArray = scriptedAvatar->toByteArrayStateful(AvatarData::MinimumData, true);
if (avatarByteArray.size() > maximumByteArraySize) {
qWarning() << " scriptedAvatar->toByteArrayStateful() MinimumData resulted in very large buffer:" << avatarByteArray.size() << "... FAIL!!";
return;
}
}
}
scriptedAvatar->doneEncoding(true);
static AvatarDataSequenceNumber sequenceNumber = 0;
@ -775,7 +802,7 @@ void Agent::aboutToFinish() {
// our entity tree is going to go away so tell that to the EntityScriptingInterface
DependencyManager::get<EntityScriptingInterface>()->setEntityTree(nullptr);
ResourceManager::cleanup();
DependencyManager::get<ResourceManager>()->cleanup();
// cleanup the AudioInjectorManager (and any still running injectors)
DependencyManager::destroy<AudioInjectorManager>();
@ -792,8 +819,7 @@ void Agent::aboutToFinish() {
DependencyManager::destroy<recording::Recorder>();
DependencyManager::destroy<recording::ClipCache>();
emit stopAvatarAudioTimer();
_avatarAudioTimerThread.quit();
QMetaObject::invokeMethod(&_avatarAudioTimer, "stop");
// cleanup codec & encoder
if (_codec && _encoder) {

View file

@ -23,7 +23,6 @@
#include <EntityEditPacketSender.h>
#include <EntityTree.h>
#include <EntityTreeHeadlessViewer.h>
#include <ScriptEngine.h>
#include <ThreadedAssignment.h>
@ -31,6 +30,7 @@
#include "AudioGate.h"
#include "MixedAudioStream.h"
#include "entities/EntityTreeHeadlessViewer.h"
#include "avatars/ScriptableAvatar.h"
class Agent : public ThreadedAssignment {
@ -81,9 +81,6 @@ private slots:
void processAgentAvatar();
void processAgentAvatarAudio();
signals:
void startAvatarAudioTimer();
void stopAvatarAudioTimer();
private:
void negotiateAudioFormat();
void selectAudioFormat(const QString& selectedCodecName);
@ -118,7 +115,7 @@ private:
CodecPluginPointer _codec;
QString _selectedCodecName;
Encoder* _encoder { nullptr };
QThread _avatarAudioTimerThread;
QTimer _avatarAudioTimer;
bool _flushEncoder { false };
};

View file

@ -16,6 +16,7 @@
#include <QThread>
#include <QTimer>
#include <shared/QtHelpers.h>
#include <AccountManager.h>
#include <AddressManager.h>
#include <Assignment.h>
@ -141,7 +142,7 @@ void AssignmentClient::stopAssignmentClient() {
QThread* currentAssignmentThread = _currentAssignment->thread();
// ask the current assignment to stop
QMetaObject::invokeMethod(_currentAssignment, "stop", Qt::BlockingQueuedConnection);
BLOCKING_INVOKE_METHOD(_currentAssignment, "stop");
// ask the current assignment to delete itself on its thread
_currentAssignment->deleteLater();

View file

@ -9,21 +9,21 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QCommandLineParser>
#include <QThread>
#include "AssignmentClientApp.h"
#include <QtCore/QCommandLineParser>
#include <QtCore/QDir>
#include <QtCore/QStandardPaths>
#include <QtCore/QThread>
#include <LogHandler.h>
#include <SharedUtil.h>
#include <HifiConfigVariantMap.h>
#include <SharedUtil.h>
#include <ShutdownEventListener.h>
#include "Assignment.h"
#include "AssignmentClient.h"
#include "AssignmentClientMonitor.h"
#include "AssignmentClientApp.h"
#include <QtCore/QDir>
#include <QtCore/QStandardPaths>
AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
QCoreApplication(argc, argv)
@ -87,6 +87,9 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
const QCommandLineOption logDirectoryOption(ASSIGNMENT_LOG_DIRECTORY, "directory to store logs", "log-directory");
parser.addOption(logDirectoryOption);
const QCommandLineOption parentPIDOption(PARENT_PID_OPTION, "PID of the parent process", "parent-pid");
parser.addOption(parentPIDOption);
if (!parser.parse(QCoreApplication::arguments())) {
qCritical() << parser.errorText() << endl;
parser.showHelp();
@ -186,6 +189,10 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
listenPort = argumentVariantMap.value(ASSIGNMENT_CLIENT_LISTEN_PORT_OPTION).toUInt();
}
if (parser.isSet(portOption)) {
listenPort = parser.value(portOption).toUInt();
}
if (parser.isSet(numChildsOption)) {
if (minForks && minForks > numForks) {
qCritical() << "--min can't be more than -n";
@ -199,6 +206,16 @@ AssignmentClientApp::AssignmentClientApp(int argc, char* argv[]) :
}
}
if (parser.isSet(parentPIDOption)) {
bool ok = false;
int parentPID = parser.value(parentPIDOption).toInt(&ok);
if (ok) {
qDebug() << "Parent process PID is" << parentPID;
watchParentProcess(parentPID);
}
}
QThread::currentThread()->setObjectName("main thread");
DependencyManager::registerInheritance<LimitedNodeList, NodeList>();

View file

@ -91,9 +91,22 @@ void AssignmentClientMonitor::simultaneousWaitOnChildren(int waitMsecs) {
}
}
void AssignmentClientMonitor::childProcessFinished(qint64 pid) {
void AssignmentClientMonitor::childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus) {
auto message = "Child process " + QString::number(pid) + " has %1 with exit code " + QString::number(exitCode) + ".";
if (_childProcesses.remove(pid)) {
qDebug() << "Child process" << pid << "has finished. Removed from internal map.";
message.append(" Removed from internal map.");
} else {
message.append(" Could not find process in internal map.");
}
switch (exitStatus) {
case QProcess::NormalExit:
qDebug() << qPrintable(message.arg("returned"));
break;
case QProcess::CrashExit:
qCritical() << qPrintable(message.arg("crashed"));
break;
}
}
@ -131,7 +144,6 @@ void AssignmentClientMonitor::aboutToQuit() {
void AssignmentClientMonitor::spawnChildClient() {
QProcess* assignmentClient = new QProcess(this);
// unparse the parts of the command-line that the child cares about
QStringList _childArguments;
if (_assignmentPool != "") {
@ -160,6 +172,9 @@ void AssignmentClientMonitor::spawnChildClient() {
_childArguments.append("--" + ASSIGNMENT_CLIENT_MONITOR_PORT_OPTION);
_childArguments.append(QString::number(DependencyManager::get<NodeList>()->getLocalSockAddr().getPort()));
_childArguments.append("--" + PARENT_PID_OPTION);
_childArguments.append(QString::number(QCoreApplication::applicationPid()));
QString nowString, stdoutFilenameTemp, stderrFilenameTemp, stdoutPathTemp, stderrPathTemp;
@ -219,7 +234,9 @@ void AssignmentClientMonitor::spawnChildClient() {
auto pid = assignmentClient->processId();
// make sure we hear that this process has finished when it does
connect(assignmentClient, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished),
this, [this, pid]() { childProcessFinished(pid); });
this, [this, pid](int exitCode, QProcess::ExitStatus exitStatus) {
childProcessFinished(pid, exitCode, exitStatus);
});
qDebug() << "Spawned a child client with PID" << assignmentClient->processId();

View file

@ -44,7 +44,7 @@ public:
void stopChildProcesses();
private slots:
void checkSpares();
void childProcessFinished(qint64 pid);
void childProcessFinished(qint64 pid, int exitCode, QProcess::ExitStatus exitStatus);
void handleChildStatusPacket(QSharedPointer<ReceivedMessage> message);
bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false) override;

View file

@ -1,36 +0,0 @@
//
// AvatarAudioTimer.cpp
// assignment-client/src
//
// Created by David Kelly on 10/12/13.
// 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 <QDebug>
#include <SharedUtil.h>
#include "AvatarAudioTimer.h"
// 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() {
auto startTime = usecTimestampNow();
quint64 frameCounter = 0;
const int TARGET_INTERVAL_USEC = 10000; // 10ms
while (!_quit) {
++frameCounter;
// tick every 10ms from startTime
quint64 targetTime = startTime + frameCounter * TARGET_INTERVAL_USEC;
quint64 now = usecTimestampNow();
// avoid quint64 underflow
if (now < targetTime) {
usleep(targetTime - now);
}
emit avatarTick();
}
qDebug() << "AvatarAudioTimer is finished";
}

View file

@ -1,31 +0,0 @@
//
// AvatarAudioTimer.h
// assignment-client/src
//
// Created by David Kelly on 10/12/13.
// 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_AvatarAudioTimer_h
#define hifi_AvatarAudioTimer_h
#include <QtCore/QObject>
class AvatarAudioTimer : public QObject {
Q_OBJECT
signals:
void avatarTick();
public slots:
void start();
void stop() { _quit = true; }
private:
bool _quit { false };
};
#endif //hifi_AvatarAudioTimer_h

View file

@ -55,7 +55,8 @@ QVector<AudioMixer::ZoneSettings> AudioMixer::_zoneSettings;
QVector<AudioMixer::ReverbSettings> AudioMixer::_zoneReverbSettings;
AudioMixer::AudioMixer(ReceivedMessage& message) :
ThreadedAssignment(message) {
ThreadedAssignment(message)
{
// Always clear settings first
// This prevents previous assignment settings from sticking around
@ -92,6 +93,15 @@ AudioMixer::AudioMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::NodeMuteRequest, this, "handleNodeMuteRequestPacket");
packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket");
packetReceiver.registerListenerForTypes({
PacketType::ReplicatedMicrophoneAudioNoEcho,
PacketType::ReplicatedMicrophoneAudioWithEcho,
PacketType::ReplicatedInjectAudio,
PacketType::ReplicatedSilentAudioFrame
},
this, "queueReplicatedAudioPacket"
);
connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled);
}
@ -103,6 +113,33 @@ void AudioMixer::queueAudioPacket(QSharedPointer<ReceivedMessage> message, Share
getOrCreateClientData(node.data())->queuePacket(message, node);
}
void AudioMixer::queueReplicatedAudioPacket(QSharedPointer<ReceivedMessage> message) {
// make sure we have a replicated node for the original sender of the packet
auto nodeList = DependencyManager::get<NodeList>();
QUuid nodeID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
auto replicatedNode = nodeList->addOrUpdateNode(nodeID, NodeType::Agent,
message->getSenderSockAddr(), message->getSenderSockAddr(),
true, true);
replicatedNode->setLastHeardMicrostamp(usecTimestampNow());
// construct a "fake" audio received message from the byte array and packet list information
auto audioData = message->getMessage().mid(NUM_BYTES_RFC4122_UUID);
PacketType rewrittenType = REPLICATED_PACKET_MAPPING.key(message->getType());
if (rewrittenType == PacketType::Unknown) {
qDebug() << "Cannot unwrap replicated packet type not present in REPLICATED_PACKET_WRAPPING";
}
auto replicatedMessage = QSharedPointer<ReceivedMessage>::create(audioData, rewrittenType,
versionForPacketType(rewrittenType),
message->getSenderSockAddr(), nodeID);
getOrCreateClientData(replicatedNode.data())->queuePacket(replicatedMessage, replicatedNode);
}
void AudioMixer::handleMuteEnvironmentPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
auto nodeList = DependencyManager::get<NodeList>();
@ -129,19 +166,6 @@ void AudioMixer::handleMuteEnvironmentPacket(QSharedPointer<ReceivedMessage> mes
}
}
DisplayPluginList getDisplayPlugins() {
DisplayPluginList result;
return result;
}
InputPluginList getInputPlugins() {
InputPluginList result;
return result;
}
// must be here to satisfy a reference in PluginManager::saveSettings()
void saveInputPluginSettings(const InputPluginList& plugins) {}
const std::pair<QString, CodecPluginPointer> AudioMixer::negotiateCodec(std::vector<QString> codecs) {
QString selectedCodecName;
CodecPluginPointer selectedCodec;
@ -347,7 +371,10 @@ void AudioMixer::start() {
auto nodeList = DependencyManager::get<NodeList>();
// prepare the NodeList
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
nodeList->addSetOfNodeTypesToNodeInterestSet({
NodeType::Agent, NodeType::EntityScriptServer,
NodeType::UpstreamAudioMixer, NodeType::DownstreamAudioMixer
});
nodeList->linkedDataCreateCallback = [&](Node* node) { getOrCreateClientData(node); };
// parse out any AudioMixer settings
@ -506,7 +533,7 @@ void AudioMixer::clearDomainSettings() {
_zoneReverbSettings.clear();
}
void AudioMixer::parseSettingsObject(const QJsonObject &settingsObject) {
void AudioMixer::parseSettingsObject(const QJsonObject& settingsObject) {
qDebug() << "AVX2 Support:" << (cpuSupportsAVX2() ? "enabled" : "disabled");
if (settingsObject.contains(AUDIO_THREADING_GROUP_KEY)) {

View file

@ -51,6 +51,11 @@ public:
static const QVector<ReverbSettings>& getReverbSettings() { return _zoneReverbSettings; }
static const std::pair<QString, CodecPluginPointer> negotiateCodec(std::vector<QString> codecs);
static bool shouldReplicateTo(const Node& from, const Node& to) {
return to.getType() == NodeType::DownstreamAudioMixer &&
to.getPublicSocket() != from.getPublicSocket() &&
to.getLocalSocket() != from.getLocalSocket();
}
public slots:
void run() override;
void sendStatsPacket() override;
@ -63,6 +68,7 @@ private slots:
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void queueAudioPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void queueReplicatedAudioPacket(QSharedPointer<ReceivedMessage> packet);
void removeHRTFsForFinishedInjector(const QUuid& streamID);
void start();

View file

@ -67,10 +67,23 @@ void AudioMixerClientData::processPackets() {
case PacketType::MicrophoneAudioNoEcho:
case PacketType::MicrophoneAudioWithEcho:
case PacketType::InjectAudio:
case PacketType::AudioStreamStats:
case PacketType::SilentAudioFrame: {
if (node->isUpstream()) {
setupCodecForReplicatedAgent(packet);
}
QMutexLocker lock(&getMutex());
parseData(*packet);
optionallyReplicatePacket(*packet, *node);
break;
}
case PacketType::AudioStreamStats: {
QMutexLocker lock(&getMutex());
parseData(*packet);
break;
}
case PacketType::NegotiateAudioFormat:
@ -97,6 +110,59 @@ void AudioMixerClientData::processPackets() {
assert(_packetQueue.empty());
}
bool isReplicatedPacket(PacketType packetType) {
return packetType == PacketType::ReplicatedMicrophoneAudioNoEcho
|| packetType == PacketType::ReplicatedMicrophoneAudioWithEcho
|| packetType == PacketType::ReplicatedInjectAudio
|| packetType == PacketType::ReplicatedSilentAudioFrame;
}
void AudioMixerClientData::optionallyReplicatePacket(ReceivedMessage& message, const Node& node) {
// first, make sure that this is a packet from a node we are supposed to replicate
if (node.isReplicated()) {
// now make sure it's a packet type that we want to replicate
// first check if it is an original type that we should replicate
PacketType mirroredType = REPLICATED_PACKET_MAPPING.value(message.getType());
if (mirroredType == PacketType::Unknown) {
// if it wasn't check if it is a replicated type that we should re-replicate
if (REPLICATED_PACKET_MAPPING.key(message.getType()) != PacketType::Unknown) {
mirroredType = message.getType();
} else {
qDebug() << "Packet passed to optionallyReplicatePacket was not a replicatable type - returning";
return;
}
}
std::unique_ptr<NLPacket> packet;
auto nodeList = DependencyManager::get<NodeList>();
// enumerate the downstream audio mixers and send them the replicated version of this packet
nodeList->unsafeEachNode([&](const SharedNodePointer& downstreamNode) {
if (AudioMixer::shouldReplicateTo(node, *downstreamNode)) {
// construct the packet only once, if we have any downstream audio mixers to send to
if (!packet) {
// construct an NLPacket to send to the replicant that has the contents of the received packet
packet = NLPacket::create(mirroredType);
if (!isReplicatedPacket(message.getType())) {
// since this packet will be non-sourced, we add the replicated node's ID here
packet->write(node.getUUID().toRfc4122());
}
packet->write(message.getMessage());
}
nodeList->sendUnreliablePacket(*packet, *downstreamNode);
}
});
}
}
void AudioMixerClientData::negotiateAudioFormat(ReceivedMessage& message, const SharedNodePointer& node) {
quint8 numberOfCodecs;
message.readPrimitive(&numberOfCodecs);
@ -188,8 +254,11 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) {
bool isMicStream = false;
if (packetType == PacketType::MicrophoneAudioWithEcho
|| packetType == PacketType::ReplicatedMicrophoneAudioWithEcho
|| packetType == PacketType::MicrophoneAudioNoEcho
|| packetType == PacketType::SilentAudioFrame) {
|| packetType == PacketType::ReplicatedMicrophoneAudioNoEcho
|| packetType == PacketType::SilentAudioFrame
|| packetType == PacketType::ReplicatedSilentAudioFrame) {
QWriteLocker writeLocker { &_streamsLock };
@ -209,7 +278,8 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) {
avatarAudioStream->setupCodec(_codec, _selectedCodecName, AudioConstants::MONO);
qDebug() << "creating new AvatarAudioStream... codec:" << _selectedCodecName;
connect(avatarAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioMixerClientData::handleMismatchAudioFormat);
connect(avatarAudioStream, &InboundAudioStream::mismatchedAudioCodec,
this, &AudioMixerClientData::handleMismatchAudioFormat);
auto emplaced = _audioStreams.emplace(
QUuid(),
@ -224,10 +294,12 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) {
writeLocker.unlock();
isMicStream = true;
} else if (packetType == PacketType::InjectAudio) {
} else if (packetType == PacketType::InjectAudio
|| packetType == PacketType::ReplicatedInjectAudio) {
// this is injected audio
// grab the stream identifier for this injected audio
message.seek(sizeof(quint16));
QUuid streamIdentifier = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID));
bool isStereo;
@ -608,3 +680,22 @@ bool AudioMixerClientData::shouldIgnore(const SharedNodePointer self, const Shar
return shouldIgnore;
}
void AudioMixerClientData::setupCodecForReplicatedAgent(QSharedPointer<ReceivedMessage> message) {
// hop past the sequence number that leads the packet
message->seek(sizeof(quint16));
// pull the codec string from the packet
auto codecString = message->readString();
if (codecString != _selectedCodecName) {
qDebug() << "Manually setting codec for replicated agent" << uuidStringWithoutCurlyBraces(getNodeID())
<< "-" << codecString;
const std::pair<QString, CodecPluginPointer> codec = AudioMixer::negotiateCodec({ codecString });
setupCodec(codec.second, codec.first);
// seek back to the beginning of the message so other readers are in the right place
message->seek(0);
}
}

View file

@ -26,7 +26,6 @@
#include "PositionalAudioStream.h"
#include "AvatarAudioStream.h"
class AudioMixerClientData : public NodeData {
Q_OBJECT
public:
@ -108,6 +107,8 @@ public:
bool getRequestsDomainListData() { return _requestsDomainListData; }
void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; }
void setupCodecForReplicatedAgent(QSharedPointer<ReceivedMessage> message);
signals:
void injectorStreamFinished(const QUuid& streamIdentifier);
@ -124,6 +125,8 @@ private:
QReadWriteLock _streamsLock;
AudioStreamMap _audioStreams; // microphone stream from avatar is stored under key of null UUID
void optionallyReplicatePacket(ReceivedMessage& packet, const Node& node);
using IgnoreZone = AABox;
class IgnoreZoneMemo {
public:

View file

@ -74,6 +74,10 @@ void AudioMixerSlave::mix(const SharedNodePointer& node) {
return;
}
if (node->isUpstream()) {
return;
}
// check that the stream is valid
auto avatarStream = data->getAvatarAudioStream();
if (avatarStream == nullptr) {
@ -493,13 +497,14 @@ float computeGain(const AvatarAudioStream& listeningNodeStream, const Positional
// avatar: apply fixed off-axis attenuation to make them quieter as they turn away
} else if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) {
glm::vec3 rotatedListenerPosition = glm::inverse(streamToAdd.getOrientation()) * relativePosition;
float angleOfDelivery = glm::angle(glm::vec3(0.0f, 0.0f, -1.0f),
glm::normalize(rotatedListenerPosition));
// source directivity is based on angle of emission, in local coordinates
glm::vec3 direction = glm::normalize(rotatedListenerPosition);
float angleOfDelivery = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward"
const float MAX_OFF_AXIS_ATTENUATION = 0.2f;
const float OFF_AXIS_ATTENUATION_STEP = (1 - MAX_OFF_AXIS_ATTENUATION) / 2.0f;
float offAxisCoefficient = MAX_OFF_AXIS_ATTENUATION +
(angleOfDelivery * (OFF_AXIS_ATTENUATION_STEP / PI_OVER_TWO));
float offAxisCoefficient = MAX_OFF_AXIS_ATTENUATION + (angleOfDelivery * (OFF_AXIS_ATTENUATION_STEP / PI_OVER_TWO));
gain *= offAxisCoefficient;
}
@ -541,7 +546,6 @@ float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const Positio
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
@ -549,11 +553,16 @@ float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const Positio
const float SOURCE_DISTANCE_THRESHOLD = 1e-30f;
if (glm::length2(rotatedSourcePosition) > SOURCE_DISTANCE_THRESHOLD) {
float rotatedSourcePositionLength2 = glm::length2(rotatedSourcePosition);
if (rotatedSourcePositionLength2 > 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;
glm::vec3 direction = rotatedSourcePosition * (1.0f / fastSqrtf(rotatedSourcePositionLength2));
float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward"
return (direction.x < 0.0f) ? -angle : angle;
} else {
// no azimuth if they are in same spot
return 0.0f;
}
}

View file

@ -76,7 +76,7 @@ void AudioMixerSlavePool::processPackets(ConstIter begin, ConstIter end) {
void AudioMixerSlavePool::mix(ConstIter begin, ConstIter end, unsigned int frame, float throttlingRatio) {
_function = &AudioMixerSlave::mix;
_configure = [&](AudioMixerSlave& slave) {
_configure = [=](AudioMixerSlave& slave) {
slave.configureMix(_begin, _end, _frame, _throttlingRatio);
};
_frame = frame;

View file

@ -54,8 +54,122 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
packetReceiver.registerListenerForTypes({
PacketType::ReplicatedAvatarIdentity,
PacketType::ReplicatedKillAvatar
}, this, "handleReplicatedPacket");
packetReceiver.registerListener(PacketType::ReplicatedBulkAvatarData, this, "handleReplicatedBulkAvatarPacket");
auto nodeList = DependencyManager::get<NodeList>();
connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch);
connect(nodeList.data(), &NodeList::nodeAdded, this, [this](const SharedNodePointer& node) {
if (node->getType() == NodeType::DownstreamAvatarMixer) {
getOrCreateClientData(node);
}
});
}
SharedNodePointer addOrUpdateReplicatedNode(const QUuid& nodeID, const HifiSockAddr& senderSockAddr) {
auto replicatedNode = DependencyManager::get<NodeList>()->addOrUpdateNode(nodeID, NodeType::Agent,
senderSockAddr,
senderSockAddr,
true, true);
replicatedNode->setLastHeardMicrostamp(usecTimestampNow());
return replicatedNode;
}
void AvatarMixer::handleReplicatedPacket(QSharedPointer<ReceivedMessage> message) {
auto nodeList = DependencyManager::get<NodeList>();
auto nodeID = QUuid::fromRfc4122(message->peek(NUM_BYTES_RFC4122_UUID));
SharedNodePointer replicatedNode;
if (message->getType() == PacketType::ReplicatedKillAvatar) {
// this is a kill packet, which we should only process if we already have the node in our list
// since it of course does not make sense to add a node just to remove it an instant later
replicatedNode = nodeList->nodeWithUUID(nodeID);
if (!replicatedNode) {
return;
}
} else {
replicatedNode = addOrUpdateReplicatedNode(nodeID, message->getSenderSockAddr());
}
// we better have a node to work with at this point
assert(replicatedNode);
if (message->getType() == PacketType::ReplicatedAvatarIdentity) {
handleAvatarIdentityPacket(message, replicatedNode);
} else if (message->getType() == PacketType::ReplicatedKillAvatar) {
handleKillAvatarPacket(message, replicatedNode);
}
}
void AvatarMixer::handleReplicatedBulkAvatarPacket(QSharedPointer<ReceivedMessage> message) {
while (message->getBytesLeftToRead()) {
// first, grab the node ID for this replicated avatar
auto nodeID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
// make sure we have an upstream replicated node that matches
auto replicatedNode = addOrUpdateReplicatedNode(nodeID, message->getSenderSockAddr());
// grab the size of the avatar byte array so we know how much to read
quint16 avatarByteArraySize;
message->readPrimitive(&avatarByteArraySize);
// read the avatar byte array
auto avatarByteArray = message->read(avatarByteArraySize);
// construct a "fake" avatar data received message from the byte array and packet list information
auto replicatedMessage = QSharedPointer<ReceivedMessage>::create(avatarByteArray, PacketType::AvatarData,
versionForPacketType(PacketType::AvatarData),
message->getSenderSockAddr(), nodeID);
// queue up the replicated avatar data with the client data for the replicated node
auto start = usecTimestampNow();
getOrCreateClientData(replicatedNode)->queuePacket(replicatedMessage, replicatedNode);
auto end = usecTimestampNow();
_queueIncomingPacketElapsedTime += (end - start);
}
}
void AvatarMixer::optionallyReplicatePacket(ReceivedMessage& message, const Node& node) {
// first, make sure that this is a packet from a node we are supposed to replicate
if (node.isReplicated()) {
// check if this is a packet type we replicate
// which means it must be a packet type present in REPLICATED_PACKET_MAPPING or must be the
// replicated version of one of those packet types
PacketType replicatedType = REPLICATED_PACKET_MAPPING.value(message.getType());
if (replicatedType == PacketType::Unknown) {
if (REPLICATED_PACKET_MAPPING.key(message.getType()) != PacketType::Unknown) {
replicatedType = message.getType();
} else {
qDebug() << __FUNCTION__ << "called without replicatable packet type - returning";
return;
}
}
std::unique_ptr<NLPacket> packet;
auto nodeList = DependencyManager::get<NodeList>();
nodeList->eachMatchingNode([&](const SharedNodePointer& downstreamNode) {
return shouldReplicateTo(node, *downstreamNode);
}, [&](const SharedNodePointer& node) {
if (!packet) {
// construct an NLPacket to send to the replicant that has the contents of the received packet
packet = NLPacket::create(replicatedType, message.getSize());
packet->write(message.getMessage());
}
nodeList->sendUnreliablePacket(*packet, *node);
});
}
}
void AvatarMixer::queueIncomingPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node) {
@ -65,17 +179,15 @@ void AvatarMixer::queueIncomingPacket(QSharedPointer<ReceivedMessage> message, S
_queueIncomingPacketElapsedTime += (end - start);
}
AvatarMixer::~AvatarMixer() {
}
void AvatarMixer::sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode) {
QByteArray individualData = nodeData->getAvatar().identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122());
auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
identityPackets->write(individualData);
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPackets), *destinationNode);
++_sumIdentityPackets;
if (destinationNode->getType() == NodeType::Agent && !destinationNode->isUpstream()) {
QByteArray individualData = nodeData->getAvatar().identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122());
auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
identityPackets->write(individualData);
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPackets), *destinationNode);
++_sumIdentityPackets;
}
}
std::chrono::microseconds AvatarMixer::timeFrame(p_high_resolution_clock::time_point& timestamp) {
@ -132,7 +244,10 @@ void AvatarMixer::start() {
auto start = usecTimestampNow();
nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) {
std::for_each(cbegin, cend, [&](const SharedNodePointer& node) {
manageDisplayName(node);
if (node->getType() == NodeType::Agent) {
manageIdentityData(node);
}
++_sumListeners;
});
}, &lockWait, &nodeTransform, &functor);
@ -183,8 +298,16 @@ void AvatarMixer::start() {
// NOTE: nodeData->getAvatar() might be side effected, must be called when access to node/nodeData
// is guaranteed to not be accessed by other thread
void AvatarMixer::manageDisplayName(const SharedNodePointer& node) {
void AvatarMixer::manageIdentityData(const SharedNodePointer& node) {
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
// there is no need to manage identity data we haven't received yet
// so bail early if we've never received an identity packet for this avatar
if (!nodeData || !nodeData->getAvatar().hasProcessedFirstIdentity()) {
return;
}
bool sendIdentity = false;
if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) {
AvatarData& avatar = nodeData->getAvatar();
const QString& existingBaseDisplayName = nodeData->getBaseDisplayName();
@ -210,9 +333,45 @@ void AvatarMixer::manageDisplayName(const SharedNodePointer& node) {
soFar.second++; // refcount
nodeData->flagIdentityChange();
nodeData->setAvatarSessionDisplayNameMustChange(false);
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name.
sendIdentity = true;
qCDebug(avatars) << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID();
}
if (nodeData && nodeData->getAvatarSkeletonModelUrlMustChange()) { // never true for an empty _avatarWhitelist
nodeData->setAvatarSkeletonModelUrlMustChange(false);
AvatarData& avatar = nodeData->getAvatar();
static const QUrl emptyURL("");
QUrl url = avatar.cannonicalSkeletonModelURL(emptyURL);
if (!isAvatarInWhitelist(url)) {
qCDebug(avatars) << "Forbidden avatar" << nodeData->getNodeID() << avatar.getSkeletonModelURL() << "replaced with" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar);
avatar.setSkeletonModelURL(_replacementAvatar);
sendIdentity = true;
}
}
if (sendIdentity && !node->isUpstream()) {
sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name or avatar.
// since this packet includes a change to either the skeleton model URL or the display name
// it needs a new sequence number
nodeData->getAvatar().pushIdentitySequenceNumber();
// tell node whose name changed about its new session display name or avatar.
sendIdentityPacket(nodeData, node);
}
}
bool AvatarMixer::isAvatarInWhitelist(const QUrl& url) {
// The avatar is in the whitelist if:
// 1. The avatar's URL's host matches one of the hosts of the URLs in the whitelist AND
// 2. The avatar's URL's path starts with the path of that same URL in the whitelist
for (const auto& whiteListedPrefix : _avatarWhitelist) {
auto whiteListURL = QUrl::fromUserInput(whiteListedPrefix);
// check if this script URL matches the whitelist domain and, optionally, is beneath the path
if (url.host().compare(whiteListURL.host(), Qt::CaseInsensitive) == 0 &&
url.path().startsWith(whiteListURL.path(), Qt::CaseInsensitive)) {
return true;
}
}
return false;
}
void AvatarMixer::throttle(std::chrono::microseconds duration, int frame) {
@ -279,13 +438,38 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) {
}
}
std::unique_ptr<NLPacket> killPacket;
std::unique_ptr<NLPacket> replicatedKillPacket;
// 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 + sizeof(KillAvatarReason));
killPacket->write(killedNode->getUUID().toRfc4122());
killPacket->writePrimitive(KillAvatarReason::AvatarDisconnected);
nodeList->eachMatchingNode([&](const SharedNodePointer& node) {
// we relay avatar kill packets to agents that are not upstream
// and downstream avatar mixers, if the node that was just killed was being replicated
return (node->getType() == NodeType::Agent && !node->isUpstream()) ||
(killedNode->isReplicated() && shouldReplicateTo(*killedNode, *node));
}, [&](const SharedNodePointer& node) {
if (node->getType() == NodeType::Agent) {
if (!killPacket) {
killPacket = NLPacket::create(PacketType::KillAvatar, NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
killPacket->write(killedNode->getUUID().toRfc4122());
killPacket->writePrimitive(KillAvatarReason::AvatarDisconnected);
}
nodeList->sendUnreliablePacket(*killPacket, *node);
} else {
// send a replicated kill packet to the downstream avatar mixer
if (!replicatedKillPacket) {
replicatedKillPacket = NLPacket::create(PacketType::ReplicatedKillAvatar,
NUM_BYTES_RFC4122_UUID + sizeof(KillAvatarReason));
replicatedKillPacket->write(killedNode->getUUID().toRfc4122());
replicatedKillPacket->writePrimitive(KillAvatarReason::AvatarDisconnected);
}
nodeList->sendUnreliablePacket(*replicatedKillPacket, *node);
}
});
nodeList->broadcastToNodes(std::move(killPacket), NodeSet() << NodeType::Agent);
// we also want to remove sequence number data for this avatar on our other avatars
// so invoke the appropriate method on the AvatarMixerClientData for other avatars
@ -398,17 +582,20 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
AvatarData& avatar = nodeData->getAvatar();
// parse the identity packet and update the change timestamp if appropriate
AvatarData::Identity identity;
AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity);
bool identityChanged = false;
bool displayNameChanged = false;
avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged);
bool skeletonModelUrlChanged = false;
avatar.processAvatarIdentity(message->getMessage(), identityChanged, displayNameChanged, skeletonModelUrlChanged);
if (identityChanged) {
QMutexLocker nodeDataLocker(&nodeData->getMutex());
nodeData->flagIdentityChange();
if (displayNameChanged) {
nodeData->setAvatarSessionDisplayNameMustChange(true);
}
if (skeletonModelUrlChanged && !_avatarWhitelist.isEmpty()) {
nodeData->setAvatarSkeletonModelUrlMustChange(true);
}
}
}
}
@ -416,11 +603,13 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
_handleAvatarIdentityPacketElapsedTime += (end - start);
}
void AvatarMixer::handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message) {
void AvatarMixer::handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer node) {
auto start = usecTimestampNow();
DependencyManager::get<NodeList>()->processKillNode(*message);
auto end = usecTimestampNow();
_handleKillAvatarPacketElapsedTime += (end - start);
optionallyReplicatePacket(*message, *node);
}
void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
@ -672,7 +861,6 @@ void AvatarMixer::run() {
connect(&domainHandler, &DomainHandler::settingsReceiveFail, this, &AvatarMixer::domainSettingsRequestFailed);
ThreadedAssignment::commonInit(AVATAR_MIXER_LOGGING_NAME, NodeType::AvatarMixer);
}
AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node) {
@ -691,7 +879,10 @@ AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node
void AvatarMixer::domainSettingsRequestComplete() {
auto nodeList = DependencyManager::get<NodeList>();
nodeList->addSetOfNodeTypesToNodeInterestSet({ NodeType::Agent, NodeType::EntityScriptServer });
nodeList->addSetOfNodeTypesToNodeInterestSet({
NodeType::Agent, NodeType::EntityScriptServer,
NodeType::UpstreamAvatarMixer, NodeType::DownstreamAvatarMixer
});
// parse the settings to pull out the values we need
parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject());
@ -764,4 +955,20 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) {
qCDebug(avatars) << "This domain requires a minimum avatar scale of" << _domainMinimumScale
<< "and a maximum avatar scale of" << _domainMaximumScale;
const QString AVATAR_WHITELIST_DEFAULT{ "" };
static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist";
_avatarWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION].toString(AVATAR_WHITELIST_DEFAULT).split(',', QString::KeepEmptyParts);
static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar";
_replacementAvatar = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION].toString(REPLACEMENT_AVATAR_DEFAULT);
if ((_avatarWhitelist.count() == 1) && _avatarWhitelist[0].isEmpty()) {
_avatarWhitelist.clear(); // KeepEmptyParts above will parse "," as ["", ""] (which is ok), but "" as [""] (which is not ok).
}
if (_avatarWhitelist.isEmpty()) {
qCDebug(avatars) << "All avatars are allowed.";
} else {
qCDebug(avatars) << "Avatars other than" << _avatarWhitelist << "will be replaced by" << (_replacementAvatar.isEmpty() ? "default" : _replacementAvatar);
}
}

View file

@ -28,7 +28,13 @@ class AvatarMixer : public ThreadedAssignment {
Q_OBJECT
public:
AvatarMixer(ReceivedMessage& message);
~AvatarMixer();
static bool shouldReplicateTo(const Node& from, const Node& to) {
return to.getType() == NodeType::DownstreamAvatarMixer &&
to.getPublicSocket() != from.getPublicSocket() &&
to.getLocalSocket() != from.getLocalSocket();
}
public slots:
/// runs the avatar mixer
void run() override;
@ -42,10 +48,12 @@ private slots:
void handleAdjustAvatarSorting(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleViewFrustumPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleRequestsDomainListDataPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode);
void handleReplicatedPacket(QSharedPointer<ReceivedMessage> message);
void handleReplicatedBulkAvatarPacket(QSharedPointer<ReceivedMessage> message);
void domainSettingsRequestComplete();
void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID);
void start();
@ -59,7 +67,14 @@ private:
void parseDomainServerSettings(const QJsonObject& domainSettings);
void sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
void manageDisplayName(const SharedNodePointer& node);
void manageIdentityData(const SharedNodePointer& node);
bool isAvatarInWhitelist(const QUrl& url);
const QString REPLACEMENT_AVATAR_DEFAULT{ "" };
QStringList _avatarWhitelist { };
QString _replacementAvatar { REPLACEMENT_AVATAR_DEFAULT };
void optionallyReplicatePacket(ReceivedMessage& message, const Node& node);
p_high_resolution_clock::time_point _lastFrameTimestamp;

View file

@ -108,9 +108,6 @@ void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointe
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);
}
}

View file

@ -65,6 +65,8 @@ public:
void flagIdentityChange() { _identityChangeTimestamp = usecTimestampNow(); }
bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; }
void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; }
bool getAvatarSkeletonModelUrlMustChange() const { return _avatarSkeletonModelUrlMustChange; }
void setAvatarSkeletonModelUrlMustChange(bool set = true) { _avatarSkeletonModelUrlMustChange = set; }
void resetNumAvatarsSentLastFrame() { _numAvatarsSentLastFrame = 0; }
void incrementNumAvatarsSentLastFrame() { ++_numAvatarsSentLastFrame; }
@ -146,6 +148,7 @@ private:
uint64_t _identityChangeTimestamp;
bool _avatarSessionDisplayNameMustChange{ true };
bool _avatarSkeletonModelUrlMustChange{ false };
int _numAvatarsSentLastFrame = 0;
int _numFramesSinceAdjustment = 0;

View file

@ -65,15 +65,32 @@ void AvatarMixerSlave::processIncomingPackets(const SharedNodePointer& node) {
_stats.processIncomingPacketsElapsedTime += (end - start);
}
int AvatarMixerSlave::sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode) {
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
identityPackets->write(individualData);
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPackets), *destinationNode);
_stats.numIdentityPackets++;
return individualData.size();
if (destinationNode->getType() == NodeType::Agent && !destinationNode->isUpstream()) {
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
auto identityPackets = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true);
identityPackets->write(individualData);
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPackets), *destinationNode);
_stats.numIdentityPackets++;
return individualData.size();
} else {
return 0;
}
}
int AvatarMixerSlave::sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode) {
if (AvatarMixer::shouldReplicateTo(agentNode, destinationNode)) {
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray(true);
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
auto identityPacket = NLPacketList::create(PacketType::ReplicatedAvatarIdentity, QByteArray(), true, true);
identityPacket->write(individualData);
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPacket), destinationNode);
_stats.numIdentityPackets++;
return individualData.size();
} else {
return 0;
}
}
static const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45;
@ -81,6 +98,18 @@ static const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45;
void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
quint64 start = usecTimestampNow();
if (node->getType() == NodeType::Agent && node->getLinkedData() && node->getActiveSocket() && !node->isUpstream()) {
broadcastAvatarDataToAgent(node);
} else if (node->getType() == NodeType::DownstreamAvatarMixer) {
broadcastAvatarDataToDownstreamMixer(node);
}
quint64 end = usecTimestampNow();
_stats.jobElapsedTime += (end - start);
}
void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) {
auto nodeList = DependencyManager::get<NodeList>();
// setup for distributed random floating point values
@ -88,308 +117,437 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
std::mt19937 generator(randomDevice());
std::uniform_real_distribution<float> distribution;
if (node->getLinkedData() && (node->getType() == NodeType::Agent) && node->getActiveSocket()) {
_stats.nodesBroadcastedTo++;
_stats.nodesBroadcastedTo++;
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
nodeData->resetInViewStats();
nodeData->resetInViewStats();
const AvatarData& avatar = nodeData->getAvatar();
glm::vec3 myPosition = avatar.getClientGlobalPosition();
const AvatarData& avatar = nodeData->getAvatar();
glm::vec3 myPosition = avatar.getClientGlobalPosition();
// reset the internal state for correct random number distribution
distribution.reset();
// reset the internal state for correct random number distribution
distribution.reset();
// reset the number of sent avatars
nodeData->resetNumAvatarsSentLastFrame();
// reset the number of sent avatars
nodeData->resetNumAvatarsSentLastFrame();
// keep a counter of the number of considered avatars
int numOtherAvatars = 0;
// keep a counter of the number of considered avatars
int numOtherAvatars = 0;
// keep track of outbound data rate specifically for avatar data
int numAvatarDataBytes = 0;
int identityBytesSent = 0;
// keep track of outbound data rate specifically for avatar data
int numAvatarDataBytes = 0;
int identityBytesSent = 0;
// max number of avatarBytes per frame
auto maxAvatarBytesPerFrame = (_maxKbpsPerNode * BYTES_PER_KILOBIT) / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND;
// max number of avatarBytes per frame
auto maxAvatarBytesPerFrame = (_maxKbpsPerNode * BYTES_PER_KILOBIT) / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND;
// FIXME - find a way to not send the sessionID for every avatar
int minimumBytesPerAvatar = AvatarDataPacket::AVATAR_HAS_FLAGS_SIZE + NUM_BYTES_RFC4122_UUID;
// FIXME - find a way to not send the sessionID for every avatar
int minimumBytesPerAvatar = AvatarDataPacket::AVATAR_HAS_FLAGS_SIZE + NUM_BYTES_RFC4122_UUID;
int overBudgetAvatars = 0;
int overBudgetAvatars = 0;
// keep track of the number of other avatars held back in this frame
int numAvatarsHeldBack = 0;
// keep track of the number of other avatars held back in this frame
int numAvatarsHeldBack = 0;
// keep track of the number of other avatar frames skipped
int numAvatarsWithSkippedFrames = 0;
// keep track of the number of other avatar frames skipped
int numAvatarsWithSkippedFrames = 0;
// When this is true, the AvatarMixer will send Avatar data to a client
// about avatars they've ignored or that are out of view
bool PALIsOpen = nodeData->getRequestsDomainListData();
// When this is true, the AvatarMixer will send Avatar data to a client
// about avatars they've ignored or that are out of view
bool PALIsOpen = nodeData->getRequestsDomainListData();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them
bool getsAnyIgnored = PALIsOpen && node->getCanKick();
// When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them
bool getsAnyIgnored = PALIsOpen && node->getCanKick();
if (PALIsOpen) {
// Increase minimumBytesPerAvatar if the PAL is open
minimumBytesPerAvatar += sizeof(AvatarDataPacket::AvatarGlobalPosition) +
sizeof(AvatarDataPacket::AudioLoudness);
}
if (PALIsOpen) {
// Increase minimumBytesPerAvatar if the PAL is open
minimumBytesPerAvatar += sizeof(AvatarDataPacket::AvatarGlobalPosition) +
sizeof(AvatarDataPacket::AudioLoudness);
}
// setup a PacketList for the avatarPackets
auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData);
// setup a PacketList for the avatarPackets
auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData);
// 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;
// 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);
}
// Quadruple the scale of both bounding boxes
nodeBox.embiggen(4.0f);
// 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;
// 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);
}
// Quadruple the scale of both bounding boxes
nodeBox.embiggen(4.0f);
// setup list of AvatarData as well as maps to map betweeen the AvatarData and the original nodes
// for calling the AvatarData::sortAvatars() function and getting our sorted list of client nodes
QList<AvatarSharedPointer> avatarList;
std::unordered_map<AvatarSharedPointer, SharedNodePointer> avatarDataToNodes;
// setup list of AvatarData as well as maps to map betweeen the AvatarData and the original nodes
// for calling the AvatarData::sortAvatars() function and getting our sorted list of client nodes
QList<AvatarSharedPointer> avatarList;
std::unordered_map<AvatarSharedPointer, SharedNodePointer> avatarDataToNodes;
std::for_each(_begin, _end, [&](const SharedNodePointer& otherNode) {
std::for_each(_begin, _end, [&](const SharedNodePointer& otherNode) {
// make sure this is an agent that we have avatar data for before considering it for inclusion
if (otherNode->getType() == NodeType::Agent
&& otherNode->getLinkedData()) {
const AvatarMixerClientData* otherNodeData = reinterpret_cast<const AvatarMixerClientData*>(otherNode->getLinkedData());
// theoretically it's possible for a Node to be in the NodeList (and therefore end up here),
// but not have yet sent data that's linked to the node. Check for that case and don't
// consider those nodes.
if (otherNodeData) {
AvatarSharedPointer otherAvatar = otherNodeData->getAvatarSharedPointer();
avatarList << otherAvatar;
avatarDataToNodes[otherAvatar] = otherNode;
}
});
AvatarSharedPointer otherAvatar = otherNodeData->getAvatarSharedPointer();
avatarList << otherAvatar;
avatarDataToNodes[otherAvatar] = otherNode;
}
});
AvatarSharedPointer thisAvatar = nodeData->getAvatarSharedPointer();
ViewFrustum cameraView = nodeData->getViewFrustom();
std::priority_queue<AvatarPriority> sortedAvatars;
AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars,
AvatarSharedPointer thisAvatar = nodeData->getAvatarSharedPointer();
ViewFrustum cameraView = nodeData->getViewFrustom();
std::priority_queue<AvatarPriority> sortedAvatars;
AvatarData::sortAvatars(avatarList, cameraView, sortedAvatars,
[&](AvatarSharedPointer avatar)->uint64_t {
auto avatarNode = avatarDataToNodes[avatar];
assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map
return nodeData->getLastBroadcastTime(avatarNode->getUUID());
}, [&](AvatarSharedPointer avatar)->float{
glm::vec3 nodeBoxHalfScale = (avatar->getPosition() - avatar->getGlobalBoundingBoxCorner());
return glm::max(nodeBoxHalfScale.x, glm::max(nodeBoxHalfScale.y, nodeBoxHalfScale.z));
}, [&](AvatarSharedPointer avatar)->bool {
if (avatar == thisAvatar) {
return true; // ignore ourselves...
}
[&](AvatarSharedPointer avatar)->uint64_t{
auto avatarNode = avatarDataToNodes[avatar];
assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map
return nodeData->getLastBroadcastTime(avatarNode->getUUID());
},
bool shouldIgnore = false;
[&](AvatarSharedPointer avatar)->float{
glm::vec3 nodeBoxHalfScale = (avatar->getPosition() - avatar->getGlobalBoundingBoxCorner());
return glm::max(nodeBoxHalfScale.x, glm::max(nodeBoxHalfScale.y, nodeBoxHalfScale.z));
},
// We will also ignore other nodes for a couple of different reasons:
// 1) ignore bubbles and ignore specific node
// 2) the node hasn't really updated it's frame data recently, this can
// happen if for example the avatar is connected on a desktop and sending
// updates at ~30hz. So every 3 frames we skip a frame.
auto avatarNode = avatarDataToNodes[avatar];
[&](AvatarSharedPointer avatar)->bool{
if (avatar == thisAvatar) {
return true; // ignore ourselves...
}
assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map
bool shouldIgnore = false;
const AvatarMixerClientData* avatarNodeData = reinterpret_cast<const AvatarMixerClientData*>(avatarNode->getLinkedData());
assert(avatarNodeData); // we can't have gotten here without avatarNode having valid data
quint64 startIgnoreCalculation = usecTimestampNow();
// We will also ignore other nodes for a couple of different reasons:
// 1) ignore bubbles and ignore specific node
// 2) the node hasn't really updated it's frame data recently, this can
// happen if for example the avatar is connected on a desktop and sending
// updates at ~30hz. So every 3 frames we skip a frame.
auto avatarNode = avatarDataToNodes[avatar];
// 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 (!avatarNode->getLinkedData()
|| avatarNode->getUUID() == node->getUUID()
|| (node->isIgnoringNodeWithID(avatarNode->getUUID()) && !PALIsOpen)
|| (avatarNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) {
shouldIgnore = true;
} else {
assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map
// Check to see if the space bubble is enabled
// Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored
if (node->isIgnoreRadiusEnabled() || (avatarNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) {
const AvatarMixerClientData* avatarNodeData = reinterpret_cast<const AvatarMixerClientData*>(avatarNode->getLinkedData());
assert(avatarNodeData); // we can't have gotten here without avatarNode having valid data
quint64 startIgnoreCalculation = usecTimestampNow();
// 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 (!avatarNode->getLinkedData()
|| avatarNode->getUUID() == node->getUUID()
|| (node->isIgnoringNodeWithID(avatarNode->getUUID()) && !PALIsOpen)
|| (avatarNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) {
shouldIgnore = true;
} else {
// Check to see if the space bubble is enabled
// Don't bother with these checks if the other avatar has their bubble enabled and we're gettingAnyIgnored
if (node->isIgnoreRadiusEnabled() || (avatarNode->isIgnoreRadiusEnabled() && !getsAnyIgnored)) {
// Define the scale of the box for the current other node
glm::vec3 otherNodeBoxScale = (avatarNodeData->getPosition() - avatarNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
// Set up the bounding box for the current other node
AABox otherNodeBox(avatarNodeData->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
otherNodeBox.embiggen(4.0f);
// Perform the collision check between the two bounding boxes
if (nodeBox.touches(otherNodeBox)) {
nodeData->ignoreOther(node, avatarNode);
shouldIgnore = !getsAnyIgnored;
}
}
// Not close enough to ignore
if (!shouldIgnore) {
nodeData->removeFromRadiusIgnoringSet(node, avatarNode->getUUID());
}
}
quint64 endIgnoreCalculation = usecTimestampNow();
_stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation);
if (!shouldIgnore) {
AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getUUID());
AvatarDataSequenceNumber lastSeqFromSender = avatarNodeData->getLastReceivedSequenceNumber();
// FIXME - This code does appear to be working. But it seems brittle.
// It supports determining if the frame of data for this "other"
// avatar has already been sent to the reciever. This has been
// verified to work on a desktop display that renders at 60hz and
// therefore sends to mixer at 30hz. Each second you'd expect to
// have 15 (45hz-30hz) duplicate frames. In this case, the stat
// avg_other_av_skips_per_second does report 15.
//
// make sure we haven't already sent this data from this sender to this receiver
// or that somehow we haven't sent
if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) {
++numAvatarsHeldBack;
shouldIgnore = true;
} else if (lastSeqFromSender - lastSeqToReceiver > 1) {
// this is a skip - we still send the packet but capture the presence of the skip so we see it happening
++numAvatarsWithSkippedFrames;
}
}
return shouldIgnore;
});
// loop through our sorted avatars and allocate our bandwidth to them accordingly
int avatarRank = 0;
// this is overly conservative, because it includes some avatars we might not consider
int remainingAvatars = (int)sortedAvatars.size();
while (!sortedAvatars.empty()) {
AvatarPriority sortData = sortedAvatars.top();
sortedAvatars.pop();
const auto& avatarData = sortData.avatar;
avatarRank++;
remainingAvatars--;
auto otherNode = avatarDataToNodes[avatarData];
assert(otherNode); // we can't have gotten here without the avatarData being a valid key in the map
// NOTE: Here's where we determine if we are over budget and drop to bare minimum data
int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars;
bool overBudget = (identityBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes) > maxAvatarBytesPerFrame;
quint64 startAvatarDataPacking = usecTimestampNow();
++numOtherAvatars;
const AvatarMixerClientData* otherNodeData = reinterpret_cast<const AvatarMixerClientData*>(otherNode->getLinkedData());
// If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO
// the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A.
if (nodeData->getLastBroadcastTime(otherNode->getUUID()) <= otherNodeData->getIdentityChangeTimestamp()) {
identityBytesSent += sendIdentityPacket(otherNodeData, node);
}
const AvatarData* otherAvatar = otherNodeData->getConstAvatarData();
glm::vec3 otherPosition = otherAvatar->getClientGlobalPosition();
// determine if avatar is in view, to determine how much data to include...
glm::vec3 otherNodeBoxScale = (otherPosition - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
bool isInView = nodeData->otherAvatarInView(otherNodeBox);
// start a new segment in the PacketList for this avatar
avatarPacketList->startSegment();
AvatarData::AvatarDataDetail detail;
if (overBudget) {
overBudgetAvatars++;
_stats.overBudgetAvatars++;
detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::NoData;
} else if (!isInView) {
detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData;
nodeData->incrementAvatarOutOfView();
} else {
detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO
? AvatarData::SendAllData : AvatarData::CullSmallData;
nodeData->incrementAvatarInView();
}
bool includeThisAvatar = true;
auto lastEncodeForOther = nodeData->getLastOtherAvatarEncodeTime(otherNode->getUUID());
QVector<JointData>& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID());
bool distanceAdjust = true;
glm::vec3 viewerPosition = myPosition;
AvatarDataPacket::HasFlags hasFlagsOut; // the result of the toByteArray
bool dropFaceTracking = false;
quint64 start = usecTimestampNow();
QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
quint64 end = usecTimestampNow();
_stats.toByteArrayElapsedTime += (end - start);
static const int MAX_ALLOWED_AVATAR_DATA = (1400 - NUM_BYTES_RFC4122_UUID);
if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
qCWarning(avatars) << "otherAvatar.toByteArray() resulted in very large buffer:" << bytes.size() << "... attempt to drop facial data";
dropFaceTracking = true; // first try dropping the facial data
bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
qCWarning(avatars) << "otherAvatar.toByteArray() without facial data resulted in very large buffer:" << bytes.size() << "... reduce to MinimumData";
bytes = otherAvatar->toByteArray(AvatarData::MinimumData, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
// Define the scale of the box for the current other node
glm::vec3 otherNodeBoxScale = (avatarNodeData->getPosition() - avatarNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
// Set up the bounding box for the current other node
AABox otherNodeBox(avatarNodeData->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
otherNodeBox.embiggen(4.0f);
// Perform the collision check between the two bounding boxes
if (nodeBox.touches(otherNodeBox)) {
nodeData->ignoreOther(node, avatarNode);
shouldIgnore = !getsAnyIgnored;
}
}
// Not close enough to ignore
if (!shouldIgnore) {
nodeData->removeFromRadiusIgnoringSet(node, avatarNode->getUUID());
}
}
quint64 endIgnoreCalculation = usecTimestampNow();
_stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation);
if (!shouldIgnore) {
AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getUUID());
AvatarDataSequenceNumber lastSeqFromSender = avatarNodeData->getLastReceivedSequenceNumber();
// FIXME - This code does appear to be working. But it seems brittle.
// It supports determining if the frame of data for this "other"
// avatar has already been sent to the reciever. This has been
// verified to work on a desktop display that renders at 60hz and
// therefore sends to mixer at 30hz. Each second you'd expect to
// have 15 (45hz-30hz) duplicate frames. In this case, the stat
// avg_other_av_skips_per_second does report 15.
//
// make sure we haven't already sent this data from this sender to this receiver
// or that somehow we haven't sent
if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) {
++numAvatarsHeldBack;
shouldIgnore = true;
} else if (lastSeqFromSender - lastSeqToReceiver > 1) {
// this is a skip - we still send the packet but capture the presence of the skip so we see it happening
++numAvatarsWithSkippedFrames;
}
}
return shouldIgnore;
});
// loop through our sorted avatars and allocate our bandwidth to them accordingly
int avatarRank = 0;
// this is overly conservative, because it includes some avatars we might not consider
int remainingAvatars = (int)sortedAvatars.size();
while (!sortedAvatars.empty()) {
AvatarPriority sortData = sortedAvatars.top();
sortedAvatars.pop();
const auto& avatarData = sortData.avatar;
avatarRank++;
remainingAvatars--;
auto otherNode = avatarDataToNodes[avatarData];
assert(otherNode); // we can't have gotten here without the avatarData being a valid key in the map
// NOTE: Here's where we determine if we are over budget and drop to bare minimum data
int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars;
bool overBudget = (identityBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes) > maxAvatarBytesPerFrame;
quint64 startAvatarDataPacking = usecTimestampNow();
++numOtherAvatars;
const AvatarMixerClientData* otherNodeData = reinterpret_cast<const AvatarMixerClientData*>(otherNode->getLinkedData());
const AvatarData* otherAvatar = otherNodeData->getConstAvatarData();
// If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO
// the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A.
if (otherAvatar->hasProcessedFirstIdentity()
&& nodeData->getLastBroadcastTime(otherNode->getUUID()) <= otherNodeData->getIdentityChangeTimestamp()) {
identityBytesSent += sendIdentityPacket(otherNodeData, node);
// remember the last time we sent identity details about this other node to the receiver
nodeData->setLastBroadcastTime(otherNode->getUUID(), usecTimestampNow());
}
glm::vec3 otherPosition = otherAvatar->getClientGlobalPosition();
// determine if avatar is in view, to determine how much data to include...
glm::vec3 otherNodeBoxScale = (otherPosition - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f;
AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale);
bool isInView = nodeData->otherAvatarInView(otherNodeBox);
// start a new segment in the PacketList for this avatar
avatarPacketList->startSegment();
AvatarData::AvatarDataDetail detail;
if (overBudget) {
overBudgetAvatars++;
_stats.overBudgetAvatars++;
detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::NoData;
} else if (!isInView) {
detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData;
nodeData->incrementAvatarOutOfView();
} else {
detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO
? AvatarData::SendAllData : AvatarData::CullSmallData;
nodeData->incrementAvatarInView();
}
bool includeThisAvatar = true;
auto lastEncodeForOther = nodeData->getLastOtherAvatarEncodeTime(otherNode->getUUID());
QVector<JointData>& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID());
bool distanceAdjust = true;
glm::vec3 viewerPosition = myPosition;
AvatarDataPacket::HasFlags hasFlagsOut; // the result of the toByteArray
bool dropFaceTracking = false;
quint64 start = usecTimestampNow();
QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
quint64 end = usecTimestampNow();
_stats.toByteArrayElapsedTime += (end - start);
static const int MAX_ALLOWED_AVATAR_DATA = (1400 - NUM_BYTES_RFC4122_UUID);
if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
qCWarning(avatars) << "otherAvatar.toByteArray() resulted in very large buffer:" << bytes.size() << "... attempt to drop facial data";
dropFaceTracking = true; // first try dropping the facial data
bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
qCWarning(avatars) << "otherAvatar.toByteArray() without facial data resulted in very large buffer:" << bytes.size() << "... reduce to MinimumData";
bytes = otherAvatar->toByteArray(AvatarData::MinimumData, lastEncodeForOther, lastSentJointsForOther,
hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther);
if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) {
qCWarning(avatars) << "otherAvatar.toByteArray() MinimumData resulted in very large buffer:" << bytes.size() << "... FAIL!!";
includeThisAvatar = false;
}
}
}
if (includeThisAvatar) {
numAvatarDataBytes += avatarPacketList->write(otherNode->getUUID().toRfc4122());
numAvatarDataBytes += avatarPacketList->write(bytes);
if (includeThisAvatar) {
numAvatarDataBytes += avatarPacketList->write(otherNode->getUUID().toRfc4122());
numAvatarDataBytes += avatarPacketList->write(bytes);
if (detail != AvatarData::NoData) {
_stats.numOthersIncluded++;
if (detail != AvatarData::NoData) {
_stats.numOthersIncluded++;
// increment the number of avatars sent to this reciever
nodeData->incrementNumAvatarsSentLastFrame();
// increment the number of avatars sent to this reciever
nodeData->incrementNumAvatarsSentLastFrame();
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(),
otherNodeData->getLastReceivedSequenceNumber());
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(),
otherNodeData->getLastReceivedSequenceNumber());
}
}
// remember the last time we sent details about this other node to the receiver
nodeData->setLastBroadcastTime(otherNode->getUUID(), start);
avatarPacketList->endSegment();
quint64 endAvatarDataPacking = usecTimestampNow();
_stats.avatarDataPackingElapsedTime += (endAvatarDataPacking - startAvatarDataPacking);
};
quint64 startPacketSending = usecTimestampNow();
// close the current packet so that we're always sending something
avatarPacketList->closeCurrentPacket(true);
_stats.numPacketsSent += (int)avatarPacketList->getNumPackets();
_stats.numBytesSent += numAvatarDataBytes;
// send the avatar data PacketList
nodeList->sendPacketList(std::move(avatarPacketList), *node);
// record the bytes sent for other avatar data in the AvatarMixerClientData
nodeData->recordSentAvatarData(numAvatarDataBytes);
// record the number of avatars held back this frame
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
quint64 endPacketSending = usecTimestampNow();
_stats.packetSendingElapsedTime += (endPacketSending - startPacketSending);
}
uint64_t REBROADCAST_IDENTITY_TO_DOWNSTREAM_EVERY_US = 5 * 1000 * 1000;
void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePointer& node) {
_stats.downstreamMixersBroadcastedTo++;
AvatarMixerClientData* nodeData = reinterpret_cast<AvatarMixerClientData*>(node->getLinkedData());
if (!nodeData) {
return;
}
// setup a PacketList for the replicated bulk avatar data
auto avatarPacketList = NLPacketList::create(PacketType::ReplicatedBulkAvatarData);
int numAvatarDataBytes = 0;
// reset the number of sent avatars
nodeData->resetNumAvatarsSentLastFrame();
std::for_each(_begin, _end, [&](const SharedNodePointer& agentNode) {
if (!AvatarMixer::shouldReplicateTo(*agentNode, *node)) {
return;
}
// collect agents that we have avatar data for that we are supposed to replicate
if (agentNode->getType() == NodeType::Agent && agentNode->getLinkedData() && agentNode->isReplicated()) {
const AvatarMixerClientData* agentNodeData = reinterpret_cast<const AvatarMixerClientData*>(agentNode->getLinkedData());
AvatarSharedPointer otherAvatar = agentNodeData->getAvatarSharedPointer();
quint64 startAvatarDataPacking = usecTimestampNow();
// we cannot send a downstream avatar mixer any updates that expect them to have previous state for this avatar
// since we have no idea if they're online and receiving our packets
// so we always send a full update for this avatar
quint64 start = usecTimestampNow();
AvatarDataPacket::HasFlags flagsOut;
QVector<JointData> emptyLastJointSendData { otherAvatar->getJointCount() };
QByteArray avatarByteArray = otherAvatar->toByteArray(AvatarData::SendAllData, 0, emptyLastJointSendData,
flagsOut, false, false, glm::vec3(0), nullptr);
quint64 end = usecTimestampNow();
_stats.toByteArrayElapsedTime += (end - start);
auto lastBroadcastTime = nodeData->getLastBroadcastTime(agentNode->getUUID());
if (lastBroadcastTime <= agentNodeData->getIdentityChangeTimestamp()
|| (start - lastBroadcastTime) >= REBROADCAST_IDENTITY_TO_DOWNSTREAM_EVERY_US) {
sendReplicatedIdentityPacket(*agentNode, agentNodeData, *node);
nodeData->setLastBroadcastTime(agentNode->getUUID(), start);
}
// figure out how large our avatar byte array can be to fit in the packet list
// given that we need it and the avatar UUID and the size of the byte array (16 bit)
// to fit in a segment of the packet list
auto maxAvatarByteArraySize = avatarPacketList->getMaxSegmentSize();
maxAvatarByteArraySize -= NUM_BYTES_RFC4122_UUID;
maxAvatarByteArraySize -= sizeof(quint16);
auto sequenceNumberSize = sizeof(agentNodeData->getLastReceivedSequenceNumber());
maxAvatarByteArraySize -= sequenceNumberSize;
if (avatarByteArray.size() > maxAvatarByteArraySize) {
qCWarning(avatars) << "Replicated avatar data too large for" << otherAvatar->getSessionUUID()
<< "-" << avatarByteArray.size() << "bytes";
avatarByteArray = otherAvatar->toByteArray(AvatarData::SendAllData, 0, emptyLastJointSendData,
flagsOut, true, false, glm::vec3(0), nullptr);
if (avatarByteArray.size() > maxAvatarByteArraySize) {
qCWarning(avatars) << "Replicated avatar data without facial data still too large for"
<< otherAvatar->getSessionUUID() << "-" << avatarByteArray.size() << "bytes";
avatarByteArray = otherAvatar->toByteArray(AvatarData::MinimumData, 0, emptyLastJointSendData,
flagsOut, true, false, glm::vec3(0), nullptr);
}
}
avatarPacketList->endSegment();
if (avatarByteArray.size() <= maxAvatarByteArraySize) {
// increment the number of avatars sent to this reciever
nodeData->incrementNumAvatarsSentLastFrame();
// set the last sent sequence number for this sender on the receiver
nodeData->setLastBroadcastSequenceNumber(agentNode->getUUID(),
agentNodeData->getLastReceivedSequenceNumber());
// increment the number of avatars sent to this reciever
nodeData->incrementNumAvatarsSentLastFrame();
// start a new segment in the packet list for this avatar
avatarPacketList->startSegment();
// write the node's UUID, the size of the replicated avatar data,
// the sequence number of the replicated avatar data, and the replicated avatar data
numAvatarDataBytes += avatarPacketList->write(agentNode->getUUID().toRfc4122());
numAvatarDataBytes += avatarPacketList->writePrimitive((quint16) (avatarByteArray.size() + sequenceNumberSize));
numAvatarDataBytes += avatarPacketList->writePrimitive(agentNodeData->getLastReceivedSequenceNumber());
numAvatarDataBytes += avatarPacketList->write(avatarByteArray);
avatarPacketList->endSegment();
} else {
qCWarning(avatars) << "Could not fit minimum data avatar for" << otherAvatar->getSessionUUID()
<< "to packet list -" << avatarByteArray.size() << "bytes";
}
quint64 endAvatarDataPacking = usecTimestampNow();
_stats.avatarDataPackingElapsedTime += (endAvatarDataPacking - startAvatarDataPacking);
};
}
});
if (avatarPacketList->getNumPackets() > 0) {
quint64 startPacketSending = usecTimestampNow();
// close the current packet so that we're always sending something
@ -398,21 +556,15 @@ void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) {
_stats.numPacketsSent += (int)avatarPacketList->getNumPackets();
_stats.numBytesSent += numAvatarDataBytes;
// send the avatar data PacketList
nodeList->sendPacketList(std::move(avatarPacketList), *node);
// send the replicated bulk avatar data
auto nodeList = DependencyManager::get<NodeList>();
nodeList->sendPacketList(std::move(avatarPacketList), node->getPublicSocket());
// record the bytes sent for other avatar data in the AvatarMixerClientData
nodeData->recordSentAvatarData(numAvatarDataBytes);
// record the number of avatars held back this frame
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
quint64 endPacketSending = usecTimestampNow();
_stats.packetSendingElapsedTime += (endPacketSending - startPacketSending);
}
quint64 end = usecTimestampNow();
_stats.jobElapsedTime += (end - start);
}

View file

@ -21,6 +21,7 @@ public:
quint64 processIncomingPacketsElapsedTime { 0 };
int nodesBroadcastedTo { 0 };
int downstreamMixersBroadcastedTo { 0 };
int numPacketsSent { 0 };
int numBytesSent { 0 };
int numIdentityPackets { 0 };
@ -41,6 +42,7 @@ public:
// sending job stats
nodesBroadcastedTo = 0;
downstreamMixersBroadcastedTo = 0;
numPacketsSent = 0;
numBytesSent = 0;
numIdentityPackets = 0;
@ -60,6 +62,7 @@ public:
processIncomingPacketsElapsedTime += rhs.processIncomingPacketsElapsedTime;
nodesBroadcastedTo += rhs.nodesBroadcastedTo;
downstreamMixersBroadcastedTo += rhs.downstreamMixersBroadcastedTo;
numPacketsSent += rhs.numPacketsSent;
numBytesSent += rhs.numBytesSent;
numIdentityPackets += rhs.numIdentityPackets;
@ -92,6 +95,10 @@ public:
private:
int sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode);
int sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode);
void broadcastAvatarDataToAgent(const SharedNodePointer& node);
void broadcastAvatarDataToDownstreamMixer(const SharedNodePointer& node);
// frame state
ConstIter _begin;

View file

@ -69,17 +69,17 @@ static AvatarMixerSlave slave;
void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end) {
_function = &AvatarMixerSlave::processIncomingPackets;
_configure = [&](AvatarMixerSlave& slave) {
_configure = [=](AvatarMixerSlave& slave) {
slave.configure(begin, end);
};
run(begin, end);
}
void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end,
p_high_resolution_clock::time_point lastFrameTimestamp,
float maxKbpsPerNode, float throttlingRatio) {
p_high_resolution_clock::time_point lastFrameTimestamp,
float maxKbpsPerNode, float throttlingRatio) {
_function = &AvatarMixerSlave::broadcastAvatarData;
_configure = [&](AvatarMixerSlave& slave) {
_configure = [=](AvatarMixerSlave& slave) {
slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio);
};
run(begin, end);

View file

@ -13,12 +13,13 @@
#include <QThread>
#include <glm/gtx/transform.hpp>
#include <shared/QtHelpers.h>
#include <GLMHelpers.h>
#include <AnimUtil.h>
#include "ScriptableAvatar.h"
QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) {
QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) {
_globalPosition = getPosition();
return AvatarData::toByteArrayStateful(dataDetail);
}
@ -49,7 +50,7 @@ void ScriptableAvatar::stopAnimation() {
AnimationDetails ScriptableAvatar::getAnimationDetails() {
if (QThread::currentThread() != thread()) {
AnimationDetails result;
QMetaObject::invokeMethod(this, "getAnimationDetails", Qt::BlockingQueuedConnection,
BLOCKING_INVOKE_METHOD(this, "getAnimationDetails",
Q_RETURN_ARG(AnimationDetails, result));
return result;
}

View file

@ -28,7 +28,7 @@ public:
Q_INVOKABLE AnimationDetails getAnimationDetails();
virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override;
virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail) override;
virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false) override;
private slots:

View file

@ -31,7 +31,7 @@ EntityServer::EntityServer(ReceivedMessage& message) :
OctreeServer(message),
_entitySimulation(NULL)
{
ResourceManager::init();
DependencyManager::set<ResourceManager>();
DependencyManager::set<ResourceCacheSharedItems>();
DependencyManager::set<ScriptCache>();
@ -50,6 +50,12 @@ EntityServer::~EntityServer() {
tree->removeNewlyCreatedHook(this);
}
void EntityServer::aboutToFinish() {
DependencyManager::get<ResourceManager>()->cleanup();
OctreeServer::aboutToFinish();
}
void EntityServer::handleEntityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
if (_octreeInboundPacketProcessor) {
_octreeInboundPacketProcessor->queueReceivedPacket(message, senderNode);

View file

@ -59,6 +59,8 @@ public:
virtual void trackSend(const QUuid& dataID, quint64 dataLastEdited, const QUuid& sessionID) override;
virtual void trackViewerGone(const QUuid& sessionID) override;
virtual void aboutToFinish() override;
public slots:
virtual void nodeAdded(SharedNodePointer node) override;
virtual void nodeKilled(SharedNodePointer node) override;

View file

@ -16,9 +16,9 @@
#include <SharedUtil.h>
#include <Octree.h>
#include <OctreePacketData.h>
#include <OctreeHeadlessViewer.h>
#include <ViewFrustum.h>
#include "../octree/OctreeHeadlessViewer.h"
#include "EntityTree.h"
class EntitySimulation;

View file

@ -9,17 +9,14 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <NodeList.h>
#include "OctreeLogging.h"
#include "OctreeHeadlessViewer.h"
OctreeHeadlessViewer::OctreeHeadlessViewer() : OctreeRenderer() {
_viewFrustum.setProjection(glm::perspective(glm::radians(DEFAULT_FIELD_OF_VIEW_DEGREES), DEFAULT_ASPECT_RATIO, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP));
}
#include <NodeList.h>
#include <OctreeLogging.h>
void OctreeHeadlessViewer::init() {
OctreeRenderer::init();
OctreeHeadlessViewer::OctreeHeadlessViewer() {
_viewFrustum.setProjection(glm::perspective(glm::radians(DEFAULT_FIELD_OF_VIEW_DEGREES), DEFAULT_ASPECT_RATIO, DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP));
}
void OctreeHeadlessViewer::queryOctree() {

View file

@ -12,28 +12,17 @@
#ifndef hifi_OctreeHeadlessViewer_h
#define hifi_OctreeHeadlessViewer_h
#include <udt/PacketHeaders.h>
#include <SharedUtil.h>
#include <ViewFrustum.h>
#include <OctreeProcessor.h>
#include <JurisdictionListener.h>
#include <OctreeQuery.h>
#include "JurisdictionListener.h"
#include "Octree.h"
#include "OctreeConstants.h"
#include "OctreeQuery.h"
#include "OctreeRenderer.h"
#include "OctreeSceneStats.h"
#include "Octree.h"
// Generic client side Octree renderer class.
class OctreeHeadlessViewer : public OctreeRenderer {
class OctreeHeadlessViewer : public OctreeProcessor {
Q_OBJECT
public:
OctreeHeadlessViewer();
virtual ~OctreeHeadlessViewer() {};
virtual void renderElement(OctreeElementPointer element, RenderArgs* args) override { /* swallow these */ }
virtual void init() override ;
virtual void render(RenderArgs* renderArgs) override { /* swallow these */ }
void setJurisdictionListener(JurisdictionListener* jurisdictionListener) { _jurisdictionListener = jurisdictionListener; }
@ -71,6 +60,7 @@ private:
JurisdictionListener* _jurisdictionListener = nullptr;
OctreeQuery _octreeQuery;
ViewFrustum _viewFrustum;
float _voxelSizeScale { DEFAULT_OCTREE_SIZE_SCALE };
int _boundaryLevelAdjust { 0 };
int _maxPacketsPerSecond { DEFAULT_MAX_OCTREE_PPS };

View file

@ -47,7 +47,7 @@ void OctreeInboundPacketProcessor::resetStats() {
_singleSenderStats.clear();
}
unsigned long OctreeInboundPacketProcessor::getMaxWait() const {
uint32_t OctreeInboundPacketProcessor::getMaxWait() const {
// calculate time until next sendNackPackets()
quint64 nextNackTime = _lastNackTime + TOO_LONG_SINCE_LAST_NACK;
quint64 now = usecTimestampNow();

View file

@ -80,7 +80,7 @@ protected:
virtual void processPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) override;
virtual unsigned long getMaxWait() const override;
virtual uint32_t getMaxWait() const override;
virtual void preProcess() override;
virtual void midProcess() override;

View file

@ -81,7 +81,6 @@ bool OctreeSendThread::process() {
// don't do any send processing until the initial load of the octree is complete...
if (_myServer->isInitialLoadComplete()) {
if (auto node = _node.lock()) {
_nodeMissingCount = 0;
OctreeQueryNode* nodeData = static_cast<OctreeQueryNode*>(node->getLinkedData());
// Sometimes the node data has not yet been linked, in which case we can't really do anything
@ -129,8 +128,7 @@ AtomicUIntStat OctreeSendThread::_totalSpecialBytes { 0 };
AtomicUIntStat OctreeSendThread::_totalSpecialPackets { 0 };
int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, int& trueBytesSent,
int& truePacketsSent, bool dontSuppressDuplicate) {
int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate) {
OctreeServer::didHandlePacketSend(this);
// if we're shutting down, then exit early
@ -141,15 +139,14 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode*
bool debug = _myServer->wantsDebugSending();
quint64 now = usecTimestampNow();
bool packetSent = false; // did we send a packet?
int packetsSent = 0;
int numPackets = 0;
// Here's where we check to see if this packet is a duplicate of the last packet. If it is, we will silently
// obscure the packet and not send it. This allows the callers and upper level logic to not need to know about
// this rate control savings.
if (!dontSuppressDuplicate && nodeData->shouldSuppressDuplicatePacket()) {
nodeData->resetOctreePacket(); // we still need to reset it though!
return packetsSent; // without sending...
return numPackets; // without sending...
}
// If we've got a stats message ready to send, then see if we can piggyback them together
@ -163,12 +160,15 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode*
// copy octree message to back of stats message
statsPacket.write(nodeData->getPacket().getData(), nodeData->getPacket().getDataSize());
// since a stats message is only included on end of scene, don't consider any of these bytes "wasted", since
int numBytes = statsPacket.getDataSize();
_totalBytes += numBytes;
_totalPackets++;
// since a stats message is only included on end of scene, don't consider any of these bytes "wasted"
// there was nothing else to send.
int thisWastedBytes = 0;
_totalWastedBytes += thisWastedBytes;
_totalBytes += statsPacket.getDataSize();
_totalPackets++;
//_totalWastedBytes += 0;
_trueBytesSent += numBytes;
numPackets++;
if (debug) {
NLPacket& sentPacket = nodeData->getPacket();
@ -191,18 +191,22 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode*
// actually send it
OctreeServer::didCallWriteDatagram(this);
DependencyManager::get<NodeList>()->sendUnreliablePacket(statsPacket, *node);
packetSent = true;
} else {
// not enough room in the packet, send two packets
// first packet
OctreeServer::didCallWriteDatagram(this);
DependencyManager::get<NodeList>()->sendUnreliablePacket(statsPacket, *node);
// since a stats message is only included on end of scene, don't consider any of these bytes "wasted", since
int numBytes = statsPacket.getDataSize();
_totalBytes += numBytes;
_totalPackets++;
// since a stats message is only included on end of scene, don't consider any of these bytes "wasted"
// there was nothing else to send.
int thisWastedBytes = 0;
_totalWastedBytes += thisWastedBytes;
_totalBytes += statsPacket.getDataSize();
_totalPackets++;
//_totalWastedBytes += 0;
_trueBytesSent += numBytes;
numPackets++;
if (debug) {
NLPacket& sentPacket = nodeData->getPacket();
@ -221,19 +225,18 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode*
"] wasted bytes:" << thisWastedBytes << " [" << _totalWastedBytes << "]";
}
trueBytesSent += statsPacket.getDataSize();
truePacketsSent++;
packetsSent++;
// second packet
OctreeServer::didCallWriteDatagram(this);
DependencyManager::get<NodeList>()->sendUnreliablePacket(nodeData->getPacket(), *node);
packetSent = true;
int packetSizeWithHeader = nodeData->getPacket().getDataSize();
thisWastedBytes = udt::MAX_PACKET_SIZE - packetSizeWithHeader;
_totalWastedBytes += thisWastedBytes;
_totalBytes += nodeData->getPacket().getDataSize();
numBytes = nodeData->getPacket().getDataSize();
_totalBytes += numBytes;
_totalPackets++;
// we count wasted bytes here because we were unable to fit the stats packet
thisWastedBytes = udt::MAX_PACKET_SIZE - numBytes;
_totalWastedBytes += thisWastedBytes;
_trueBytesSent += numBytes;
numPackets++;
if (debug) {
NLPacket& sentPacket = nodeData->getPacket();
@ -259,13 +262,14 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode*
// just send the octree packet
OctreeServer::didCallWriteDatagram(this);
DependencyManager::get<NodeList>()->sendUnreliablePacket(nodeData->getPacket(), *node);
packetSent = true;
int packetSizeWithHeader = nodeData->getPacket().getDataSize();
int thisWastedBytes = udt::MAX_PACKET_SIZE - packetSizeWithHeader;
_totalWastedBytes += thisWastedBytes;
_totalBytes += packetSizeWithHeader;
int numBytes = nodeData->getPacket().getDataSize();
_totalBytes += numBytes;
_totalPackets++;
int thisWastedBytes = udt::MAX_PACKET_SIZE - numBytes;
_totalWastedBytes += thisWastedBytes;
numPackets++;
_trueBytesSent += numBytes;
if (debug) {
NLPacket& sentPacket = nodeData->getPacket();
@ -280,23 +284,21 @@ int OctreeSendThread::handlePacketSend(SharedNodePointer node, OctreeQueryNode*
qDebug() << "Sending packet at " << now << " [" << _totalPackets <<"]: sequence: " << sequence <<
" timestamp: " << timestamp <<
" size: " << packetSizeWithHeader << " [" << _totalBytes <<
" size: " << numBytes << " [" << _totalBytes <<
"] wasted bytes:" << thisWastedBytes << " [" << _totalWastedBytes << "]";
}
}
}
// remember to track our stats
if (packetSent) {
if (numPackets > 0) {
nodeData->stats.packetSent(nodeData->getPacket().getPayloadSize());
trueBytesSent += nodeData->getPacket().getPayloadSize();
truePacketsSent++;
packetsSent++;
nodeData->octreePacketSent();
nodeData->resetOctreePacket();
}
return packetsSent;
_truePacketsSent += numPackets;
return numPackets;
}
/// Version of octree element distributor that sends the deepest LOD level at once
@ -315,13 +317,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
preDistributionProcessing();
}
// calculate max number of packets that can be sent during this interval
int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND));
int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval());
int truePacketsSent = 0;
int trueBytesSent = 0;
int packetsSentThisInterval = 0;
_truePacketsSent = 0;
_trueBytesSent = 0;
_packetsSentThisInterval = 0;
bool isFullScene = nodeData->shouldForceFullScene();
if (isFullScene) {
@ -334,17 +332,9 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
&& ((!viewFrustumChanged && nodeData->getViewFrustumJustStoppedChanging()) || nodeData->hasLodChanged()));
}
bool somethingToSend = true; // assume we have something
// If our packet already has content in it, then we must use the color choice of the waiting packet.
// If we're starting a fresh packet, then...
// If we're moving, and the client asked for low res, then we force monochrome, otherwise, use
// the clients requested color state.
// If we have a packet waiting, and our desired want color, doesn't match the current waiting packets color
// then let's just send that waiting packet.
if (nodeData->isPacketWaiting()) {
packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent);
// send the waiting packet
_packetsSentThisInterval += handlePacketSend(node, nodeData);
} else {
nodeData->resetOctreePacket();
}
@ -375,8 +365,7 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
//unsigned long encodeTime = nodeData->stats.getTotalEncodeTime();
//unsigned long elapsedTime = nodeData->stats.getElapsedTime();
int packetsJustSent = handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent, isFullScene);
packetsSentThisInterval += packetsJustSent;
_packetsSentThisInterval += handlePacketSend(node, nodeData, isFullScene);
// If we're starting a full scene, then definitely we want to empty the elementBag
if (isFullScene) {
@ -404,185 +393,44 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
// If we have something in our elementBag, then turn them into packets and send them out...
if (!nodeData->elementBag.isEmpty()) {
int bytesWritten = 0;
quint64 start = usecTimestampNow();
// TODO: add these to stats page
//quint64 startCompressTimeMsecs = OctreePacketData::getCompressContentTime() / 1000;
//quint64 startCompressCalls = OctreePacketData::getCompressContentCalls();
int extraPackingAttempts = 0;
bool completedScene = false;
while (somethingToSend && packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) {
float lockWaitElapsedUsec = OctreeServer::SKIP_TIME;
float encodeElapsedUsec = OctreeServer::SKIP_TIME;
float compressAndWriteElapsedUsec = OctreeServer::SKIP_TIME;
float packetSendingElapsedUsec = OctreeServer::SKIP_TIME;
quint64 startInside = usecTimestampNow();
bool lastNodeDidntFit = false; // assume each node fits
if (!nodeData->elementBag.isEmpty()) {
quint64 lockWaitStart = usecTimestampNow();
_myServer->getOctree()->withReadLock([&]{
quint64 lockWaitEnd = usecTimestampNow();
lockWaitElapsedUsec = (float)(lockWaitEnd - lockWaitStart);
quint64 encodeStart = usecTimestampNow();
OctreeElementPointer subTree = nodeData->elementBag.extract();
if (!subTree) {
return;
}
float octreeSizeScale = nodeData->getOctreeSizeScale();
int boundaryLevelAdjustClient = nodeData->getBoundaryLevelAdjust();
int boundaryLevelAdjust = boundaryLevelAdjustClient +
(viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST);
EncodeBitstreamParams params(INT_MAX, WANT_EXISTS_BITS, DONT_CHOP,
viewFrustumChanged, boundaryLevelAdjust, octreeSizeScale,
isFullScene, _myServer->getJurisdiction(), nodeData);
nodeData->copyCurrentViewFrustum(params.viewFrustum);
if (viewFrustumChanged) {
nodeData->copyLastKnownViewFrustum(params.lastViewFrustum);
}
// Our trackSend() function is implemented by the server subclass, and will be called back
// during the encodeTreeBitstream() as new entities/data elements are sent
params.trackSend = [this, node](const QUuid& dataID, quint64 dataEdited) {
_myServer->trackSend(dataID, dataEdited, node->getUUID());
};
// TODO: should this include the lock time or not? This stat is sent down to the client,
// it seems like it may be a good idea to include the lock time as part of the encode time
// are reported to client. Since you can encode without the lock
nodeData->stats.encodeStarted();
bytesWritten = _myServer->getOctree()->encodeTreeBitstream(subTree, &_packetData, nodeData->elementBag, params);
quint64 encodeEnd = usecTimestampNow();
encodeElapsedUsec = (float)(encodeEnd - encodeStart);
// If after calling encodeTreeBitstream() there are no nodes left to send, then we know we've
// sent the entire scene. We want to know this below so we'll actually write this content into
// the packet and send it
completedScene = nodeData->elementBag.isEmpty();
if (params.stopReason == EncodeBitstreamParams::DIDNT_FIT) {
lastNodeDidntFit = true;
extraPackingAttempts++;
}
nodeData->stats.encodeStopped();
});
} else {
// If the bag was empty then we didn't even attempt to encode, and so we know the bytesWritten were 0
bytesWritten = 0;
somethingToSend = false; // this will cause us to drop out of the loop...
}
// If the last node didn't fit, but we're in compressed mode, then we actually want to see if we can fit a
// little bit more in this packet. To do this we write into the packet, but don't send it yet, we'll
// keep attempting to write in compressed mode to add more compressed segments
// We only consider sending anything if there is something in the _packetData to send... But
// if bytesWritten == 0 it means either the subTree couldn't fit or we had an empty bag... Both cases
// mean we should send the previous packet contents and reset it.
if (completedScene || lastNodeDidntFit) {
if (_packetData.hasContent()) {
quint64 compressAndWriteStart = usecTimestampNow();
// if for some reason the finalized size is greater than our available size, then probably the "compressed"
// form actually inflated beyond our padding, and in this case we will send the current packet, then
// write to out new packet...
unsigned int writtenSize = _packetData.getFinalizedSize() + sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE);
if (writtenSize > nodeData->getAvailable()) {
packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent);
}
nodeData->writeToPacket(_packetData.getFinalizedData(), _packetData.getFinalizedSize());
quint64 compressAndWriteEnd = usecTimestampNow();
compressAndWriteElapsedUsec = (float)(compressAndWriteEnd - compressAndWriteStart);
}
// If we're not running compressed, then we know we can just send now. Or if we're running compressed, but
// the packet doesn't have enough space to bother attempting to pack more...
bool sendNow = true;
if (!completedScene && (nodeData->getAvailable() >= MINIMUM_ATTEMPT_MORE_PACKING &&
extraPackingAttempts <= REASONABLE_NUMBER_OF_PACKING_ATTEMPTS)) {
sendNow = false; // try to pack more
}
int targetSize = MAX_OCTREE_PACKET_DATA_SIZE;
if (sendNow) {
quint64 packetSendingStart = usecTimestampNow();
packetsSentThisInterval += handlePacketSend(node, nodeData, trueBytesSent, truePacketsSent);
quint64 packetSendingEnd = usecTimestampNow();
packetSendingElapsedUsec = (float)(packetSendingEnd - packetSendingStart);
targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE);
extraPackingAttempts = 0;
} else {
// If we're in compressed mode, then we want to see if we have room for more in this wire packet.
// but we've finalized the _packetData, so we want to start a new section, we will do that by
// resetting the packet settings with the max uncompressed size of our current available space
// in the wire packet. We also include room for our section header, and a little bit of padding
// to account for the fact that whenc compressing small amounts of data, we sometimes end up with
// a larger compressed size then uncompressed size
targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE) - COMPRESS_PADDING;
}
_packetData.changeSettings(true, targetSize); // will do reset - NOTE: Always compressed
}
OctreeServer::trackTreeWaitTime(lockWaitElapsedUsec);
OctreeServer::trackEncodeTime(encodeElapsedUsec);
OctreeServer::trackCompressAndWriteTime(compressAndWriteElapsedUsec);
OctreeServer::trackPacketSendingTime(packetSendingElapsedUsec);
quint64 endInside = usecTimestampNow();
quint64 elapsedInsideUsecs = endInside - startInside;
OctreeServer::trackInsideTime((float)elapsedInsideUsecs);
}
if (somethingToSend && _myServer->wantsVerboseDebug()) {
qCDebug(octree) << "Hit PPS Limit, packetsSentThisInterval =" << packetsSentThisInterval
<< " maxPacketsPerInterval = " << maxPacketsPerInterval
<< " clientMaxPacketsPerInterval = " << clientMaxPacketsPerInterval;
}
traverseTreeAndSendContents(node, nodeData, viewFrustumChanged, isFullScene);
// Here's where we can/should allow the server to send other data...
// send the environment packet
// TODO: should we turn this into a while loop to better handle sending multiple special packets
if (_myServer->hasSpecialPacketsToSend(node) && !nodeData->isShuttingDown()) {
int specialPacketsSent = 0;
trueBytesSent += _myServer->sendSpecialPackets(node, nodeData, specialPacketsSent);
int specialBytesSent = _myServer->sendSpecialPackets(node, nodeData, specialPacketsSent);
nodeData->resetOctreePacket(); // because nodeData's _sequenceNumber has changed
truePacketsSent += specialPacketsSent;
packetsSentThisInterval += specialPacketsSent;
_truePacketsSent += specialPacketsSent;
_trueBytesSent += specialBytesSent;
_packetsSentThisInterval += specialPacketsSent;
_totalPackets += specialPacketsSent;
_totalBytes += trueBytesSent;
_totalBytes += specialBytesSent;
_totalSpecialPackets += specialPacketsSent;
_totalSpecialBytes += trueBytesSent;
_totalSpecialBytes += specialBytesSent;
}
// calculate max number of packets that can be sent during this interval
int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND));
int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval());
// Re-send packets that were nacked by the client
while (nodeData->hasNextNackedPacket() && packetsSentThisInterval < maxPacketsPerInterval) {
while (nodeData->hasNextNackedPacket() && _packetsSentThisInterval < maxPacketsPerInterval) {
const NLPacket* packet = nodeData->getNextNackedPacket();
if (packet) {
DependencyManager::get<NodeList>()->sendUnreliablePacket(*packet, *node);
truePacketsSent++;
packetsSentThisInterval++;
int numBytes = packet->getDataSize();
_truePacketsSent++;
_trueBytesSent += numBytes;
_packetsSentThisInterval++;
_totalBytes += packet->getDataSize();
_totalPackets++;
_totalBytes += numBytes;
_totalWastedBytes += udt::MAX_PACKET_SIZE - packet->getDataSize();
}
}
@ -591,12 +439,6 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
int elapsedmsec = (end - start) / USECS_PER_MSEC;
OctreeServer::trackLoopTime(elapsedmsec);
// TODO: add these to stats page
//quint64 endCompressCalls = OctreePacketData::getCompressContentCalls();
//int elapsedCompressCalls = endCompressCalls - startCompressCalls;
//quint64 endCompressTimeMsecs = OctreePacketData::getCompressContentTime() / 1000;
//int elapsedCompressTimeMsecs = endCompressTimeMsecs - startCompressTimeMsecs;
// if after sending packets we've emptied our bag, then we want to remember that we've sent all
// the octree elements from the current view frustum
if (nodeData->elementBag.isEmpty()) {
@ -606,17 +448,147 @@ int OctreeSendThread::packetDistributor(SharedNodePointer node, OctreeQueryNode*
// If this was a full scene then make sure we really send out a stats packet at this point so that
// the clients will know the scene is stable
if (isFullScene) {
int thisTrueBytesSent = 0;
int thisTruePacketsSent = 0;
nodeData->stats.sceneCompleted();
int packetsJustSent = handlePacketSend(node, nodeData, thisTrueBytesSent, thisTruePacketsSent, true);
_totalBytes += thisTrueBytesSent;
_totalPackets += thisTruePacketsSent;
truePacketsSent += packetsJustSent;
handlePacketSend(node, nodeData, true);
}
}
} // end if bag wasn't empty, and so we sent stuff...
return truePacketsSent;
return _truePacketsSent;
}
void OctreeSendThread::traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene) {
// calculate max number of packets that can be sent during this interval
int clientMaxPacketsPerInterval = std::max(1, (nodeData->getMaxQueryPacketsPerSecond() / INTERVALS_PER_SECOND));
int maxPacketsPerInterval = std::min(clientMaxPacketsPerInterval, _myServer->getPacketsPerClientPerInterval());
int extraPackingAttempts = 0;
bool completedScene = false;
bool somethingToSend = true; // assume we have something
while (somethingToSend && _packetsSentThisInterval < maxPacketsPerInterval && !nodeData->isShuttingDown()) {
float lockWaitElapsedUsec = OctreeServer::SKIP_TIME;
float encodeElapsedUsec = OctreeServer::SKIP_TIME;
float compressAndWriteElapsedUsec = OctreeServer::SKIP_TIME;
float packetSendingElapsedUsec = OctreeServer::SKIP_TIME;
quint64 startInside = usecTimestampNow();
bool lastNodeDidntFit = false; // assume each node fits
if (!nodeData->elementBag.isEmpty()) {
quint64 lockWaitStart = usecTimestampNow();
_myServer->getOctree()->withReadLock([&]{
quint64 lockWaitEnd = usecTimestampNow();
lockWaitElapsedUsec = (float)(lockWaitEnd - lockWaitStart);
quint64 encodeStart = usecTimestampNow();
OctreeElementPointer subTree = nodeData->elementBag.extract();
if (!subTree) {
return;
}
float octreeSizeScale = nodeData->getOctreeSizeScale();
int boundaryLevelAdjustClient = nodeData->getBoundaryLevelAdjust();
int boundaryLevelAdjust = boundaryLevelAdjustClient +
(viewFrustumChanged ? LOW_RES_MOVING_ADJUST : NO_BOUNDARY_ADJUST);
EncodeBitstreamParams params(INT_MAX, WANT_EXISTS_BITS, DONT_CHOP,
viewFrustumChanged, boundaryLevelAdjust, octreeSizeScale,
isFullScene, _myServer->getJurisdiction(), nodeData);
nodeData->copyCurrentViewFrustum(params.viewFrustum);
if (viewFrustumChanged) {
nodeData->copyLastKnownViewFrustum(params.lastViewFrustum);
}
// Our trackSend() function is implemented by the server subclass, and will be called back
// during the encodeTreeBitstream() as new entities/data elements are sent
params.trackSend = [this](const QUuid& dataID, quint64 dataEdited) {
_myServer->trackSend(dataID, dataEdited, _nodeUuid);
};
// TODO: should this include the lock time or not? This stat is sent down to the client,
// it seems like it may be a good idea to include the lock time as part of the encode time
// are reported to client. Since you can encode without the lock
nodeData->stats.encodeStarted();
// NOTE: this is where the tree "contents" are actaully packed
_myServer->getOctree()->encodeTreeBitstream(subTree, &_packetData, nodeData->elementBag, params);
quint64 encodeEnd = usecTimestampNow();
encodeElapsedUsec = (float)(encodeEnd - encodeStart);
// If after calling encodeTreeBitstream() there are no nodes left to send, then we know we've
// sent the entire scene. We want to know this below so we'll actually write this content into
// the packet and send it
completedScene = nodeData->elementBag.isEmpty();
if (params.stopReason == EncodeBitstreamParams::DIDNT_FIT) {
lastNodeDidntFit = true;
extraPackingAttempts++;
}
nodeData->stats.encodeStopped();
});
} else {
somethingToSend = false; // this will cause us to drop out of the loop...
}
if (completedScene || lastNodeDidntFit) {
// we probably want to flush what has accumulated in nodeData but:
// do we have more data to send? and is there room?
if (_packetData.hasContent()) {
// yes, more data to send
quint64 compressAndWriteStart = usecTimestampNow();
unsigned int additionalSize = _packetData.getFinalizedSize() + sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE);
if (additionalSize > nodeData->getAvailable()) {
// no room --> flush what we've got
_packetsSentThisInterval += handlePacketSend(node, nodeData);
}
// either there is room, or we've flushed and reset nodeData's data buffer
// so we can transfer whatever is in _packetData to nodeData
nodeData->writeToPacket(_packetData.getFinalizedData(), _packetData.getFinalizedSize());
compressAndWriteElapsedUsec = (float)(usecTimestampNow()- compressAndWriteStart);
}
bool sendNow = completedScene ||
nodeData->getAvailable() < MINIMUM_ATTEMPT_MORE_PACKING ||
extraPackingAttempts > REASONABLE_NUMBER_OF_PACKING_ATTEMPTS;
int targetSize = MAX_OCTREE_PACKET_DATA_SIZE;
if (sendNow) {
quint64 packetSendingStart = usecTimestampNow();
_packetsSentThisInterval += handlePacketSend(node, nodeData);
quint64 packetSendingEnd = usecTimestampNow();
packetSendingElapsedUsec = (float)(packetSendingEnd - packetSendingStart);
targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE);
extraPackingAttempts = 0;
} else {
// We want to see if we have room for more in this wire packet but we've copied the _packetData,
// so we want to start a new section. We will do that by resetting the packet settings with the max
// size of our current available space in the wire packet plus room for our section header and a
// little bit of padding.
targetSize = nodeData->getAvailable() - sizeof(OCTREE_PACKET_INTERNAL_SECTION_SIZE) - COMPRESS_PADDING;
}
_packetData.changeSettings(true, targetSize); // will do reset - NOTE: Always compressed
}
OctreeServer::trackTreeWaitTime(lockWaitElapsedUsec);
OctreeServer::trackEncodeTime(encodeElapsedUsec);
OctreeServer::trackCompressAndWriteTime(compressAndWriteElapsedUsec);
OctreeServer::trackPacketSendingTime(packetSendingElapsedUsec);
quint64 endInside = usecTimestampNow();
quint64 elapsedInsideUsecs = endInside - startInside;
OctreeServer::trackInsideTime((float)elapsedInsideUsecs);
}
if (somethingToSend && _myServer->wantsVerboseDebug()) {
qCDebug(octree) << "Hit PPS Limit, packetsSentThisInterval =" << _packetsSentThisInterval
<< " maxPacketsPerInterval = " << maxPacketsPerInterval
<< " clientMaxPacketsPerInterval = " << clientMaxPacketsPerInterval;
}
}

View file

@ -34,7 +34,7 @@ public:
void setIsShuttingDown();
bool isShuttingDown() { return _isShuttingDown; }
QUuid getNodeUuid() const { return _nodeUuid; }
static AtomicUIntStat _totalBytes;
@ -53,20 +53,23 @@ protected:
/// Called before a packetDistributor pass to allow for pre-distribution processing
virtual void preDistributionProcessing() {};
virtual void traverseTreeAndSendContents(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged, bool isFullScene);
OctreeServer* _myServer { nullptr };
QWeakPointer<Node> _node;
private:
int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, int& trueBytesSent, int& truePacketsSent, bool dontSuppressDuplicate = false);
int handlePacketSend(SharedNodePointer node, OctreeQueryNode* nodeData, bool dontSuppressDuplicate = false);
int packetDistributor(SharedNodePointer node, OctreeQueryNode* nodeData, bool viewFrustumChanged);
QUuid _nodeUuid;
OctreePacketData _packetData;
int _nodeMissingCount { 0 };
int _truePacketsSent { 0 }; // available for debug stats
int _trueBytesSent { 0 }; // available for debug stats
int _packetsSentThisInterval { 0 }; // used for bandwidth throttle condition
bool _isShuttingDown { false };
};

View file

@ -16,6 +16,7 @@
#include <AudioConstants.h>
#include <AudioInjectorManager.h>
#include <ClientServerUtils.h>
#include <DebugDraw.h>
#include <EntityNodeData.h>
#include <EntityScriptingInterface.h>
#include <LogHandler.h>
@ -54,7 +55,7 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig
DependencyManager::get<EntityScriptingInterface>()->setPacketSender(&_entityEditSender);
ResourceManager::init();
DependencyManager::set<ResourceManager>();
DependencyManager::registerInheritance<SpatialParentFinder, AssignmentParentFinder>();
@ -67,6 +68,8 @@ EntityScriptServer::EntityScriptServer(ReceivedMessage& message) : ThreadedAssig
DependencyManager::set<ScriptCache>();
DependencyManager::set<ScriptEngines>(ScriptEngine::ENTITY_SERVER_SCRIPT);
// Needed to ensure the creation of the DebugDraw instance on the main thread
DebugDraw::getInstance();
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListenerForTypes({ PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase },
@ -493,7 +496,7 @@ void EntityScriptServer::checkAndCallPreload(const EntityItemID& entityID, bool
if (entity && (reload || notRunning || details.scriptText != entity->getServerScripts())) {
QString scriptUrl = entity->getServerScripts();
if (!scriptUrl.isEmpty()) {
scriptUrl = ResourceManager::normalizeURL(scriptUrl);
scriptUrl = DependencyManager::get<ResourceManager>()->normalizeURL(scriptUrl);
qCDebug(entity_script_server) << "Loading entity server script" << scriptUrl << "for" << entityID;
_entitiesScriptEngine->loadEntityScript(entityID, scriptUrl, reload);
}
@ -551,7 +554,7 @@ void EntityScriptServer::aboutToFinish() {
// our entity tree is going to go away so tell that to the EntityScriptingInterface
DependencyManager::get<EntityScriptingInterface>()->setEntityTree(nullptr);
ResourceManager::cleanup();
DependencyManager::get<ResourceManager>()->cleanup();
// cleanup the AudioInjectorManager (and any still running injectors)
DependencyManager::destroy<AudioInjectorManager>();

View file

@ -19,10 +19,10 @@
#include <QtCore/QUuid>
#include <EntityEditPacketSender.h>
#include <EntityTreeHeadlessViewer.h>
#include <plugins/CodecPlugin.h>
#include <ScriptEngine.h>
#include <ThreadedAssignment.h>
#include "../entities/EntityTreeHeadlessViewer.h"
class EntityScriptServer : public ThreadedAssignment {
Q_OBJECT

View file

@ -12,12 +12,19 @@ elseif ($ENV{QT_CMAKE_PREFIX_PATH})
set(QT_CMAKE_PREFIX_PATH $ENV{QT_CMAKE_PREFIX_PATH})
endif ()
set(QUAZIP_CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> -DCMAKE_PREFIX_PATH=${QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=<INSTALL_DIR>/lib -DZLIB_ROOT=${ZLIB_ROOT} -DCMAKE_POSITION_INDEPENDENT_CODE=ON)
if (APPLE)
else ()
set(QUAZIP_CMAKE_ARGS ${QUAZIP_CMAKE_ARGS} -DCMAKE_CXX_STANDARD=11)
endif ()
ExternalProject_Add(
${EXTERNAL_NAME}
URL https://s3-us-west-1.amazonaws.com/hifi-production/dependencies/quazip-0.7.2.zip
URL_MD5 2955176048a31262c09259ca8d309d19
URL https://hifi-public.s3.amazonaws.com/dependencies/quazip-0.7.3.zip
URL_MD5 ed03754d39b9da1775771819b8001d45
BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR> -DCMAKE_PREFIX_PATH=${QT_CMAKE_PREFIX_PATH} -DCMAKE_INSTALL_NAME_DIR:PATH=<INSTALL_DIR>/lib -DZLIB_ROOT=${ZLIB_ROOT} -DCMAKE_POSITION_INDEPENDENT_CODE=ON
CMAKE_ARGS ${QUAZIP_CMAKE_ARGS}
LOG_DOWNLOAD 1
LOG_CONFIGURE 1
LOG_BUILD 1

View file

@ -7,16 +7,18 @@
# See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
#
macro(LINK_HIFI_LIBRARIES)
function(LINK_HIFI_LIBRARIES)
file(RELATIVE_PATH RELATIVE_LIBRARY_DIR_PATH ${CMAKE_CURRENT_SOURCE_DIR} "${HIFI_LIBRARY_DIR}")
set(LIBRARIES_TO_LINK ${ARGN})
foreach(HIFI_LIBRARY ${LIBRARIES_TO_LINK})
foreach(HIFI_LIBRARY ${LIBRARIES_TO_LINK})
if (NOT TARGET ${HIFI_LIBRARY})
add_subdirectory("${RELATIVE_LIBRARY_DIR_PATH}/${HIFI_LIBRARY}" "${RELATIVE_LIBRARY_DIR_PATH}/${HIFI_LIBRARY}")
endif ()
endforeach()
foreach(HIFI_LIBRARY ${LIBRARIES_TO_LINK})
include_directories("${HIFI_LIBRARY_DIR}/${HIFI_LIBRARY}/src")
include_directories("${CMAKE_BINARY_DIR}/libraries/${HIFI_LIBRARY}/shaders")
@ -29,4 +31,4 @@ macro(LINK_HIFI_LIBRARIES)
setup_memory_debugger()
endmacro(LINK_HIFI_LIBRARIES)
endfunction()

View file

@ -126,8 +126,14 @@ macro(SET_PACKAGING_PARAMETERS)
# check if we need to find signtool
if (PRODUCTION_BUILD OR PR_BUILD)
find_program(SIGNTOOL_EXECUTABLE signtool PATHS "C:/Program Files (x86)/Windows Kits/8.1" PATH_SUFFIXES "bin/x64")
if (MSVC_VERSION GREATER_EQUAL 1910) # VS 2017
find_program(SIGNTOOL_EXECUTABLE signtool PATHS "C:/Program Files (x86)/Windows Kits/10" PATH_SUFFIXES "bin/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}/x64")
elseif (MSVC_VERSION GREATER_EQUAL 1800) # VS 2013
find_program(SIGNTOOL_EXECUTABLE signtool PATHS "C:/Program Files (x86)/Windows Kits/8.1" PATH_SUFFIXES "bin/x64")
else()
message( FATAL_ERROR "Visual Studio 2013 or higher required." )
endif()
if (NOT SIGNTOOL_EXECUTABLE)
message(FATAL_ERROR "Code signing of executables was requested but signtool.exe could not be found.")
endif ()

View file

@ -35,6 +35,23 @@ macro(SETUP_HIFI_LIBRARY)
endif()
endforeach()
# add compiler flags to AVX512 source files, if supported by compiler
include(CheckCXXCompilerFlag)
file(GLOB_RECURSE AVX512_SRCS "src/avx512/*.cpp" "src/avx512/*.c")
foreach(SRC ${AVX512_SRCS})
if (WIN32)
check_cxx_compiler_flag("/arch:AVX512" COMPILER_SUPPORTS_AVX512)
if (COMPILER_SUPPORTS_AVX512)
set_source_files_properties(${SRC} PROPERTIES COMPILE_FLAGS /arch:AVX512)
endif()
elseif (APPLE OR UNIX)
check_cxx_compiler_flag("-mavx512f" COMPILER_SUPPORTS_AVX512)
if (COMPILER_SUPPORTS_AVX512)
set_source_files_properties(${SRC} PROPERTIES COMPILE_FLAGS -mavx512f)
endif()
endif()
endforeach()
setup_memory_debugger()
# create a library and set the property so it can be referenced later

View file

@ -0,0 +1,12 @@
#
# Created by David Rowe on 16 Jun 2017.
# Copyright 2017 High Fidelity, Inc.
#
# Distributed under the Apache License, Version 2.0.
# See the accompanying file LICENSE or http:#www.apache.org/licenses/LICENSE-2.0.html
#
macro(TARGET_LEAPMOTION)
target_include_directories(${TARGET_NAME} PRIVATE ${LEAPMOTION_INCLUDE_DIRS})
target_link_libraries(${TARGET_NAME} ${LEAPMOTION_LIBRARIES})
endmacro()

View file

@ -29,6 +29,7 @@ string(REGEX REPLACE "^[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_MINOR "${FBX_VER
string(REGEX REPLACE "^[0-9]+\\.[0-9]+\\.([0-9]+).*$" "\\1" FBX_VERSION_PATCH "${FBX_VERSION}")
set(FBX_MAC_LOCATIONS "/Applications/Autodesk/FBX\ SDK/${FBX_VERSION}")
set(FBX_LINUX_LOCATIONS "/usr/local/lib/gcc4/x64/debug/")
if (WIN32)
string(REGEX REPLACE "\\\\" "/" WIN_PROGRAM_FILES_X64_DIRECTORY $ENV{ProgramW6432})
@ -36,7 +37,7 @@ endif()
set(FBX_WIN_LOCATIONS "${WIN_PROGRAM_FILES_X64_DIRECTORY}/Autodesk/FBX/FBX SDK/${FBX_VERSION}")
set(FBX_SEARCH_LOCATIONS $ENV{FBX_ROOT} ${FBX_ROOT} ${FBX_MAC_LOCATIONS} ${FBX_WIN_LOCATIONS})
set(FBX_SEARCH_LOCATIONS $ENV{FBX_ROOT} ${FBX_ROOT} ${FBX_MAC_LOCATIONS} ${FBX_WIN_LOCATIONS} ${FBX_LINUX_LOCATIONS})
function(_fbx_append_debugs _endvar _library)
if (${_library} AND ${_library}_DEBUG)
@ -55,19 +56,17 @@ elseif (${CMAKE_CXX_COMPILER_ID} MATCHES "GNU")
endif()
function(_fbx_find_library _name _lib _suffix)
if (MSVC12)
if (MSVC_VERSION EQUAL 1910)
set(VS_PREFIX vs2017)
elseif (MSVC_VERSION EQUAL 1900)
set(VS_PREFIX vs2015)
elseif (MSVC_VERSION EQUAL 1800)
set(VS_PREFIX vs2013)
endif()
if (MSVC11)
elseif (MSVC_VERSION EQUAL 1700)
set(VS_PREFIX vs2012)
endif()
if (MSVC10)
elseif (MSVC_VERSION EQUAL 1600)
set(VS_PREFIX vs2010)
endif()
if (MSVC90)
elseif (MSVC_VERSION EQUAL 1500)
set(VS_PREFIX vs2008)
endif()
@ -94,6 +93,8 @@ elseif (APPLE)
find_library(SYSTEM_CONFIGURATION NAMES SystemConfiguration)
_fbx_find_library(FBX_LIBRARY libfbxsdk.a release)
_fbx_find_library(FBX_LIBRARY_DEBUG libfbxsdk.a debug)
else ()
_fbx_find_library(FBX_LIBRARY libfbxsdk.a release)
endif()
include(FindPackageHandleStandardArgs)

View file

@ -29,6 +29,63 @@
!include "WinVer.nsh"
;--------------------------------
; Utilities and Functions
;--------------------------------
; START String Contains Macro
; Taken from http://nsis.sourceforge.net/StrContains
;--------------------------------
; StrContains
; This function does a case sensitive searches for an occurrence of a substring in a string.
; It returns the substring if it is found.
; Otherwise it returns null("").
; Written by kenglish_hi
; Adapted from StrReplace written by dandaman32
Var STR_HAYSTACK
Var STR_NEEDLE
Var STR_CONTAINS_VAR_1
Var STR_CONTAINS_VAR_2
Var STR_CONTAINS_VAR_3
Var STR_CONTAINS_VAR_4
Var STR_RETURN_VAR
Function StrContains
Exch $STR_NEEDLE
Exch 1
Exch $STR_HAYSTACK
; Uncomment to debug
;MessageBox MB_OK 'STR_NEEDLE = $STR_NEEDLE STR_HAYSTACK = $STR_HAYSTACK '
StrCpy $STR_RETURN_VAR ""
StrCpy $STR_CONTAINS_VAR_1 -1
StrLen $STR_CONTAINS_VAR_2 $STR_NEEDLE
StrLen $STR_CONTAINS_VAR_4 $STR_HAYSTACK
loop:
IntOp $STR_CONTAINS_VAR_1 $STR_CONTAINS_VAR_1 + 1
StrCpy $STR_CONTAINS_VAR_3 $STR_HAYSTACK $STR_CONTAINS_VAR_2 $STR_CONTAINS_VAR_1
StrCmp $STR_CONTAINS_VAR_3 $STR_NEEDLE found
StrCmp $STR_CONTAINS_VAR_1 $STR_CONTAINS_VAR_4 done
Goto loop
found:
StrCpy $STR_RETURN_VAR $STR_NEEDLE
Goto done
done:
Pop $STR_NEEDLE ;Prevent "invalid opcode" errors and keep the
Exch $STR_RETURN_VAR
FunctionEnd
!macro _StrContainsConstructor OUT NEEDLE HAYSTACK
Push `${HAYSTACK}`
Push `${NEEDLE}`
Call StrContains
Pop `${OUT}`
!macroend
!define StrContains '!insertmacro "_StrContainsConstructor"'
;--------------------------------
; END String Contains Macro
;--------------------------------
;--------------------------------
;General
; leverage the UAC NSIS plugin to promote uninstaller to elevated privileges
!include UAC.nsh
@ -92,6 +149,7 @@
; inter-component dependencies.
Var AR_SecFlags
Var AR_RegFlags
Var substringResult
@CPACK_NSIS_SECTION_SELECTED_VARS@
; Loads the "selected" flag for the section named SecName into the
@ -119,9 +177,11 @@ Var AR_RegFlags
ClearErrors
;Reading component status from registry
ReadRegDWORD $AR_RegFlags HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\@CPACK_PACKAGE_INSTALL_REGISTRY_KEY@\Components\${SecName}" "Installed"
IfErrors "default_${SecName}"
IfErrors "checkBeforeDefault_${SecName}"
;Status will stay default if registry value not found
;(component was never installed)
"set_initSection_${SecName}:"
IntOp $AR_RegFlags $AR_RegFlags & ${SF_SELECTED} ;Turn off all other bits
SectionGetFlags ${${SecName}} $AR_SecFlags ;Reading default section flags
IntOp $AR_SecFlags $AR_SecFlags & 0xFFFE ;Turn lowest (enabled) bit off
@ -137,6 +197,18 @@ Var AR_RegFlags
"default_${SecName}:"
!insertmacro LoadSectionSelectedIntoVar ${SecName} ${SecName}_selected
Goto "end_initSection_${SecName}"
"checkBeforeDefault_${SecName}:"
${StrContains} $substringResult "/nSandboxIfNew" $CMDLINE
${If} "${SecName}" == "server"
${AndIfNot} $substringResult == ""
StrCpy $AR_RegFlags 0
Goto "set_initSection_${SecName}"
${Else}
Goto "default_${SecName}"
${EndIf}
"end_initSection_${SecName}:"
!macroend
!macro FinishSection SecName
@ -366,6 +438,7 @@ Var DesktopServerCheckbox
Var ServerStartupCheckbox
Var LaunchServerNowCheckbox
Var LaunchClientNowCheckbox
Var CleanInstallCheckbox
Var CurrentOffset
Var OffsetUnits
Var CopyFromProductionCheckbox
@ -403,27 +476,18 @@ Function PostInstallOptionsPage
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @INTERFACE_HF_SHORTCUT_NAME@"
Pop $DesktopClientCheckbox
IntOp $CurrentOffset $CurrentOffset + 15
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $DesktopClientCheckbox @CLIENT_DESKTOP_SHORTCUT_REG_KEY@ ${BST_CHECKED}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Create a desktop shortcut for @CONSOLE_HF_SHORTCUT_NAME@"
Pop $DesktopServerCheckbox
IntOp $CurrentOffset $CurrentOffset + 15
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $DesktopServerCheckbox @CONSOLE_DESKTOP_SHORTCUT_REG_KEY@ ${BST_UNCHECKED}
IntOp $CurrentOffset $CurrentOffset + 15
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ on startup"
Pop $ServerStartupCheckbox
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED}
IntOp $CurrentOffset $CurrentOffset + 15
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
@ -432,16 +496,40 @@ Function PostInstallOptionsPage
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $LaunchServerNowCheckbox @SERVER_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
${StrContains} $substringResult "/forceNoLaunchServer" $CMDLINE
${IfNot} $substringResult == ""
${NSD_SetState} $LaunchServerNowCheckbox ${BST_UNCHECKED}
${EndIf}
IntOp $CurrentOffset $CurrentOffset + 15
${EndIf}
${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 $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @INTERFACE_HF_SHORTCUT_NAME@ after install"
Pop $LaunchClientNowCheckbox
IntOp $CurrentOffset $CurrentOffset + 30
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $LaunchClientNowCheckbox @CLIENT_LAUNCH_NOW_REG_KEY@ ${BST_CHECKED}
${StrContains} $substringResult "/forceNoLaunchClient" $CMDLINE
${IfNot} $substringResult == ""
${NSD_SetState} $LaunchClientNowCheckbox ${BST_UNCHECKED}
${EndIf}
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Launch @CONSOLE_HF_SHORTCUT_NAME@ on startup"
Pop $ServerStartupCheckbox
IntOp $CurrentOffset $CurrentOffset + 15
; set the checkbox state depending on what is present in the registry
!insertmacro SetPostInstallOption $ServerStartupCheckbox @CONSOLE_STARTUP_REG_KEY@ ${BST_CHECKED}
${EndIf}
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
${NSD_CreateCheckbox} 0 $CurrentOffset$OffsetUnits 100% 10u "&Perform a clean install (Delete older settings and content)"
Pop $CleanInstallCheckbox
IntOp $CurrentOffset $CurrentOffset + 15
${EndIf}
${If} @PR_BUILD@ == 1
@ -463,7 +551,7 @@ Function PostInstallOptionsPage
${NSD_SetState} $CopyFromProductionCheckbox ${BST_CHECKED}
${EndIf}
nsDialogs::Show
FunctionEnd
@ -478,6 +566,7 @@ Var ServerStartupState
Var LaunchServerNowState
Var LaunchClientNowState
Var CopyFromProductionState
Var CleanInstallState
Function ReadPostInstallOptions
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
@ -499,13 +588,18 @@ Function ReadPostInstallOptions
${EndIf}
${If} ${SectionIsSelected} ${@SERVER_COMPONENT_NAME@}
; check if we need to launch the server post-install
${NSD_GetState} $LaunchServerNowCheckbox $LaunchServerNowState
; 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
; check if we need to launch the client post-install
${NSD_GetState} $LaunchClientNowCheckbox $LaunchClientNowState
${EndIf}
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
; check if the user asked for a clean install
${NSD_GetState} $CleanInstallCheckbox $CleanInstallState
${EndIf}
FunctionEnd
@ -548,6 +642,15 @@ Function HandlePostInstallOptions
!insertmacro WritePostInstallOption @CONSOLE_STARTUP_REG_KEY@ NO
${EndIf}
${EndIf}
${If} ${SectionIsSelected} ${@CLIENT_COMPONENT_NAME@}
; check if the user asked for a clean install
${If} $CleanInstallState == ${BST_CHECKED}
SetShellVarContext current
RMDir /r "$APPDATA\@BUILD_ORGANIZATION@"
RMDir /r "$LOCALAPPDATA\@BUILD_ORGANIZATION@"
${EndIf}
${EndIf}
${If} @PR_BUILD@ == 1

View file

@ -14,6 +14,8 @@ if (APPLE)
set_target_properties(${TARGET_NAME} PROPERTIES INSTALL_RPATH "@executable_path/../Frameworks")
endif ()
setup_memory_debugger()
# TODO: find a solution that will handle web file changes in resources on windows without a re-build.
# Currently the resources are only copied on post-build. If one is changed but the domain-server is not, they will
# not be re-copied. This is worked-around on OS X/UNIX by using a symlink.

View file

@ -50,6 +50,7 @@
{
"label": "Places / Paths",
"html_id": "places_paths",
"restart": false,
"settings": [
{
"name": "paths",
@ -75,6 +76,7 @@
{
"name": "descriptors",
"label": "Description",
"restart": false,
"help": "This data will be queryable from your server. It may be collected by High Fidelity and used to share your domain with others.",
"settings": [
{
@ -147,6 +149,7 @@
{
"name": "security",
"label": "Security",
"restart": false,
"settings": [
{
"name": "http_username",
@ -866,6 +869,22 @@
"help": "Limits the scale of avatars in your domain. Cannot be greater than 1000.",
"placeholder": 3.0,
"default": 3.0
},
{
"name": "avatar_whitelist",
"label": "Avatars Allowed from:",
"help": "Comma separated list of URLs (with optional paths) that avatar .fst files are allowed from. If someone attempts to use an avatar with a different domain, it will be rejected and the replacement avatar will be used. If left blank, any domain is allowed.",
"placeholder": "",
"default": "",
"advanced": true
},
{
"name": "replacement_avatar",
"label": "Replacement Avatar for disallowed avatars",
"help": "A URL for an avatar .fst to be used when someone tries to use an avatar that is not allowed. If left blank, the generic default avatar is used.",
"placeholder": "",
"default": "",
"advanced": true
}
]
},
@ -1318,6 +1337,109 @@
"advanced": true
}
]
},
{
"name": "broadcasting",
"label": "Broadcasting",
"restart": false,
"settings": [
{
"name": "users",
"label": "Broadcasted Users",
"type": "table",
"advanced": true,
"can_add_new_rows": true,
"help": "Users that are broadcasted to downstream servers",
"numbered": false,
"columns": [
{
"name": "user",
"label": "User",
"can_set": true
}
]
},
{
"name": "downstream_servers",
"label": "Receiving Servers",
"assignment-types": [0,1],
"type": "table",
"advanced": true,
"can_add_new_rows": true,
"help": "Servers that receive data for broadcasted users",
"numbered": false,
"columns": [
{
"name": "address",
"label": "Address",
"can_set": true
},
{
"name": "port",
"label": "Port",
"can_set": true
},
{
"name": "server_type",
"label": "Server Type",
"type": "select",
"placeholder": "Audio Mixer",
"default": "Audio Mixer",
"can_set": true,
"options": [
{
"value": "Audio Mixer",
"label": "Audio Mixer"
},
{
"value": "Avatar Mixer",
"label": "Avatar Mixer"
}
]
}
]
},
{
"name": "upstream_servers",
"label": "Broadcasting Servers",
"assignment-types": [0,1],
"type": "table",
"advanced": true,
"can_add_new_rows": true,
"help": "Servers that broadcast data to this domain",
"numbered": false,
"columns": [
{
"name": "address",
"label": "Address",
"can_set": true
},
{
"name": "port",
"label": "Port",
"can_set": true
},
{
"name": "server_type",
"label": "Server Type",
"type": "select",
"placeholder": "Audio Mixer",
"default": "Audio Mixer",
"can_set": true,
"options": [
{
"value": "Audio Mixer",
"label": "Audio Mixer"
},
{
"value": "Avatar Mixer",
"label": "Avatar Mixer"
}
]
}
]
}
]
}
]
}

View file

@ -10,7 +10,7 @@
<link href="/css/sweetalert.css" rel="stylesheet" media="screen">
<link href="/css/bootstrap-switch.min.css" rel="stylesheet" media="screen">
<script src='/js/sweetalert.min.js'></script>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
@ -39,7 +39,24 @@
<li><a href="/content/">Content</a></li>
<li><a href="/settings/">Settings</a></li>
</ul>
<ul class="nav navbar-right navbar-nav">
<li><a href="#" id="restart-server"><span class="glyphicon glyphicon-refresh"></span> Restart</a></li>
</ul>
</div>
</div><!-- /.container-fluid -->
</nav>
<div class="modal fade" id="restart-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">domain-server is restarting</h4>
</div>
<div class="modal-body">
<h5>This page will automatically refresh in <span id="refresh-time">3 seconds</span>.</h5>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<div class="container-fluid">

View file

@ -1,3 +1,28 @@
function showRestartModal() {
$('#restart-modal').modal({
backdrop: 'static',
keyboard: false
});
var secondsElapsed = 0;
var numberOfSecondsToWait = 3;
var refreshSpan = $('span#refresh-time')
refreshSpan.html(numberOfSecondsToWait + " seconds");
// call ourselves every 1 second to countdown
var refreshCountdown = setInterval(function(){
secondsElapsed++;
secondsLeft = numberOfSecondsToWait - secondsElapsed
refreshSpan.html(secondsLeft + (secondsLeft == 1 ? " second" : " seconds"))
if (secondsElapsed == numberOfSecondsToWait) {
location.reload(true);
clearInterval(refreshCountdown);
}
}, 1000);
}
$(document).ready(function(){
var url = window.location;
// Will only work if string in href matches with location
@ -7,4 +32,18 @@ $(document).ready(function(){
$('ul.nav a').filter(function() {
return this.href == url;
}).parent().addClass('active');
$('body').on('click', '#restart-server', function(e) {
swal( {
title: "Are you sure?",
text: "This will restart your domain server, causing your domain to be briefly offline.",
type: "warning",
html: true,
showCancelButton: true
}, function() {
$.get("/restart");
showRestartModal();
});
return false;
});
});

View file

@ -2,11 +2,11 @@ $(document).ready(function(){
// setup the underscore templates
var nodeTemplate = _.template($('#nodes-template').html());
var queuedTemplate = _.template($('#queued-template').html());
// setup a function to grab the assignments
function getNodesAndAssignments() {
$.getJSON("nodes.json", function(json){
json.nodes.sort(function(a, b){
if (a.type === b.type) {
if (a.uptime < b.uptime) {
@ -16,36 +16,50 @@ $(document).ready(function(){
} else {
return 0;
}
}
}
if (a.type === "agent" && b.type !== "agent") {
return 1;
} else if (b.type === "agent" && a.type !== "agent") {
return -1;
}
if (a.type > b.type) {
return 1;
}
if (a.type < b.type) {
return -1;
}
}
});
$('#nodes-table tbody').html(nodeTemplate(json));
}).fail(function(jqXHR, textStatus, errorThrown) {
// we assume a 401 means the DS has restarted
// and no longer has our OAuth produced uuid
// so just reload and re-auth
if (jqXHR.status == 401) {
location.reload();
}
});
$.getJSON("assignments.json", function(json){
$.getJSON("assignments.json", function(json){
$('#assignments-table tbody').html(queuedTemplate(json));
}).fail(function(jqXHR, textStatus, errorThrown) {
// we assume a 401 means the DS has restarted
// and no longer has our OAuth produced uuid
// so just reload and re-auth
if (jqXHR.status == 401) {
location.reload();
}
});
}
// do the first GET on page load
getNodesAndAssignments();
// grab the new assignments JSON every two seconds
var getNodesAndAssignmentsInterval = setInterval(getNodesAndAssignments, 2000);
// hook the node delete to the X button
$(document.body).on('click', '.glyphicon-remove', function(){
// fire off a delete for this node
@ -57,10 +71,10 @@ $(document).ready(function(){
}
});
});
$(document.body).on('click', '#kill-all-btn', function() {
var confirmed_kill = confirm("Are you sure?");
if (confirmed_kill == true) {
$.ajax({
url: "/nodes/",

View file

@ -26,7 +26,7 @@
</ul>
<button id="advanced-toggle-button" class="btn btn-info advanced-toggle">Show advanced</button>
<button class="btn btn-success save-button">Save and restart</button>
<button class="btn btn-success save-button">Save</button>
</div>
</div>
@ -77,29 +77,15 @@
</div>
<div class="col-xs-12 hidden-sm hidden-md hidden-lg">
<button class="btn btn-success save-button" id="small-save-button">Save and restart</button>
<button class="btn btn-success save-button" id="small-save-button">Save</button>
</div>
</div>
<div class="modal fade" id="restart-modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">domain-server is restarting</h4>
</div>
<div class="modal-body">
<h5>This page will automatically refresh in <span id="refresh-time">3 seconds</span>.</h5>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!--#include virtual="footer.html"-->
<script src='/js/underscore-min.js'></script>
<script src='/js/underscore-keypath.min.js'></script>
<script src='/js/bootbox.min.js'></script>
<script src='js/bootstrap-switch.min.js'></script>
<script src='/js/sweetalert.min.js'></script>
<script src='js/settings.js'></script>
<script src='js/form2js.min.js'></script>
<script src='js/sha256.js'></script>

View file

@ -39,7 +39,8 @@ var Settings = {
ACCESS_TOKEN_SELECTOR: '[name="metaverse.access_token"]',
PLACES_TABLE_ID: 'places-table',
FORM_ID: 'settings-form',
INVALID_ROW_CLASS: 'invalid-input'
INVALID_ROW_CLASS: 'invalid-input',
DATA_ROW_INDEX: 'data-row-index'
};
var viewHelpers = {
@ -223,6 +224,14 @@ $(document).ready(function(){
// set focus to the first input in the new row
$target.closest('table').find('tr.inputs input:first').focus();
}
var tableRows = sibling.parent();
var tableBody = tableRows.parent();
// if theres no more siblings, we should jump to a new row
if (sibling.next().length == 0 && tableRows.nextAll().length == 1) {
tableBody.find("." + Settings.ADD_ROW_BUTTON_CLASS).click();
}
}
} else if ($target.is('input')) {
@ -415,7 +424,11 @@ function postSettings(jsonSettings) {
type: 'POST'
}).done(function(data){
if (data.status == "success") {
showRestartModal();
if ($(".save-button").html() === SAVE_BUTTON_LABEL_RESTART) {
showRestartModal();
} else {
location.reload(true);
}
} else {
showErrorMessage("Error", SETTINGS_ERROR_MESSAGE)
reloadSettings();
@ -997,7 +1010,7 @@ function saveSettings() {
var password = formJSON["security"]["http_password"];
var verify_password = formJSON["security"]["verify_http_password"];
// if they've only emptied out the default password field, we should go ahead and acknowledge
// if they've only emptied out the default password field, we should go ahead and acknowledge
// the verify password field
if (password != undefined && verify_password == undefined) {
verify_password = "";
@ -1150,8 +1163,9 @@ function makeTable(setting, keypath, setting_value) {
}
html += "<tr class='" + Settings.DATA_ROW_CLASS + "' " +
(isCategorized ? ("data-category='" + categoryValue + "'") : "") + " " +
(isArray ? "" : "name='" + keypath + "." + rowIndexOrName + "'") + ">";
(isCategorized ? ("data-category='" + categoryValue + "'") : "") + " " +
(isArray ? "" : "name='" + keypath + "." + rowIndexOrName + "'") +
(isArray ? Settings.DATA_ROW_INDEX + "='" + (row_num - 1) + "'" : "" ) + ">";
if (setting.numbered === true) {
html += "<td class='numbered'>" + row_num + "</td>"
@ -1281,6 +1295,17 @@ function makeTableHiddenInputs(setting, initialValues, categoryValue) {
"<input type='checkbox' style='display: none;' class='form-control table-checkbox' " +
"name='" + col.name + "'" + (defaultValue ? " checked" : "") + "/>" +
"</td>";
} else if (col.type === "select") {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>"
html += "<select style='display: none;' class='form-control' data-hidden-input='" + col.name + "'>'"
for (var i in col.options) {
var option = col.options[i];
html += "<option value='" + option.value + "' " + (option.value == defaultValue ? 'selected' : '') + ">" + option.label + "</option>";
}
html += "</select>";
html += "<input type='hidden' class='table-dropdown form-control trigger-change' name='" + col.name + "' value='" + defaultValue + "'></td>";
} else {
html +=
"<td " + (col.hidden ? "style='display: none;'" : "") + " class='" + Settings.DATA_COL_CLASS + "' " +
@ -1320,6 +1345,18 @@ function makeTableCategoryInput(setting, numVisibleColumns) {
return html;
}
function getDescriptionForKey(key) {
for (var i in Settings.data.descriptions) {
if (Settings.data.descriptions[i].name === key) {
return Settings.data.descriptions[i];
}
}
}
var SAVE_BUTTON_LABEL_SAVE = "Save";
var SAVE_BUTTON_LABEL_RESTART = "Save and restart";
var reasonsForRestart = [];
function badgeSidebarForDifferences(changedElement) {
// figure out which group this input is in
var panelParentID = changedElement.closest('.panel').attr('id');
@ -1342,13 +1379,24 @@ function badgeSidebarForDifferences(changedElement) {
}
var badgeValue = 0
var description = getDescriptionForKey(panelParentID);
// badge for any settings we have that are not the same or are not present in initialValues
for (var setting in panelJSON) {
if ((!_.has(initialPanelJSON, setting) && panelJSON[setting] !== "") ||
(!_.isEqual(panelJSON[setting], initialPanelJSON[setting])
&& (panelJSON[setting] !== "" || _.has(initialPanelJSON, setting)))) {
badgeValue += 1
badgeValue += 1;
// add a reason to restart
if (description && description.restart != false) {
reasonsForRestart.push(setting);
}
} else {
// remove a reason to restart
if (description && description.restart != false) {
reasonsForRestart = $.grep(reasonsForRestart, function(v) { return v != setting; });
}
}
}
@ -1357,6 +1405,7 @@ function badgeSidebarForDifferences(changedElement) {
badgeValue = ""
}
$(".save-button").html(reasonsForRestart.length > 0 ? SAVE_BUTTON_LABEL_RESTART : SAVE_BUTTON_LABEL_SAVE);
$("a[href='#" + panelParentID + "'] .badge").html(badgeValue);
}
@ -1379,6 +1428,21 @@ function addTableRow(row) {
var setting_name = table.attr("name");
row.addClass(Settings.DATA_ROW_CLASS + " " + Settings.NEW_ROW_CLASS);
// if this is an array, add the row index (which is the index of the last row + 1)
// as a data attribute to the row
var row_index = 0;
if (isArray) {
var previous_row = row.siblings('.' + Settings.DATA_ROW_CLASS + ':last');
if (previous_row.length > 0) {
row_index = parseInt(previous_row.attr(Settings.DATA_ROW_INDEX), 10) + 1;
} else {
row_index = 0;
}
row.attr(Settings.DATA_ROW_INDEX, row_index);
}
var focusChanged = false;
_.each(row.children(), function(element) {
@ -1408,19 +1472,23 @@ function addTableRow(row) {
input.show();
var isCheckbox = input.hasClass("table-checkbox");
var isDropdown = input.hasClass("table-dropdown");
if (isArray) {
var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length
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
var newName = setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : "");
if (isCheckbox) {
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
} else {
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
input.attr("name", newName);
if (isDropdown) {
// default values for hidden inputs inside child selects gets cleared so we need to remind it
var selectElement = $(element).children("select");
selectElement.attr("data-hidden-input", newName);
$(element).children("input").val(selectElement.val());
}
} else {
// because the name of the setting in question requires the key
@ -1436,6 +1504,12 @@ function addTableRow(row) {
focusChanged = true;
}
// if we are adding a dropdown, we should go ahead and make its select
// element is visible
if (isDropdown) {
$(element).children("select").attr("style", "");
}
if (isCheckbox) {
$(input).find("input").attr("data-changed", "true");
} else {
@ -1648,31 +1722,6 @@ function updateDataChangedForSiblingRows(row, forceTrue) {
})
}
function showRestartModal() {
$('#restart-modal').modal({
backdrop: 'static',
keyboard: false
});
var secondsElapsed = 0;
var numberOfSecondsToWait = 3;
var refreshSpan = $('span#refresh-time')
refreshSpan.html(numberOfSecondsToWait + " seconds");
// call ourselves every 1 second to countdown
var refreshCountdown = setInterval(function(){
secondsElapsed++;
secondsLeft = numberOfSecondsToWait - secondsElapsed
refreshSpan.html(secondsLeft + (secondsLeft == 1 ? " second" : " seconds"))
if (secondsElapsed == numberOfSecondsToWait) {
location.reload(true);
clearInterval(refreshCountdown);
}
}, 1000);
}
function cleanupFormValues(node) {
if (node.type && node.type === 'checkbox') {
return { name: node.name, value: node.checked ? true : false };

View file

@ -385,7 +385,7 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
// user is attempting to prove their identity to us, but we don't have enough information
sendConnectionTokenPacket(username, nodeConnection.senderSockAddr);
// ask for their public key right now to make sure we have it
requestUserPublicKey(username);
requestUserPublicKey(username, true);
getGroupMemberships(username); // optimistically get started on group memberships
#ifdef WANT_DEBUG
qDebug() << "stalling login because we have no username-signature:" << username;
@ -435,8 +435,29 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
return SharedNodePointer();
}
QUuid hintNodeID;
// in case this is a node that's failing to connect
// double check we don't have the same node whose sockets match exactly already in the list
limitedNodeList->eachNodeBreakable([&](const SharedNodePointer& node){
if (node->getPublicSocket() == nodeConnection.publicSockAddr && node->getLocalSocket() == nodeConnection.localSockAddr) {
// we have a node that already has these exact sockets - this can occur if a node
// is failing to connect to the domain
// we'll re-use the existing node ID
// as long as the user hasn't changed their username (by logging in or logging out)
auto existingNodeData = static_cast<DomainServerNodeData*>(node->getLinkedData());
if (existingNodeData->getUsername() == username) {
hintNodeID = node->getUUID();
return false;
}
}
return true;
});
// add the connecting node (or re-use the matched one from eachNodeBreakable above)
SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection);
SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection, hintNodeID);
// set the edit rights for this user
newNode->setPermissions(userPerms);
@ -464,12 +485,11 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
return newNode;
}
SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection) {
SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection,
QUuid nodeID) {
HifiSockAddr discoveredSocket = nodeConnection.senderSockAddr;
SharedNetworkPeer connectedPeer = _icePeers.value(nodeConnection.connectUUID);
QUuid nodeID;
if (connectedPeer) {
// this user negotiated a connection with us via ICE, so re-use their ICE client ID
nodeID = nodeConnection.connectUUID;
@ -479,8 +499,10 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node
discoveredSocket = *connectedPeer->getActiveSocket();
}
} else {
// we got a connectUUID we didn't recognize, randomly generate a new one
nodeID = QUuid::createUuid();
// we got a connectUUID we didn't recognize, either use the hinted node ID or randomly generate a new one
if (nodeID.isNull()) {
nodeID = QUuid::createUuid();
}
}
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
@ -499,7 +521,10 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
const HifiSockAddr& senderSockAddr) {
// it's possible this user can be allowed to connect, but we need to check their username signature
auto lowerUsername = username.toLower();
QByteArray publicKeyArray = _userPublicKeys.value(lowerUsername);
KeyFlagPair publicKeyPair = _userPublicKeys.value(lowerUsername);
QByteArray publicKeyArray = publicKeyPair.first;
bool isOptimisticKey = publicKeyPair.second;
const QUuid& connectionToken = _connectionTokenHash.value(lowerUsername);
@ -533,10 +558,16 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
return true;
} else {
if (!senderSockAddr.isNull()) {
qDebug() << "Error decrypting username signature for " << username << "- denying connection.";
// we only send back a LoginError if this wasn't an "optimistic" key
// (a key that we hoped would work but is probably stale)
if (!senderSockAddr.isNull() && !isOptimisticKey) {
qDebug() << "Error decrypting username signature for" << username << "- denying connection.";
sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::LoginError);
} else if (!senderSockAddr.isNull()) {
qDebug() << "Error decrypting username signature for" << username << "with optimisitic key -"
<< "re-requesting public key and delaying connection";
}
// free up the public key, we don't need it anymore
@ -582,20 +613,7 @@ bool DomainGatekeeper::isWithinMaxCapacity() {
return true;
}
void DomainGatekeeper::preloadAllowedUserPublicKeys() {
QStringList allowedUsers = _server->_settingsManager.getAllNames();
if (allowedUsers.size() > 0) {
// in the future we may need to limit how many requests here - for now assume that lists of allowed users are not
// going to create > 100 requests
foreach(const QString& username, allowedUsers) {
requestUserPublicKey(username);
}
}
}
void DomainGatekeeper::requestUserPublicKey(const QString& username) {
void DomainGatekeeper::requestUserPublicKey(const QString& username, bool isOptimistic) {
// don't request public keys for the standard psuedo-account-names
if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) {
return;
@ -606,7 +624,7 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) {
// public-key request for this username is already flight, not rerequesting
return;
}
_inFlightPublicKeyRequests += lowerUsername;
_inFlightPublicKeyRequests.insert(lowerUsername, isOptimistic);
// even if we have a public key for them right now, request a new one in case it has just changed
JSONCallbackParameters callbackParams;
@ -618,7 +636,7 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) {
const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key";
qDebug() << "Requesting public key for user" << username;
qDebug().nospace() << "Requesting " << (isOptimistic ? "optimistic " : " ") << "public key for user " << username;
DependencyManager::get<AccountManager>()->sendRequest(USER_PUBLIC_KEY_PATH.arg(username),
AccountManagerAuth::None,
@ -640,16 +658,21 @@ void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply& requestReply) {
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
QString username = extractUsernameFromPublicKeyRequest(requestReply);
bool isOptimisticKey = _inFlightPublicKeyRequests.take(username);
if (jsonObject["status"].toString() == "success" && !username.isEmpty()) {
// pull the public key as a QByteArray from this response
const QString JSON_DATA_KEY = "data";
const QString JSON_PUBLIC_KEY_KEY = "public_key";
_userPublicKeys[username.toLower()] =
QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8());
}
qDebug().nospace() << "Extracted " << (isOptimisticKey ? "optimistic " : " ") << "public key for " << username.toLower();
_inFlightPublicKeyRequests.remove(username);
_userPublicKeys[username.toLower()] =
{
QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8()),
isOptimisticKey
};
}
}
void DomainGatekeeper::publicKeyJSONErrorCallback(QNetworkReply& requestReply) {
@ -905,9 +928,12 @@ void DomainGatekeeper::getDomainOwnerFriendsList() {
callbackParams.errorCallbackMethod = "getDomainOwnerFriendsListErrorCallback";
const QString GET_FRIENDS_LIST_PATH = "api/v1/user/friends";
DependencyManager::get<AccountManager>()->sendRequest(GET_FRIENDS_LIST_PATH, AccountManagerAuth::Required,
QNetworkAccessManager::GetOperation, callbackParams, QByteArray(),
NULL, QVariantMap());
if (DependencyManager::get<AccountManager>()->hasValidAccessToken()) {
DependencyManager::get<AccountManager>()->sendRequest(GET_FRIENDS_LIST_PATH, AccountManagerAuth::Required,
QNetworkAccessManager::GetOperation, callbackParams, QByteArray(),
NULL, QVariantMap());
}
}
void DomainGatekeeper::getDomainOwnerFriendsListJSONCallback(QNetworkReply& requestReply) {

View file

@ -39,8 +39,6 @@ public:
const QUuid& walletUUID, const QString& nodeVersion);
QUuid assignmentUUIDForPendingAssignment(const QUuid& tempUUID);
void preloadAllowedUserPublicKeys();
void removeICEPeer(const QUuid& peerUUID) { _icePeers.remove(peerUUID); }
static void sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr);
@ -76,7 +74,8 @@ private:
SharedNodePointer processAgentConnectRequest(const NodeConnectionData& nodeConnection,
const QString& username,
const QByteArray& usernameSignature);
SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection);
SharedNodePointer addVerifiedNodeFromConnectRequest(const NodeConnectionData& nodeConnection,
QUuid nodeID = QUuid());
bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr);
@ -92,7 +91,7 @@ private:
void pingPunchForConnectingPeer(const SharedNetworkPeer& peer);
void requestUserPublicKey(const QString& username);
void requestUserPublicKey(const QString& username, bool isOptimistic = false);
DomainServer* _server;
@ -101,8 +100,17 @@ private:
QHash<QUuid, SharedNetworkPeer> _icePeers;
QHash<QString, QUuid> _connectionTokenHash;
QHash<QString, QByteArray> _userPublicKeys;
QSet<QString> _inFlightPublicKeyRequests; // keep track of which we've already asked for
// the word "optimistic" below is used for keys that we request during user connection before the user has
// had a chance to upload a new public key
// we don't send back user signature decryption errors for those keys so that there isn't a thrasing of key re-generation
// and connection refusal
using KeyFlagPair = QPair<QByteArray, bool>;
QHash<QString, KeyFlagPair> _userPublicKeys; // keep track of keys and flag them as optimistic or not
QHash<QString, bool> _inFlightPublicKeyRequests; // keep track of keys we've asked for (and if it was optimistic)
QSet<QString> _domainOwnerFriends; // keep track of friends of the domain owner
QSet<QString> _inFlightGroupMembershipsRequests; // keep track of which we've already asked for

View file

@ -171,7 +171,7 @@ void DomainMetadata::maybeUpdateUsers() {
if (linkedData) {
auto nodeData = static_cast<DomainServerNodeData*>(linkedData);
if (!nodeData->wasAssigned()) {
if (!nodeData->wasAssigned() && node->getType() == NodeType::Agent) {
++numConnected;
if (nodeData->getUsername().isEmpty()) {

View file

@ -40,11 +40,11 @@
#include <LogHandler.h>
#include <PathUtils.h>
#include <NumericalConstants.h>
#include <Trace.h>
#include <StatTracker.h>
#include "DomainServerNodeData.h"
#include "NodeConnectionData.h"
#include <Trace.h>
#include <StatTracker.h>
int const DomainServer::EXIT_CODE_REBOOT = 234923;
@ -87,7 +87,7 @@ DomainServer::DomainServer(int argc, char* argv[]) :
qDebug() << "[VERSION] VERSION:" << BuildInfo::VERSION;
qDebug() << "[VERSION] BUILD_BRANCH:" << BuildInfo::BUILD_BRANCH;
qDebug() << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES;
qDebug() << "[VERSION] We will be using this default ICE server:" << ICE_SERVER_DEFAULT_HOSTNAME;
qDebug() << "[VERSION] We will be using this name to find ICE servers:" << _iceServerAddr;
// make sure we have a fresh AccountManager instance
@ -117,6 +117,12 @@ DomainServer::DomainServer(int argc, char* argv[]) :
// if permissions are updated, relay the changes to the Node datastructures
connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions,
&_gatekeeper, &DomainGatekeeper::updateNodePermissions);
connect(&_settingsManager, &DomainServerSettingsManager::settingsUpdated,
this, &DomainServer::updateReplicatedNodes);
connect(&_settingsManager, &DomainServerSettingsManager::settingsUpdated,
this, &DomainServer::updateDownstreamNodes);
connect(&_settingsManager, &DomainServerSettingsManager::settingsUpdated,
this, &DomainServer::updateUpstreamNodes);
setupGroupCacheRefresh();
@ -129,6 +135,10 @@ DomainServer::DomainServer(int argc, char* argv[]) :
setupNodeListAndAssignments();
updateReplicatedNodes();
updateDownstreamNodes();
updateUpstreamNodes();
if (_type != NonMetaverse) {
// if we have a metaverse domain, we'll use an access token for API calls
resetAccountManagerAccessToken();
@ -150,10 +160,10 @@ DomainServer::DomainServer(int argc, char* argv[]) :
getTemporaryName();
}
_gatekeeper.preloadAllowedUserPublicKeys(); // so they can connect on first request
// send signal to DomainMetadata when descriptors changed
_metadata = new DomainMetadata(this);
connect(&_settingsManager, &DomainServerSettingsManager::settingsUpdated,
_metadata, &DomainMetadata::descriptorsChanged);
qDebug() << "domain-server is running";
static const QString AC_SUBNET_WHITELIST_SETTING_PATH = "security.ac_subnet_whitelist";
@ -211,6 +221,8 @@ void DomainServer::parseCommandLine() {
const QCommandLineOption masterConfigOption("master-config", "Deprecated config-file option");
parser.addOption(masterConfigOption);
const QCommandLineOption parentPIDOption(PARENT_PID_OPTION, "PID of the parent process", "parent-pid");
parser.addOption(parentPIDOption);
if (!parser.parse(QCoreApplication::arguments())) {
qWarning() << parser.errorText() << endl;
@ -239,6 +251,17 @@ void DomainServer::parseCommandLine() {
_overrideDomainID = true;
qDebug() << "domain-server ID is" << _overridingDomainID;
}
if (parser.isSet(parentPIDOption)) {
bool ok = false;
int parentPID = parser.value(parentPIDOption).toInt(&ok);
if (ok) {
qDebug() << "Parent process PID is" << parentPID;
watchParentProcess(parentPID);
}
}
}
DomainServer::~DomainServer() {
@ -958,6 +981,11 @@ void DomainServer::handleConnectedNode(SharedNodePointer newNode) {
emit userConnected();
}
if (shouldReplicateNode(*newNode)) {
qDebug() << "Setting node to replicated: " << newNode->getUUID();
newNode->setIsReplicated(true);
}
// send out this node to our other connected nodes
broadcastNewNode(newNode);
}
@ -1537,7 +1565,7 @@ void DomainServer::sendHeartbeatToIceServer() {
} else {
qDebug() << "Not sending ice-server heartbeat since there is no selected ice-server.";
qDebug() << "Waiting for" << ICE_SERVER_DEFAULT_HOSTNAME << "host lookup response";
qDebug() << "Waiting for" << _iceServerAddr << "host lookup response";
}
}
@ -1650,6 +1678,7 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
const QString URI_NODES = "/nodes";
const QString URI_SETTINGS = "/settings";
const QString URI_ENTITY_FILE_UPLOAD = "/content/upload";
const QString URI_RESTART = "/restart";
const QString UUID_REGEX_STRING = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
@ -1804,6 +1833,10 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
// send the response
connection->respond(HTTPConnection::StatusCode200, nodesDocument.toJson(), qPrintable(JSON_MIME_TYPE));
return true;
} else if (url.path() == URI_RESTART) {
connection->respond(HTTPConnection::StatusCode200);
restart();
return true;
} else {
// check if this is for json stats for a node
@ -1939,7 +1972,8 @@ bool DomainServer::handleHTTPRequest(HTTPConnection* connection, const QUrl& url
return _settingsManager.handleAuthenticatedHTTPRequest(connection, url);
}
const QString HIFI_SESSION_COOKIE_KEY = "DS_WEB_SESSION_UUID";
static const QString HIFI_SESSION_COOKIE_KEY = "DS_WEB_SESSION_UUID";
static const QString STATE_QUERY_KEY = "state";
bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &url, bool skipSubHandler) {
qDebug() << "HTTPS request received at" << url.toString();
@ -1950,10 +1984,9 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u
const QString CODE_QUERY_KEY = "code";
QString authorizationCode = codeURLQuery.queryItemValue(CODE_QUERY_KEY);
const QString STATE_QUERY_KEY = "state";
QUuid stateUUID = QUuid(codeURLQuery.queryItemValue(STATE_QUERY_KEY));
if (!authorizationCode.isEmpty() && !stateUUID.isNull()) {
if (!authorizationCode.isEmpty() && !stateUUID.isNull() && _webAuthenticationStateSet.remove(stateUUID)) {
// fire off a request with this code and state to get an access token for the user
const QString OAUTH_TOKEN_REQUEST_PATH = "/oauth/token";
@ -1971,47 +2004,83 @@ bool DomainServer::handleHTTPSRequest(HTTPSConnection* connection, const QUrl &u
tokenRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
QNetworkReply* tokenReply = NetworkAccessManager::getInstance().post(tokenRequest, tokenPostBody.toLocal8Bit());
connect(tokenReply, &QNetworkReply::finished, this, &DomainServer::tokenGrantFinished);
if (_webAuthenticationStateSet.remove(stateUUID)) {
// this is a web user who wants to auth to access web interface
// we hold the response back to them until we get their profile information
// and can decide if they are let in or not
// add this connection to our list of pending connections so that we can hold the response
_pendingOAuthConnections.insert(stateUUID, connection);
QEventLoop loop;
connect(tokenReply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
// set the state UUID on the reply so that we can associate the response with the connection later
tokenReply->setProperty(STATE_QUERY_KEY.toLocal8Bit(), stateUUID);
// start the loop for the token request
loop.exec();
return true;
} else {
connection->respond(HTTPConnection::StatusCode400);
QNetworkReply* profileReply = profileRequestGivenTokenReply(tokenReply);
return true;
}
} else {
return false;
}
}
// stop the loop once the profileReply is complete
connect(profileReply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
HTTPSConnection* DomainServer::connectionFromReplyWithState(QNetworkReply* reply) {
// grab the UUID state property from the reply
QUuid stateUUID = reply->property(STATE_QUERY_KEY.toLocal8Bit()).toUuid();
// restart the loop for the profile request
loop.exec();
if (!stateUUID.isNull()) {
return _pendingOAuthConnections.take(stateUUID);
} else {
return nullptr;
}
}
void DomainServer::tokenGrantFinished() {
auto tokenReply = qobject_cast<QNetworkReply*>(sender());
if (tokenReply) {
if (tokenReply->error() == QNetworkReply::NoError) {
// now that we have a token for this profile, send off a profile request
QNetworkReply* profileReply = profileRequestGivenTokenReply(tokenReply);
// forward along the state UUID that we kept with the token request
profileReply->setProperty(STATE_QUERY_KEY.toLocal8Bit(), tokenReply->property(STATE_QUERY_KEY.toLocal8Bit()));
connect(profileReply, &QNetworkReply::finished, this, &DomainServer::profileRequestFinished);
} else {
// the token grant failed, send back a 500 (assuming the connection is still around)
auto connection = connectionFromReplyWithState(tokenReply);
if (connection) {
connection->respond(HTTPConnection::StatusCode500);
}
}
tokenReply->deleteLater();
}
}
void DomainServer::profileRequestFinished() {
auto profileReply = qobject_cast<QNetworkReply*>(sender());
if (profileReply) {
auto connection = connectionFromReplyWithState(profileReply);
if (connection) {
if (profileReply->error() == QNetworkReply::NoError) {
// call helper method to get cookieHeaders
Headers cookieHeaders = setupCookieHeadersFromProfileReply(profileReply);
connection->respond(HTTPConnection::StatusCode302, QByteArray(),
HTTPConnection::DefaultContentType, cookieHeaders);
delete tokenReply;
delete profileReply;
// we've redirected the user back to our homepage
return true;
} else {
// the profile request failed, send back a 500 (assuming the connection is still around)
connection->respond(HTTPConnection::StatusCode500);
}
}
// respond with a 200 code indicating that login is complete
connection->respond(HTTPConnection::StatusCode200);
return true;
} else {
return false;
profileReply->deleteLater();
}
}
@ -2071,22 +2140,31 @@ bool DomainServer::isAuthenticatedRequest(HTTPConnection* connection, const QUrl
// the user does not have allowed username or role, return 401
return false;
} else {
// re-direct this user to OAuth page
static const QByteArray REQUESTED_WITH_HEADER = "X-Requested-With";
static const QString XML_REQUESTED_WITH = "XMLHttpRequest";
// generate a random state UUID to use
QUuid stateUUID = QUuid::createUuid();
if (connection->requestHeaders().value(REQUESTED_WITH_HEADER) == XML_REQUESTED_WITH) {
// unauthorized XHR requests get a 401 and not a 302, since there isn't an XHR
// path to OAuth authorize
connection->respond(HTTPConnection::StatusCode401, UNAUTHENTICATED_BODY);
} else {
// re-direct this user to OAuth page
// add it to the set so we can handle the callback from the OAuth provider
_webAuthenticationStateSet.insert(stateUUID);
// generate a random state UUID to use
QUuid stateUUID = QUuid::createUuid();
QUrl authURL = oauthAuthorizationURL(stateUUID);
// add it to the set so we can handle the callback from the OAuth provider
_webAuthenticationStateSet.insert(stateUUID);
Headers redirectHeaders;
QUrl authURL = oauthAuthorizationURL(stateUUID);
redirectHeaders.insert("Location", authURL.toEncoded());
Headers redirectHeaders;
connection->respond(HTTPConnection::StatusCode302,
QByteArray(), HTTPConnection::DefaultContentType, redirectHeaders);
redirectHeaders.insert("Location", authURL.toEncoded());
connection->respond(HTTPConnection::StatusCode302,
QByteArray(), HTTPConnection::DefaultContentType, redirectHeaders);
}
// we don't know about this user yet, so they are not yet authenticated
return false;
@ -2210,6 +2288,172 @@ void DomainServer::refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer&
_unfulfilledAssignments.enqueue(assignment);
}
static const QString BROADCASTING_SETTINGS_KEY = "broadcasting";
struct ReplicationServerInfo {
NodeType_t nodeType;
HifiSockAddr sockAddr;
};
ReplicationServerInfo serverInformationFromSettings(QVariantMap serverMap, ReplicationServerDirection direction) {
static const QString REPLICATION_SERVER_ADDRESS = "address";
static const QString REPLICATION_SERVER_PORT = "port";
static const QString REPLICATION_SERVER_TYPE = "server_type";
if (serverMap.contains(REPLICATION_SERVER_ADDRESS) && serverMap.contains(REPLICATION_SERVER_PORT)
&& serverMap.contains(REPLICATION_SERVER_TYPE)) {
auto nodeType = NodeType::fromString(serverMap[REPLICATION_SERVER_TYPE].toString());
ReplicationServerInfo serverInfo;
if (direction == Upstream) {
serverInfo.nodeType = NodeType::upstreamType(nodeType);
} else if (direction == Downstream) {
serverInfo.nodeType = NodeType::downstreamType(nodeType);
}
// read the address and port and construct a HifiSockAddr from them
serverInfo.sockAddr = {
serverMap[REPLICATION_SERVER_ADDRESS].toString(),
(quint16) serverMap[REPLICATION_SERVER_PORT].toString().toInt()
};
return serverInfo;
}
return { NodeType::Unassigned, HifiSockAddr() };
}
void DomainServer::updateReplicationNodes(ReplicationServerDirection direction) {
auto settings = _settingsManager.getSettingsMap();
if (settings.contains(BROADCASTING_SETTINGS_KEY)) {
auto nodeList = DependencyManager::get<LimitedNodeList>();
std::vector<HifiSockAddr> replicationNodesInSettings;
auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap();
QString serversKey = direction == Upstream ? "upstream_servers" : "downstream_servers";
QString replicationDirection = direction == Upstream ? "upstream" : "downstream";
if (replicationSettings.contains(serversKey)) {
auto serversSettings = replicationSettings.value(serversKey).toList();
std::vector<HifiSockAddr> knownReplicationNodes;
nodeList->eachNode([&](const SharedNodePointer& otherNode) {
if ((direction == Upstream && NodeType::isUpstream(otherNode->getType()))
|| (direction == Downstream && NodeType::isDownstream(otherNode->getType()))) {
knownReplicationNodes.push_back(otherNode->getPublicSocket());
}
});
for (auto& server : serversSettings) {
auto replicationServer = serverInformationFromSettings(server.toMap(), direction);
if (!replicationServer.sockAddr.isNull() && replicationServer.nodeType != NodeType::Unassigned) {
// make sure we have the settings we need for this replication server
replicationNodesInSettings.push_back(replicationServer.sockAddr);
bool knownNode = find(knownReplicationNodes.cbegin(), knownReplicationNodes.cend(),
replicationServer.sockAddr) != knownReplicationNodes.cend();
if (!knownNode) {
// manually add the replication node to our node list
auto node = nodeList->addOrUpdateNode(QUuid::createUuid(), replicationServer.nodeType,
replicationServer.sockAddr, replicationServer.sockAddr,
false, direction == Upstream);
node->setIsForcedNeverSilent(true);
qDebug() << "Adding" << (direction == Upstream ? "upstream" : "downstream")
<< "node:" << node->getUUID() << replicationServer.sockAddr;
// manually activate the public socket for the replication node
node->activatePublicSocket();
}
}
}
}
// enumerate the nodes to determine which are no longer downstream for this domain
// collect them in a vector to separately remove them with handleKillNode (since eachNode has a read lock and
// we cannot recursively take the write lock required by handleKillNode)
std::vector<SharedNodePointer> nodesToKill;
nodeList->eachNode([&](const SharedNodePointer& otherNode) {
if ((direction == Upstream && NodeType::isUpstream(otherNode->getType()))
|| (direction == Downstream && NodeType::isDownstream(otherNode->getType()))) {
bool nodeInSettings = find(replicationNodesInSettings.cbegin(), replicationNodesInSettings.cend(),
otherNode->getPublicSocket()) != replicationNodesInSettings.cend();
if (!nodeInSettings) {
qDebug() << "Removing" << replicationDirection
<< "node:" << otherNode->getUUID() << otherNode->getPublicSocket();
nodesToKill.push_back(otherNode);
}
}
});
for (auto& node : nodesToKill) {
handleKillNode(node);
}
}
}
void DomainServer::updateDownstreamNodes() {
updateReplicationNodes(Downstream);
}
void DomainServer::updateUpstreamNodes() {
updateReplicationNodes(Upstream);
}
void DomainServer::updateReplicatedNodes() {
// Make sure we have downstream nodes in our list
auto settings = _settingsManager.getSettingsMap();
static const QString REPLICATED_USERS_KEY = "users";
_replicatedUsernames.clear();
if (settings.contains(BROADCASTING_SETTINGS_KEY)) {
auto replicationSettings = settings.value(BROADCASTING_SETTINGS_KEY).toMap();
if (replicationSettings.contains(REPLICATED_USERS_KEY)) {
auto usersSettings = replicationSettings.value(REPLICATED_USERS_KEY).toList();
for (auto& username : usersSettings) {
_replicatedUsernames.push_back(username.toString().toLower());
}
}
}
auto nodeList = DependencyManager::get<LimitedNodeList>();
nodeList->eachMatchingNode([this](const SharedNodePointer& otherNode) -> bool {
return otherNode->getType() == NodeType::Agent;
}, [this](const SharedNodePointer& otherNode) {
auto shouldReplicate = shouldReplicateNode(*otherNode);
auto isReplicated = otherNode->isReplicated();
if (isReplicated && !shouldReplicate) {
qDebug() << "Setting node to NOT be replicated:"
<< otherNode->getPermissions().getVerifiedUserName() << otherNode->getUUID();
} else if (!isReplicated && shouldReplicate) {
qDebug() << "Setting node to replicated:"
<< otherNode->getPermissions().getVerifiedUserName() << otherNode->getUUID();
}
otherNode->setIsReplicated(shouldReplicate);
}
);
}
bool DomainServer::shouldReplicateNode(const Node& node) {
if (node.getType() == NodeType::Agent) {
QString verifiedUsername = node.getPermissions().getVerifiedUserName();
// Both the verified username and usernames in _replicatedUsernames are lowercase, so
// comparisons here are case-insensitive.
auto it = find(_replicatedUsernames.cbegin(), _replicatedUsernames.cend(), verifiedUsername);
return it != _replicatedUsernames.end();
} else {
return false;
}
};
void DomainServer::nodeAdded(SharedNodePointer node) {
// we don't use updateNodeWithData, so add the DomainServerNodeData to the node here
node->setLinkedData(std::unique_ptr<DomainServerNodeData> { new DomainServerNodeData() });
@ -2497,7 +2741,7 @@ void DomainServer::handleICEHostInfo(const QHostInfo& hostInfo) {
_iceAddressLookupID = -1;
if (hostInfo.error() != QHostInfo::NoError) {
qWarning() << "IP address lookup failed for" << ICE_SERVER_DEFAULT_HOSTNAME << ":" << hostInfo.errorString();
qWarning() << "IP address lookup failed for" << _iceServerAddr << ":" << hostInfo.errorString();
// if we don't have an ICE server to use yet, trigger a retry
if (_iceServerSocket.isNull()) {
@ -2512,7 +2756,7 @@ void DomainServer::handleICEHostInfo(const QHostInfo& hostInfo) {
_iceServerAddresses = hostInfo.addresses();
if (countBefore == 0) {
qInfo() << "Found" << _iceServerAddresses.count() << "ice-server IP addresses for" << ICE_SERVER_DEFAULT_HOSTNAME;
qInfo() << "Found" << _iceServerAddresses.count() << "ice-server IP addresses for" << _iceServerAddr;
}
if (_iceServerSocket.isNull()) {
@ -2547,7 +2791,7 @@ void DomainServer::randomizeICEServerAddress(bool shouldTriggerHostLookup) {
// so clear the set of failed addresses and start going through them again
qWarning() << "All current ice-server addresses have failed - re-attempting all current addresses for"
<< ICE_SERVER_DEFAULT_HOSTNAME;
<< _iceServerAddr;
_failedIceServerAddresses.clear();
candidateICEAddresses = _iceServerAddresses;

View file

@ -39,6 +39,11 @@ typedef QMultiHash<QUuid, WalletTransaction*> TransactionHash;
using Subnet = QPair<QHostAddress, int>;
using SubnetList = std::vector<Subnet>;
enum ReplicationServerDirection {
Upstream,
Downstream
};
class DomainServer : public QCoreApplication, public HTTPSRequestHandler {
Q_OBJECT
public:
@ -102,6 +107,13 @@ private slots:
void handleOctreeFileReplacement(QByteArray octreeFile);
void updateReplicatedNodes();
void updateDownstreamNodes();
void updateUpstreamNodes();
void tokenGrantFinished();
void profileRequestFinished();
signals:
void iceServerChanged();
void userConnected();
@ -161,12 +173,20 @@ private:
QJsonObject jsonForSocket(const HifiSockAddr& socket);
QJsonObject jsonObjectForNode(const SharedNodePointer& node);
bool shouldReplicateNode(const Node& node);
void setupGroupCacheRefresh();
QString pathForRedirect(QString path = QString()) const;
void updateReplicationNodes(ReplicationServerDirection direction);
HTTPSConnection* connectionFromReplyWithState(QNetworkReply* reply);
SubnetList _acSubnetWhitelist;
std::vector<QString> _replicatedUsernames;
DomainGatekeeper _gatekeeper;
HTTPManager _httpManager;
@ -220,6 +240,8 @@ private:
bool _sendICEServerAddressToMetaverseAPIInProgress { false };
bool _sendICEServerAddressToMetaverseAPIRedo { false };
QHash<QUuid, QPointer<HTTPSConnection>> _pendingOAuthConnections;
};

View file

@ -991,6 +991,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
unpackPermissions();
apiRefreshGroupInformation();
emit updateNodePermissions();
emit settingsUpdated();
}
return true;
@ -1196,13 +1197,15 @@ 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";
static const QString BROADCASTING_KEY = "broadcasting";
static const QString DESCRIPTION_ROOT_KEY = "descriptors";
auto& settingsVariant = _configMap.getConfig();
bool needRestart = false;
// Iterate on the setting groups
foreach(const QString& rootKey, postedObject.keys()) {
QJsonValue rootValue = postedObject[rootKey];
const QJsonValue& rootValue = postedObject[rootKey];
if (!settingsVariant.contains(rootKey)) {
// we don't have a map below this key yet, so set it up now
@ -1247,7 +1250,7 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
if (rootKey != SECURITY_ROOT_KEY) {
if (rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY && rootKey != SETTINGS_PATHS_KEY ) {
needRestart = true;
}
} else {
@ -1261,9 +1264,10 @@ bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
// if we matched the setting then update the value
if (!matchingDescriptionObject.isEmpty()) {
QJsonValue settingValue = rootValue.toObject()[settingKey];
const QJsonValue& settingValue = rootValue.toObject()[settingKey];
updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
if (rootKey != SECURITY_ROOT_KEY || settingKey == AC_SUBNET_WHITELIST_KEY) {
if ((rootKey != SECURITY_ROOT_KEY && rootKey != BROADCASTING_KEY && rootKey != DESCRIPTION_ROOT_KEY)
|| settingKey == AC_SUBNET_WHITELIST_KEY) {
needRestart = true;
}
} else {

View file

@ -108,6 +108,7 @@ public:
signals:
void updateNodePermissions();
void settingsUpdated();
public slots:
void apiGetGroupIDJSONCallback(QNetworkReply& requestReply);

View file

@ -63,10 +63,7 @@ void RenderingClient::sendAvatarPacket() {
}
void RenderingClient::cleanupBeforeQuit() {
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(),
"stop", Qt::BlockingQueuedConnection);
DependencyManager::get<AudioClient>()->cleanupBeforeQuit();
// destroy the AudioClient so it and its thread will safely go down
DependencyManager::destroy<AudioClient>();
}

View file

@ -18,5 +18,7 @@ endif ()
include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}")
setup_memory_debugger()
# append OpenSSL to our list of libraries to link
target_link_libraries(${TARGET_NAME} ${OPENSSL_LIBRARIES})

View file

@ -4,6 +4,8 @@ project(${TARGET_NAME})
# set a default root dir for each of our optional externals if it was not passed
set(OPTIONAL_EXTERNALS "LeapMotion")
setup_memory_debugger()
foreach(EXTERNAL ${OPTIONAL_EXTERNALS})
string(TOUPPER ${EXTERNAL} ${EXTERNAL}_UPPERCASE)
if (NOT ${${EXTERNAL}_UPPERCASE}_ROOT_DIR)

View file

@ -10,7 +10,7 @@ Interface has been tested with SDK versions:
1. Copy the LeapSDK folders from the LeapDeveloperKit installation directory (Lib, Include) into the interface/externals/leapmotion folder.
This readme.txt should be there as well.
The files neeeded in the folders are:
The files needed in the folders are:
include/
- Leap.h
@ -21,8 +21,8 @@ Interface has been tested with SDK versions:
x86/
- Leap.dll
- Leap.lib
- mscvcp120.dll (optional if you already have the Msdev 2012 SDK redistriuable installed)
- mscvcr120.dll (optional if you already have the Msdev 2012 SDK redistriuable installed)
- mscvcp120.dll (optional if you already have the Msdev 2012 SDK redistributable installed)
- mscvcr120.dll (optional if you already have the Msdev 2012 SDK redistributable installed)
- lipLeap.dylib
libc++/
-libLeap.dylib
@ -30,4 +30,4 @@ Interface has been tested with SDK versions:
You may optionally choose to copy the SDK folders to a location outside the repository (so you can re-use with different checkouts and different projects).
If so our CMake find module expects you to set the ENV variable 'HIFI_LIB_DIR' to a directory containing a subfolder 'leapmotion' that contains the 2 folders mentioned above (Include, Lib).
2. Clear your build directory, run cmake and build, and you should be all set.
2. Clear your build directory, run cmake and build, and you should be all set.

View file

@ -68,7 +68,10 @@
"typeVar": "rightHandType",
"weightVar": "rightHandWeight",
"weight": 1.0,
"flexCoefficients": [1, 0.5, 0.5, 0.2, 0.01, 0.005, 0.001, 0.0, 0.0]
"flexCoefficients": [1, 0.5, 0.5, 0.2, 0.01, 0.005, 0.001, 0.0, 0.0],
"poleVectorEnabledVar": "rightHandPoleVectorEnabled",
"poleReferenceVectorVar": "rightHandPoleReferenceVector",
"poleVectorVar": "rightHandPoleVector"
},
{
"jointName": "LeftHand",
@ -77,7 +80,10 @@
"typeVar": "leftHandType",
"weightVar": "leftHandWeight",
"weight": 1.0,
"flexCoefficients": [1, 0.5, 0.5, 0.2, 0.01, 0.005, 0.001, 0.0, 0.0]
"flexCoefficients": [1, 0.5, 0.5, 0.2, 0.01, 0.005, 0.001, 0.0, 0.0],
"poleVectorEnabledVar": "leftHandPoleVectorEnabled",
"poleReferenceVectorVar": "leftHandPoleReferenceVector",
"poleVectorVar": "leftHandPoleVector"
},
{
"jointName": "RightFoot",
@ -86,7 +92,10 @@
"typeVar": "rightFootType",
"weightVar": "rightFootWeight",
"weight": 1.0,
"flexCoefficients": [1, 0.45, 0.45]
"flexCoefficients": [1, 0.45, 0.45],
"poleVectorEnabledVar": "rightFootPoleVectorEnabled",
"poleReferenceVectorVar": "rightFootPoleReferenceVector",
"poleVectorVar": "rightFootPoleVector"
},
{
"jointName": "LeftFoot",
@ -95,7 +104,10 @@
"typeVar": "leftFootType",
"weightVar": "leftFootWeight",
"weight": 1.0,
"flexCoefficients": [1, 0.45, 0.45]
"flexCoefficients": [1, 0.45, 0.45],
"poleVectorEnabledVar": "leftFootPoleVectorEnabled",
"poleReferenceVectorVar": "leftFootPoleReferenceVector",
"poleVectorVar": "leftFootPoleVector"
},
{
"jointName": "Spine2",
@ -138,28 +150,19 @@
"children": []
},
{
"id": "hipsManipulatorOverlay",
"id": "defaultPoseOverlay",
"type": "overlay",
"data": {
"alpha": 0.0,
"boneSet": "hipsOnly"
"alphaVar": "defaultPoseOverlayAlpha",
"boneSet": "fullBody",
"boneSetVar": "defaultPoseOverlayBoneSet"
},
"children": [
{
"id": "hipsManipulator",
"type": "manipulator",
"id": "defaultPose",
"type": "defaultPose",
"data": {
"alpha": 0.0,
"alphaVar": "hipsManipulatorAlpha",
"joints": [
{
"jointName": "Hips",
"rotationType": "absolute",
"translationType": "absolute",
"rotationVar": "hipsManipulatorRotation",
"translationVar": "hipsManipulatorPosition"
}
]
},
"children": []
},

View file

@ -81,7 +81,11 @@
{ "from": { "makeAxis" : ["Keyboard.MouseMoveLeft", "Keyboard.MouseMoveRight"] },
"when": "Keyboard.RightMouseButton",
"to": "Actions.Yaw"
"to": "Actions.Yaw",
"filters":
[
{ "type": "scale", "scale": 0.6 }
]
},
{ "from": "Keyboard.W", "to": "Actions.LONGITUDINAL_FORWARD" },
@ -102,8 +106,19 @@
{ "from": "Keyboard.PgDown", "to": "Actions.VERTICAL_DOWN" },
{ "from": "Keyboard.PgUp", "to": "Actions.VERTICAL_UP" },
{ "from": "Keyboard.MouseMoveUp", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_UP" },
{ "from": "Keyboard.MouseMoveDown", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_DOWN" },
{ "from": "Keyboard.MouseMoveUp", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_UP",
"filters":
[
{ "type": "scale", "scale": 0.6 }
]
},
{ "from": "Keyboard.MouseMoveDown", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_DOWN",
"filters":
[
{ "type": "scale", "scale": 0.6 }
]
},
{ "from": "Keyboard.TouchpadDown", "to": "Actions.PITCH_DOWN" },
{ "from": "Keyboard.TouchpadUp", "to": "Actions.PITCH_UP" },

View file

@ -0,0 +1,48 @@
{
"name": "Leap Motion to Standard",
"channels": [
{ "from": "LeapMotion.LeftHand", "to": "Standard.LeftHand" },
{ "from": "LeapMotion.LeftHandThumb1", "to": "Standard.LeftHandThumb1"},
{ "from": "LeapMotion.LeftHandThumb2", "to": "Standard.LeftHandThumb2"},
{ "from": "LeapMotion.LeftHandThumb3", "to": "Standard.LeftHandThumb3"},
{ "from": "LeapMotion.LeftHandThumb4", "to": "Standard.LeftHandThumb4"},
{ "from": "LeapMotion.LeftHandIndex1", "to": "Standard.LeftHandIndex1"},
{ "from": "LeapMotion.LeftHandIndex2", "to": "Standard.LeftHandIndex2"},
{ "from": "LeapMotion.LeftHandIndex3", "to": "Standard.LeftHandIndex3"},
{ "from": "LeapMotion.LeftHandIndex4", "to": "Standard.LeftHandIndex4"},
{ "from": "LeapMotion.LeftHandMiddle1", "to": "Standard.LeftHandMiddle1"},
{ "from": "LeapMotion.LeftHandMiddle2", "to": "Standard.LeftHandMiddle2"},
{ "from": "LeapMotion.LeftHandMiddle3", "to": "Standard.LeftHandMiddle3"},
{ "from": "LeapMotion.LeftHandMiddle4", "to": "Standard.LeftHandMiddle4"},
{ "from": "LeapMotion.LeftHandRing1", "to": "Standard.LeftHandRing1"},
{ "from": "LeapMotion.LeftHandRing2", "to": "Standard.LeftHandRing2"},
{ "from": "LeapMotion.LeftHandRing3", "to": "Standard.LeftHandRing3"},
{ "from": "LeapMotion.LeftHandRing4", "to": "Standard.LeftHandRing4"},
{ "from": "LeapMotion.LeftHandPinky1", "to": "Standard.LeftHandPinky1"},
{ "from": "LeapMotion.LeftHandPinky2", "to": "Standard.LeftHandPinky2"},
{ "from": "LeapMotion.LeftHandPinky3", "to": "Standard.LeftHandPinky3"},
{ "from": "LeapMotion.LeftHandPinky4", "to": "Standard.LeftHandPinky4"},
{ "from": "LeapMotion.RightHand", "to": "Standard.RightHand" },
{ "from": "LeapMotion.RightHandThumb1", "to": "Standard.RightHandThumb1"},
{ "from": "LeapMotion.RightHandThumb2", "to": "Standard.RightHandThumb2"},
{ "from": "LeapMotion.RightHandThumb3", "to": "Standard.RightHandThumb3"},
{ "from": "LeapMotion.RightHandThumb4", "to": "Standard.RightHandThumb4"},
{ "from": "LeapMotion.RightHandIndex1", "to": "Standard.RightHandIndex1"},
{ "from": "LeapMotion.RightHandIndex2", "to": "Standard.RightHandIndex2"},
{ "from": "LeapMotion.RightHandIndex3", "to": "Standard.RightHandIndex3"},
{ "from": "LeapMotion.RightHandIndex4", "to": "Standard.RightHandIndex4"},
{ "from": "LeapMotion.RightHandMiddle1", "to": "Standard.RightHandMiddle1"},
{ "from": "LeapMotion.RightHandMiddle2", "to": "Standard.RightHandMiddle2"},
{ "from": "LeapMotion.RightHandMiddle3", "to": "Standard.RightHandMiddle3"},
{ "from": "LeapMotion.RightHandMiddle4", "to": "Standard.RightHandMiddle4"},
{ "from": "LeapMotion.RightHandRing1", "to": "Standard.RightHandRing1"},
{ "from": "LeapMotion.RightHandRing2", "to": "Standard.RightHandRing2"},
{ "from": "LeapMotion.RightHandRing3", "to": "Standard.RightHandRing3"},
{ "from": "LeapMotion.RightHandRing4", "to": "Standard.RightHandRing4"},
{ "from": "LeapMotion.RightHandPinky1", "to": "Standard.RightHandPinky1"},
{ "from": "LeapMotion.RightHandPinky2", "to": "Standard.RightHandPinky2"},
{ "from": "LeapMotion.RightHandPinky3", "to": "Standard.RightHandPinky3"},
{ "from": "LeapMotion.RightHandPinky4", "to": "Standard.RightHandPinky4"}
]
}

View file

@ -58,7 +58,48 @@
{ "from": "Standard.RT", "to": "Actions.RightHandClick" },
{ "from": "Standard.LeftHand", "to": "Actions.LeftHand" },
{ "from": "Standard.LeftHandThumb1", "to": "Actions.LeftHandThumb1"},
{ "from": "Standard.LeftHandThumb2", "to": "Actions.LeftHandThumb2"},
{ "from": "Standard.LeftHandThumb3", "to": "Actions.LeftHandThumb3"},
{ "from": "Standard.LeftHandThumb4", "to": "Actions.LeftHandThumb4"},
{ "from": "Standard.LeftHandIndex1", "to": "Actions.LeftHandIndex1"},
{ "from": "Standard.LeftHandIndex2", "to": "Actions.LeftHandIndex2"},
{ "from": "Standard.LeftHandIndex3", "to": "Actions.LeftHandIndex3"},
{ "from": "Standard.LeftHandIndex4", "to": "Actions.LeftHandIndex4"},
{ "from": "Standard.LeftHandMiddle1", "to": "Actions.LeftHandMiddle1"},
{ "from": "Standard.LeftHandMiddle2", "to": "Actions.LeftHandMiddle2"},
{ "from": "Standard.LeftHandMiddle3", "to": "Actions.LeftHandMiddle3"},
{ "from": "Standard.LeftHandMiddle4", "to": "Actions.LeftHandMiddle4"},
{ "from": "Standard.LeftHandRing1", "to": "Actions.LeftHandRing1"},
{ "from": "Standard.LeftHandRing2", "to": "Actions.LeftHandRing2"},
{ "from": "Standard.LeftHandRing3", "to": "Actions.LeftHandRing3"},
{ "from": "Standard.LeftHandRing4", "to": "Actions.LeftHandRing4"},
{ "from": "Standard.LeftHandPinky1", "to": "Actions.LeftHandPinky1"},
{ "from": "Standard.LeftHandPinky2", "to": "Actions.LeftHandPinky2"},
{ "from": "Standard.LeftHandPinky3", "to": "Actions.LeftHandPinky3"},
{ "from": "Standard.LeftHandPinky4", "to": "Actions.LeftHandPinky4"},
{ "from": "Standard.RightHand", "to": "Actions.RightHand" },
{ "from": "Standard.RightHandThumb1", "to": "Actions.RightHandThumb1"},
{ "from": "Standard.RightHandThumb2", "to": "Actions.RightHandThumb2"},
{ "from": "Standard.RightHandThumb3", "to": "Actions.RightHandThumb3"},
{ "from": "Standard.RightHandThumb4", "to": "Actions.RightHandThumb4"},
{ "from": "Standard.RightHandIndex1", "to": "Actions.RightHandIndex1"},
{ "from": "Standard.RightHandIndex2", "to": "Actions.RightHandIndex2"},
{ "from": "Standard.RightHandIndex3", "to": "Actions.RightHandIndex3"},
{ "from": "Standard.RightHandIndex4", "to": "Actions.RightHandIndex4"},
{ "from": "Standard.RightHandMiddle1", "to": "Actions.RightHandMiddle1"},
{ "from": "Standard.RightHandMiddle2", "to": "Actions.RightHandMiddle2"},
{ "from": "Standard.RightHandMiddle3", "to": "Actions.RightHandMiddle3"},
{ "from": "Standard.RightHandMiddle4", "to": "Actions.RightHandMiddle4"},
{ "from": "Standard.RightHandRing1", "to": "Actions.RightHandRing1"},
{ "from": "Standard.RightHandRing2", "to": "Actions.RightHandRing2"},
{ "from": "Standard.RightHandRing3", "to": "Actions.RightHandRing3"},
{ "from": "Standard.RightHandRing4", "to": "Actions.RightHandRing4"},
{ "from": "Standard.RightHandPinky1", "to": "Actions.RightHandPinky1"},
{ "from": "Standard.RightHandPinky2", "to": "Actions.RightHandPinky2"},
{ "from": "Standard.RightHandPinky3", "to": "Actions.RightHandPinky3"},
{ "from": "Standard.RightHandPinky4", "to": "Actions.RightHandPinky4"},
{ "from": "Standard.LeftFoot", "to": "Actions.LeftFoot" },
{ "from": "Standard.RightFoot", "to": "Actions.RightFoot" },

View file

@ -34,36 +34,32 @@
{ "from": "Vive.RSCenter", "to": "Standard.RightPrimaryThumb" },
{ "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" },
{ "from": "Vive.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] },
{ "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] },
{ "from": "Vive.LeftHand", "to": "Standard.LeftHand"},
{ "from": "Vive.RightHand", "to": "Standard.RightHand"},
{
"from": "Vive.LeftFoot", "to" : "Standard.LeftFoot",
"filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}],
"when": [ "Application.InHMD" ]
"filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}]
},
{
"from": "Vive.RightFoot", "to" : "Standard.RightFoot",
"filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}],
"when": [ "Application.InHMD" ]
"filters" : [{"type" : "lowVelocity", "rotation" : 1.0, "translation": 1.0}]
},
{
"from": "Vive.Hips", "to" : "Standard.Hips",
"filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}],
"when": [ "Application.InHMD" ]
"filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}]
},
{
"from": "Vive.Spine2", "to" : "Standard.Spine2",
"filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}],
"when": [ "Application.InHMD" ]
"filters" : [{"type" : "lowVelocity", "rotation" : 0.01, "translation": 0.01}]
},
{ "from": "Vive.Head", "to" : "Standard.Head", "when": [ "Application.InHMD" ] },
{ "from": "Vive.Head", "to" : "Standard.Head"},
{ "from": "Vive.RightArm", "to" : "Standard.RightArm", "when": [ "Application.InHMD" ] },
{ "from": "Vive.LeftArm", "to" : "Standard.LeftArm", "when": [ "Application.InHMD" ] }
{ "from": "Vive.RightArm", "to" : "Standard.RightArm"},
{ "from": "Vive.LeftArm", "to" : "Standard.LeftArm"}
]
}

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" opacity="0.33">
<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

(image error) Size: 1.8 KiB

View file

@ -1,70 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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 50"
style="enable-background:new 0 0 50 50;"
xml:space="preserve"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="mic-mute-a.svg"><metadata
id="metadata22"><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="defs20" /><sodipodi:namedview
pagecolor="#ff0000"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="852"
inkscape:window-height="480"
id="namedview18"
showgrid="false"
inkscape:zoom="4.72"
inkscape:cx="25"
inkscape:cy="25"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" /><style
type="text/css"
id="style4">
.st0{fill:#FFFFFF;}
</style><g
id="Layer_2" /><g
id="Layer_1"
style="fill:#000000;fill-opacity: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"
id="path8"
style="fill:#000000;fill-opacity:1" /><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"
id="path10"
style="fill:#000000;fill-opacity:1" /><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"
id="path12"
style="fill:#000000;fill-opacity:1" /><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"
id="path14"
style="fill:#000000;fill-opacity:1" /><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"
id="path16"
style="fill:#000000;fill-opacity:1" /></g></svg>
<svg version="1.1"
id="svg2" inkscape:version="0.91 r13725" sodipodi:docname="mic-mute-a.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
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:#EA4C5F;}
</style>
<sodipodi:namedview bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" id="namedview18" inkscape:current-layer="svg2" inkscape:cx="25" inkscape:cy="25" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="480" inkscape:window-maximized="0" inkscape:window-width="852" inkscape:window-x="0" inkscape:window-y="0" inkscape:zoom="4.72" objecttolerance="10" pagecolor="#ff0000" showgrid="false">
</sodipodi:namedview>
<g id="Layer_2">
</g>
<g id="Layer_1">
<path id="path8" class="st0" d="M28.9,17.1v-0.5c0-2-1.7-3.6-3.7-3.6l0,0c-2,0-3.7,1.6-3.7,3.6v6.9L28.9,17.1z"/>
<path id="path10" class="st0" d="M21.5,29.2v0.2c0,2,1.6,3.6,3.7,3.6l0,0c2,0,3.7-1.6,3.7-3.6v-6.6L21.5,29.2z"/>
<path id="path12" class="st0" d="M39.1,16.8L13.6,39.1c-0.7,0.6-1.8,0.5-2.4-0.2L11,38.7c-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 id="path14" class="st0" d="M23.4,40.2v3.4h-4.3c-1,0-1.8,0.8-1.8,1.8s0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8
s-0.8-1.8-1.8-1.8H27v-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.8c0,0.3,0,4.8,0,5.2
c0,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 id="path16" 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>

Before

(image error) Size: 2.9 KiB

After

(image error) Size: 2.1 KiB

View file

@ -1,21 +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" 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">
<svg version="1.1"
id="svg2" inkscape:version="0.91 r13725" sodipodi:docname="mic-mute-a.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
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;}
.st0{fill:#EA4C5F;}
</style>
<sodipodi:namedview bordercolor="#666666" borderopacity="1" gridtolerance="10" guidetolerance="10" id="namedview18" inkscape:current-layer="svg2" inkscape:cx="25" inkscape:cy="25" inkscape:pageopacity="0" inkscape:pageshadow="2" inkscape:window-height="480" inkscape:window-maximized="0" inkscape:window-width="852" inkscape:window-x="0" inkscape:window-y="0" inkscape:zoom="4.72" objecttolerance="10" pagecolor="#ff0000" showgrid="false">
</sodipodi:namedview>
<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
<path id="path8" class="st0" d="M28.9,17.1v-0.5c0-2-1.7-3.6-3.7-3.6l0,0c-2,0-3.7,1.6-3.7,3.6v6.9L28.9,17.1z"/>
<path id="path10" class="st0" d="M21.5,29.2v0.2c0,2,1.6,3.6,3.7,3.6l0,0c2,0,3.7-1.6,3.7-3.6v-6.6L21.5,29.2z"/>
<path id="path12" class="st0" d="M39.1,16.8L13.6,39.1c-0.7,0.6-1.8,0.5-2.4-0.2L11,38.7c-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
<path id="path14" class="st0" d="M23.4,40.2v3.4h-4.3c-1,0-1.8,0.8-1.8,1.8s0.8,1.8,1.8,1.8h12.3c1,0,1.8-0.8,1.8-1.8
s-0.8-1.8-1.8-1.8H27v-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.8c0,0.3,0,4.8,0,5.2
c0,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 id="path16" 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>

Before

(image error) Size: 1.3 KiB

After

(image error) Size: 2.1 KiB

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.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">
<path d="M43.9,13.3c-1.3-0.6-2.4-0.3-3.5,0.4c-1.6,1.2-3.3,2.4-5,3.5c-1.4-3.4-3.3-5-6.3-5c-5.9-0.1-11.9-0.1-17.9,0
c-3.8,0.1-6.4,3.1-6.4,7.3c0,3.7-0.1,7.6,0,11.4c0,1.1,0.2,2.1,0.6,3.1c1.2,2.7,3.3,3.8,6,3.8c5.6,0,11-0.1,16.5,0
c3.5,0.1,6-1.5,7.4-5.1c1.7,1.2,3.4,2.4,5.1,3.5c1.1,0.7,2.2,1.1,3.5,0.3c1.2-0.7,1.6-1.9,1.6-3.3c0-5.6,0-11,0-16.6
C45.5,15.3,45.2,14.1,43.9,13.3z M32.2,30.5c0,2.5-1,3.6-3.4,3.6c-2.9,0-5.8,0-8.7,0s-5.6,0-8.5,0.1c-2.4-0.1-3.4-1.2-3.4-3.7
c0-3.7,0-7.5,0-11.2c0-2.2,1.1-3.4,3.1-3.4c5.9,0,11.9,0,17.8,0c2,0,3.1,1.2,3.1,3.4C32.2,23,32.2,26.8,32.2,30.5z M41.9,32.8
c-2.1-1.4-4.2-2.9-6.3-4.3c-0.1-0.1-0.2-0.4-0.2-0.7c0-1.9,0-3.7,0-5.5c0-0.3,0.1-0.7,0.3-0.8c2-1.4,4-2.8,6.2-4.3
C41.9,22.3,41.9,27.4,41.9,32.8z"/>
<path d="M27.4,25C27.4,24.7,27.4,25.2,27.4,25c0-1.1-0.1-2-0.2-2.7c-0.2-1.4-0.7-2.7-1.6-4c-0.2-0.3-0.5-0.5-1-0.6
c-0.4-0.1-0.9,0-1.3,0.2c-0.5,0.3-0.7,1.3-0.3,1.8c1.2,1.6,1.4,3,1.4,4.8c0.1,2.1-0.2,3.4-1.5,5.2c-0.2,0.3-0.2,1.1,0.1,1.6
c0.1,0.2,0.3,0.4,0.6,0.6c0.2,0.1,0.3,0.1,0.5,0.1c0.5,0,1-0.3,1.3-0.9C27,29.3,27.3,27.3,27.4,25L27.4,25z"/>
<ellipse cx="15.2" cy="24.7" rx="2.1" ry="2.4"/>
<path d="M22.3,24.8C22.3,24.7,22.3,25,22.3,24.8c0-0.7-0.1-1.5-0.1-1.9c-0.2-1-0.6-2.1-1.3-3c-0.1-0.2-0.4-0.5-0.9-0.5
c-0.7,0-0.9,0.2-1.2,0.4c-0.4,0.2-0.5,0.9-0.2,1.3c0.9,1.2,1,2.1,1.1,3.5c0,1.6-0.2,2.5-1.1,3.8c-0.1,0.2-0.1,0.7,0,1.2
c0.1,0.2,0.2,0.3,0.5,0.4c0.1,0,0.2,0.1,0.3,0.1c0.5,0.2,1.2,0.1,1.5-0.5C21.7,28,22.2,26.5,22.3,24.8L22.3,24.8z"/>
</svg>

After

(image error) Size: 1.8 KiB

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.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>
<path class="st0" d="M43.9,13.3c-1.3-0.6-2.4-0.3-3.5,0.4c-1.6,1.2-3.3,2.4-5,3.5c-1.4-3.4-3.3-5-6.3-5c-5.9-0.1-11.9-0.1-17.9,0
c-3.8,0.1-6.4,3.1-6.4,7.3c0,3.7-0.1,7.6,0,11.4c0,1.1,0.2,2.1,0.6,3.1c1.2,2.7,3.3,3.8,6,3.8c5.6,0,11-0.1,16.5,0
c3.5,0.1,6-1.5,7.4-5.1c1.7,1.2,3.4,2.4,5.1,3.5c1.1,0.7,2.2,1.1,3.5,0.3c1.2-0.7,1.6-1.9,1.6-3.3c0-5.6,0-11,0-16.6
C45.5,15.3,45.2,14.1,43.9,13.3z M32.2,30.5c0,2.5-1,3.6-3.4,3.6c-2.9,0-5.8,0-8.7,0s-5.6,0-8.5,0.1c-2.4-0.1-3.4-1.2-3.4-3.7
c0-3.7,0-7.5,0-11.2c0-2.2,1.1-3.4,3.1-3.4c5.9,0,11.9,0,17.8,0c2,0,3.1,1.2,3.1,3.4C32.2,23,32.2,26.8,32.2,30.5z M41.9,32.8
c-2.1-1.4-4.2-2.9-6.3-4.3c-0.1-0.1-0.2-0.4-0.2-0.7c0-1.9,0-3.7,0-5.5c0-0.3,0.1-0.7,0.3-0.8c2-1.4,4-2.8,6.2-4.3
C41.9,22.3,41.9,27.4,41.9,32.8z"/>
<path class="st0" d="M27.4,25C27.4,24.7,27.4,25.2,27.4,25c0-1.1-0.1-2-0.2-2.7c-0.2-1.4-0.7-2.7-1.6-4c-0.2-0.3-0.5-0.5-1-0.6
c-0.4-0.1-0.9,0-1.3,0.2c-0.5,0.3-0.7,1.3-0.3,1.8c1.2,1.6,1.4,3,1.4,4.8c0.1,2.1-0.2,3.4-1.5,5.2c-0.2,0.3-0.2,1.1,0.1,1.6
c0.1,0.2,0.3,0.4,0.6,0.6c0.2,0.1,0.3,0.1,0.5,0.1c0.5,0,1-0.3,1.3-0.9C27,29.3,27.3,27.3,27.4,25L27.4,25z"/>
<ellipse class="st0" cx="15.2" cy="24.7" rx="2.1" ry="2.4"/>
<path class="st0" d="M22.3,24.8C22.3,24.7,22.3,25,22.3,24.8c0-0.7-0.1-1.5-0.1-1.9c-0.2-1-0.6-2.1-1.3-3c-0.1-0.2-0.4-0.5-0.9-0.5
c-0.7,0-0.9,0.2-1.2,0.4c-0.4,0.2-0.5,0.9-0.2,1.3c0.9,1.2,1,2.1,1.1,3.5c0,1.6-0.2,2.5-1.1,3.8c-0.1,0.2-0.1,0.7,0,1.2
c0.1,0.2,0.2,0.3,0.5,0.4c0.1,0,0.2,0.1,0.3,0.1c0.5,0.2,1.2,0.1,1.5-0.5C21.7,28,22.2,26.5,22.3,24.8L22.3,24.8z"/>
</svg>

After

(image error) Size: 1.9 KiB

Binary file not shown.

After

(image error) Size: 50 KiB

Binary file not shown.

After

(image error) Size: 7.7 KiB

Binary file not shown.

After

(image error) Size: 7.2 KiB

Binary file not shown.

After

(image error) Size: 899 KiB

View file

@ -12,84 +12,21 @@ import QtQuick.Controls 1.3
import QtGraphicalEffects 1.0
import Qt.labs.settings 1.0
import "./hifi/audio" as HifiAudio
Hifi.AvatarInputs {
id: root
id: root;
objectName: "AvatarInputs"
width: rootWidth
height: controls.height
x: 10; y: 5
width: audio.width;
height: audio.height;
x: 10; y: 5;
readonly property int rootWidth: 265
readonly property int iconSize: 24
readonly property int iconPadding: 5
readonly property bool shouldReposition: true;
readonly property bool shouldReposition: true
Settings {
category: "Overlay.AvatarInputs"
property alias x: root.x
property alias y: root.y
}
MouseArea {
id: hover
hoverEnabled: true
drag.target: parent
anchors.fill: parent
}
Item {
id: controls
width: root.rootWidth
height: 44
visible: root.showAudioTools
Rectangle {
anchors.fill: parent
color: "#00000000"
Item {
id: audioMeter
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: root.iconPadding
anchors.right: parent.right
anchors.rightMargin: root.iconPadding
height: 8
Rectangle {
id: blueRect
color: "blue"
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
width: parent.width / 4
}
Rectangle {
id: greenRect
color: "green"
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: blueRect.right
anchors.right: redRect.left
}
Rectangle {
id: redRect
color: "red"
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
width: parent.width / 5
}
Rectangle {
z: 100
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
width: (1.0 - root.audioLevel) * parent.width
color: "black"
}
}
}
HifiAudio.MicBar {
id: audio;
visible: root.showAudioTools;
standalone: true;
dragTarget: parent;
}
}

View file

@ -65,6 +65,12 @@ Windows.Window {
root.dynamicContent.fromScript(message);
}
}
function clearDebugWindow() {
if (root.dynamicContent && root.dynamicContent.clearWindow) {
root.dynamicContent.clearWindow();
}
}
// Handle message traffic from our loaded QML to the script that launched us
signal sendToScript(var message);

View file

@ -18,10 +18,12 @@ Original.CheckBox {
id: checkBox
property int colorScheme: hifi.colorSchemes.light
property string color: hifi.colors.lightGrayText
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
property bool isRedCheck: false
property int boxSize: 14
readonly property int boxRadius: 3
property int boxRadius: 3
property bool wrap: true;
readonly property int checkSize: Math.max(boxSize - 8, 10)
readonly property int checkRadius: 2
activeFocusOnPress: true
@ -89,9 +91,10 @@ Original.CheckBox {
label: Label {
text: control.text
colorScheme: checkBox.colorScheme
color: control.color
x: 2
wrapMode: Text.Wrap
wrapMode: checkBox.wrap ? Text.Wrap : Text.NoWrap
elide: checkBox.wrap ? Text.ElideNone : Text.ElideRight
enabled: checkBox.enabled
}
}

View file

@ -20,6 +20,7 @@ FocusScope {
HifiConstants { id: hifi }
property alias model: comboBox.model;
property alias editable: comboBox.editable
property alias comboBox: comboBox
readonly property alias currentText: comboBox.currentText;
property alias currentIndex: comboBox.currentIndex;
@ -216,7 +217,7 @@ FocusScope {
anchors.leftMargin: hifi.dimensions.textPadding
anchors.verticalCenter: parent.verticalCenter
id: popupText
text: listView.model[index] ? listView.model[index] : (listView.model.get(index).text ? listView.model.get(index).text : "")
text: listView.model[index] ? listView.model[index] : (listView.model.get && listView.model.get(index).text ? listView.model.get(index).text : "")
size: hifi.fontSizes.textFieldInput
color: hifi.colors.baseGray
}

View file

@ -0,0 +1,64 @@
//
// ImageMessageBox.qml
//
// Created by Dante Ruiz on 7/5/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import "../styles-uit"
Item {
id: imageBox
visible: false
anchors.fill: parent
property alias source: image.source
property alias imageWidth: image.width
property alias imageHeight: image.height
Rectangle {
anchors.fill: parent
color: "black"
opacity: 0.3
}
Image {
id: image
anchors.centerIn: parent
HiFiGlyphs {
id: closeGlyphButton
text: hifi.glyphs.close
size: 25
anchors {
top: parent.top
topMargin: 15
right: parent.right
rightMargin: 15
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
parent.text = hifi.glyphs.closeInverted;
}
onExited: {
parent.text = hifi.glyphs.close;
}
onClicked: {
imageBox.visible = false;
}
}
}
}
}

View file

@ -0,0 +1,71 @@
//
// RadioButton.qml
//
// Created by Cain Kilgore on 20th July 2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import QtQuick.Controls 1.4 as Original
import QtQuick.Controls.Styles 1.4
import "../styles-uit"
import "../controls-uit" as HifiControls
Original.RadioButton {
id: radioButton
HifiConstants { id: hifi }
property int colorScheme: hifi.colorSchemes.light
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light
readonly property int boxSize: 14
readonly property int boxRadius: 3
readonly property int checkSize: 10
readonly property int checkRadius: 2
style: RadioButtonStyle {
indicator: Rectangle {
id: box
width: boxSize
height: boxSize
radius: 7
gradient: Gradient {
GradientStop {
position: 0.2
color: pressed || hovered
? (radioButton.isLightColorScheme ? hifi.colors.checkboxDarkStart : hifi.colors.checkboxLightStart)
: (radioButton.isLightColorScheme ? hifi.colors.checkboxLightStart : hifi.colors.checkboxDarkStart)
}
GradientStop {
position: 1.0
color: pressed || hovered
? (radioButton.isLightColorScheme ? hifi.colors.checkboxDarkFinish : hifi.colors.checkboxLightFinish)
: (radioButton.isLightColorScheme ? hifi.colors.checkboxLightFinish : hifi.colors.checkboxDarkFinish)
}
}
Rectangle {
id: check
width: checkSize
height: checkSize
radius: 7
anchors.centerIn: parent
color: "#00B4EF"
border.width: 1
border.color: "#36CDFF"
visible: checked && !pressed || !checked && pressed
}
}
label: RalewaySemiBold {
text: control.text
size: hifi.fontSizes.inputLabel
color: isLightColorScheme ? hifi.colors.lightGray : hifi.colors.lightGrayText
x: radioButton.boxSize / 2
}
}
}

View file

@ -0,0 +1,38 @@
//
// Separator.qml
//
// Created by Zach Fox on 2017-06-06
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import "../styles-uit"
Item {
// Size
height: 2;
Rectangle {
// Size
width: parent.width;
height: 1;
// Anchors
anchors.left: parent.left;
anchors.bottom: parent.bottom;
anchors.bottomMargin: height;
// Style
color: hifi.colors.baseGrayShadow;
}
Rectangle {
// Size
width: parent.width;
height: 1;
// Anchors
anchors.left: parent.left;
anchors.bottom: parent.bottom;
// Style
color: hifi.colors.baseGrayHighlight;
}
}

View file

@ -36,7 +36,7 @@ Slider {
Rectangle {
width: parent.height - 2
height: slider.value * (slider.width/(slider.maximumValue - slider.minimumValue)) - 1
height: slider.width * (slider.value - slider.minimumValue) / (slider.maximumValue - slider.minimumValue) - 1
radius: height / 2
anchors {
top: parent.top

View file

@ -0,0 +1,156 @@
//
// Switch.qml
//
// Created by Zach Fox on 2017-06-06
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import QtQuick.Controls 1.4 as Original
import QtQuick.Controls.Styles 1.4
import "../styles-uit"
Item {
id: rootSwitch;
property int colorScheme: hifi.colorSchemes.light;
readonly property bool isLightColorScheme: colorScheme == hifi.colorSchemes.light;
property int switchWidth: 70;
readonly property int switchRadius: height/2;
property string labelTextOff: "";
property string labelGlyphOffText: "";
property int labelGlyphOffSize: 32;
property string labelTextOn: "";
property string labelGlyphOnText: "";
property int labelGlyphOnSize: 32;
property alias checked: originalSwitch.checked;
signal onCheckedChanged;
signal clicked;
Original.Switch {
id: originalSwitch;
activeFocusOnPress: true;
anchors.top: rootSwitch.top;
anchors.left: rootSwitch.left;
anchors.leftMargin: rootSwitch.width/2 - rootSwitch.switchWidth/2;
onCheckedChanged: rootSwitch.onCheckedChanged();
onClicked: rootSwitch.clicked();
style: SwitchStyle {
padding {
top: 3;
left: 3;
right: 3;
bottom: 3;
}
groove: Rectangle {
color: "#252525";
implicitWidth: rootSwitch.switchWidth;
implicitHeight: rootSwitch.height;
radius: rootSwitch.switchRadius;
}
handle: Rectangle {
id: switchHandle;
implicitWidth: rootSwitch.height - padding.top - padding.bottom;
implicitHeight: implicitWidth;
radius: implicitWidth/2;
border.color: hifi.colors.lightGrayText;
color: hifi.colors.lightGray;
MouseArea {
anchors.fill: parent;
hoverEnabled: true;
onEntered: parent.color = hifi.colors.blueHighlight;
onExited: parent.color = hifi.colors.lightGray;
}
}
}
}
// OFF Label
Item {
anchors.right: originalSwitch.left;
anchors.rightMargin: 10;
anchors.top: rootSwitch.top;
height: rootSwitch.height;
RalewaySemiBold {
id: labelOff;
text: labelTextOff;
size: hifi.fontSizes.inputLabel;
color: originalSwitch.checked ? hifi.colors.lightGrayText : "#FFFFFF";
anchors.top: parent.top;
anchors.right: parent.right;
width: paintedWidth;
height: parent.height;
verticalAlignment: Text.AlignVCenter;
}
HiFiGlyphs {
id: labelGlyphOff;
text: labelGlyphOffText;
size: labelGlyphOffSize;
color: labelOff.color;
anchors.top: parent.top;
anchors.topMargin: 2;
anchors.right: labelOff.left;
anchors.rightMargin: 4;
}
MouseArea {
anchors.top: parent.top;
anchors.bottom: parent.bottom;
anchors.left: labelGlyphOff.left;
anchors.right: labelOff.right;
onClicked: {
originalSwitch.checked = false;
}
}
}
// ON Label
Item {
anchors.left: originalSwitch.right;
anchors.leftMargin: 10;
anchors.top: rootSwitch.top;
height: rootSwitch.height;
RalewaySemiBold {
id: labelOn;
text: labelTextOn;
size: hifi.fontSizes.inputLabel;
color: originalSwitch.checked ? "#FFFFFF" : hifi.colors.lightGrayText;
anchors.top: parent.top;
anchors.left: parent.left;
width: paintedWidth;
height: parent.height;
verticalAlignment: Text.AlignVCenter;
}
HiFiGlyphs {
id: labelGlyphOn;
text: labelGlyphOnText;
size: labelGlyphOnSize;
color: labelOn.color;
anchors.top: parent.top;
anchors.left: labelOn.right;
}
MouseArea {
anchors.top: parent.top;
anchors.bottom: parent.bottom;
anchors.left: labelOn.left;
anchors.right: labelGlyphOn.right;
onClicked: {
originalSwitch.checked = true;
}
}
}
}

View file

@ -0,0 +1,17 @@
import QtQuick.Controls 1.3 as Original
import QtQuick.Controls.Styles 1.3
import "../styles"
import "."
Original.RadioButton {
text: "Radio Button"
style: RadioButtonStyle {
indicator: FontAwesome {
text: control.checked ? "\uf046" : "\uf096"
}
label: Text {
text: control.text
renderType: Text.QtRendering
}
}
}

View file

@ -353,6 +353,14 @@ FocusScope {
showDesktop();
}
function ensureTitleBarVisible(targetWindow) {
// Reposition window to ensure that title bar is vertically inside window.
if (targetWindow.frame && targetWindow.frame.decoration) {
var topMargin = -targetWindow.frame.decoration.anchors.topMargin; // Frame's topMargin is a negative value.
targetWindow.y = Math.max(targetWindow.y, topMargin);
}
}
function centerOnVisible(item) {
var targetWindow = d.getDesktopWindow(item);
if (!targetWindow) {
@ -375,11 +383,12 @@ FocusScope {
targetWindow.x = newX;
targetWindow.y = newY;
ensureTitleBarVisible(targetWindow);
// If we've noticed that our recommended desktop rect has changed, record that change here.
if (recommendedRect != newRecommendedRect) {
recommendedRect = newRecommendedRect;
}
}
function repositionOnVisible(item) {
@ -394,7 +403,6 @@ FocusScope {
return;
}
var oldRecommendedRect = recommendedRect;
var oldRecommendedDimmensions = { x: oldRecommendedRect.width, y: oldRecommendedRect.height };
var newRecommendedRect = Controller.getRecommendedOverlayRect();
@ -426,7 +434,6 @@ FocusScope {
newPosition.y = -1
}
if (newPosition.x === -1 && newPosition.y === -1) {
var originRelativeX = (targetWindow.x - oldRecommendedRect.x);
var originRelativeY = (targetWindow.y - oldRecommendedRect.y);
@ -444,6 +451,8 @@ FocusScope {
}
targetWindow.x = newPosition.x;
targetWindow.y = newPosition.y;
ensureTitleBarVisible(targetWindow);
}
Component { id: messageDialogBuilder; MessageDialog { } }

View file

@ -34,6 +34,8 @@ ModalWindow {
HifiConstants { id: hifi }
property var filesModel: ListModel { }
Settings {
category: "FileDialog"
property alias width: root.width
@ -253,7 +255,9 @@ ModalWindow {
}
currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath);
currentSelectionIsFolder = fileTableView.model.isFolder(row);
currentSelectionIsFolder = fileTableView.model !== filesModel ?
fileTableView.model.isFolder(row) :
fileTableModel.isFolder(row);
if (root.selectDirectory || !currentSelectionIsFolder) {
currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl));
} else {
@ -331,7 +335,12 @@ ModalWindow {
}
}
ListModel {
Component {
id: filesModelBuilder
ListModel { }
}
QtObject {
id: fileTableModel
// FolderListModel has a couple of problems:
@ -383,7 +392,11 @@ ModalWindow {
if (row === -1) {
return false;
}
return get(row).fileIsDir;
return filesModel.get(row).fileIsDir;
}
function get(row) {
return filesModel.get(row)
}
function update() {
@ -401,7 +414,7 @@ ModalWindow {
rows = 0,
i;
clear();
var newFilesModel = filesModelBuilder.createObject(root);
comparisonFunction = sortOrder === Qt.AscendingOrder
? function(a, b) { return a < b; }
@ -423,7 +436,7 @@ ModalWindow {
while (lower < upper) {
middle = Math.floor((lower + upper) / 2);
var lessThan;
if (comparisonFunction(sortValue, get(middle)[sortField])) {
if (comparisonFunction(sortValue, newFilesModel.get(middle)[sortField])) {
lessThan = true;
upper = middle;
} else {
@ -432,7 +445,7 @@ ModalWindow {
}
}
insert(lower, {
newFilesModel.insert(lower, {
fileName: fileName,
fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")),
fileSize: model.getItem(i, "fileSize"),
@ -443,6 +456,7 @@ ModalWindow {
rows++;
}
filesModel = newFilesModel;
d.clearSelection();
}
@ -469,7 +483,7 @@ ModalWindow {
sortIndicatorOrder: Qt.AscendingOrder
sortIndicatorVisible: true
model: fileTableModel
model: filesModel
function updateSort() {
model.sortOrder = sortIndicatorOrder;
@ -561,11 +575,12 @@ ModalWindow {
}
function navigateToCurrentRow() {
var currentModel = fileTableView.model !== filesModel ? fileTableView.model : fileTableModel
var row = fileTableView.currentRow
var isFolder = model.isFolder(row);
var file = model.get(row).filePath;
var isFolder = currentModel.isFolder(row);
var file = currentModel.get(row).filePath;
if (isFolder) {
fileTableView.model.folder = helper.pathToUrl(file);
currentModel.folder = helper.pathToUrl(file);
} else {
okAction.trigger();
}
@ -580,7 +595,8 @@ ModalWindow {
var newPrefix = prefix + event.text.toLowerCase();
var matchedIndex = -1;
for (var i = 0; i < model.count; ++i) {
var name = model.get(i).fileName.toLowerCase();
var name = model !== filesModel ? model.get(i).fileName.toLowerCase() :
filesModel.get(i).fileName.toLowerCase();
if (0 === name.indexOf(newPrefix)) {
matchedIndex = i;
break;

View file

@ -25,11 +25,14 @@ import "fileDialog"
//FIXME implement shortcuts for favorite location
TabletModalWindow {
id: root
anchors.fill: parent
width: parent.width
height: parent.height
HifiConstants { id: hifi }
property var filesModel: ListModel { }
Settings {
category: "FileDialog"
property alias width: root.width
@ -250,7 +253,9 @@ TabletModalWindow {
}
currentSelectionUrl = helper.pathToUrl(fileTableView.model.get(row).filePath);
currentSelectionIsFolder = fileTableView.model.isFolder(row);
currentSelectionIsFolder = fileTableView.model !== filesModel ?
fileTableView.model.isFolder(row) :
fileTableModel.isFolder(row);
if (root.selectDirectory || !currentSelectionIsFolder) {
currentSelection.text = capitalizeDrive(helper.urlToPath(currentSelectionUrl));
} else {
@ -288,7 +293,7 @@ TabletModalWindow {
}
onFolderChanged: {
fileTableModel.update(); // Update once the data from the folder change is available.
fileTableModel.update()
}
function getItem(index, field) {
@ -328,7 +333,12 @@ TabletModalWindow {
}
}
ListModel {
Component {
id: filesModelBuilder
ListModel { }
}
QtObject {
id: fileTableModel
// FolderListModel has a couple of problems:
@ -359,17 +369,16 @@ TabletModalWindow {
}
onFolderChanged: {
if (folder === rootFolder) {
model = driveListModel;
helper.monitorDirectory("");
update();
} else {
var needsUpdate = model === driveListModel && folder === folderListModel.folder;
model = folderListModel;
folderListModel.folder = folder;
helper.monitorDirectory(helper.urlToPath(folder));
if (needsUpdate) {
update();
}
@ -380,7 +389,11 @@ TabletModalWindow {
if (row === -1) {
return false;
}
return get(row).fileIsDir;
return filesModel.get(row).fileIsDir;
}
function get(row) {
return filesModel.get(row)
}
function update() {
@ -398,7 +411,7 @@ TabletModalWindow {
rows = 0,
i;
clear();
var newFilesModel = filesModelBuilder.createObject(root);
comparisonFunction = sortOrder === Qt.AscendingOrder
? function(a, b) { return a < b; }
@ -420,7 +433,7 @@ TabletModalWindow {
while (lower < upper) {
middle = Math.floor((lower + upper) / 2);
var lessThan;
if (comparisonFunction(sortValue, get(middle)[sortField])) {
if (comparisonFunction(sortValue, newFilesModel.get(middle)[sortField])) {
lessThan = true;
upper = middle;
} else {
@ -429,7 +442,7 @@ TabletModalWindow {
}
}
insert(lower, {
newFilesModel.insert(lower, {
fileName: fileName,
fileModified: (fileIsDir ? new Date(0) : model.getItem(i, "fileModified")),
fileSize: model.getItem(i, "fileSize"),
@ -440,6 +453,7 @@ TabletModalWindow {
rows++;
}
filesModel = newFilesModel;
d.clearSelection();
}
@ -467,7 +481,7 @@ TabletModalWindow {
sortIndicatorOrder: Qt.AscendingOrder
sortIndicatorVisible: true
model: fileTableModel
model: filesModel
function updateSort() {
model.sortOrder = sortIndicatorOrder;
@ -559,11 +573,12 @@ TabletModalWindow {
}
function navigateToCurrentRow() {
var currentModel = fileTableView.model !== filesModel ? fileTableView.model : fileTableModel
var row = fileTableView.currentRow
var isFolder = model.isFolder(row);
var file = model.get(row).filePath;
var isFolder = currentModel.isFolder(row);
var file = currentModel.get(row).filePath;
if (isFolder) {
fileTableView.model.folder = helper.pathToUrl(file);
currentModel.folder = helper.pathToUrl(file);
} else {
okAction.trigger();
}
@ -578,7 +593,8 @@ TabletModalWindow {
var newPrefix = prefix + event.text.toLowerCase();
var matchedIndex = -1;
for (var i = 0; i < model.count; ++i) {
var name = model.get(i).fileName.toLowerCase();
var name = model !== filesModel ? model.get(i).fileName.toLowerCase() :
filesModel.get(i).fileName.toLowerCase();
if (0 === name.indexOf(newPrefix)) {
matchedIndex = i;
break;

View file

@ -0,0 +1,101 @@
//
// PrimaryHandPreference.qml
//
// Created by Cain Kilgore on 20th July 2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import "../../controls-uit"
Preference {
id: root
property alias box1: box1
property alias box2: box2
height: control.height + hifi.dimensions.controlInterlineHeight
Component.onCompleted: {
if (preference.value == "left") {
box1.checked = true;
} else {
box2.checked = true;
}
}
function save() {
if (box1.checked && !box2.checked) {
preference.value = "left";
}
if (!box1.checked && box2.checked) {
preference.value = "right";
}
preference.save();
}
Item {
id: control
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: Math.max(labelName.height, box1.height, box2.height)
Label {
id: labelName
text: root.label + ":"
colorScheme: hifi.colorSchemes.dark
anchors {
left: parent.left
right: box1.left
rightMargin: hifi.dimensions.labelPadding
verticalCenter: parent.verticalCenter
}
horizontalAlignment: Text.AlignRight
wrapMode: Text.Wrap
}
RadioButton {
id: box1
text: "Left"
width: 60
anchors {
right: box2.left
verticalCenter: parent.verticalCenter
}
onClicked: {
if (box2.checked) {
box2.checked = false;
}
if (!box1.checked && !box2.checked) {
box1.checked = true;
}
}
colorScheme: hifi.colorSchemes.dark
}
RadioButton {
id: box2
text: "Right"
width: 60
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
onClicked: {
if (box1.checked) {
box1.checked = false;
}
if (!box1.checked && !box2.checked) {
box2.checked = true;
}
}
colorScheme: hifi.colorSchemes.dark
}
}
}

View file

@ -72,6 +72,8 @@ Preference {
property var avatarBuilder: Component { AvatarPreference { } }
property var buttonBuilder: Component { ButtonPreference { } }
property var comboBoxBuilder: Component { ComboBoxPreference { } }
property var spinnerSliderBuilder: Component { SpinnerSliderPreference { } }
property var primaryHandBuilder: Component { PrimaryHandPreference { } }
property var preferences: []
property int checkBoxCount: 0
@ -86,7 +88,7 @@ Preference {
}
function buildPreference(preference) {
console.log("\tPreference type " + preference.type + " name " + preference.name)
console.log("\tPreference type " + preference.type + " name " + preference.name);
var builder;
switch (preference.type) {
case Preference.Editable:
@ -128,6 +130,16 @@ Preference {
checkBoxCount = 0;
builder = comboBoxBuilder;
break;
case Preference.SpinnerSlider:
checkBoxCount = 0;
builder = spinnerSliderBuilder;
break;
case Preference.PrimaryHand:
checkBoxCount++;
builder = primaryHandBuilder;
break;
};
if (builder) {

View file

@ -0,0 +1,111 @@
//
// SpinnerSliderPreference.qml
//
// Created by Cain Kilgore on 11th July 2017
// 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
//
import QtQuick 2.5
import "../../dialogs"
import "../../controls-uit"
Preference {
id: root
property alias slider: slider
property alias spinner: spinner
height: control.height + hifi.dimensions.controlInterlineHeight
Component.onCompleted: {
slider.value = preference.value;
spinner.value = preference.value;
}
function save() {
preference.value = slider.value;
preference.save();
}
Item {
id: control
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
height: Math.max(labelText.height, slider.height, spinner.height, button.height)
Label {
id: labelText
text: root.label + ":"
colorScheme: hifi.colorSchemes.dark
anchors {
left: parent.left
right: slider.left
rightMargin: hifi.dimensions.labelPadding
verticalCenter: parent.verticalCenter
}
horizontalAlignment: Text.AlignRight
wrapMode: Text.Wrap
}
Slider {
id: slider
value: preference.value
width: 100
minimumValue: MyAvatar.getDomainMinScale()
maximumValue: MyAvatar.getDomainMaxScale()
stepSize: preference.step
onValueChanged: {
spinner.value = value
}
anchors {
right: spinner.left
rightMargin: 10
verticalCenter: parent.verticalCenter
}
colorScheme: hifi.colorSchemes.dark
}
SpinBox {
id: spinner
decimals: preference.decimals
value: preference.value
minimumValue: MyAvatar.getDomainMinScale()
maximumValue: MyAvatar.getDomainMaxScale()
width: 100
onValueChanged: {
slider.value = value;
}
anchors {
right: button.left
rightMargin: 10
verticalCenter: parent.verticalCenter
}
colorScheme: hifi.colorSchemes.dark
}
GlyphButton {
id: button
onClicked: {
if (spinner.maximumValue >= 1) {
spinner.value = 1
slider.value = 1
} else {
spinner.value = spinner.maximumValue
slider.value = spinner.maximumValue
}
}
width: 30
glyph: hifi.glyphs.reload
anchors {
right: parent.right
verticalCenter: parent.verticalCenter
}
colorScheme: hifi.colorSchemes.dark
}
}
}

View file

@ -1,266 +0,0 @@
//
// Audio.qml
// qml/hifi
//
// Audio setup
//
// Created by Vlad Stelmahovsky on 03/22/2017
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtGraphicalEffects 1.0
import "../styles-uit"
import "../controls-uit" as HifiControls
import "components"
Rectangle {
id: audio;
//put info text here
property alias infoText: infoArea.text
color: "#404040";
HifiConstants { id: hifi; }
objectName: "AudioWindow"
property string title: "Audio Options"
signal sendToScript(var message);
Component {
id: separator
LinearGradient {
start: Qt.point(0, 0)
end: Qt.point(0, 4)
gradient: Gradient {
GradientStop { position: 0.0; color: "#303030" }
GradientStop { position: 0.33; color: "#252525" } // Equivalent of darkGray0 over baseGray background.
GradientStop { position: 0.5; color: "#303030" }
GradientStop { position: 0.6; color: "#454a49" }
GradientStop { position: 1.0; color: "#454a49" }
}
cached: true
}
}
Column {
anchors { left: parent.left; right: parent.right }
spacing: 8
RalewayRegular {
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
height: 45
size: 20
color: "white"
text: audio.title
}
Loader {
width: parent.width
height: 5
sourceComponent: separator
}
//connections required to syncronize with Menu
Connections {
target: AudioDevice
onMuteToggled: {
audioMute.checkbox.checked = AudioDevice.getMuted()
}
}
Connections {
target: AvatarInputs !== undefined ? AvatarInputs : null
onShowAudioToolsChanged: {
audioTools.checkbox.checked = showAudioTools
}
}
AudioCheckbox {
id: audioMute
width: parent.width
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
checkbox.checked: AudioDevice.muted
text.text: qsTr("Mute microphone")
onCheckBoxClicked: {
AudioDevice.muted = checked
}
}
AudioCheckbox {
id: audioTools
width: parent.width
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
checkbox.checked: AvatarInputs !== undefined ? AvatarInputs.showAudioTools : false
text.text: qsTr("Show audio level meter")
onCheckBoxClicked: {
if (AvatarInputs !== undefined) {
AvatarInputs.showAudioTools = checked
}
}
}
Loader {
width: parent.width
height: 5
sourceComponent: separator
}
Row {
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
height: 40
spacing: 8
HiFiGlyphs {
text: hifi.glyphs.mic
color: hifi.colors.primaryHighlight
anchors.verticalCenter: parent.verticalCenter
size: 32
}
RalewayRegular {
anchors.verticalCenter: parent.verticalCenter
size: 16
color: "#AFAFAF"
text: qsTr("CHOOSE INPUT DEVICE")
}
}
ListView {
id: inputAudioListView
anchors { left: parent.left; right: parent.right; leftMargin: 70 }
height: 125
spacing: 0
clip: true
snapMode: ListView.SnapToItem
model: AudioDevice
delegate: Item {
width: parent.width
visible: devicemode === 0
height: visible ? 36 : 0
AudioCheckbox {
id: cbin
anchors.verticalCenter: parent.verticalCenter
Binding {
target: cbin.checkbox
property: 'checked'
value: devicechecked
}
width: parent.width
cbchecked: devicechecked
text.text: devicename
onCheckBoxClicked: {
if (checked) {
if (devicename.length > 0) {
console.log("Audio.qml about to call AudioDevice.setInputDeviceAsync().devicename:" + devicename);
AudioDevice.setInputDeviceAsync(devicename);
} else {
console.log("Audio.qml attempted to set input device to empty device name.");
}
}
}
}
}
}
Loader {
width: parent.width
height: 5
sourceComponent: separator
}
Row {
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
height: 40
spacing: 8
HiFiGlyphs {
text: hifi.glyphs.unmuted
color: hifi.colors.primaryHighlight
anchors.verticalCenter: parent.verticalCenter
size: 32
}
RalewayRegular {
anchors.verticalCenter: parent.verticalCenter
size: 16
color: "#AFAFAF"
text: qsTr("CHOOSE OUTPUT DEVICE")
}
}
ListView {
id: outputAudioListView
anchors { left: parent.left; right: parent.right; leftMargin: 70 }
height: 250
spacing: 0
clip: true
snapMode: ListView.SnapToItem
model: AudioDevice
delegate: Item {
width: parent.width
visible: devicemode === 1
height: visible ? 36 : 0
AudioCheckbox {
id: cbout
width: parent.width
anchors.verticalCenter: parent.verticalCenter
Binding {
target: cbout.checkbox
property: 'checked'
value: devicechecked
}
text.text: devicename
onCheckBoxClicked: {
if (checked) {
if (devicename.length > 0) {
console.log("Audio.qml about to call AudioDevice.setOutputDeviceAsync().devicename:" + devicename);
AudioDevice.setOutputDeviceAsync(devicename);
} else {
console.log("Audio.qml attempted to set output device to empty device name.");
}
}
}
}
}
}
Loader {
id: lastSeparator
width: parent.width
height: 6
sourceComponent: separator
}
Row {
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
height: 40
spacing: 8
HiFiGlyphs {
id: infoSign
text: hifi.glyphs.info
color: "#AFAFAF"
anchors.verticalCenter: parent.verticalCenter
size: 60
}
RalewayRegular {
id: infoArea
width: parent.width - infoSign.implicitWidth - parent.spacing - 10
wrapMode: Text.WordWrap
anchors.verticalCenter: parent.verticalCenter
size: 12
color: hifi.colors.baseGrayHighlight
}
}
}
}

View file

@ -32,14 +32,15 @@ Item {
radius: popupRadius
}
Rectangle {
width: Math.max(parent.width * 0.75, 400)
id: textContainer;
width: Math.max(parent.width * 0.8, 400)
height: contentContainer.height + 50
anchors.centerIn: parent
radius: popupRadius
color: "white"
Item {
id: contentContainer
width: parent.width - 60
width: parent.width - 50
height: childrenRect.height
anchors.centerIn: parent
Item {
@ -92,7 +93,7 @@ Item {
anchors.top: parent.top
anchors.topMargin: -20
anchors.right: parent.right
anchors.rightMargin: -25
anchors.rightMargin: -20
MouseArea {
anchors.fill: closeGlyphButton
hoverEnabled: true
@ -127,11 +128,51 @@ Item {
color: hifi.colors.darkGray
wrapMode: Text.WordWrap
textFormat: Text.StyledText
onLinkActivated: {
Qt.openUrlExternally(link)
}
}
}
}
// Left gray MouseArea
MouseArea {
anchors.fill: parent
anchors.left: parent.left;
anchors.right: textContainer.left;
anchors.top: textContainer.top;
anchors.bottom: textContainer.bottom;
acceptedButtons: Qt.LeftButton
onClicked: {
letterbox.visible = false
}
}
// Right gray MouseArea
MouseArea {
anchors.left: textContainer.left;
anchors.right: parent.left;
anchors.top: textContainer.top;
anchors.bottom: textContainer.bottom;
acceptedButtons: Qt.LeftButton
onClicked: {
letterbox.visible = false
}
}
// Top gray MouseArea
MouseArea {
anchors.left: parent.left;
anchors.right: parent.right;
anchors.top: parent.top;
anchors.bottom: textContainer.top;
acceptedButtons: Qt.LeftButton
onClicked: {
letterbox.visible = false
}
}
// Bottom gray MouseArea
MouseArea {
anchors.left: parent.left;
anchors.right: parent.right;
anchors.top: textContainer.bottom;
anchors.bottom: parent.bottom;
acceptedButtons: Qt.LeftButton
onClicked: {
letterbox.visible = false

View file

@ -43,6 +43,7 @@ Item {
property bool selected: false
property bool isAdmin: false
property bool isPresent: true
property bool isReplicated: false
property string placeName: ""
property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : "transparent"))
property alias avImage: avatarImage
@ -590,14 +591,11 @@ Item {
console.log("This avatar is no longer present. goToUserInDomain() failed.");
return;
}
var vector = Vec3.subtract(avatar.position, MyAvatar.position);
var distance = Vec3.length(vector);
var target = Vec3.multiply(Vec3.normalize(vector), distance - 2.0);
// FIXME: We would like the avatar to recompute the avatar's "maybe fly" test at the new position, so that if high enough up,
// the avatar goes into fly mode rather than falling. However, that is not exposed to Javascript right now.
// FIXME: it would be nice if this used the same teleport steps and smoothing as in the teleport.js script.
// Note, however, that this script allows teleporting to a person in the air, while teleport.js is going to a grounded target.
MyAvatar.orientation = Quat.lookAtSimple(MyAvatar.position, avatar.position);
MyAvatar.position = Vec3.sum(MyAvatar.position, target);
MyAvatar.position = Vec3.sum(avatar.position, Vec3.multiplyQbyV(avatar.orientation, {x: 0, y: 0, z: -2}));
MyAvatar.orientation = Quat.multiply(avatar.orientation, {y: 1});
}
}

View file

@ -473,6 +473,7 @@ Rectangle {
visible: !isCheckBox && !isButton && !isAvgAudio;
uuid: model ? model.sessionId : "";
selected: styleData.selected;
isReplicated: model.isReplicated;
isAdmin: model && model.admin;
isPresent: model && model.isPresent;
// Size
@ -553,6 +554,7 @@ Rectangle {
id: actionButton;
color: 2; // Red
visible: isButton;
enabled: !nameCard.isReplicated;
anchors.centerIn: parent;
width: 32;
height: 32;
@ -1099,9 +1101,9 @@ Rectangle {
case 'nearbyUsers':
var data = message.params;
var index = -1;
iAmAdmin = Users.canKick;
index = findNearbySessionIndex('', data);
if (index !== -1) {
iAmAdmin = Users.canKick;
myData = data[index];
data.splice(index, 1);
} else {

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