mirror of
https://github.com/overte-org/overte.git
synced 2025-04-11 09:52:20 +02:00
Merge branch 'master' of https://github.com/highfidelity/hifi into 21391
This commit is contained in:
commit
a1f2e786af
792 changed files with 30817 additions and 13010 deletions
BUILD_LINUX.mdCMakeGraphvizOptions.cmakeCMakeLists.txtCONTRIBUTING.md
assignment-client
CMakeLists.txt
src
Agent.cppAgent.hAssignmentClient.cppAssignmentClientApp.cppAssignmentClientMonitor.cppAssignmentClientMonitor.hAvatarAudioTimer.cppAvatarAudioTimer.h
audio
AudioMixer.cppAudioMixer.hAudioMixerClientData.cppAudioMixerClientData.hAudioMixerSlave.cppAudioMixerSlavePool.cpp
avatars
AvatarMixer.cppAvatarMixer.hAvatarMixerClientData.cppAvatarMixerClientData.hAvatarMixerSlave.cppAvatarMixerSlave.hAvatarMixerSlavePool.cppScriptableAvatar.cppScriptableAvatar.h
entities
octree
OctreeHeadlessViewer.cppOctreeHeadlessViewer.hOctreeInboundPacketProcessor.cppOctreeInboundPacketProcessor.hOctreeSendThread.cppOctreeSendThread.h
scripts
cmake
externals/quazip
macros
modules
templates
domain-server
gvr-interface/src
ice-server
interface
CMakeLists.txt
external/leapmotion
resources
avatar
controllers
fonts
icons/tablet-icons
images
qml
AvatarInputs.qmlQmlWindow.qml
controls-uit
controls
desktop
dialogs
hifi
|
@ -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.
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
set(GRAPHVIZ_EXTERNAL_LIBS FALSE)
|
||||
set(GRAPHVIZ_EXTERNAL_LIBS FALSE)
|
||||
set(GRAPHVIZ_IGNORE_TARGETS "shared;networking")
|
|
@ -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 ()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -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
|
|
@ -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)) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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() {
|
|
@ -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 };
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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
|
||||
|
|
13
cmake/externals/quazip/CMakeLists.txt
vendored
13
cmake/externals/quazip/CMakeLists.txt
vendored
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 ()
|
||||
|
|
|
@ -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
|
||||
|
|
12
cmake/macros/TargetLeapMotion.cmake
Normal file
12
cmake/macros/TargetLeapMotion.cmake
Normal 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()
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
});
|
|
@ -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/",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -108,6 +108,7 @@ public:
|
|||
|
||||
signals:
|
||||
void updateNodePermissions();
|
||||
void settingsUpdated();
|
||||
|
||||
public slots:
|
||||
void apiGetGroupIDJSONCallback(QNetworkReply& requestReply);
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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)
|
||||
|
|
8
interface/external/leapmotion/readme.txt
vendored
8
interface/external/leapmotion/readme.txt
vendored
|
@ -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.
|
||||
|
|
|
@ -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": []
|
||||
},
|
||||
|
|
|
@ -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" },
|
||||
|
|
48
interface/resources/controllers/leapmotion.json
Normal file
48
interface/resources/controllers/leapmotion.json
Normal 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"}
|
||||
]
|
||||
}
|
|
@ -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" },
|
||||
|
|
|
@ -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"}
|
||||
]
|
||||
}
|
||||
|
|
Binary file not shown.
25
interface/resources/icons/tablet-icons/edit-disabled.svg
Normal file
25
interface/resources/icons/tablet-icons/edit-disabled.svg
Normal 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 |
|
@ -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 |
|
@ -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 |
19
interface/resources/icons/tablet-icons/spectator-a.svg
Normal file
19
interface/resources/icons/tablet-icons/spectator-a.svg
Normal 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 |
22
interface/resources/icons/tablet-icons/spectator-i.svg
Normal file
22
interface/resources/icons/tablet-icons/spectator-i.svg
Normal 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 |
BIN
interface/resources/images/calibration-help.png
Normal file
BIN
interface/resources/images/calibration-help.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 50 KiB |
BIN
interface/resources/images/loader-calibrate-blue.png
Normal file
BIN
interface/resources/images/loader-calibrate-blue.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 7.7 KiB |
BIN
interface/resources/images/loader-calibrate-white.png
Normal file
BIN
interface/resources/images/loader-calibrate-white.png
Normal file
Binary file not shown.
After ![]() (image error) Size: 7.2 KiB |
BIN
interface/resources/images/static.gif
Normal file
BIN
interface/resources/images/static.gif
Normal file
Binary file not shown.
After ![]() (image error) Size: 899 KiB |
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
64
interface/resources/qml/controls-uit/ImageMessageBox.qml
Normal file
64
interface/resources/qml/controls-uit/ImageMessageBox.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
71
interface/resources/qml/controls-uit/RadioButton.qml
Normal file
71
interface/resources/qml/controls-uit/RadioButton.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
38
interface/resources/qml/controls-uit/Separator.qml
Normal file
38
interface/resources/qml/controls-uit/Separator.qml
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
156
interface/resources/qml/controls-uit/Switch.qml
Normal file
156
interface/resources/qml/controls-uit/Switch.qml
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
interface/resources/qml/controls/RadioButton.qml
Normal file
17
interface/resources/qml/controls/RadioButton.qml
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 { } }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue