From 6032fde5e55fb6688f43f701fefcb20b132052d2 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 12:43:34 -0700 Subject: [PATCH 01/63] Unique command line to run Interface on Quest. --- tools/nitpick/src/Nitpick.cpp | 2 +- tools/nitpick/src/TestRunnerMobile.cpp | 28 +++++++++++++++++--------- tools/nitpick/src/TestRunnerMobile.h | 2 ++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index cf50774617..e72de9d1ad 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.1.2"); + setWindowTitle("Nitpick - v3.1.3"); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 4d0d18ef3d..284b4de15c 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -98,12 +98,12 @@ void TestRunnerMobile::connectDevice() { QString deviceID = tokens[0]; QString modelID = tokens[3].split(':')[1]; - QString modelName = "UNKNOWN"; + _modelName = "UNKNOWN"; if (modelNames.count(modelID) == 1) { - modelName = modelNames[modelID]; + _modelName = modelNames[modelID]; } - _detectedDeviceLabel->setText(modelName + " [" + deviceID + "]"); + _detectedDeviceLabel->setText(_modelName + " [" + deviceID + "]"); _pullFolderButton->setEnabled(true); _folderLineEdit->setEnabled(true); _downloadAPKPushbutton->setEnabled(true); @@ -198,14 +198,22 @@ void TestRunnerMobile::runInterface() { ? QString("https://raw.githubusercontent.com/") + nitpick->getSelectedUser() + "/hifi_tests/" + nitpick->getSelectedBranch() + "/tests/testRecursive.js" : _scriptURL->text(); + // Quest and Android have different commands to run interface + QString startCommand; + if (_modelName == "Quest") { + startCommand = "io.highfidelity.questInterface/.PermissionsChecker"; + } else { + startCommand = "io.highfidelity.hifiinterface/.PermissionChecker"; + } + QString command = _adbInterface->getAdbCommand() + - " shell am start -n io.highfidelity.hifiinterface/.PermissionChecker" + - " --es args \\\"" + - " --url file:///~/serverless/tutorial.json" + - " --no-updater" + - " --no-login-suggestion" + - " --testScript " + testScript + " quitWhenFinished" + - " --testResultsLocation /sdcard/snapshots" + + " shell am start -n " + startCommand + + " --es args \\\"" + + " --url file:///~/serverless/tutorial.json" + + " --no-updater" + + " --no-login-suggestion" + + " --testScript " + testScript + " quitWhenFinished" + + " --testResultsLocation /sdcard/snapshots" + "\\\""; appendLog(command); diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index f7b16da6f8..7dbf5456b3 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -75,5 +75,7 @@ private: std::map modelNames; AdbInterface* _adbInterface; + + QString _modelName; }; #endif From 577ff9695fb1e59cec27e96b16ca89a0bb9e9433 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 13:53:19 -0700 Subject: [PATCH 02/63] TEST!!! --- interface/src/Application.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6d9a1823a1..3d6a26d7e3 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1795,7 +1795,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) +#if !defined(DISABLE_QML) // Do not show login dialog if requested not to on the command line QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); int index = arguments().indexOf(hifiNoLoginCommandLineKey); From 7c7e632589eb25608c82a830ebaf9931672d32e4 Mon Sep 17 00:00:00 2001 From: amantley Date: Mon, 11 Mar 2019 17:50:20 -0700 Subject: [PATCH 03/63] debug statements to find the node parsing error --- libraries/animation/src/AnimClip.cpp | 2 +- libraries/fbx/src/FBXSerializer.cpp | 20 +++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 4fe02e9307..5d846a8f84 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -154,7 +154,7 @@ void AnimClip::copyFromNetworkAnim() { const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); - + qCDebug(animation) << " avatar unit scale " << avatarUnitScale << " animation unit scale " << animationUnitScale << " avatar height " << avatarHeightInMeters << " animation height " << animHeightInMeters << " avatar scale " << avatarHipsParentScale << " animation scale " << animHipsParentScale; boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; } diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 5246242a1e..e6e3d73815 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -142,6 +142,7 @@ glm::mat4 getGlobalTransform(const QMultiMap& _connectionParen visitedNodes.append(nodeID); // Append each node we visit const FBXModel& fbxModel = fbxModels.value(nodeID); + qCDebug(modelformat) << "this fbx model name is " << fbxModel.name; globalTransform = glm::translate(fbxModel.translation) * fbxModel.preTransform * glm::mat4_cast(fbxModel.preRotation * fbxModel.rotation * fbxModel.postRotation) * fbxModel.postTransform * globalTransform; if (fbxModel.hasGeometricOffset) { @@ -201,13 +202,23 @@ public: void appendModelIDs(const QString& parentID, const QMultiMap& connectionChildMap, QHash& fbxModels, QSet& remainingModels, QVector& modelIDs, bool isRootNode = false) { if (remainingModels.contains(parentID)) { + qCDebug(modelformat) << " remaining models contains parent " << parentID; modelIDs.append(parentID); remainingModels.remove(parentID); } - int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; + int parentIndex = 1000; + if (isRootNode) { + qCDebug(modelformat) << " found a root node " << parentID; + parentIndex = -1; + } else { + parentIndex = modelIDs.size() - 1; + } + //int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; foreach (const QString& childID, connectionChildMap.values(parentID)) { + qCDebug(modelformat) << " searching children, parent id " << parentID; if (remainingModels.contains(childID)) { FBXModel& fbxModel = fbxModels[childID]; + qCDebug(modelformat) << " child id " << fbxModel.name; if (fbxModel.parentIndex == -1) { fbxModel.parentIndex = parentIndex; appendModelIDs(childID, connectionChildMap, fbxModels, remainingModels, modelIDs); @@ -441,6 +452,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; + int nodeParentId = -1; foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { @@ -497,6 +509,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (child.name == "Objects") { foreach (const FBXNode& object, child.children) { + nodeParentId++; if (object.name == "Geometry") { if (object.properties.at(2) == "Mesh") { meshes.insert(getID(object.properties), extractMesh(object, meshIndex)); @@ -505,6 +518,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr blendshapes.append(extracted); } } else if (object.name == "Model") { + qCDebug(modelformat) << "model name from object properties " << getName(object.properties) << " node parentID " << nodeParentId; QString name = getName(object.properties); QString id = getID(object.properties); modelIDsToNames.insert(id, name); @@ -1181,6 +1195,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr #endif } + // TODO: check if is code is needed if (!lights.empty()) { if (hifiGlobalNodeID.isEmpty()) { @@ -1211,6 +1226,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr for (QHash::const_iterator fbxModel = fbxModels.constBegin(); fbxModel != fbxModels.constEnd(); fbxModel++) { // models with clusters must be parented to the cluster top // Unless the model is a root node. + qCDebug(modelformat) << "fbx model name " << fbxModel.key(); bool isARootNode = !modelIDs.contains(_connectionParentMap.value(fbxModel.key())); if (!isARootNode) { foreach(const QString& deformerID, _connectionChildMap.values(fbxModel.key())) { @@ -1266,6 +1282,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.parentIndex = fbxModel.parentIndex; int jointIndex = hfmModel.joints.size(); + qCDebug(modelformat) << "fbx joint name " << fbxModel.name << " joint index " << jointIndex << " parent index " << joint.parentIndex; + joint.translation = fbxModel.translation; // these are usually in centimeters joint.preTransform = fbxModel.preTransform; joint.preRotation = fbxModel.preRotation; From 065897a8f341ee38058c155a1626daa8982c9132 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 18:56:23 -0700 Subject: [PATCH 04/63] TEST!!! --- interface/src/Application.cpp | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3d6a26d7e3..852c4eb695 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1795,19 +1795,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(DISABLE_QML) - // Do not show login dialog if requested not to on the command line - QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); - int index = arguments().indexOf(hifiNoLoginCommandLineKey); - if (index != -1) { - resumeAfterLoginDialogActionTaken(); - return; - } - - showLoginScreen(); -#else - resumeAfterLoginDialogActionTaken(); -#endif + resumeAfterLoginDialogActionTaken(); }); // Make sure we don't time out during slow operations at startup From 80168058f7a427b49a87a0bdbbd68e595e429629 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 20:22:07 -0700 Subject: [PATCH 05/63] Correct APK installation. --- tools/nitpick/src/TestRunnerMobile.cpp | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 284b4de15c..eaebb6ca5a 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -164,22 +164,20 @@ void TestRunnerMobile::installAPK() { _adbInterface = new AdbInterface(); } - if (_installerFilename.isNull()) { - QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, - "Available APKs (*.apk)" - ); + QString installerPathname = QFileDialog::getOpenFileName(nullptr, "Please select the APK", _workingFolder, + "Available APKs (*.apk)" + ); - if (installerPathname.isNull()) { - return; - } - - // Remove the path - QStringList parts = installerPathname.split('/'); - _installerFilename = parts[parts.length() - 1]; + if (installerPathname.isNull()) { + return; } + // Remove the path + QStringList parts = installerPathname.split('/'); + _installerFilename = parts[parts.length() - 1]; + _statusLabel->setText("Installing"); - QString command = _adbInterface->getAdbCommand() + " install -r -d " + _workingFolder + "/" + _installerFilename + " >" + _workingFolder + "/installOutput.txt"; + QString command = _adbInterface->getAdbCommand() + " install -r -d " + installerPathname + " >" + _workingFolder + "/installOutput.txt"; appendLog(command); system(command.toStdString().c_str()); _statusLabel->setText("Installation complete"); From 1a9f33ef660228e0e947cc8bc443b9655640d046 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Mon, 11 Mar 2019 20:24:34 -0700 Subject: [PATCH 06/63] Removed test. --- interface/src/Application - Copy.cpp | 9191 ++++++++++++++++++++++++++ interface/src/Application.cpp | 14 +- 2 files changed, 9204 insertions(+), 1 deletion(-) create mode 100644 interface/src/Application - Copy.cpp diff --git a/interface/src/Application - Copy.cpp b/interface/src/Application - Copy.cpp new file mode 100644 index 0000000000..ca8883f660 --- /dev/null +++ b/interface/src/Application - Copy.cpp @@ -0,0 +1,9191 @@ +// +// Application.cpp +// interface/src +// +// Created by Andrzej Kapolka on 5/10/13. +// Copyright 2013 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + + +#include "Application.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include +#include + + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ui/overlays/ContextOverlayInterface.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "LocationBookmarks.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "recording/ClipCache.h" + +#include "AudioClient.h" +#include "audio/AudioScope.h" +#include "avatar/AvatarManager.h" +#include "avatar/MyHead.h" +#include "avatar/AvatarPackager.h" +#include "avatar/MyCharacterController.h" +#include "CrashRecoveryHandler.h" +#include "CrashHandler.h" +#include "devices/DdeFaceTracker.h" +#include "DiscoverabilityManager.h" +#include "GLCanvas.h" +#include "InterfaceDynamicFactory.h" +#include "InterfaceLogging.h" +#include "LODManager.h" +#include "ModelPackager.h" +#include "scripting/Audio.h" +#include "networking/CloseEventSender.h" +#include "scripting/TestScriptingInterface.h" +#include "scripting/PlatformInfoScriptingInterface.h" +#include "scripting/AssetMappingsScriptingInterface.h" +#include "scripting/ClipboardScriptingInterface.h" +#include "scripting/DesktopScriptingInterface.h" +#include "scripting/AccountServicesScriptingInterface.h" +#include "scripting/HMDScriptingInterface.h" +#include "scripting/MenuScriptingInterface.h" +#include "graphics-scripting/GraphicsScriptingInterface.h" +#include "scripting/SettingsScriptingInterface.h" +#include "scripting/WindowScriptingInterface.h" +#include "scripting/ControllerScriptingInterface.h" +#include "scripting/RatesScriptingInterface.h" +#include "scripting/SelectionScriptingInterface.h" +#include "scripting/WalletScriptingInterface.h" +#include "scripting/TTSScriptingInterface.h" +#include "scripting/KeyboardScriptingInterface.h" + + + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) +#include "SpeechRecognizer.h" +#endif +#include "ui/ResourceImageItem.h" +#include "ui/AddressBarDialog.h" +#include "ui/AvatarInputs.h" +#include "ui/DialogsManager.h" +#include "ui/LoginDialog.h" +#include "ui/Snapshot.h" +#include "ui/SnapshotAnimated.h" +#include "ui/StandAloneJSConsole.h" +#include "ui/Stats.h" +#include "ui/AnimStats.h" +#include "ui/UpdateDialog.h" +#include "ui/DomainConnectionModel.h" +#include "ui/Keyboard.h" +#include "Util.h" +#include "InterfaceParentFinder.h" +#include "ui/OctreeStatsProvider.h" + +#include "avatar/GrabManager.h" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "commerce/Ledger.h" +#include "commerce/Wallet.h" +#include "commerce/QmlCommerce.h" +#include "commerce/QmlMarketplace.h" +#include "ResourceRequestObserver.h" + +#include "webbrowser/WebBrowserSuggestionsEngine.h" +#include + + +#include "AboutUtil.h" + +#if defined(Q_OS_WIN) +#include + +#ifdef DEBUG_EVENT_QUEUE +// This is a HACK that uses private headers included with the qt source distrubution. +// To use this feature you need to add these directores to your include path: +// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1/QtCore +// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1 +#define QT_BOOTSTRAPPED +#include +#include +#undef QT_BOOTSTRAPPED +#endif + +// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU +// FIXME seems to be broken. +extern "C" { + _declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; +} +#endif + +#if defined(Q_OS_ANDROID) +#include +#include "AndroidHelper.h" +#endif + +#include "graphics/RenderEventHandler.h" + +Q_LOGGING_CATEGORY(trace_app_input_mouse, "trace.app.input.mouse") + +using namespace std; + +static QTimer locationUpdateTimer; +static QTimer identityPacketTimer; +static QTimer pingTimer; + +#if defined(Q_OS_ANDROID) +static bool DISABLE_WATCHDOG = true; +#else +static const QString DISABLE_WATCHDOG_FLAG{ "HIFI_DISABLE_WATCHDOG" }; +static bool DISABLE_WATCHDOG = nsightActive() || QProcessEnvironment::systemEnvironment().contains(DISABLE_WATCHDOG_FLAG); +#endif + +#if defined(USE_GLES) +static bool DISABLE_DEFERRED = true; +#else +static const QString RENDER_FORWARD{ "HIFI_RENDER_FORWARD" }; +static bool DISABLE_DEFERRED = QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); +#endif + +#if !defined(Q_OS_ANDROID) +static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; +#else +static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 4; +#endif + +// For processing on QThreadPool, we target a number of threads after reserving some +// based on how many are being consumed by the application and the display plugin. However, +// we will never drop below the 'min' value +static const int MIN_PROCESSING_THREAD_POOL_SIZE = 1; + +static const QString SNAPSHOT_EXTENSION = ".jpg"; +static const QString JPG_EXTENSION = ".jpg"; +static const QString PNG_EXTENSION = ".png"; +static const QString SVO_EXTENSION = ".svo"; +static const QString SVO_JSON_EXTENSION = ".svo.json"; +static const QString JSON_GZ_EXTENSION = ".json.gz"; +static const QString JSON_EXTENSION = ".json"; +static const QString JS_EXTENSION = ".js"; +static const QString FST_EXTENSION = ".fst"; +static const QString FBX_EXTENSION = ".fbx"; +static const QString OBJ_EXTENSION = ".obj"; +static const QString AVA_JSON_EXTENSION = ".ava.json"; +static const QString WEB_VIEW_TAG = "noDownload=true"; +static const QString ZIP_EXTENSION = ".zip"; +static const QString CONTENT_ZIP_EXTENSION = ".content.zip"; + +static const float MIRROR_FULLSCREEN_DISTANCE = 0.789f; + +static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; + +static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; +static const QString INFO_HELP_PATH = "html/tabletHelp.html"; + +static const unsigned int THROTTLED_SIM_FRAMERATE = 15; +static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; +static const int ENTITY_SERVER_ADDED_TIMEOUT = 5000; +static const int ENTITY_SERVER_CONNECTION_TIMEOUT = 5000; + +static const float INITIAL_QUERY_RADIUS = 10.0f; // priority radius for entities before physics enabled + +static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + +Setting::Handle maxOctreePacketsPerSecond{"maxOctreePPS", DEFAULT_MAX_OCTREE_PPS}; + +Setting::Handle loginDialogPoppedUp{"loginDialogPoppedUp", false}; + +static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; +static const QString NO_MOVEMENT_MAPPING_NAME = "Standard to Action (No Movement)"; +static const QString NO_MOVEMENT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_nomovement.json"; + +static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; +static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds +static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; +static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin"; +static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; +static const QString KEEP_ME_LOGGED_IN_SETTING_NAME = "keepMeLoggedIn"; + +static const float FOCUS_HIGHLIGHT_EXPANSION_FACTOR = 1.05f; + +#if defined(Q_OS_ANDROID) +static const QString TESTER_FILE = "/sdcard/_hifi_test_device.txt"; +#endif +const std::vector> Application::_acceptedExtensions { + { SVO_EXTENSION, &Application::importSVOFromURL }, + { SVO_JSON_EXTENSION, &Application::importSVOFromURL }, + { AVA_JSON_EXTENSION, &Application::askToWearAvatarAttachmentUrl }, + { JSON_EXTENSION, &Application::importJSONFromURL }, + { JS_EXTENSION, &Application::askToLoadScript }, + { FST_EXTENSION, &Application::askToSetAvatarUrl }, + { JSON_GZ_EXTENSION, &Application::askToReplaceDomainContent }, + { CONTENT_ZIP_EXTENSION, &Application::askToReplaceDomainContent }, + { ZIP_EXTENSION, &Application::importFromZIP }, + { JPG_EXTENSION, &Application::importImage }, + { PNG_EXTENSION, &Application::importImage } +}; + +class DeadlockWatchdogThread : public QThread { +public: + static const unsigned long HEARTBEAT_UPDATE_INTERVAL_SECS = 1; + static const unsigned long MAX_HEARTBEAT_AGE_USECS = 120 * USECS_PER_SECOND; // 2 mins with no checkin probably a deadlock + static const int WARNING_ELAPSED_HEARTBEAT = 500 * USECS_PER_MSEC; // warn if elapsed heartbeat average is large + static const int HEARTBEAT_SAMPLES = 100000; // ~5 seconds worth of samples + + // Set the heartbeat on launch + DeadlockWatchdogThread() { + setObjectName("Deadlock Watchdog"); + // Give the heartbeat an initial value + _heartbeat = usecTimestampNow(); + _paused = false; + connect(qApp, &QCoreApplication::aboutToQuit, [this] { + _quit = true; + }); + } + + void setMainThreadID(Qt::HANDLE threadID) { + _mainThreadID = threadID; + } + + static void updateHeartbeat() { + auto now = usecTimestampNow(); + auto elapsed = now - _heartbeat; + _movingAverage.addSample(elapsed); + _heartbeat = now; + } + + void deadlockDetectionCrash() { + setCrashAnnotation("_mod_faulting_tid", std::to_string((uint64_t)_mainThreadID)); + setCrashAnnotation("deadlock", "1"); + uint32_t* crashTrigger = nullptr; + *crashTrigger = 0xDEAD10CC; + } + + static void withPause(const std::function& lambda) { + pause(); + lambda(); + resume(); + } + static void pause() { + _paused = true; + } + + static void resume() { + // Update the heartbeat BEFORE resuming the checks + updateHeartbeat(); + _paused = false; + } + + void run() override { + while (!_quit) { + QThread::sleep(HEARTBEAT_UPDATE_INTERVAL_SECS); + // Don't do heartbeat detection under nsight + if (_paused) { + continue; + } + uint64_t lastHeartbeat = _heartbeat; // sample atomic _heartbeat, because we could context switch away and have it updated on us + uint64_t now = usecTimestampNow(); + auto lastHeartbeatAge = (now > lastHeartbeat) ? now - lastHeartbeat : 0; + auto elapsedMovingAverage = _movingAverage.getAverage(); + + if (elapsedMovingAverage > _maxElapsedAverage) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" + << "lastHeartbeatAge:" << lastHeartbeatAge + << "elapsedMovingAverage:" << elapsedMovingAverage + << "maxElapsed:" << _maxElapsed + << "PREVIOUS maxElapsedAverage:" << _maxElapsedAverage + << "NEW maxElapsedAverage:" << elapsedMovingAverage << "** NEW MAX ELAPSED AVERAGE **" + << "samples:" << _movingAverage.getSamples(); + _maxElapsedAverage = elapsedMovingAverage; + } + if (lastHeartbeatAge > _maxElapsed) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" + << "lastHeartbeatAge:" << lastHeartbeatAge + << "elapsedMovingAverage:" << elapsedMovingAverage + << "PREVIOUS maxElapsed:" << _maxElapsed + << "NEW maxElapsed:" << lastHeartbeatAge << "** NEW MAX ELAPSED **" + << "maxElapsedAverage:" << _maxElapsedAverage + << "samples:" << _movingAverage.getSamples(); + _maxElapsed = lastHeartbeatAge; + } + if (elapsedMovingAverage > WARNING_ELAPSED_HEARTBEAT) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" + << "lastHeartbeatAge:" << lastHeartbeatAge + << "elapsedMovingAverage:" << elapsedMovingAverage << "** OVER EXPECTED VALUE **" + << "maxElapsed:" << _maxElapsed + << "maxElapsedAverage:" << _maxElapsedAverage + << "samples:" << _movingAverage.getSamples(); + } + + if (lastHeartbeatAge > MAX_HEARTBEAT_AGE_USECS) { + qCDebug(interfaceapp_deadlock) << "DEADLOCK DETECTED -- " + << "lastHeartbeatAge:" << lastHeartbeatAge + << "[ lastHeartbeat :" << lastHeartbeat + << "now:" << now << " ]" + << "elapsedMovingAverage:" << elapsedMovingAverage + << "maxElapsed:" << _maxElapsed + << "maxElapsedAverage:" << _maxElapsedAverage + << "samples:" << _movingAverage.getSamples(); + + // Don't actually crash in debug builds, in case this apparent deadlock is simply from + // the developer actively debugging code + #ifdef NDEBUG + deadlockDetectionCrash(); + #endif + } + } + } + + static std::atomic _paused; + static std::atomic _heartbeat; + static std::atomic _maxElapsed; + static std::atomic _maxElapsedAverage; + static ThreadSafeMovingAverage _movingAverage; + + bool _quit { false }; + + Qt::HANDLE _mainThreadID = nullptr; +}; + +std::atomic DeadlockWatchdogThread::_paused; +std::atomic DeadlockWatchdogThread::_heartbeat; +std::atomic DeadlockWatchdogThread::_maxElapsed; +std::atomic DeadlockWatchdogThread::_maxElapsedAverage; +ThreadSafeMovingAverage DeadlockWatchdogThread::_movingAverage; + +bool isDomainURL(QUrl url) { + if (!url.isValid()) { + return false; + } + if (url.scheme() == URL_SCHEME_HIFI) { + return true; + } + if (url.scheme() != HIFI_URL_SCHEME_FILE) { + // TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can + // be loaded over http(s) + // && url.scheme() != HIFI_URL_SCHEME_HTTP && + // url.scheme() != HIFI_URL_SCHEME_HTTPS + return false; + } + if (url.path().endsWith(".json", Qt::CaseInsensitive) || + url.path().endsWith(".json.gz", Qt::CaseInsensitive)) { + return true; + } + return false; +} + +#ifdef Q_OS_WIN +class MyNativeEventFilter : public QAbstractNativeEventFilter { +public: + static MyNativeEventFilter& getInstance() { + static MyNativeEventFilter staticInstance; + return staticInstance; + } + + bool nativeEventFilter(const QByteArray &eventType, void* msg, long* result) Q_DECL_OVERRIDE { + if (eventType == "windows_generic_MSG") { + MSG* message = (MSG*)msg; + + if (message->message == UWM_IDENTIFY_INSTANCES) { + *result = UWM_IDENTIFY_INSTANCES; + return true; + } + + if (message->message == UWM_SHOW_APPLICATION) { + MainWindow* applicationWindow = qApp->getWindow(); + if (applicationWindow->isMinimized()) { + applicationWindow->showNormal(); // Restores to windowed or maximized state appropriately. + } + qApp->setActiveWindow(applicationWindow); // Flashes the taskbar icon if not focus. + return true; + } + + if (message->message == WM_COPYDATA) { + COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)(message->lParam); + QUrl url = QUrl((const char*)(pcds->lpData)); + if (isDomainURL(url)) { + DependencyManager::get()->handleLookupString(url.toString()); + return true; + } + } + + if (message->message == WM_DEVICECHANGE) { + const float MIN_DELTA_SECONDS = 2.0f; // de-bounce signal + static float lastTriggerTime = 0.0f; + const float deltaSeconds = secTimestampNow() - lastTriggerTime; + lastTriggerTime = secTimestampNow(); + if (deltaSeconds > MIN_DELTA_SECONDS) { + Midi::USBchanged(); // re-scan the MIDI bus + } + } + } + return false; + } +}; +#endif + +class LambdaEvent : public QEvent { + std::function _fun; +public: + LambdaEvent(const std::function & fun) : + QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { + } + LambdaEvent(std::function && fun) : + QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { + } + void call() const { _fun(); } +}; + +void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { + QString logMessage = LogHandler::getInstance().printMessage((LogMsgType) type, context, message); + + if (!logMessage.isEmpty()) { +#ifdef Q_OS_ANDROID + const char * local=logMessage.toStdString().c_str(); + switch (type) { + case QtDebugMsg: + __android_log_write(ANDROID_LOG_DEBUG,"Interface",local); + break; + case QtInfoMsg: + __android_log_write(ANDROID_LOG_INFO,"Interface",local); + break; + case QtWarningMsg: + __android_log_write(ANDROID_LOG_WARN,"Interface",local); + break; + case QtCriticalMsg: + __android_log_write(ANDROID_LOG_ERROR,"Interface",local); + break; + case QtFatalMsg: + default: + __android_log_write(ANDROID_LOG_FATAL,"Interface",local); + abort(); + } +#else + qApp->getLogger()->addMessage(qPrintable(logMessage)); +#endif + } +} + + +class ApplicationMeshProvider : public scriptable::ModelProviderFactory { +public: + virtual scriptable::ModelProviderPointer lookupModelProvider(const QUuid& uuid) override { + bool success; + if (auto nestable = DependencyManager::get()->find(uuid, success).lock()) { + auto type = nestable->getNestableType(); +#ifdef SCRIPTABLE_MESH_DEBUG + qCDebug(interfaceapp) << "ApplicationMeshProvider::lookupModelProvider" << uuid << SpatiallyNestable::nestableTypeToString(type); +#endif + switch (type) { + case NestableType::Entity: + return getEntityModelProvider(static_cast(uuid)); + case NestableType::Avatar: + return getAvatarModelProvider(uuid); + } + } + return nullptr; + } + +private: + scriptable::ModelProviderPointer getEntityModelProvider(EntityItemID entityID) { + scriptable::ModelProviderPointer provider; + auto entityTreeRenderer = qApp->getEntities(); + auto entityTree = entityTreeRenderer->getTree(); + if (auto entity = entityTree->findEntityByID(entityID)) { + if (auto renderer = entityTreeRenderer->renderableForEntityId(entityID)) { + provider = std::dynamic_pointer_cast(renderer); + provider->modelProviderType = NestableType::Entity; + } else { + qCWarning(interfaceapp) << "no renderer for entity ID" << entityID.toString(); + } + } + return provider; + } + + scriptable::ModelProviderPointer getAvatarModelProvider(QUuid sessionUUID) { + scriptable::ModelProviderPointer provider; + auto avatarManager = DependencyManager::get(); + if (auto avatar = avatarManager->getAvatarBySessionID(sessionUUID)) { + provider = std::dynamic_pointer_cast(avatar); + provider->modelProviderType = NestableType::Avatar; + } + return provider; + } +}; + +/**jsdoc + *

The Controller.Hardware.Application object has properties representing Interface's state. The property + * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or + * Controller.Standard items in a {@link RouteObject} mapping (e.g., using the {@link RouteObject#when} method). + * Each data value is either 1.0 for "true" or 0.0 for "false".

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PropertyTypeDataDescription
CameraFirstPersonnumbernumberThe camera is in first-person mode. + *
CameraThirdPersonnumbernumberThe camera is in third-person mode. + *
CameraFSMnumbernumberThe camera is in full screen mirror mode.
CameraIndependentnumbernumberThe camera is in independent mode.
CameraEntitynumbernumberThe camera is in entity mode.
InHMDnumbernumberThe user is in HMD mode.
AdvancedMovementnumbernumberAdvanced movement controls are enabled. + *
SnapTurnnumbernumberSnap turn is enabled.
GroundednumbernumberThe user's avatar is on the ground.
NavigationFocusednumbernumberNot used.
+ * @typedef {object} Controller.Hardware-Application + */ + +static const QString STATE_IN_HMD = "InHMD"; +static const QString STATE_CAMERA_FULL_SCREEN_MIRROR = "CameraFSM"; +static const QString STATE_CAMERA_FIRST_PERSON = "CameraFirstPerson"; +static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; +static const QString STATE_CAMERA_ENTITY = "CameraEntity"; +static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; +static const QString STATE_SNAP_TURN = "SnapTurn"; +static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; +static const QString STATE_GROUNDED = "Grounded"; +static const QString STATE_NAV_FOCUSED = "NavigationFocused"; +static const QString STATE_PLATFORM_WINDOWS = "PlatformWindows"; +static const QString STATE_PLATFORM_MAC = "PlatformMac"; +static const QString STATE_PLATFORM_ANDROID = "PlatformAndroid"; + +// Statically provided display and input plugins +extern DisplayPluginList getDisplayPlugins(); +extern InputPluginList getInputPlugins(); +extern void saveInputPluginSettings(const InputPluginList& plugins); + +// Parameters used for running tests from teh command line +const QString TEST_SCRIPT_COMMAND{ "--testScript" }; +const QString TEST_QUIT_WHEN_FINISHED_OPTION{ "quitWhenFinished" }; +const QString TEST_RESULTS_LOCATION_COMMAND{ "--testResultsLocation" }; + +bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { + const char** constArgv = const_cast(argv); + + qInstallMessageHandler(messageHandler); + + // HRS: I could not figure out how to move these any earlier in startup, so when using this option, be sure to also supply + // --allowMultipleInstances + auto reportAndQuit = [&](const char* commandSwitch, std::function report) { + const char* reportfile = getCmdOption(argc, constArgv, commandSwitch); + // Reports to the specified file, because stdout is set up to be captured for logging. + if (reportfile) { + FILE* fp = fopen(reportfile, "w"); + if (fp) { + report(fp); + fclose(fp); + if (!runningMarkerExisted) { // don't leave ours around + RunningMarker runingMarker(RUNNING_MARKER_FILENAME); + runingMarker.deleteRunningMarkerFile(); // happens in deleter, but making the side-effect explicit. + } + _exit(0); + } + } + }; + reportAndQuit("--protocolVersion", [&](FILE* fp) { + auto version = protocolVersionsSignatureBase64(); + fputs(version.toLatin1().data(), fp); + }); + reportAndQuit("--version", [&](FILE* fp) { + fputs(BuildInfo::VERSION.toLatin1().data(), fp); + }); + + const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); + const int listenPort = portStr ? atoi(portStr) : INVALID_PORT; + + static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; + bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); + + // set the OCULUS_STORE property so the oculus plugin can know if we ran from the Oculus Store + static const auto OCULUS_STORE_ARG = "--oculus-store"; + bool isStore = cmdOptionExists(argc, const_cast(argv), OCULUS_STORE_ARG); + qApp->setProperty(hifi::properties::OCULUS_STORE, isStore); + + // Ignore any previous crashes if running from command line with a test script. + bool inTestMode { false }; + for (int i = 0; i < argc; ++i) { + QString parameter(argv[i]); + if (parameter == TEST_SCRIPT_COMMAND) { + inTestMode = true; + break; + } + } + + bool previousSessionCrashed { false }; + if (!inTestMode) { + previousSessionCrashed = CrashRecoveryHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt); + } + + // get dir to use for cache + static const auto CACHE_SWITCH = "--cache"; + QString cacheDir = getCmdOption(argc, const_cast(argv), CACHE_SWITCH); + if (!cacheDir.isEmpty()) { + qApp->setProperty(hifi::properties::APP_LOCAL_DATA_PATH, cacheDir); + } + + { + const QString resourcesBinaryFile = PathUtils::getRccPath(); + if (!QFile::exists(resourcesBinaryFile)) { + throw std::runtime_error("Unable to find primary resources"); + } + if (!QResource::registerResource(resourcesBinaryFile)) { + throw std::runtime_error("Unable to load primary resources"); + } + } + + // Tell the plugin manager about our statically linked plugins + DependencyManager::set(); + auto pluginManager = PluginManager::getInstance(); + pluginManager->setInputPluginProvider([] { return getInputPlugins(); }); + pluginManager->setDisplayPluginProvider([] { return getDisplayPlugins(); }); + pluginManager->setInputPluginSettingsPersister([](const InputPluginList& plugins) { saveInputPluginSettings(plugins); }); + if (auto steamClient = pluginManager->getSteamClientPlugin()) { + steamClient->init(); + } + if (auto oculusPlatform = pluginManager->getOculusPlatformPlugin()) { + oculusPlatform->init(); + } + + PROFILE_SET_THREAD_NAME("Main Thread"); + +#if defined(Q_OS_WIN) + // Select appropriate audio DLL + QString audioDLLPath = QCoreApplication::applicationDirPath(); + if (IsWindows8OrGreater()) { + audioDLLPath += "/audioWin8"; + } else { + audioDLLPath += "/audioWin7"; + } + QCoreApplication::addLibraryPath(audioDLLPath); +#endif + + DependencyManager::registerInheritance(); + DependencyManager::registerInheritance(); + DependencyManager::registerInheritance(); + DependencyManager::registerInheritance(); + + // Set dependencies + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); +#if defined(Q_OS_ANDROID) + DependencyManager::set(); // use the default user agent getter +#else + DependencyManager::set(std::bind(&Application::getUserAgent, qApp)); +#endif + DependencyManager::set(); + DependencyManager::set(ScriptEngine::CLIENT_SCRIPT); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(NodeType::Agent, listenPort); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache constructor. + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(true); + DependencyManager::set(); + DependencyManager::registerInheritance(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + DependencyManager::set(); +#endif + DependencyManager::set(); + DependencyManager::set(); +#if !defined(DISABLE_QML) + DependencyManager::set(); +#endif + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, + STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, + STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED, + STATE_PLATFORM_WINDOWS, STATE_PLATFORM_MAC, STATE_PLATFORM_ANDROID } }); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(true, qApp, qApp); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + + return previousSessionCrashed; +} + +// FIXME move to header, or better yet, design some kind of UI manager +// to take care of highlighting keyboard focused items, rather than +// continuing to overburden Application.cpp +QUuid _keyboardFocusHighlightID; + +OffscreenGLCanvas* _qmlShareContext { nullptr }; + +// FIXME hack access to the internal share context for the Chromium helper +// Normally we'd want to use QWebEngine::initialize(), but we can't because +// our primary context is a QGLWidget, which can't easily be initialized to share +// from a QOpenGLContext. +// +// So instead we create a new offscreen context to share with the QGLWidget, +// and manually set THAT to be the shared context for the Chromium helper +#if !defined(DISABLE_QML) +OffscreenGLCanvas* _chromiumShareContext { nullptr }; +#endif + +Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); +Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); + +Setting::Handle sessionRunTime{ "sessionRunTime", 0 }; + +const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 60.0f; +const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; +const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; +const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; +const bool DEFAULT_PREFER_STYLUS_OVER_LASER = false; +const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; +const QString DEFAULT_CURSOR_NAME = "DEFAULT"; +const bool DEFAULT_MINI_TABLET_ENABLED = true; + +QSharedPointer getOffscreenUI() { +#if !defined(DISABLE_QML) + return DependencyManager::get(); +#else + return nullptr; +#endif +} + +Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runningMarkerExisted) : + QApplication(argc, argv), + _window(new MainWindow(desktop())), + _sessionRunTimer(startupTimer), +#ifndef Q_OS_ANDROID + _logger(new FileLogger(this)), +#endif + _previousSessionCrashed(setupEssentials(argc, argv, runningMarkerExisted)), + _entitySimulation(new PhysicalEntitySimulation()), + _physicsEngine(new PhysicsEngine(Vectors::ZERO)), + _entityClipboard(new EntityTree()), + _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), + _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), + _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), + _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), + _firstRun(Settings::firstRun, true), + _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), + _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), + _preferStylusOverLaserSetting("preferStylusOverLaser", DEFAULT_PREFER_STYLUS_OVER_LASER), + _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS), + _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), + _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME), + _miniTabletEnabledSetting("miniTabletEnabled", DEFAULT_MINI_TABLET_ENABLED), + _scaleMirror(1.0f), + _mirrorYawOffset(0.0f), + _raiseMirror(0.0f), + _enableProcessOctreeThread(true), + _lastNackTime(usecTimestampNow()), + _lastSendDownstreamAudioStats(usecTimestampNow()), + _notifiedPacketVersionMismatchThisDomain(false), + _maxOctreePPS(maxOctreePacketsPerSecond.get()), + _lastFaceTrackerUpdate(0), + _snapshotSound(nullptr), + _sampleSound(nullptr) +{ + + auto steamClient = PluginManager::getInstance()->getSteamClientPlugin(); + setProperty(hifi::properties::STEAM, (steamClient && steamClient->isRunning())); + setProperty(hifi::properties::CRASHED, _previousSessionCrashed); + + { + const QStringList args = arguments(); + + for (int i = 0; i < args.size() - 1; ++i) { + if (args.at(i) == TEST_SCRIPT_COMMAND && (i + 1) < args.size()) { + QString testScriptPath = args.at(i + 1); + + // If the URL scheme is http(s) or ftp, then use as is, else - treat it as a local file + // This is done so as not break previous command line scripts + if (testScriptPath.left(HIFI_URL_SCHEME_HTTP.length()) == HIFI_URL_SCHEME_HTTP || + testScriptPath.left(HIFI_URL_SCHEME_FTP.length()) == HIFI_URL_SCHEME_FTP) { + + setProperty(hifi::properties::TEST, QUrl::fromUserInput(testScriptPath)); + } else if (QFileInfo(testScriptPath).exists()) { + setProperty(hifi::properties::TEST, QUrl::fromLocalFile(testScriptPath)); + } + + // quite when finished parameter must directly follow the test script + if ((i + 2) < args.size() && args.at(i + 2) == TEST_QUIT_WHEN_FINISHED_OPTION) { + quitWhenFinished = true; + } + } else if (args.at(i) == TEST_RESULTS_LOCATION_COMMAND) { + // Set test snapshot location only if it is a writeable directory + QString path(args.at(i + 1)); + + QFileInfo fileInfo(path); + if (fileInfo.isDir() && fileInfo.isWritable()) { + TestScriptingInterface::getInstance()->setTestResultsLocation(path); + } + } + } + } + + // make sure the debug draw singleton is initialized on the main thread. + DebugDraw::getInstance().removeMarker(""); + + PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care + PluginManager::getInstance()->setContainer(pluginContainer); + + QThreadPool::globalInstance()->setMaxThreadCount(MIN_PROCESSING_THREAD_POOL_SIZE); + thread()->setPriority(QThread::HighPriority); + thread()->setObjectName("Main Thread"); + + setInstance(this); + + auto controllerScriptingInterface = DependencyManager::get().data(); + _controllerScriptingInterface = dynamic_cast(controllerScriptingInterface); + connect(PluginManager::getInstance().data(), &PluginManager::inputDeviceRunningChanged, + controllerScriptingInterface, &controller::ScriptingInterface::updateRunningInputDevices); + + EntityTree::setEntityClicksCapturedOperator([this] { + return _controllerScriptingInterface->areEntityClicksCaptured(); + }); + + _entityClipboard->createRootElement(); + +#ifdef Q_OS_WIN + installNativeEventFilter(&MyNativeEventFilter::getInstance()); +#endif + + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/fontawesome-webfont.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/hifi-glyphs.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/AnonymousPro-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-SemiBold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Light.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Regular.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/rawline-500.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Bold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-SemiBold.ttf"); + QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Cairo-SemiBold.ttf"); + _window->setWindowTitle("High Fidelity Interface"); + + Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us + + auto nodeList = DependencyManager::get(); + nodeList->startThread(); + nodeList->setFlagTimeForConnectionStep(true); + + // move the AddressManager to the NodeList thread so that domain resets due to domain changes always occur + // before we tell MyAvatar to go to a new location in the new domain + auto addressManager = DependencyManager::get(); + addressManager->moveToThread(nodeList->thread()); + + const char** constArgv = const_cast(argv); + if (cmdOptionExists(argc, constArgv, "--disableWatchdog")) { + DISABLE_WATCHDOG = true; + } + // Set up a watchdog thread to intentionally crash the application on deadlocks + if (!DISABLE_WATCHDOG) { + auto deadlockWatchdogThread = new DeadlockWatchdogThread(); + deadlockWatchdogThread->setMainThreadID(QThread::currentThreadId()); + deadlockWatchdogThread->start(); + } + + // Set File Logger Session UUID + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + if (avatarManager) { + workload::SpacePointer space = getEntities()->getWorkloadSpace(); + avatarManager->setSpace(space); + } + auto accountManager = DependencyManager::get(); + +#ifndef Q_OS_ANDROID + _logger->setSessionID(accountManager->getSessionID()); +#endif + + setCrashAnnotation("metaverse_session_id", accountManager->getSessionID().toString().toStdString()); + setCrashAnnotation("main_thread_id", std::to_string((size_t)QThread::currentThreadId())); + + if (steamClient) { + qCDebug(interfaceapp) << "[VERSION] SteamVR buildID:" << steamClient->getSteamVRBuildID(); + } + setCrashAnnotation("steam", property(hifi::properties::STEAM).toBool() ? "1" : "0"); + + qCDebug(interfaceapp) << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); + qCDebug(interfaceapp) << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; + qCDebug(interfaceapp) << "[VERSION] VERSION:" << BuildInfo::VERSION; + qCDebug(interfaceapp) << "[VERSION] BUILD_TYPE_STRING:" << BuildInfo::BUILD_TYPE_STRING; + qCDebug(interfaceapp) << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES; +#if USE_STABLE_GLOBAL_SERVICES + qCDebug(interfaceapp) << "[VERSION] We will use STABLE global services."; +#else + qCDebug(interfaceapp) << "[VERSION] We will use DEVELOPMENT global services."; +#endif + + bool isStore = property(hifi::properties::OCULUS_STORE).toBool(); + + DependencyManager::get()->setLimitedCommerce(isStore); // Or we could make it a separate arg, or if either arg is set, etc. And should this instead by a hifi::properties? + + updateHeartbeat(); + + // setup a timer for domain-server check ins + QTimer* domainCheckInTimer = new QTimer(this); + QWeakPointer nodeListWeak = nodeList; + connect(domainCheckInTimer, &QTimer::timeout, [this, nodeListWeak] { + auto nodeList = nodeListWeak.lock(); + if (!isServerlessMode() && nodeList) { + nodeList->sendDomainServerCheckIn(); + } + }); + domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); + connect(this, &QCoreApplication::aboutToQuit, [domainCheckInTimer] { + domainCheckInTimer->stop(); + domainCheckInTimer->deleteLater(); + }); + + { + auto audioIO = DependencyManager::get().data(); + audioIO->setPositionGetter([] { + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + + return myAvatar ? myAvatar->getPositionForAudio() : Vectors::ZERO; + }); + audioIO->setOrientationGetter([] { + auto avatarManager = DependencyManager::get(); + auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; + + return myAvatar ? myAvatar->getOrientationForAudio() : Quaternions::IDENTITY; + }); + + recording::Frame::registerFrameHandler(AudioConstants::getAudioFrameName(), [&audioIO](recording::Frame::ConstPointer frame) { + audioIO->handleRecordedAudioInput(frame->data); + }); + + connect(audioIO, &AudioClient::inputReceived, [](const QByteArray& audio) { + static auto recorder = DependencyManager::get(); + if (recorder->isRecording()) { + static const recording::FrameType AUDIO_FRAME_TYPE = recording::Frame::registerFrameType(AudioConstants::getAudioFrameName()); + recorder->recordFrame(AUDIO_FRAME_TYPE, audio); + } + }); + audioIO->startThread(); + } + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + // Setup MessagesClient + DependencyManager::get()->startThread(); + + const DomainHandler& domainHandler = nodeList->getDomainHandler(); + + connect(&domainHandler, SIGNAL(domainURLChanged(QUrl)), SLOT(domainURLChanged(QUrl))); + connect(&domainHandler, SIGNAL(redirectToErrorDomainURL(QUrl)), SLOT(goToErrorDomainURL(QUrl))); + connect(&domainHandler, &DomainHandler::domainURLChanged, [](QUrl domainURL){ + setCrashAnnotation("domain", domainURL.toString().toStdString()); + }); + connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); + connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle())); + connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { + auto tabletScriptingInterface = DependencyManager::get(); + if (tabletScriptingInterface) { + tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr); + } + auto entityScriptingInterface = DependencyManager::get(); + entityScriptingInterface->deleteEntity(getTabletScreenID()); + entityScriptingInterface->deleteEntity(getTabletHomeButtonID()); + entityScriptingInterface->deleteEntity(getTabletFrameID()); + _failedToConnectToEntityServer = false; + }); + + _entityServerConnectionTimer.setSingleShot(true); + connect(&_entityServerConnectionTimer, &QTimer::timeout, this, &Application::setFailedToConnectToEntityServer); + + connect(&domainHandler, &DomainHandler::connectedToDomain, this, [this]() { + if (!isServerlessMode()) { + _entityServerConnectionTimer.setInterval(ENTITY_SERVER_ADDED_TIMEOUT); + _entityServerConnectionTimer.start(); + _failedToConnectToEntityServer = false; + } + }); + connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); + + nodeList->getDomainHandler().setErrorDomainURL(QUrl(REDIRECT_HIFI_ADDRESS)); + + // We could clear ATP assets only when changing domains, but it's possible that the domain you are connected + // to has gone down and switched to a new content set, so when you reconnect the cached ATP assets will no longer be valid. + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, DependencyManager::get().data(), &ScriptCache::clearATPScriptsFromCache); + + // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one + const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SECOND; + + auto discoverabilityManager = DependencyManager::get(); + connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); + connect(&locationUpdateTimer, &QTimer::timeout, + DependencyManager::get().data(), &AddressManager::storeCurrentAddress); + locationUpdateTimer.start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS); + + // if we get a domain change, immediately attempt update location in metaverse server + connect(&nodeList->getDomainHandler(), &DomainHandler::connectedToDomain, + discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); + + // send a location update immediately + discoverabilityManager->updateLocation(); + + connect(nodeList.data(), &NodeList::nodeAdded, this, &Application::nodeAdded); + connect(nodeList.data(), &NodeList::nodeKilled, this, &Application::nodeKilled); + connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); + connect(nodeList.data(), &NodeList::uuidChanged, myAvatar.get(), &MyAvatar::setSessionUUID); + connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); + connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); + + // you might think we could just do this in NodeList but we only want this connection for Interface + connect(&nodeList->getDomainHandler(), SIGNAL(limitOfSilentDomainCheckInsReached()), + nodeList.data(), SLOT(reset())); + + auto dialogsManager = DependencyManager::get(); +#if defined(Q_OS_ANDROID) + connect(accountManager.data(), &AccountManager::authRequired, this, []() { + auto addressManager = DependencyManager::get(); + AndroidHelper::instance().showLoginDialog(addressManager->currentAddress()); + }); +#else + connect(accountManager.data(), &AccountManager::authRequired, dialogsManager.data(), &DialogsManager::showLoginDialog); +#endif + connect(accountManager.data(), &AccountManager::usernameChanged, this, &Application::updateWindowTitle); + + // set the account manager's root URL and trigger a login request if we don't have the access token + accountManager->setIsAgent(true); + accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL()); + + // use our MyAvatar position and quat for address manager path + addressManager->setPositionGetter([this]{ return getMyAvatar()->getWorldFeetPosition(); }); + addressManager->setOrientationGetter([this]{ return getMyAvatar()->getWorldOrientation(); }); + + connect(addressManager.data(), &AddressManager::hostChanged, this, &Application::updateWindowTitle); + connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); + + connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateThreadPoolCount); + connect(this, &Application::activeDisplayPluginChanged, this, [](){ + qApp->setProperty(hifi::properties::HMD, qApp->isHMDMode()); + auto displayPlugin = qApp->getActiveDisplayPlugin(); + setCrashAnnotation("display_plugin", displayPlugin->getName().toStdString()); + setCrashAnnotation("hmd", displayPlugin->isHmd() ? "1" : "0"); + }); + connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateSystemTabletMode); + connect(this, &Application::activeDisplayPluginChanged, this, [&](){ + if (getLoginDialogPoppedUp()) { + auto dialogsManager = DependencyManager::get(); + auto keyboard = DependencyManager::get(); + if (_firstRun.get()) { + // display mode changed. Don't allow auto-switch to work after this session. + _firstRun.set(false); + } + if (isHMDMode()) { + emit loginDialogFocusDisabled(); + dialogsManager->hideLoginDialog(); + createLoginDialog(); + } else { + DependencyManager::get()->deleteEntity(_loginDialogID); + _loginDialogID = QUuid(); + _loginStateManager.tearDown(); + dialogsManager->showLoginDialog(); + emit loginDialogFocusEnabled(); + } + } + }); + + // Save avatar location immediately after a teleport. + connect(myAvatar.get(), &MyAvatar::positionGoneTo, + DependencyManager::get().data(), &AddressManager::storeCurrentAddress); + + connect(myAvatar.get(), &MyAvatar::skeletonModelURLChanged, [](){ + QUrl avatarURL = qApp->getMyAvatar()->getSkeletonModelURL(); + setCrashAnnotation("avatar", avatarURL.toString().toStdString()); + }); + + + // Inititalize sample before registering + _sampleSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/sample.wav")); + + { + auto scriptEngines = DependencyManager::get().data(); + scriptEngines->registerScriptInitializer([this](ScriptEnginePointer engine) { + registerScriptEngineWithApplicationServices(engine); + }); + + connect(scriptEngines, &ScriptEngines::scriptCountChanged, this, [this] { + auto scriptEngines = DependencyManager::get(); + if (scriptEngines->getRunningScripts().isEmpty()) { + getMyAvatar()->clearScriptableSettings(); + } + }, Qt::QueuedConnection); + + connect(scriptEngines, &ScriptEngines::scriptsReloading, this, [this] { + getEntities()->reloadEntityScripts(); + loadAvatarScripts(getMyAvatar()->getScriptUrls()); + }, Qt::QueuedConnection); + + connect(scriptEngines, &ScriptEngines::scriptLoadError, + this, [](const QString& filename, const QString& error) { + OffscreenUi::asyncWarning(nullptr, "Error Loading Script", filename + " failed to load."); + }, Qt::QueuedConnection); + } + +#ifdef _WIN32 + WSADATA WsaData; + int wsaresult = WSAStartup(MAKEWORD(2, 2), &WsaData); +#endif + + // tell the NodeList instance who to tell the domain server we care about + nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer + << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer << NodeType::EntityScriptServer); + + // connect to the packet sent signal of the _entityEditSender + connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent); + connect(&_entityEditSender, &EntityEditPacketSender::addingEntityWithCertificate, this, &Application::addingEntityWithCertificate); + + QString concurrentDownloadsStr = getCmdOption(argc, constArgv, "--concurrent-downloads"); + bool success; + uint32_t concurrentDownloads = concurrentDownloadsStr.toUInt(&success); + if (!success) { + concurrentDownloads = MAX_CONCURRENT_RESOURCE_DOWNLOADS; + } + ResourceCache::setRequestLimit(concurrentDownloads); + + // perhaps override the avatar url. Since we will test later for validity + // we don't need to do so here. + QString avatarURL = getCmdOption(argc, constArgv, "--avatarURL"); + _avatarOverrideUrl = QUrl::fromUserInput(avatarURL); + + // If someone specifies both --avatarURL and --replaceAvatarURL, + // the replaceAvatarURL wins. So only set the _overrideUrl if this + // does have a non-empty string. + QString replaceURL = getCmdOption(argc, constArgv, "--replaceAvatarURL"); + if (!replaceURL.isEmpty()) { + _avatarOverrideUrl = QUrl::fromUserInput(replaceURL); + _saveAvatarOverrideUrl = true; + } + + _glWidget = new GLCanvas(); + getApplicationCompositor().setRenderingWidget(_glWidget); + _window->setCentralWidget(_glWidget); + + _window->restoreGeometry(); + _window->setVisible(true); + + _glWidget->setFocusPolicy(Qt::StrongFocus); + _glWidget->setFocus(); + + if (cmdOptionExists(argc, constArgv, "--system-cursor")) { + _preferredCursor.set(Cursor::Manager::getIconName(Cursor::Icon::SYSTEM)); + } + showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); + + // enable mouse tracking; otherwise, we only get drag events + _glWidget->setMouseTracking(true); + // Make sure the window is set to the correct size by processing the pending events + QCoreApplication::processEvents(); + + // Create the main thread context, the GPU backend + initializeGL(); + qCDebug(interfaceapp, "Initialized GL"); + + // Initialize the display plugin architecture + initializeDisplayPlugins(); + qCDebug(interfaceapp, "Initialized Display"); + + // An audio device changed signal received before the display plugins are set up will cause a crash, + // so we defer the setup of the `scripting::Audio` class until this point + { + auto audioScriptingInterface = DependencyManager::set(); + auto audioIO = DependencyManager::get().data(); + connect(audioIO, &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); + connect(audioIO, &AudioClient::receivedFirstPacket, audioScriptingInterface.data(), &AudioScriptingInterface::receivedFirstPacket); + connect(audioIO, &AudioClient::disconnected, audioScriptingInterface.data(), &AudioScriptingInterface::disconnected); + connect(audioIO, &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { + auto audioClient = DependencyManager::get(); + auto audioScriptingInterface = DependencyManager::get(); + auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getWorldPosition(); + float distance = glm::distance(myAvatarPosition, position); + + if (distance < radius) { + audioClient->setMuted(true); + audioScriptingInterface->environmentMuted(); + } + }); + connect(this, &Application::activeDisplayPluginChanged, + reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::onContextChanged); + } + + // Create the rendering engine. This can be slow on some machines due to lots of + // GPU pipeline creation. + initializeRenderEngine(); + qCDebug(interfaceapp, "Initialized Render Engine."); + + // Overlays need to exist before we set the ContextOverlayInterface dependency + _overlays.init(); // do this before scripts load + DependencyManager::set(); + + // Initialize the user interface and menu system + // Needs to happen AFTER the render engine initialization to access its configuration + initializeUi(); + + init(); + qCDebug(interfaceapp, "init() complete."); + + // create thread for parsing of octree data independent of the main network and rendering threads + _octreeProcessor.initialize(_enableProcessOctreeThread); + connect(&_octreeProcessor, &OctreePacketProcessor::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); + _entityEditSender.initialize(_enableProcessOctreeThread); + + _idleLoopStdev.reset(); + + // update before the first render + update(0); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + static const QString TESTER = "HIFI_TESTER"; + bool isTester = false; +#if defined (Q_OS_ANDROID) + // Since we cannot set environment variables in Android we use a file presence + // to denote that this is a testing device + QFileInfo check_tester_file(TESTER_FILE); + isTester = check_tester_file.exists() && check_tester_file.isFile(); +#endif + + constexpr auto INSTALLER_INI_NAME = "installer.ini"; + auto iniPath = QDir(applicationDirPath()).filePath(INSTALLER_INI_NAME); + QFile installerFile { iniPath }; + std::unordered_map installerKeyValues; + if (installerFile.open(QIODevice::ReadOnly)) { + while (!installerFile.atEnd()) { + auto line = installerFile.readLine(); + if (!line.isEmpty()) { + auto index = line.indexOf("="); + if (index >= 0) { + installerKeyValues[line.mid(0, index).trimmed()] = line.mid(index + 1).trimmed(); + } + } + } + } + + // In practice we shouldn't run across installs that don't have a known installer type. + // Client or Client+Server installs should always have the installer.ini next to their + // respective interface.exe, and Steam installs will be detected as such. If a user were + // to delete the installer.ini, though, and as an example, we won't know the context of the + // original install. + constexpr auto INSTALLER_KEY_TYPE = "type"; + constexpr auto INSTALLER_KEY_CAMPAIGN = "campaign"; + constexpr auto INSTALLER_TYPE_UNKNOWN = "unknown"; + constexpr auto INSTALLER_TYPE_STEAM = "steam"; + + auto typeIt = installerKeyValues.find(INSTALLER_KEY_TYPE); + QString installerType = INSTALLER_TYPE_UNKNOWN; + if (typeIt == installerKeyValues.end()) { + if (property(hifi::properties::STEAM).toBool()) { + installerType = INSTALLER_TYPE_STEAM; + } + } else { + installerType = typeIt->second; + } + + auto campaignIt = installerKeyValues.find(INSTALLER_KEY_CAMPAIGN); + QString installerCampaign = campaignIt != installerKeyValues.end() ? campaignIt->second : ""; + + qDebug() << "Detected installer type:" << installerType; + qDebug() << "Detected installer campaign:" << installerCampaign; + + auto& userActivityLogger = UserActivityLogger::getInstance(); + if (userActivityLogger.isEnabled()) { + // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. + // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. + static const QString TESTER = "HIFI_TESTER"; + auto gpuIdent = GPUIdent::getInstance(); + auto glContextData = getGLContextData(); + QJsonObject properties = { + { "version", applicationVersion() }, + { "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) || isTester }, + { "installer_campaign", installerCampaign }, + { "installer_type", installerType }, + { "build_type", BuildInfo::BUILD_TYPE_STRING }, + { "previousSessionCrashed", _previousSessionCrashed }, + { "previousSessionRuntime", sessionRunTime.get() }, + { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, + { "kernel_type", QSysInfo::kernelType() }, + { "kernel_version", QSysInfo::kernelVersion() }, + { "os_type", QSysInfo::productType() }, + { "os_version", QSysInfo::productVersion() }, + { "gpu_name", gpuIdent->getName() }, + { "gpu_driver", gpuIdent->getDriver() }, + { "gpu_memory", static_cast(gpuIdent->getMemory()) }, + { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, + { "gl_version", glContextData["version"] }, + { "gl_vender", glContextData["vendor"] }, + { "gl_sl_version", glContextData["sl_version"] }, + { "gl_renderer", glContextData["renderer"] }, + { "ideal_thread_count", QThread::idealThreadCount() } + }; + auto macVersion = QSysInfo::macVersion(); + if (macVersion != QSysInfo::MV_None) { + properties["os_osx_version"] = QSysInfo::macVersion(); + } + auto windowsVersion = QSysInfo::windowsVersion(); + if (windowsVersion != QSysInfo::WV_None) { + properties["os_win_version"] = QSysInfo::windowsVersion(); + } + + ProcessorInfo procInfo; + if (getProcessorInfo(procInfo)) { + properties["processor_core_count"] = procInfo.numProcessorCores; + properties["logical_processor_count"] = procInfo.numLogicalProcessors; + properties["processor_l1_cache_count"] = procInfo.numProcessorCachesL1; + properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2; + properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; + } + + properties["first_run"] = _firstRun.get(); + + // add the user's machine ID to the launch event + QString machineFingerPrint = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); + properties["machine_fingerprint"] = machineFingerPrint; + + userActivityLogger.logAction("launch", properties); + } + + _entityEditSender.setMyAvatar(myAvatar.get()); + + // The entity octree will have to know about MyAvatar for the parentJointName import + getEntities()->getTree()->setMyAvatar(myAvatar); + _entityClipboard->setMyAvatar(myAvatar); + + // For now we're going to set the PPS for outbound packets to be super high, this is + // probably not the right long term solution. But for now, we're going to do this to + // allow you to move an entity around in your hand + _entityEditSender.setPacketsPerSecond(3000); // super high!! + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + connect(this, SIGNAL(aboutToQuit()), this, SLOT(onAboutToQuit())); + + // FIXME -- I'm a little concerned about this. + connect(myAvatar->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded, + this, &Application::checkSkeleton, Qt::QueuedConnection); + + // Setup the userInputMapper with the actions + auto userInputMapper = DependencyManager::get(); + connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { + using namespace controller; + auto tabletScriptingInterface = DependencyManager::get(); + { + auto actionEnum = static_cast(action); + int key = Qt::Key_unknown; + static int lastKey = Qt::Key_unknown; + bool navAxis = false; + switch (actionEnum) { + case Action::UI_NAV_VERTICAL: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Up; + } else if (state < 0.0f) { + key = Qt::Key_Down; + } + break; + + case Action::UI_NAV_LATERAL: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Right; + } else if (state < 0.0f) { + key = Qt::Key_Left; + } + break; + + case Action::UI_NAV_GROUP: + navAxis = true; + if (state > 0.0f) { + key = Qt::Key_Tab; + } else if (state < 0.0f) { + key = Qt::Key_Backtab; + } + break; + + case Action::UI_NAV_BACK: + key = Qt::Key_Escape; + break; + + case Action::UI_NAV_SELECT: + key = Qt::Key_Return; + break; + default: + break; + } + + auto window = tabletScriptingInterface->getTabletWindow(); + if (navAxis && window) { + if (lastKey != Qt::Key_unknown) { + QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier); + sendEvent(window, &event); + lastKey = Qt::Key_unknown; + } + + if (key != Qt::Key_unknown) { + QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); + sendEvent(window, &event); + tabletScriptingInterface->processEvent(&event); + lastKey = key; + } + } else if (key != Qt::Key_unknown && window) { + if (state) { + QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); + sendEvent(window, &event); + tabletScriptingInterface->processEvent(&event); + } else { + QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier); + sendEvent(window, &event); + } + return; + } + } + + if (action == controller::toInt(controller::Action::RETICLE_CLICK)) { + auto reticlePos = getApplicationCompositor().getReticlePosition(); + QPoint localPos(reticlePos.x, reticlePos.y); // both hmd and desktop already handle this in our coordinates. + if (state) { + QMouseEvent mousePress(QEvent::MouseButtonPress, localPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + sendEvent(_glWidget, &mousePress); + _reticleClickPressed = true; + } else { + QMouseEvent mouseRelease(QEvent::MouseButtonRelease, localPos, Qt::LeftButton, Qt::NoButton, Qt::NoModifier); + sendEvent(_glWidget, &mouseRelease); + _reticleClickPressed = false; + } + return; // nothing else to do + } + + if (state) { + if (action == controller::toInt(controller::Action::TOGGLE_MUTE)) { + auto audioClient = DependencyManager::get(); + audioClient->setMuted(!audioClient->isMuted()); + } else if (action == controller::toInt(controller::Action::CYCLE_CAMERA)) { + cycleCamera(); + } else if (action == controller::toInt(controller::Action::CONTEXT_MENU) && !isInterstitialMode()) { + toggleTabletUI(); + } else if (action == controller::toInt(controller::Action::RETICLE_X)) { + auto oldPos = getApplicationCompositor().getReticlePosition(); + getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y }); + } else if (action == controller::toInt(controller::Action::RETICLE_Y)) { + auto oldPos = getApplicationCompositor().getReticlePosition(); + getApplicationCompositor().setReticlePosition({ oldPos.x, oldPos.y + state }); + } else if (action == controller::toInt(controller::Action::TOGGLE_OVERLAY)) { + toggleOverlays(); + } + } + }); + + _applicationStateDevice = userInputMapper->getStateDevice(); + + _applicationStateDevice->setInputVariant(STATE_IN_HMD, []() -> float { + return qApp->isHMDMode() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_FULL_SCREEN_MIRROR, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_MIRROR ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_FIRST_PERSON, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_FIRST_PERSON ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_THIRD_PERSON, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_THIRD_PERSON ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_ENTITY, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_ENTITY ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_CAMERA_INDEPENDENT, []() -> float { + return qApp->getCamera().getMode() == CAMERA_MODE_INDEPENDENT ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { + return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { + return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; + }); + + _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { + return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; + }); + _applicationStateDevice->setInputVariant(STATE_NAV_FOCUSED, []() -> float { + auto offscreenUi = getOffscreenUI(); + return offscreenUi ? (offscreenUi->navigationFocused() ? 1 : 0) : 0; + }); + _applicationStateDevice->setInputVariant(STATE_PLATFORM_WINDOWS, []() -> float { +#if defined(Q_OS_WIN) + return 1; +#else + return 0; +#endif + }); + _applicationStateDevice->setInputVariant(STATE_PLATFORM_MAC, []() -> float { +#if defined(Q_OS_MAC) + return 1; +#else + return 0; +#endif + }); + _applicationStateDevice->setInputVariant(STATE_PLATFORM_ANDROID, []() -> float { +#if defined(Q_OS_ANDROID) + return 1 ; +#else + return 0; +#endif + }); + + + // Setup the _keyboardMouseDevice, _touchscreenDevice, _touchscreenVirtualPadDevice and the user input mapper with the default bindings + userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); + // if the _touchscreenDevice is not supported it will not be registered + if (_touchscreenDevice) { + userInputMapper->registerDevice(_touchscreenDevice->getInputDevice()); + } + if (_touchscreenVirtualPadDevice) { + userInputMapper->registerDevice(_touchscreenVirtualPadDevice->getInputDevice()); + } + + QString scriptsSwitch = QString("--").append(SCRIPTS_SWITCH); + _defaultScriptsLocation = getCmdOption(argc, constArgv, scriptsSwitch.toStdString().c_str()); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + loadSettings(); + + updateVerboseLogging(); + + // Now that we've loaded the menu and thus switched to the previous display plugin + // we can unlock the desktop repositioning code, since all the positions will be + // relative to the desktop size for this plugin + auto offscreenUi = getOffscreenUI(); + connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() { + auto offscreenUi = getOffscreenUI(); + auto desktop = offscreenUi->getDesktop(); + if (desktop) { + desktop->setProperty("repositionLocked", false); + } + }); + + connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) + // Do not show login dialog if requested not to on the command line + QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); + int index = arguments().indexOf(hifiNoLoginCommandLineKey); + if (index != -1) { + resumeAfterLoginDialogActionTaken(); + return; + } + + showLoginScreen(); +#else + resumeAfterLoginDialogActionTaken(); +#endif + }); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + QTimer* settingsTimer = new QTimer(); + moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{ + // This needs to run on the settings thread, so we need to pass the `settingsTimer` as the + // receiver object, otherwise it will run on the application thread and trigger a warning + // about trying to kill the timer on the main thread. + connect(qApp, &Application::beforeAboutToQuit, settingsTimer, [this, settingsTimer]{ + // Disconnect the signal from the save settings + QObject::disconnect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); + // Stop the settings timer + settingsTimer->stop(); + // Delete it (this will trigger the thread destruction + settingsTimer->deleteLater(); + // Mark the settings thread as finished, so we know we can safely save in the main application + // shutdown code + _settingsGuard.trigger(); + }); + + int SAVE_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND; // Let's save every seconds for now + settingsTimer->setSingleShot(false); + settingsTimer->setInterval(SAVE_SETTINGS_INTERVAL); // 10s, Qt::CoarseTimer acceptable + QObject::connect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); + settingsTimer->start(); + }, QThread::LowestPriority); + + if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); // So that camera doesn't auto-switch to third person. + } else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) { + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + cameraMenuChanged(); + } else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) { + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + cameraMenuChanged(); + } + + { + auto audioIO = DependencyManager::get().data(); + // set the local loopback interface for local sounds + AudioInjector::setLocalAudioInterface(audioIO); + auto audioScriptingInterface = DependencyManager::get(); + audioScriptingInterface->setLocalAudioInterface(audioIO); + connect(audioIO, &AudioClient::noiseGateOpened, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateOpened); + connect(audioIO, &AudioClient::noiseGateClosed, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateClosed); + connect(audioIO, &AudioClient::inputReceived, audioScriptingInterface.data(), &AudioScriptingInterface::inputReceived); + } + + this->installEventFilter(this); + + + +#ifdef HAVE_DDE + auto ddeTracker = DependencyManager::get(); + ddeTracker->init(); + connect(ddeTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); +#endif + +#ifdef HAVE_IVIEWHMD + auto eyeTracker = DependencyManager::get(); + eyeTracker->init(); + setActiveEyeTracker(); +#endif + + // If launched from Steam, let it handle updates + const QString HIFI_NO_UPDATER_COMMAND_LINE_KEY = "--no-updater"; + bool noUpdater = arguments().indexOf(HIFI_NO_UPDATER_COMMAND_LINE_KEY) != -1; + bool buildCanUpdate = BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable + || BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Master; + if (!noUpdater && buildCanUpdate) { + constexpr auto INSTALLER_TYPE_CLIENT_ONLY = "client_only"; + + auto applicationUpdater = DependencyManager::set(); + + AutoUpdater::InstallerType type = installerType == INSTALLER_TYPE_CLIENT_ONLY + ? AutoUpdater::InstallerType::CLIENT_ONLY : AutoUpdater::InstallerType::FULL; + + applicationUpdater->setInstallerType(type); + applicationUpdater->setInstallerCampaign(installerCampaign); + connect(applicationUpdater.data(), &AutoUpdater::newVersionIsAvailable, dialogsManager.data(), &DialogsManager::showUpdateDialog); + applicationUpdater->checkForUpdate(); + } + + Menu::getInstance()->setIsOptionChecked(MenuOption::ActionMotorControl, true); + +// FIXME spacemouse code still needs cleanup +#if 0 + // the 3Dconnexion device wants to be initialized after a window is displayed. + SpacemouseManager::getInstance().init(); +#endif + + // If the user clicks on an object, we will check that it's a web surface, and if so, set the focus to it + auto pointerManager = DependencyManager::get(); + auto keyboardFocusOperator = [this](const QUuid& id, const PointerEvent& event) { + if (event.shouldFocus()) { + auto keyboard = DependencyManager::get(); + if (getEntities()->wantsKeyboardFocus(id)) { + setKeyboardFocusEntity(id); + } else if (!keyboard->containsID(id)) { // FIXME: this is a hack to make the keyboard work for now, since the keys would otherwise steal focus + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + } + } + }; + connect(pointerManager.data(), &PointerManager::triggerBeginEntity, keyboardFocusOperator); + connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, keyboardFocusOperator); + + auto entityScriptingInterface = DependencyManager::get(); + connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityItemID) { + if (entityItemID == _keyboardFocusedEntity.get()) { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + } + }, Qt::QueuedConnection); + + EntityTreeRenderer::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) { + if (_aboutToQuit) { + return false; + } + + auto renderable = getEntities()->renderableForEntityId(entityID); + if (renderable) { + renderable->addMaterial(material, parentMaterialName); + return true; + } + + return false; + }); + EntityTreeRenderer::setRemoveMaterialFromEntityOperator([this](const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) { + if (_aboutToQuit) { + return false; + } + + auto renderable = getEntities()->renderableForEntityId(entityID); + if (renderable) { + renderable->removeMaterial(material, parentMaterialName); + return true; + } + + return false; + }); + + EntityTreeRenderer::setAddMaterialToAvatarOperator([](const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) { + auto avatarManager = DependencyManager::get(); + auto avatar = avatarManager->getAvatarBySessionID(avatarID); + if (avatar) { + avatar->addMaterial(material, parentMaterialName); + return true; + } + return false; + }); + EntityTreeRenderer::setRemoveMaterialFromAvatarOperator([](const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) { + auto avatarManager = DependencyManager::get(); + auto avatar = avatarManager->getAvatarBySessionID(avatarID); + if (avatar) { + avatar->removeMaterial(material, parentMaterialName); + return true; + } + return false; + }); + + EntityTree::setGetEntityObjectOperator([this](const QUuid& id) -> QObject* { + auto entities = getEntities(); + if (auto entity = entities->renderableForEntityId(id)) { + return qobject_cast(entity.get()); + } + return nullptr; + }); + + EntityTree::setTextSizeOperator([this](const QUuid& id, const QString& text) { + auto entities = getEntities(); + if (auto entity = entities->renderableForEntityId(id)) { + if (auto renderable = std::dynamic_pointer_cast(entity)) { + return renderable->textSize(text); + } + } + return QSizeF(0.0f, 0.0f); + }); + + connect(this, &Application::aboutToQuit, [this]() { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + }); + + // Add periodic checks to send user activity data + static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; + static int NEARBY_AVATAR_RADIUS_METERS = 10; + + // setup the stats interval depending on if the 1s faster hearbeat was requested + static const QString FAST_STATS_ARG = "--fast-heartbeat"; + static int SEND_STATS_INTERVAL_MS = arguments().indexOf(FAST_STATS_ARG) != -1 ? 1000 : 10000; + + static glm::vec3 lastAvatarPosition = myAvatar->getWorldPosition(); + static glm::mat4 lastHMDHeadPose = getHMDSensorPose(); + static controller::Pose lastLeftHandPose = myAvatar->getLeftHandPose(); + static controller::Pose lastRightHandPose = myAvatar->getRightHandPose(); + + // Periodically send fps as a user activity event + QTimer* sendStatsTimer = new QTimer(this); + sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); // 10s, Qt::CoarseTimer acceptable + connect(sendStatsTimer, &QTimer::timeout, this, [this]() { + + QJsonObject properties = {}; + MemoryInfo memInfo; + if (getMemoryInfo(memInfo)) { + properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); + properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); + properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); + } + + // content location and build info - useful for filtering stats + auto addressManager = DependencyManager::get(); + auto currentDomain = addressManager->currentShareableAddress(true).toString(); // domain only + auto currentPath = addressManager->currentPath(true); // with orientation + properties["current_domain"] = currentDomain; + properties["current_path"] = currentPath; + properties["build_version"] = BuildInfo::VERSION; + + auto displayPlugin = qApp->getActiveDisplayPlugin(); + + properties["render_rate"] = getRenderLoopRate(); + properties["target_render_rate"] = getTargetRenderFrameRate(); + properties["present_rate"] = displayPlugin->presentRate(); + properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); + properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); + properties["stutter_rate"] = displayPlugin->stutterRate(); + properties["game_rate"] = getGameLoopRate(); + properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); + properties["hardware_stats"] = displayPlugin->getHardwareStats(); + + // deadlock watchdog related stats + properties["deadlock_watchdog_maxElapsed"] = (int)DeadlockWatchdogThread::_maxElapsed; + properties["deadlock_watchdog_maxElapsedAverage"] = (int)DeadlockWatchdogThread::_maxElapsedAverage; + + auto nodeList = DependencyManager::get(); + properties["packet_rate_in"] = nodeList->getInboundPPS(); + properties["packet_rate_out"] = nodeList->getOutboundPPS(); + properties["kbps_in"] = nodeList->getInboundKbps(); + properties["kbps_out"] = nodeList->getOutboundKbps(); + + SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer); + SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); + SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); + SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); + SharedNodePointer messagesMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer); + properties["entity_ping"] = entityServerNode ? entityServerNode->getPingMs() : -1; + properties["audio_ping"] = audioMixerNode ? audioMixerNode->getPingMs() : -1; + properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1; + properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1; + properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1; + properties["atp_in_kbps"] = assetServerNode ? assetServerNode->getInboundKbps() : 0.0f; + + auto loadingRequests = ResourceCache::getLoadingRequests(); + + QJsonArray loadingRequestsStats; + for (const auto& request : loadingRequests) { + QJsonObject requestStats; + requestStats["filename"] = request->getURL().fileName(); + requestStats["received"] = request->getBytesReceived(); + requestStats["total"] = request->getBytesTotal(); + requestStats["attempts"] = (int)request->getDownloadAttempts(); + loadingRequestsStats.append(requestStats); + } + + properties["active_downloads"] = loadingRequests.size(); + properties["pending_downloads"] = (int)ResourceCache::getPendingRequestCount(); + properties["active_downloads_details"] = loadingRequestsStats; + + auto statTracker = DependencyManager::get(); + + properties["processing_resources"] = statTracker->getStat("Processing").toInt(); + properties["pending_processing_resources"] = statTracker->getStat("PendingProcessing").toInt(); + + QJsonObject startedRequests; + startedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_STARTED).toInt(); + startedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_STARTED).toInt(); + startedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_STARTED).toInt(); + startedRequests["total"] = startedRequests["atp"].toInt() + startedRequests["http"].toInt() + + startedRequests["file"].toInt(); + properties["started_requests"] = startedRequests; + + QJsonObject successfulRequests; + successfulRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_SUCCESS).toInt(); + successfulRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_SUCCESS).toInt(); + successfulRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_SUCCESS).toInt(); + successfulRequests["total"] = successfulRequests["atp"].toInt() + successfulRequests["http"].toInt() + + successfulRequests["file"].toInt(); + properties["successful_requests"] = successfulRequests; + + QJsonObject failedRequests; + failedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_FAILED).toInt(); + failedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_FAILED).toInt(); + failedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_FAILED).toInt(); + failedRequests["total"] = failedRequests["atp"].toInt() + failedRequests["http"].toInt() + + failedRequests["file"].toInt(); + properties["failed_requests"] = failedRequests; + + QJsonObject cacheRequests; + cacheRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_CACHE).toInt(); + cacheRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_CACHE).toInt(); + cacheRequests["total"] = cacheRequests["atp"].toInt() + cacheRequests["http"].toInt(); + properties["cache_requests"] = cacheRequests; + + QJsonObject atpMappingRequests; + atpMappingRequests["started"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_STARTED).toInt(); + atpMappingRequests["failed"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_FAILED).toInt(); + atpMappingRequests["successful"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_SUCCESS).toInt(); + properties["atp_mapping_requests"] = atpMappingRequests; + + properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; + + QJsonObject bytesDownloaded; + auto atpBytes = statTracker->getStat(STAT_ATP_RESOURCE_TOTAL_BYTES).toLongLong(); + auto httpBytes = statTracker->getStat(STAT_HTTP_RESOURCE_TOTAL_BYTES).toLongLong(); + auto fileBytes = statTracker->getStat(STAT_FILE_RESOURCE_TOTAL_BYTES).toLongLong(); + bytesDownloaded["atp"] = atpBytes; + bytesDownloaded["http"] = httpBytes; + bytesDownloaded["file"] = fileBytes; + bytesDownloaded["total"] = atpBytes + httpBytes + fileBytes; + properties["bytes_downloaded"] = bytesDownloaded; + + auto myAvatar = getMyAvatar(); + glm::vec3 avatarPosition = myAvatar->getWorldPosition(); + properties["avatar_has_moved"] = lastAvatarPosition != avatarPosition; + lastAvatarPosition = avatarPosition; + + auto entityScriptingInterface = DependencyManager::get(); + auto entityActivityTracking = entityScriptingInterface->getActivityTracking(); + entityScriptingInterface->resetActivityTracking(); + properties["added_entity_cnt"] = entityActivityTracking.addedEntityCount; + properties["deleted_entity_cnt"] = entityActivityTracking.deletedEntityCount; + properties["edited_entity_cnt"] = entityActivityTracking.editedEntityCount; + + NodeToOctreeSceneStats* octreeServerSceneStats = getOcteeSceneStats(); + unsigned long totalServerOctreeElements = 0; + for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) { + totalServerOctreeElements += i->second.getTotalElements(); + } + + properties["local_octree_elements"] = (qint64) OctreeElement::getInternalNodeCount(); + properties["server_octree_elements"] = (qint64) totalServerOctreeElements; + + properties["active_display_plugin"] = getActiveDisplayPlugin()->getName(); + properties["using_hmd"] = isHMDMode(); + + _autoSwitchDisplayModeSupportedHMDPlugin = nullptr; + foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { + if (displayPlugin->isHmd() && + displayPlugin->getSupportsAutoSwitch()) { + _autoSwitchDisplayModeSupportedHMDPlugin = displayPlugin; + _autoSwitchDisplayModeSupportedHMDPluginName = + _autoSwitchDisplayModeSupportedHMDPlugin->getName(); + _previousHMDWornStatus = + _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); + break; + } + } + + if (_autoSwitchDisplayModeSupportedHMDPlugin) { + if (getActiveDisplayPlugin() != _autoSwitchDisplayModeSupportedHMDPlugin && + !_autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { + startHMDStandBySession(); + } + // Poll periodically to check whether the user has worn HMD or not. Switch Display mode accordingly. + // If the user wears HMD then switch to VR mode. If the user removes HMD then switch to Desktop mode. + QTimer* autoSwitchDisplayModeTimer = new QTimer(this); + connect(autoSwitchDisplayModeTimer, SIGNAL(timeout()), this, SLOT(switchDisplayMode())); + autoSwitchDisplayModeTimer->start(INTERVAL_TO_CHECK_HMD_WORN_STATUS); + } + + auto glInfo = getGLContextData(); + properties["gl_info"] = glInfo; + properties["gpu_used_memory"] = (int)BYTES_TO_MB(gpu::Context::getUsedGPUMemSize()); + properties["gpu_free_memory"] = (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemSize()); + properties["gpu_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerGPUAverage()); + properties["batch_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerBatchAverage()); + properties["ideal_thread_count"] = QThread::idealThreadCount(); + + auto hmdHeadPose = getHMDSensorPose(); + properties["hmd_head_pose_changed"] = isHMDMode() && (hmdHeadPose != lastHMDHeadPose); + lastHMDHeadPose = hmdHeadPose; + + auto leftHandPose = myAvatar->getLeftHandPose(); + auto rightHandPose = myAvatar->getRightHandPose(); + // controller::Pose considers two poses to be different if either are invalid. In our case, we actually + // want to consider the pose to be unchanged if it was invalid and still is invalid, so we check that first. + properties["hand_pose_changed"] = + ((leftHandPose.valid || lastLeftHandPose.valid) && (leftHandPose != lastLeftHandPose)) + || ((rightHandPose.valid || lastRightHandPose.valid) && (rightHandPose != lastRightHandPose)); + lastLeftHandPose = leftHandPose; + lastRightHandPose = rightHandPose; + + UserActivityLogger::getInstance().logAction("stats", properties); + }); + sendStatsTimer->start(); + + // Periodically check for count of nearby avatars + static int lastCountOfNearbyAvatars = -1; + QTimer* checkNearbyAvatarsTimer = new QTimer(this); + checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); // 10 seconds, Qt::CoarseTimer ok + connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, []() { + auto avatarManager = DependencyManager::get(); + int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getWorldPosition(), + NEARBY_AVATAR_RADIUS_METERS) - 1; + if (nearbyAvatars != lastCountOfNearbyAvatars) { + lastCountOfNearbyAvatars = nearbyAvatars; + UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); + } + }); + checkNearbyAvatarsTimer->start(); + + // Track user activity event when we receive a mute packet + auto onMutedByMixer = []() { + UserActivityLogger::getInstance().logAction("received_mute_packet"); + }; + connect(DependencyManager::get().data(), &AudioClient::mutedByMixer, this, onMutedByMixer); + + // Track when the address bar is opened + auto onAddressBarShown = [this]() { + // Record time + UserActivityLogger::getInstance().logAction("opened_address_bar", { { "uptime_ms", _sessionRunTimer.elapsed() } }); + }; + connect(DependencyManager::get().data(), &DialogsManager::addressBarShown, this, onAddressBarShown); + + // Make sure we don't time out during slow operations at startup + updateHeartbeat(); + + OctreeEditPacketSender* packetSender = entityScriptingInterface->getPacketSender(); + EntityEditPacketSender* entityPacketSender = static_cast(packetSender); + entityPacketSender->setMyAvatar(myAvatar.get()); + + connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); + connect(_window, SIGNAL(windowMinimizedChanged(bool)), this, SLOT(windowMinimizedChanged(bool))); + qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); + + EntityTreeRenderer::setEntitiesShouldFadeFunction([this]() { + SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); + return entityServerNode && !isPhysicsEnabled(); + }); + + _snapshotSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/snapshot/snap.wav")); + + // Monitor model assets (e.g., from Clara.io) added to the world that may need resizing. + static const int ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS = 1000; + _addAssetToWorldResizeTimer.setInterval(ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable + connect(&_addAssetToWorldResizeTimer, &QTimer::timeout, this, &Application::addAssetToWorldCheckModelSize); + + // Auto-update and close adding asset to world info message box. + static const int ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS = 5000; + _addAssetToWorldInfoTimer.setInterval(ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS); // 5s, Qt::CoarseTimer acceptable + _addAssetToWorldInfoTimer.setSingleShot(true); + connect(&_addAssetToWorldInfoTimer, &QTimer::timeout, this, &Application::addAssetToWorldInfoTimeout); + static const int ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS = 8000; + _addAssetToWorldErrorTimer.setInterval(ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS); // 8s, Qt::CoarseTimer acceptable + _addAssetToWorldErrorTimer.setSingleShot(true); + connect(&_addAssetToWorldErrorTimer, &QTimer::timeout, this, &Application::addAssetToWorldErrorTimeout); + + connect(this, &QCoreApplication::aboutToQuit, this, &Application::addAssetToWorldMessageClose); + connect(&domainHandler, &DomainHandler::domainURLChanged, this, &Application::addAssetToWorldMessageClose); + connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &Application::addAssetToWorldMessageClose); + + updateSystemTabletMode(); + + connect(&_myCamera, &Camera::modeUpdated, this, &Application::cameraModeChanged); + + DependencyManager::get()->setShouldPickHUDOperator([]() { return DependencyManager::get()->isHMDMode(); }); + DependencyManager::get()->setCalculatePos2DFromHUDOperator([this](const glm::vec3& intersection) { + const glm::vec2 MARGIN(25.0f); + glm::vec2 maxPos = _controllerScriptingInterface->getViewportDimensions() - MARGIN; + glm::vec2 pos2D = DependencyManager::get()->overlayFromWorldPoint(intersection); + return glm::max(MARGIN, glm::min(pos2D, maxPos)); + }); + + // Setup the mouse ray pick and related operators + { + auto mouseRayPick = std::make_shared(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_LOCAL_ENTITIES()), 0.0f, true); + mouseRayPick->parentTransform = std::make_shared(); + mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE); + auto mouseRayPickID = DependencyManager::get()->addPick(PickQuery::Ray, mouseRayPick); + DependencyManager::get()->setMouseRayPickID(mouseRayPickID); + } + DependencyManager::get()->setMouseRayPickResultOperator([](unsigned int rayPickID) { + RayToEntityIntersectionResult entityResult; + entityResult.intersects = false; + auto pickResult = DependencyManager::get()->getPrevPickResultTyped(rayPickID); + if (pickResult) { + entityResult.intersects = pickResult->type != IntersectionType::NONE; + if (entityResult.intersects) { + entityResult.intersection = pickResult->intersection; + entityResult.distance = pickResult->distance; + entityResult.surfaceNormal = pickResult->surfaceNormal; + entityResult.entityID = pickResult->objectID; + entityResult.extraInfo = pickResult->extraInfo; + } + } + return entityResult; + }); + DependencyManager::get()->setSetPrecisionPickingOperator([](unsigned int rayPickID, bool value) { + DependencyManager::get()->setPrecisionPicking(rayPickID, value); + }); + + EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { + if (billboardMode == BillboardMode::YAW) { + //rotate about vertical to face the camera + glm::vec3 dPosition = frustumPos - position; + // If x and z are 0, atan(x, z) is undefined, so default to 0 degrees + float yawRotation = dPosition.x == 0.0f && dPosition.z == 0.0f ? 0.0f : glm::atan(dPosition.x, dPosition.z); + return glm::quat(glm::vec3(0.0f, yawRotation, 0.0f)); + } else if (billboardMode == BillboardMode::FULL) { + // use the referencial from the avatar, y isn't always up + glm::vec3 avatarUP = DependencyManager::get()->getMyAvatar()->getWorldOrientation() * Vectors::UP; + // check to see if glm::lookAt will work / using glm::lookAt variable name + glm::highp_vec3 s(glm::cross(position - frustumPos, avatarUP)); + + // make sure s is not NaN for any component + if (glm::length2(s) > 0.0f) { + return glm::conjugate(glm::toQuat(glm::lookAt(frustumPos, position, avatarUP))); + } + } + return rotation; + }); + EntityItem::setPrimaryViewFrustumPositionOperator([this]() { + ViewFrustum viewFrustum; + copyViewFrustum(viewFrustum); + return viewFrustum.getPosition(); + }); + + render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { + bool isTablet = url == TabletScriptingInterface::QML; + if (htmlContent) { + webSurface = DependencyManager::get()->acquire(render::entities::WebEntityRenderer::QML); + cachedWebSurface = true; + auto rootItemLoadedFunctor = [url, webSurface] { + webSurface->getRootItem()->setProperty(render::entities::WebEntityRenderer::URL_PROPERTY, url); + }; + if (webSurface->getRootItem()) { + rootItemLoadedFunctor(); + } else { + QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); + } + auto surfaceContext = webSurface->getSurfaceContext(); + surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); + } else { + // FIXME: the tablet should use the OffscreenQmlSurfaceCache + webSurface = QSharedPointer(new OffscreenQmlSurface(), [](OffscreenQmlSurface* webSurface) { + AbstractViewStateInterface::instance()->sendLambdaEvent([webSurface] { + // WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown + // if the application has already stopped its event loop, delete must be explicit + delete webSurface; + }); + }); + auto rootItemLoadedFunctor = [webSurface, url, isTablet] { + Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString()); + }; + if (webSurface->getRootItem()) { + rootItemLoadedFunctor(); + } else { + QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); + } + webSurface->load(url); + cachedWebSurface = false; + } + const uint8_t DEFAULT_MAX_FPS = 10; + const uint8_t TABLET_FPS = 90; + webSurface->setMaxFps(isTablet ? TABLET_FPS : DEFAULT_MAX_FPS); + }); + render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([this](QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { + QQuickItem* rootItem = webSurface->getRootItem(); + + // Fix for crash in QtWebEngineCore when rapidly switching domains + // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. + if (rootItem && !cachedWebSurface) { + // stop loading + QMetaObject::invokeMethod(rootItem, "stop"); + } + + webSurface->pause(); + + for (auto& connection : connections) { + QObject::disconnect(connection); + } + connections.clear(); + + // If the web surface was fetched out of the cache, release it back into the cache + if (cachedWebSurface) { + // If it's going back into the cache make sure to explicitly set the URL to a blank page + // in order to stop any resource consumption or audio related to the page. + if (rootItem) { + rootItem->setProperty("url", "about:blank"); + } + auto offscreenCache = DependencyManager::get(); + if (offscreenCache) { + offscreenCache->release(render::entities::WebEntityRenderer::QML, webSurface); + } + cachedWebSurface = false; + } + webSurface.reset(); + }); + + // Preload Tablet sounds + DependencyManager::get()->setEntityTree(qApp->getEntities()->getTree()); + DependencyManager::get()->preloadSounds(); + DependencyManager::get()->createKeyboard(); + + _pendingIdleEvent = false; + _graphicsEngine.startup(); + + qCDebug(interfaceapp) << "Metaverse session ID is" << uuidStringWithoutCurlyBraces(accountManager->getSessionID()); + +#if defined(Q_OS_ANDROID) + connect(&AndroidHelper::instance(), &AndroidHelper::beforeEnterBackground, this, &Application::beforeEnterBackground); + connect(&AndroidHelper::instance(), &AndroidHelper::enterBackground, this, &Application::enterBackground); + connect(&AndroidHelper::instance(), &AndroidHelper::enterForeground, this, &Application::enterForeground); + connect(&AndroidHelper::instance(), &AndroidHelper::toggleAwayMode, this, &Application::toggleAwayMode); + AndroidHelper::instance().notifyLoadComplete(); +#endif + pauseUntilLoginDetermined(); +} + +void Application::updateVerboseLogging() { + auto menu = Menu::getInstance(); + if (!menu) { + return; + } + bool enable = menu->isOptionChecked(MenuOption::VerboseLogging); + + QString rules = + "hifi.*.info=%1\n" + "hifi.audio-stream.debug=false\n" + "hifi.audio-stream.info=false"; + rules = rules.arg(enable ? "true" : "false"); + QLoggingCategory::setFilterRules(rules); +} + +void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { + DomainHandler::ConnectionRefusedReason reasonCode = static_cast(reasonCodeInt); + + if (reasonCode == DomainHandler::ConnectionRefusedReason::TooManyUsers && !extraInfo.isEmpty()) { + DependencyManager::get()->handleLookupString(extraInfo); + return; + } + + switch (reasonCode) { + case DomainHandler::ConnectionRefusedReason::ProtocolMismatch: + case DomainHandler::ConnectionRefusedReason::TooManyUsers: + case DomainHandler::ConnectionRefusedReason::Unknown: { + QString message = "Unable to connect to the location you are visiting.\n"; + message += reasonMessage; + OffscreenUi::asyncWarning("", message); + getMyAvatar()->setWorldVelocity(glm::vec3(0.0f)); + break; + } + default: + // nothing to do. + break; + } +} + +QString Application::getUserAgent() { + if (QThread::currentThread() != thread()) { + QString userAgent; + + BLOCKING_INVOKE_METHOD(this, "getUserAgent", Q_RETURN_ARG(QString, userAgent)); + + return userAgent; + } + + QString userAgent = "Mozilla/5.0 (HighFidelityInterface/" + BuildInfo::VERSION + "; " + + QSysInfo::productType() + " " + QSysInfo::productVersion() + ")"; + + auto formatPluginName = [](QString name) -> QString { return name.trimmed().replace(" ", "-"); }; + + // For each plugin, add to userAgent + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + for (auto& dp : displayPlugins) { + if (dp->isActive() && dp->isHmd()) { + userAgent += " " + formatPluginName(dp->getName()); + } + } + auto inputPlugins= PluginManager::getInstance()->getInputPlugins(); + for (auto& ip : inputPlugins) { + if (ip->isActive()) { + userAgent += " " + formatPluginName(ip->getName()); + } + } + // for codecs, we include all of them, even if not active + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + for (auto& cp : codecPlugins) { + userAgent += " " + formatPluginName(cp->getName()); + } + + return userAgent; +} + +void Application::toggleTabletUI(bool shouldOpen) const { + auto hmd = DependencyManager::get(); + if (!(shouldOpen && hmd->getShouldShowTablet())) { + auto HMD = DependencyManager::get(); + HMD->toggleShouldShowTablet(); + + if (!HMD->getShouldShowTablet()) { + DependencyManager::get()->setRaised(false); + _window->activateWindow(); + auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); + tablet->unfocus(); + } + } +} + +void Application::checkChangeCursor() { + QMutexLocker locker(&_changeCursorLock); + if (_cursorNeedsChanging) { +#ifdef Q_OS_MAC + auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget +#else + // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the + // window menu, which is a pain, so only hide it for the GL surface + auto cursorTarget = _glWidget; +#endif + cursorTarget->setCursor(_desiredCursor); + + _cursorNeedsChanging = false; + } +} + +void Application::showCursor(const Cursor::Icon& cursor) { + QMutexLocker locker(&_changeCursorLock); + + auto managedCursor = Cursor::Manager::instance().getCursor(); + auto curIcon = managedCursor->getIcon(); + if (curIcon != cursor) { + managedCursor->setIcon(cursor); + curIcon = cursor; + } + _desiredCursor = cursor == Cursor::Icon::SYSTEM ? Qt::ArrowCursor : Qt::BlankCursor; + _cursorNeedsChanging = true; +} + +void Application::updateHeartbeat() const { + DeadlockWatchdogThread::updateHeartbeat(); +} + +void Application::onAboutToQuit() { + // quickly save AvatarEntityData before the EntityTree is dismantled + getMyAvatar()->saveAvatarEntityDataToSettings(); + + emit beforeAboutToQuit(); + + if (getLoginDialogPoppedUp() && _firstRun.get()) { + _firstRun.set(false); + } + + foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { + if (inputPlugin->isActive()) { + inputPlugin->deactivate(); + } + } + + // The active display plugin needs to be loaded before the menu system is active, + // so its persisted explicitly here + Setting::Handle{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME }.set(getActiveDisplayPlugin()->getName()); + + loginDialogPoppedUp.set(false); + + getActiveDisplayPlugin()->deactivate(); + if (_autoSwitchDisplayModeSupportedHMDPlugin + && _autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { + _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); + } + // use the CloseEventSender via a QThread to send an event that says the user asked for the app to close + DependencyManager::get()->startThread(); + + // Hide Running Scripts dialog so that it gets destroyed in an orderly manner; prevents warnings at shutdown. +#if !defined(DISABLE_QML) + getOffscreenUI()->hide("RunningScripts"); +#endif + + _aboutToQuit = true; + + cleanupBeforeQuit(); +} + +void Application::cleanupBeforeQuit() { + // add a logline indicating if QTWEBENGINE_REMOTE_DEBUGGING is set or not + QString webengineRemoteDebugging = QProcessEnvironment::systemEnvironment().value("QTWEBENGINE_REMOTE_DEBUGGING", "false"); + qCDebug(interfaceapp) << "QTWEBENGINE_REMOTE_DEBUGGING =" << webengineRemoteDebugging; + + DependencyManager::prepareToExit(); + + if (tracing::enabled()) { + auto tracer = DependencyManager::get(); + tracer->stopTracing(); + auto outputFile = property(hifi::properties::TRACING).toString(); + tracer->serialize(outputFile); + } + + // Stop third party processes so that they're not left running in the event of a subsequent shutdown crash. +#ifdef HAVE_DDE + DependencyManager::get()->setEnabled(false); +#endif +#ifdef HAVE_IVIEWHMD + DependencyManager::get()->setEnabled(false, true); +#endif + AnimDebugDraw::getInstance().shutdown(); + + // FIXME: once we move to shared pointer for the INputDevice we shoud remove this naked delete: + _applicationStateDevice.reset(); + + { + if (_keyboardFocusHighlightID != UNKNOWN_ENTITY_ID) { + DependencyManager::get()->deleteEntity(_keyboardFocusHighlightID); + _keyboardFocusHighlightID = UNKNOWN_ENTITY_ID; + } + } + + { + auto nodeList = DependencyManager::get(); + + // send the domain a disconnect packet, force stoppage of domain-server check-ins + nodeList->getDomainHandler().disconnect(); + nodeList->setIsShuttingDown(true); + + // tell the packet receiver we're shutting down, so it can drop packets + nodeList->getPacketReceiver().setShouldDropPackets(true); + } + + getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts + + // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) + QThreadPool::globalInstance()->clear(); + + DependencyManager::destroy(); + + // FIXME: Something is still holding on to the ScriptEnginePointers contained in ScriptEngines, and they hold backpointers to ScriptEngines, + // so this doesn't shut down properly + DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts + // These classes hold ScriptEnginePointers, so they must be destroyed before ScriptEngines + // Must be done after shutdownScripting in case any scripts try to access these things + { + DependencyManager::destroy(); + EntityTreePointer tree = getEntities()->getTree(); + tree->setSimulation(nullptr); + DependencyManager::destroy(); + } + DependencyManager::destroy(); + + bool keepMeLoggedIn = Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, false).get(); + if (!keepMeLoggedIn) { + DependencyManager::get()->removeAccountFromFile(); + } + + _displayPlugin.reset(); + PluginManager::getInstance()->shutdown(); + + // Cleanup all overlays after the scripts, as scripts might add more + _overlays.cleanupAllOverlays(); + + // first stop all timers directly or by invokeMethod + // depending on what thread they run in + locationUpdateTimer.stop(); + identityPacketTimer.stop(); + pingTimer.stop(); + + // Wait for the settings thread to shut down, and save the settings one last time when it's safe + if (_settingsGuard.wait()) { + // save state + saveSettings(); + } + + _window->saveGeometry(); + + // Destroy third party processes after scripts have finished using them. +#ifdef HAVE_DDE + DependencyManager::destroy(); +#endif +#ifdef HAVE_IVIEWHMD + DependencyManager::destroy(); +#endif + + DependencyManager::destroy(); // Must be destroyed before TabletScriptingInterface + + // stop QML + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + DependencyManager::destroy(); + + if (_snapshotSoundInjector != nullptr) { + _snapshotSoundInjector->stop(); + } + + // destroy Audio so it and its threads have a chance to go down safely + // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine + QMetaObject::invokeMethod(DependencyManager::get().data(), "stop"); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + // The PointerManager must be destroyed before the PickManager because when a Pointer is deleted, + // it accesses the PickManager to delete its associated Pick + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; +} + +Application::~Application() { + // remove avatars from physics engine + auto avatarManager = DependencyManager::get(); + avatarManager->clearOtherAvatars(); + + PhysicsEngine::Transaction transaction; + avatarManager->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + avatarManager->handleProcessedPhysicsTransaction(transaction); + + avatarManager->deleteAllAvatars(); + + auto myCharacterController = getMyAvatar()->getCharacterController(); + myCharacterController->clearDetailedMotionStates(); + + myCharacterController->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + myCharacterController->handleProcessedPhysicsTransaction(transaction); + + _physicsEngine->setCharacterController(nullptr); + + // the _shapeManager should have zero references + _shapeManager.collectGarbage(); + assert(_shapeManager.getNumShapes() == 0); + + // shutdown graphics engine + _graphicsEngine.shutdown(); + + _gameWorkload.shutdown(); + + DependencyManager::destroy(); + + _entityClipboard->eraseAllOctreeElements(); + _entityClipboard.reset(); + + _octreeProcessor.terminate(); + _entityEditSender.terminate(); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + steamClient->shutdown(); + } + + if (auto oculusPlatform = PluginManager::getInstance()->getOculusPlatformPlugin()) { + oculusPlatform->shutdown(); + } + + DependencyManager::destroy(); + + DependencyManager::destroy(); // must be destroyed before the FramebufferCache + + DependencyManager::destroy(); + + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + DependencyManager::destroy(); + + DependencyManager::get()->cleanup(); + + // remove the NodeList from the DependencyManager + DependencyManager::destroy(); + +#if 0 + ConnexionClient::getInstance().destroy(); +#endif + // The window takes ownership of the menu, so this has the side effect of destroying it. + _window->setMenuBar(nullptr); + + _window->deleteLater(); + + // make sure that the quit event has finished sending before we take the application down + auto closeEventSender = DependencyManager::get(); + while (!closeEventSender->hasFinishedQuitEvent() && !closeEventSender->hasTimedOutQuitEvent()) { + // sleep a little so we're not spinning at 100% + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + // quit the thread used by the closure event sender + closeEventSender->thread()->quit(); + + // Can't log to file past this point, FileLogger about to be deleted + qInstallMessageHandler(LogHandler::verboseMessageHandler); +} + +void Application::initializeGL() { + qCDebug(interfaceapp) << "Created Display Window."; + +#ifdef DISABLE_QML + setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); +#endif + + // initialize glut for shape drawing; Qt apparently initializes it on OS X + if (_isGLInitialized) { + return; + } else { + _isGLInitialized = true; + } + + _glWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat()); + + // When loading QtWebEngineWidgets, it creates a global share context on startup. + // We have to account for this possibility by checking here for an existing + // global share context + auto globalShareContext = qt_gl_global_share_context(); + +#if !defined(DISABLE_QML) + // Build a shared canvas / context for the Chromium processes + if (!globalShareContext) { + // Chromium rendering uses some GL functions that prevent nSight from capturing + // frames, so we only create the shared context if nsight is NOT active. + if (!nsightActive()) { + _chromiumShareContext = new OffscreenGLCanvas(); + _chromiumShareContext->setObjectName("ChromiumShareContext"); + auto format =QSurfaceFormat::defaultFormat(); +#ifdef Q_OS_MAC + // On mac, the primary shared OpenGL context must be a 3.2 core context, + // or chromium flips out and spews error spam (but renders fine) + format.setMajorVersion(3); + format.setMinorVersion(2); +#endif + _chromiumShareContext->setFormat(format); + _chromiumShareContext->create(); + if (!_chromiumShareContext->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make chromium shared context current"); + } + globalShareContext = _chromiumShareContext->getContext(); + qt_gl_set_global_share_context(globalShareContext); + _chromiumShareContext->doneCurrent(); + } + } +#endif + + + _glWidget->createContext(globalShareContext); + + if (!_glWidget->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make window context current"); + } + +#if !defined(DISABLE_QML) + // Disable signed distance field font rendering on ATI/AMD GPUs, due to + // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app + std::string vendor{ (const char*)glGetString(GL_VENDOR) }; + if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { + qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); + } +#endif + + if (!globalShareContext) { + globalShareContext = _glWidget->qglContext(); + qt_gl_set_global_share_context(globalShareContext); + } + + // Build a shared canvas / context for the QML rendering +#if !defined(DISABLE_QML) + { + _qmlShareContext = new OffscreenGLCanvas(); + _qmlShareContext->setObjectName("QmlShareContext"); + _qmlShareContext->create(globalShareContext); + if (!_qmlShareContext->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make QML shared context current"); + } + OffscreenQmlSurface::setSharedContext(_qmlShareContext->getContext()); + _qmlShareContext->doneCurrent(); + if (!_glWidget->makeCurrent()) { + qCWarning(interfaceapp, "Unable to make window context current"); + } + } +#endif + + + // Build an offscreen GL context for the main thread. + _glWidget->makeCurrent(); + glClearColor(0.2f, 0.2f, 0.2f, 1); + glClear(GL_COLOR_BUFFER_BIT); + _glWidget->swapBuffers(); + + _graphicsEngine.initializeGPU(_glWidget); +} + +void Application::initializeDisplayPlugins() { + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + Setting::Handle activeDisplayPluginSetting{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME, displayPlugins.at(0)->getName() }; + auto lastActiveDisplayPluginName = activeDisplayPluginSetting.get(); + + auto defaultDisplayPlugin = displayPlugins.at(0); + // Once time initialization code + DisplayPluginPointer targetDisplayPlugin; + foreach(auto displayPlugin, displayPlugins) { + displayPlugin->setContext(_graphicsEngine.getGPUContext()); + if (displayPlugin->getName() == lastActiveDisplayPluginName) { + targetDisplayPlugin = displayPlugin; + } + QObject::connect(displayPlugin.get(), &DisplayPlugin::recommendedFramebufferSizeChanged, + [this](const QSize& size) { resizeGL(); }); + QObject::connect(displayPlugin.get(), &DisplayPlugin::resetSensorsRequested, this, &Application::requestReset); + if (displayPlugin->isHmd()) { + auto hmdDisplayPlugin = dynamic_cast(displayPlugin.get()); + QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdMountedChanged, + DependencyManager::get().data(), &HMDScriptingInterface::mountedChanged); + QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdVisibleChanged, this, &Application::hmdVisibleChanged); + } + } + + // The default display plugin needs to be activated first, otherwise the display plugin thread + // may be launched by an external plugin, which is bad + setDisplayPlugin(defaultDisplayPlugin); + + // Now set the desired plugin if it's not the same as the default plugin + if (targetDisplayPlugin && (targetDisplayPlugin != defaultDisplayPlugin)) { + setDisplayPlugin(targetDisplayPlugin); + } + + // Submit a default frame to render until the engine starts up + updateRenderArgs(0.0f); +} + +void Application::initializeRenderEngine() { + // FIXME: on low end systems os the shaders take up to 1 minute to compile, so we pause the deadlock watchdog thread. + DeadlockWatchdogThread::withPause([&] { + _graphicsEngine.initializeRender(DISABLE_DEFERRED); + DependencyManager::get()->registerKeyboardHighlighting(); + }); +} + +extern void setupPreferences(); +#if !defined(DISABLE_QML) +static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active = false); +#endif + +void Application::showLoginScreen() { +#if !defined(DISABLE_QML) + auto accountManager = DependencyManager::get(); + auto dialogsManager = DependencyManager::get(); + if (!accountManager->isLoggedIn()) { + if (!isHMDMode()) { + auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); + toolbar->writeProperty("visible", false); + } + _loginDialogPoppedUp = true; + dialogsManager->showLoginDialog(); + emit loginDialogFocusEnabled(); + QJsonObject loginData = {}; + loginData["action"] = "login dialog popped up"; + UserActivityLogger::getInstance().logAction("encourageLoginDialog", loginData); + _window->setWindowTitle("High Fidelity Interface"); + } else { + resumeAfterLoginDialogActionTaken(); + } + _loginDialogPoppedUp = !accountManager->isLoggedIn(); + loginDialogPoppedUp.set(_loginDialogPoppedUp); +#else + resumeAfterLoginDialogActionTaken(); +#endif +} + +void Application::initializeUi() { + AddressBarDialog::registerType(); + ErrorDialog::registerType(); + LoginDialog::registerType(); + Tooltip::registerType(); + UpdateDialog::registerType(); + QmlContextCallback commerceCallback = [](QQmlContext* context) { + context->setContextProperty("Commerce", DependencyManager::get().data()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/checkout/Checkout.qml" }, + QUrl{ "hifi/commerce/common/CommerceLightbox.qml" }, + QUrl{ "hifi/commerce/common/EmulatedMarketplaceHeader.qml" }, + QUrl{ "hifi/commerce/common/FirstUseTutorial.qml" }, + QUrl{ "hifi/commerce/common/sendAsset/SendAsset.qml" }, + QUrl{ "hifi/commerce/common/SortableListModel.qml" }, + QUrl{ "hifi/commerce/inspectionCertificate/InspectionCertificate.qml" }, + QUrl{ "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"}, + QUrl{ "hifi/commerce/purchases/PurchasedItem.qml" }, + QUrl{ "hifi/commerce/purchases/Purchases.qml" }, + QUrl{ "hifi/commerce/wallet/Help.qml" }, + QUrl{ "hifi/commerce/wallet/NeedsLogIn.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseChange.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseModal.qml" }, + QUrl{ "hifi/commerce/wallet/PassphraseSelection.qml" }, + QUrl{ "hifi/commerce/wallet/Wallet.qml" }, + QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, + QUrl{ "hifi/commerce/wallet/WalletSetup.qml" }, + QUrl{ "hifi/dialogs/security/Security.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageChange.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, + QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, + QUrl{ "hifi/tablet/TabletMenu.qml" }, + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, commerceCallback); + + QmlContextCallback marketplaceCallback = [](QQmlContext* context) { + context->setContextProperty("MarketplaceScriptingInterface", new QmlMarketplace()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, marketplaceCallback); + + QmlContextCallback platformInfoCallback = [](QQmlContext* context) { + context->setContextProperty("PlatformInfo", new PlatformInfoScriptingInterface()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, + }, platformInfoCallback); + + QmlContextCallback ttsCallback = [](QQmlContext* context) { + context->setContextProperty("TextToSpeech", DependencyManager::get().data()); + }; + OffscreenQmlSurface::addWhitelistContextHandler({ + QUrl{ "hifi/tts/TTS.qml" } + }, ttsCallback); + qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); + qmlRegisterType("Hifi", 1, 0, "Preference"); + qmlRegisterType("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine"); + + { + auto tabletScriptingInterface = DependencyManager::get(); + tabletScriptingInterface->getTablet(SYSTEM_TABLET); + } + + auto offscreenUi = getOffscreenUI(); + connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootContextCreated, + this, &Application::onDesktopRootContextCreated); + connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootItemCreated, + this, &Application::onDesktopRootItemCreated); + +#if !defined(DISABLE_QML) + offscreenUi->setProxyWindow(_window->windowHandle()); + // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to + // support the window management and scripting proxies for VR use + DeadlockWatchdogThread::withPause([&] { + offscreenUi->createDesktop(PathUtils::qmlUrl("hifi/Desktop.qml")); + }); + // FIXME either expose so that dialogs can set this themselves or + // do better detection in the offscreen UI of what has focus + offscreenUi->setNavigationFocused(false); +#else + _window->setMenuBar(new Menu()); +#endif + + setupPreferences(); + +#if !defined(DISABLE_QML) + _glWidget->installEventFilter(offscreenUi.data()); + offscreenUi->setMouseTranslator([=](const QPointF& pt) { + QPointF result = pt; + auto displayPlugin = getActiveDisplayPlugin(); + if (displayPlugin->isHmd()) { + getApplicationCompositor().handleRealMouseMoveEvent(false); + auto resultVec = getApplicationCompositor().getReticlePosition(); + result = QPointF(resultVec.x, resultVec.y); + } + return result.toPoint(); + }); + offscreenUi->resume(); +#endif + connect(_window, &MainWindow::windowGeometryChanged, [this](const QRect& r){ + resizeGL(); + if (_touchscreenVirtualPadDevice) { + _touchscreenVirtualPadDevice->resize(); + } + }); + + // This will set up the input plugins UI + _activeInputPlugins.clear(); + foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { + if (KeyboardMouseDevice::NAME == inputPlugin->getName()) { + _keyboardMouseDevice = std::dynamic_pointer_cast(inputPlugin); + } + if (TouchscreenDevice::NAME == inputPlugin->getName()) { + _touchscreenDevice = std::dynamic_pointer_cast(inputPlugin); + } + if (TouchscreenVirtualPadDevice::NAME == inputPlugin->getName()) { + _touchscreenVirtualPadDevice = std::dynamic_pointer_cast(inputPlugin); +#if defined(ANDROID_APP_INTERFACE) + auto& virtualPadManager = VirtualPad::Manager::instance(); + connect(&virtualPadManager, &VirtualPad::Manager::hapticFeedbackRequested, + this, [](int duration) { + AndroidHelper::instance().performHapticFeedback(duration); + }); +#endif + } + } + + auto compositorHelper = DependencyManager::get(); + connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, this, [=] { + if (isHMDMode()) { + auto compositorHelper = DependencyManager::get(); // don't capture outer smartpointer + showCursor(compositorHelper->getAllowMouseCapture() ? + Cursor::Manager::lookupIcon(_preferredCursor.get()) : + Cursor::Icon::SYSTEM); + } + }); + +#if !defined(DISABLE_QML) + // Pre-create a couple of offscreen surfaces to speed up tablet UI + auto offscreenSurfaceCache = DependencyManager::get(); + offscreenSurfaceCache->setOnRootContextCreated([&](const QString& rootObject, QQmlContext* surfaceContext) { + if (rootObject == TabletScriptingInterface::QML) { + // in Qt 5.10.0 there is already an "Audio" object in the QML context + // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" + surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + } + }); + + offscreenSurfaceCache->reserve(TabletScriptingInterface::QML, 1); + offscreenSurfaceCache->reserve(render::entities::WebEntityRenderer::QML, 2); +#endif + + flushMenuUpdates(); + +#if !defined(DISABLE_QML) + // Now that the menu is instantiated, ensure the display plugin menu is properly updated + { + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + // first sort the plugins into groupings: standard, advanced, developer + std::stable_sort(displayPlugins.begin(), displayPlugins.end(), + [](const DisplayPluginPointer& a, const DisplayPluginPointer& b) -> bool { return a->getGrouping() < b->getGrouping(); }); + int dpIndex = 1; + // concatenate the groupings into a single list in the order: standard, advanced, developer + for(const auto& displayPlugin : displayPlugins) { + addDisplayPluginToMenu(displayPlugin, dpIndex, _displayPlugin == displayPlugin); + dpIndex++; + } + + // after all plugins have been added to the menu, add a separator to the menu + auto parent = getPrimaryMenu()->getMenu(MenuOption::OutputMenu); + parent->addSeparator(); + } +#endif + + + // The display plugins are created before the menu now, so we need to do this here to hide the menu bar + // now that it exists + if (_window && _window->isFullScreen()) { + setFullscreen(nullptr, true); + } + + + setIsInterstitialMode(true); +} + + +void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { + auto engine = surfaceContext->engine(); + // in Qt 5.10.0 there is already an "Audio" object in the QML context + // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" + surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); + + surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); + surfaceContext->setContextProperty("AudioScope", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); + surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); + _fileDownload = new FileScriptingInterface(engine); + surfaceContext->setContextProperty("File", _fileDownload); + connect(_fileDownload, &FileScriptingInterface::unzipResult, this, &Application::handleUnzip); + surfaceContext->setContextProperty("MyAvatar", getMyAvatar().get()); + surfaceContext->setContextProperty("Messages", DependencyManager::get().data()); + surfaceContext->setContextProperty("Recording", DependencyManager::get().data()); + surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); + surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("FrameTimings", &_graphicsEngine._frameTimingsScriptingInterface); + surfaceContext->setContextProperty("Rates", new RatesScriptingInterface(this)); + + surfaceContext->setContextProperty("TREE_SCALE", TREE_SCALE); + // FIXME Quat and Vec3 won't work with QJSEngine used by QML + surfaceContext->setContextProperty("Quat", new Quat()); + surfaceContext->setContextProperty("Vec3", new Vec3()); + surfaceContext->setContextProperty("Uuid", new ScriptUUID()); + surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); + surfaceContext->setContextProperty("Keyboard", DependencyManager::get().data()); + + surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); + surfaceContext->setContextProperty("Users", DependencyManager::get().data()); + + surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Camera", &_myCamera); + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + surfaceContext->setContextProperty("SpeechRecognizer", DependencyManager::get().data()); +#endif + + surfaceContext->setContextProperty("Overlays", &_overlays); + surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); + surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); + surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); + surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); + + // Caches + surfaceContext->setContextProperty("AnimationCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("TextureCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("ModelCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); + + surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); + + surfaceContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); + surfaceContext->setContextProperty("FaceTracker", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); + surfaceContext->setContextProperty("Scene", DependencyManager::get().data()); + surfaceContext->setContextProperty("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); + surfaceContext->setContextProperty("Workload", _gameWorkload._engine->getConfiguration().get()); + surfaceContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); + surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); + + surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); + + surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + surfaceContext->setContextProperty("Selection", DependencyManager::get().data()); + surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); + surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); + surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); + } + + _window->setMenuBar(new Menu()); +} + +void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { + Stats::show(); + AnimStats::show(); + auto surfaceContext = getOffscreenUI()->getSurfaceContext(); + surfaceContext->setContextProperty("Stats", Stats::getInstance()); + surfaceContext->setContextProperty("AnimStats", AnimStats::getInstance()); + +#if !defined(Q_OS_ANDROID) + auto offscreenUi = getOffscreenUI(); + auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml"); + offscreenUi->show(qml, "AvatarInputsBar"); +#endif +} + +void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) { + surfaceContext->setContextProperty("Users", DependencyManager::get().data()); + surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); + surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); + surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); + surfaceContext->setContextProperty("Vec3", new Vec3()); + surfaceContext->setContextProperty("Quat", new Quat()); + surfaceContext->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); + surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); + surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); + surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); + + if (setAdditionalContextProperties) { + auto tabletScriptingInterface = DependencyManager::get(); + auto flags = tabletScriptingInterface->getFlags(); + + surfaceContext->setContextProperty("offscreenFlags", flags); + surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); + + surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); + surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); + + surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); + + // in Qt 5.10.0 there is already an "Audio" object in the QML context + // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" + surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); + + surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); + surfaceContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); + surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); + surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); + surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); + surfaceContext->setContextProperty("OctreeStats", DependencyManager::get().data()); + surfaceContext->setContextProperty("DCModel", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); + surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); + surfaceContext->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); + surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); + surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); + surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); + surfaceContext->setContextProperty("Render", AbstractViewStateInterface::instance()->getRenderEngine()->getConfiguration().get()); + surfaceContext->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); + surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); + surfaceContext->setContextProperty("Pointers", DependencyManager::get().data()); + surfaceContext->setContextProperty("Window", DependencyManager::get().data()); + surfaceContext->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); + surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); + surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); + surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); + } +} + +void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { + PROFILE_RANGE(render, __FUNCTION__); + PerformanceTimer perfTimer("updateCamera"); + + glm::vec3 boomOffset; + auto myAvatar = getMyAvatar(); + boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; + + // The render mode is default or mirror if the camera is in mirror mode, assigned further below + renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; + + // Always use the default eye position, not the actual head eye position. + // Using the latter will cause the camera to wobble with idle animations, + // or with changes from the face tracker + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { + _thirdPersonHMDCameraBoomValid= false; + if (isHMDMode()) { + mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); + _myCamera.setPosition(extractTranslation(camMat)); + _myCamera.setOrientation(glmExtractRotation(camMat)); + } + else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition()); + _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); + } + } + else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { + if (isHMDMode()) { + + if (!_thirdPersonHMDCameraBoomValid) { + const glm::vec3 CAMERA_OFFSET = glm::vec3(0.0f, 0.0f, 0.7f); + _thirdPersonHMDCameraBoom = cancelOutRollAndPitch(myAvatar->getHMDSensorOrientation()) * CAMERA_OFFSET; + _thirdPersonHMDCameraBoomValid = true; + } + + glm::mat4 thirdPersonCameraSensorToWorldMatrix = myAvatar->getSensorToWorldMatrix(); + + const glm::vec3 cameraPos = myAvatar->getHMDSensorPosition() + _thirdPersonHMDCameraBoom * myAvatar->getBoomLength(); + glm::mat4 sensorCameraMat = createMatFromQuatAndPos(myAvatar->getHMDSensorOrientation(), cameraPos); + glm::mat4 worldCameraMat = thirdPersonCameraSensorToWorldMatrix * sensorCameraMat; + + _myCamera.setOrientation(glm::normalize(glmExtractRotation(worldCameraMat))); + _myCamera.setPosition(extractTranslation(worldCameraMat)); + } + else { + _thirdPersonHMDCameraBoomValid = false; + + _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); + if (isOptionChecked(MenuOption::CenterPlayerInView)) { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + _myCamera.getOrientation() * boomOffset); + } + else { + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + myAvatar->getWorldOrientation() * boomOffset); + } + } + } + else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { + _thirdPersonHMDCameraBoomValid= false; + + if (isHMDMode()) { + auto mirrorBodyOrientation = myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f)); + + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD yaw and roll + glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); + mirrorHmdEulers.y = -mirrorHmdEulers.y; + mirrorHmdEulers.z = -mirrorHmdEulers.z; + glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); + + glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; + + _myCamera.setOrientation(worldMirrorRotation); + + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + // Mirror HMD lateral offsets + hmdOffset.x = -hmdOffset.x; + + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) + + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror + + mirrorBodyOrientation * hmdOffset); + } + else { + auto userInputMapper = DependencyManager::get(); + const float YAW_SPEED = TWO_PI / 5.0f; + float deltaYaw = userInputMapper->getActionState(controller::Action::YAW) * YAW_SPEED * deltaTime; + _mirrorYawOffset += deltaYaw; + _myCamera.setOrientation(myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f))); + _myCamera.setPosition(myAvatar->getDefaultEyePosition() + + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) + + (myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, _mirrorYawOffset, 0.0f))) * + glm::vec3(0.0f, 0.0f, -1.0f) * myAvatar->getBoomLength() * _scaleMirror); + } + renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; + } + else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { + _thirdPersonHMDCameraBoomValid= false; + EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); + if (cameraEntity != nullptr) { + if (isHMDMode()) { + glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); + _myCamera.setOrientation(cameraEntity->getWorldOrientation() * hmdRotation); + glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); + _myCamera.setPosition(cameraEntity->getWorldPosition() + (hmdRotation * hmdOffset)); + } + else { + _myCamera.setOrientation(cameraEntity->getWorldOrientation()); + _myCamera.setPosition(cameraEntity->getWorldPosition()); + } + } + } + // Update camera position + if (!isHMDMode()) { + _myCamera.update(); + } + + renderArgs._cameraMode = (int8_t)_myCamera.getMode(); +} + +void Application::runTests() { + runTimingTests(); + runUnitTests(); +} + +void Application::faceTrackerMuteToggled() { + + QAction* muteAction = Menu::getInstance()->getActionForOption(MenuOption::MuteFaceTracking); + Q_CHECK_PTR(muteAction); + bool isMuted = getSelectedFaceTracker()->isMuted(); + muteAction->setChecked(isMuted); + getSelectedFaceTracker()->setEnabled(!isMuted); + Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setEnabled(!isMuted); +} + +void Application::setFieldOfView(float fov) { + if (fov != _fieldOfView.get()) { + _fieldOfView.set(fov); + resizeGL(); + } +} + +void Application::setHMDTabletScale(float hmdTabletScale) { + _hmdTabletScale.set(hmdTabletScale); +} + +void Application::setDesktopTabletScale(float desktopTabletScale) { + _desktopTabletScale.set(desktopTabletScale); +} + +void Application::setDesktopTabletBecomesToolbarSetting(bool value) { + _desktopTabletBecomesToolbarSetting.set(value); + updateSystemTabletMode(); +} + +void Application::setHmdTabletBecomesToolbarSetting(bool value) { + _hmdTabletBecomesToolbarSetting.set(value); + updateSystemTabletMode(); +} + +void Application::setPreferStylusOverLaser(bool value) { + _preferStylusOverLaserSetting.set(value); +} + +void Application::setPreferAvatarFingerOverStylus(bool value) { + _preferAvatarFingerOverStylusSetting.set(value); +} + +void Application::setPreferredCursor(const QString& cursorName) { + qCDebug(interfaceapp) << "setPreferredCursor" << cursorName; + _preferredCursor.set(cursorName.isEmpty() ? DEFAULT_CURSOR_NAME : cursorName); + showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); +} + +void Application::setSettingConstrainToolbarPosition(bool setting) { + _constrainToolbarPosition.set(setting); + getOffscreenUI()->setConstrainToolbarToCenterX(setting); +} + +void Application::setMiniTabletEnabled(bool enabled) { + _miniTabletEnabledSetting.set(enabled); + emit miniTabletEnabledChanged(enabled); +} + +void Application::showHelp() { + static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; + static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; + static const QString HAND_CONTROLLER_NAME_WINDOWS_MR = "windowsMR"; + + static const QString VIVE_PLUGIN_NAME = "HTC Vive"; + static const QString OCULUS_RIFT_PLUGIN_NAME = "Oculus Rift"; + static const QString WINDOWS_MR_PLUGIN_NAME = "WindowsMR"; + + static const QString TAB_KEYBOARD_MOUSE = "kbm"; + static const QString TAB_GAMEPAD = "gamepad"; + static const QString TAB_HAND_CONTROLLERS = "handControllers"; + + QString handControllerName; + QString defaultTab = TAB_KEYBOARD_MOUSE; + + if (PluginUtils::isHMDAvailable(WINDOWS_MR_PLUGIN_NAME)) { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_WINDOWS_MR; + } else if (PluginUtils::isHMDAvailable(VIVE_PLUGIN_NAME)) { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_VIVE; + } else if (PluginUtils::isHMDAvailable(OCULUS_RIFT_PLUGIN_NAME)) { + if (PluginUtils::isOculusTouchControllerAvailable()) { + defaultTab = TAB_HAND_CONTROLLERS; + handControllerName = HAND_CONTROLLER_NAME_OCULUS_TOUCH; + } else if (PluginUtils::isXboxControllerAvailable()) { + defaultTab = TAB_GAMEPAD; + } else { + defaultTab = TAB_KEYBOARD_MOUSE; + } + } else if (PluginUtils::isXboxControllerAvailable()) { + defaultTab = TAB_GAMEPAD; + } else { + defaultTab = TAB_KEYBOARD_MOUSE; + } + + QUrlQuery queryString; + queryString.addQueryItem("handControllerName", handControllerName); + queryString.addQueryItem("defaultTab", defaultTab); + TabletProxy* tablet = dynamic_cast(DependencyManager::get()->getTablet(SYSTEM_TABLET)); + tablet->gotoWebScreen(PathUtils::resourcesUrl() + INFO_HELP_PATH + "?" + queryString.toString()); + DependencyManager::get()->openTablet(); + //InfoView::show(INFO_HELP_PATH, false, queryString.toString()); +} + +void Application::resizeEvent(QResizeEvent* event) { + resizeGL(); +} + +void Application::resizeGL() { + PROFILE_RANGE(render, __FUNCTION__); + if (nullptr == _displayPlugin) { + return; + } + + auto displayPlugin = getActiveDisplayPlugin(); + // Set the desired FBO texture size. If it hasn't changed, this does nothing. + // Otherwise, it must rebuild the FBOs + uvec2 framebufferSize = displayPlugin->getRecommendedRenderSize(); + uvec2 renderSize = uvec2(framebufferSize); + if (_renderResolution != renderSize) { + _renderResolution = renderSize; + DependencyManager::get()->setFrameBufferSize(fromGlm(renderSize)); + } + + auto renderResolutionScale = getRenderResolutionScale(); + if (displayPlugin->getRenderResolutionScale() != renderResolutionScale) { + auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); + assert(renderConfig); + auto mainView = renderConfig->getConfig("RenderMainView.RenderDeferredTask"); + // mainView can be null if we're rendering in forward mode + if (mainView) { + mainView->setProperty("resolutionScale", renderResolutionScale); + } + displayPlugin->setRenderResolutionScale(renderResolutionScale); + } + + // FIXME the aspect ratio for stereo displays is incorrect based on this. + float aspectRatio = displayPlugin->getRecommendedAspectRatio(); + _myCamera.setProjection(glm::perspective(glm::radians(_fieldOfView.get()), aspectRatio, + DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); + // Possible change in aspect ratio + { + QMutexLocker viewLocker(&_viewMutex); + _myCamera.loadViewFrustum(_viewFrustum); + } + +#if !defined(DISABLE_QML) + getOffscreenUI()->resize(fromGlm(displayPlugin->getRecommendedUiSize())); +#endif +} + +void Application::handleSandboxStatus(QNetworkReply* reply) { + PROFILE_RANGE(render, __FUNCTION__); + + bool sandboxIsRunning = SandboxUtils::readStatus(reply->readAll()); + + enum HandControllerType { + Vive, + Oculus + }; + static const std::map MIN_CONTENT_VERSION = { + { Vive, 1 }, + { Oculus, 27 } + }; + + // Get sandbox content set version + auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; + auto contentVersionPath = acDirPath + "content-version.txt"; + qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; + int contentVersion = 0; + QFile contentVersionFile(contentVersionPath); + if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QString line = contentVersionFile.readAll(); + contentVersion = line.toInt(); // returns 0 if conversion fails + } + + // Get controller availability +#ifdef ANDROID_APP_QUEST_INTERFACE + bool hasHandControllers = true; +#else + bool hasHandControllers = false; + if (PluginUtils::isViveControllerAvailable() || PluginUtils::isOculusTouchControllerAvailable()) { + hasHandControllers = true; + } +#endif + + // Check HMD use (may be technically available without being in use) + bool hasHMD = PluginUtils::isHMDAvailable(); + bool isUsingHMD = _displayPlugin->isHmd(); + bool isUsingHMDAndHandControllers = hasHMD && hasHandControllers && isUsingHMD; + + qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMDAndHandControllers; + + // when --url in command line, teleport to location + const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; + int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); + QString addressLookupString; + if (urlIndex != -1) { + QUrl url(arguments().value(urlIndex + 1)); + if (url.scheme() == URL_SCHEME_HIFIAPP) { + Setting::Handle("startUpApp").set(url.path()); + } else { + addressLookupString = url.toString(); + } + } + + static const QString SENT_TO_PREVIOUS_LOCATION = "previous_location"; + static const QString SENT_TO_ENTRY = "entry"; + + QString sentTo; + + // If this is a first run we short-circuit the address passed in + if (_firstRun.get()) { +#if !defined(Q_OS_ANDROID) + DependencyManager::get()->goToEntry(); + sentTo = SENT_TO_ENTRY; +#endif + _firstRun.set(false); + + } else { +#if !defined(Q_OS_ANDROID) + QString goingTo = ""; + if (addressLookupString.isEmpty()) { + if (Menu::getInstance()->isOptionChecked(MenuOption::HomeLocation)) { + auto locationBookmarks = DependencyManager::get(); + addressLookupString = locationBookmarks->addressForBookmark(LocationBookmarks::HOME_BOOKMARK); + goingTo = "home location"; + } else { + goingTo = "previous location"; + } + } + qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(!goingTo.isEmpty() ? goingTo : addressLookupString); + DependencyManager::get()->loadSettings(addressLookupString); + sentTo = SENT_TO_PREVIOUS_LOCATION; +#endif + } + + UserActivityLogger::getInstance().logAction("startup_sent_to", { + { "sent_to", sentTo }, + { "sandbox_is_running", sandboxIsRunning }, + { "has_hmd", hasHMD }, + { "has_hand_controllers", hasHandControllers }, + { "is_using_hmd", isUsingHMD }, + { "is_using_hmd_and_hand_controllers", isUsingHMDAndHandControllers }, + { "content_version", contentVersion } + }); + + _connectionMonitor.init(); +} + +bool Application::importJSONFromURL(const QString& urlString) { + // we only load files that terminate in just .json (not .svo.json and not .ava.json) + QUrl jsonURL { urlString }; + + emit svoImportRequested(urlString); + return true; +} + +bool Application::importSVOFromURL(const QString& urlString) { + emit svoImportRequested(urlString); + return true; +} + +bool Application::importFromZIP(const QString& filePath) { + qDebug() << "A zip file has been dropped in: " << filePath; + QUrl empty; + // handle Blocks download from Marketplace + if (filePath.contains("poly.google.com/downloads")) { + addAssetToWorldFromURL(filePath); + } else { + qApp->getFileDownloadInterface()->runUnzip(filePath, empty, true, true, false); + } + return true; +} + +bool Application::isServerlessMode() const { + auto tree = getEntities()->getTree(); + if (tree) { + return tree->isServerlessMode(); + } + return false; +} + +void Application::setIsInterstitialMode(bool interstitialMode) { + bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + if (enableInterstitial) { + if (_interstitialMode != interstitialMode) { + _interstitialMode = interstitialMode; + emit interstitialModeChanged(_interstitialMode); + + DependencyManager::get()->setAudioPaused(_interstitialMode); + DependencyManager::get()->setMyAvatarDataPacketsPaused(_interstitialMode); + } + } +} + +void Application::setIsServerlessMode(bool serverlessDomain) { + auto tree = getEntities()->getTree(); + if (tree) { + tree->setIsServerlessMode(serverlessDomain); + } +} + +std::map Application::prepareServerlessDomainContents(QUrl domainURL) { + QUuid serverlessSessionID = QUuid::createUuid(); + getMyAvatar()->setSessionUUID(serverlessSessionID); + auto nodeList = DependencyManager::get(); + nodeList->setSessionUUID(serverlessSessionID); + + // there is no domain-server to tell us our permissions, so enable all + NodePermissions permissions; + permissions.setAll(true); + nodeList->setPermissions(permissions); + + // we can't import directly into the main tree because we would need to lock it, and + // Octree::readFromURL calls loop.exec which can run code which will also attempt to lock the tree. + EntityTreePointer tmpTree(new EntityTree()); + tmpTree->setIsServerlessMode(true); + tmpTree->createRootElement(); + auto myAvatar = getMyAvatar(); + tmpTree->setMyAvatar(myAvatar); + bool success = tmpTree->readFromURL(domainURL.toString()); + if (success) { + tmpTree->reaverageOctreeElements(); + tmpTree->sendEntities(&_entityEditSender, getEntities()->getTree(), 0, 0, 0); + } + std::map namedPaths = tmpTree->getNamedPaths(); + + // we must manually eraseAllOctreeElements(false) else the tmpTree will mem-leak + tmpTree->eraseAllOctreeElements(false); + + return namedPaths; +} + +void Application::loadServerlessDomain(QUrl domainURL) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "loadServerlessDomain", Q_ARG(QUrl, domainURL)); + return; + } + + if (domainURL.isEmpty()) { + return; + } + + auto namedPaths = prepareServerlessDomainContents(domainURL); + auto nodeList = DependencyManager::get(); + + nodeList->getDomainHandler().connectedToServerless(namedPaths); + + _fullSceneReceivedCounter++; +} + +void Application::loadErrorDomain(QUrl domainURL) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "loadErrorDomain", Q_ARG(QUrl, domainURL)); + return; + } + + if (domainURL.isEmpty()) { + return; + } + + auto namedPaths = prepareServerlessDomainContents(domainURL); + auto nodeList = DependencyManager::get(); + + nodeList->getDomainHandler().loadedErrorDomain(namedPaths); + + _fullSceneReceivedCounter++; +} + +bool Application::importImage(const QString& urlString) { + qCDebug(interfaceapp) << "An image file has been dropped in"; + QString filepath(urlString); + filepath.remove("file:///"); + addAssetToWorld(filepath, "", false, false); + return true; +} + +// thread-safe +void Application::onPresent(quint32 frameCount) { + bool expected = false; + if (_pendingIdleEvent.compare_exchange_strong(expected, true)) { + postEvent(this, new QEvent((QEvent::Type)ApplicationEvent::Idle), Qt::HighEventPriority); + } + expected = false; + if (_graphicsEngine.checkPendingRenderEvent() && !isAboutToQuit()) { + postEvent(_graphicsEngine._renderEventHandler, new QEvent((QEvent::Type)ApplicationEvent::Render)); + } +} + +static inline bool isKeyEvent(QEvent::Type type) { + return type == QEvent::KeyPress || type == QEvent::KeyRelease; +} + +bool Application::handleKeyEventForFocusedEntity(QEvent* event) { + if (_keyboardFocusedEntity.get() != UNKNOWN_ENTITY_ID) { + switch (event->type()) { + case QEvent::KeyPress: + case QEvent::KeyRelease: + { + auto eventHandler = getEntities()->getEventHandler(_keyboardFocusedEntity.get()); + if (eventHandler) { + event->setAccepted(false); + QCoreApplication::sendEvent(eventHandler, event); + if (event->isAccepted()) { + _lastAcceptedKeyPress = usecTimestampNow(); + return true; + } + } + break; + } + default: + break; + } + } + + return false; +} + +bool Application::handleFileOpenEvent(QFileOpenEvent* fileEvent) { + QUrl url = fileEvent->url(); + if (!url.isEmpty()) { + QString urlString = url.toString(); + if (canAcceptURL(urlString)) { + return acceptURL(urlString); + } + } + return false; +} + +#ifdef DEBUG_EVENT_QUEUE +static int getEventQueueSize(QThread* thread) { + auto threadData = QThreadData::get2(thread); + QMutexLocker locker(&threadData->postEventList.mutex); + return threadData->postEventList.size(); +} + +static void dumpEventQueue(QThread* thread) { + auto threadData = QThreadData::get2(thread); + QMutexLocker locker(&threadData->postEventList.mutex); + qDebug() << "Event list, size =" << threadData->postEventList.size(); + for (auto& postEvent : threadData->postEventList) { + QEvent::Type type = (postEvent.event ? postEvent.event->type() : QEvent::None); + qDebug() << " " << type; + } +} +#endif // DEBUG_EVENT_QUEUE + +bool Application::event(QEvent* event) { + + if (_aboutToQuit) { + return false; + } + + if (!Menu::getInstance()) { + return false; + } + + // Allow focused Entities to handle keyboard input + if (isKeyEvent(event->type()) && handleKeyEventForFocusedEntity(event)) { + return true; + } + + int type = event->type(); + switch (type) { + case ApplicationEvent::Lambda: + static_cast(event)->call(); + return true; + + // Explicit idle keeps the idle running at a lower interval, but without any rendering + // see (windowMinimizedChanged) + case ApplicationEvent::Idle: + idle(); + +#ifdef DEBUG_EVENT_QUEUE + { + int count = getEventQueueSize(QThread::currentThread()); + if (count > 400) { + dumpEventQueue(QThread::currentThread()); + } + } +#endif // DEBUG_EVENT_QUEUE + + _pendingIdleEvent.store(false); + + return true; + + case QEvent::MouseMove: + mouseMoveEvent(static_cast(event)); + return true; + case QEvent::MouseButtonPress: + mousePressEvent(static_cast(event)); + return true; + case QEvent::MouseButtonDblClick: + mouseDoublePressEvent(static_cast(event)); + return true; + case QEvent::MouseButtonRelease: + mouseReleaseEvent(static_cast(event)); + return true; + case QEvent::KeyPress: + keyPressEvent(static_cast(event)); + return true; + case QEvent::KeyRelease: + keyReleaseEvent(static_cast(event)); + return true; + case QEvent::FocusOut: + focusOutEvent(static_cast(event)); + return true; + case QEvent::TouchBegin: + touchBeginEvent(static_cast(event)); + event->accept(); + return true; + case QEvent::TouchEnd: + touchEndEvent(static_cast(event)); + return true; + case QEvent::TouchUpdate: + touchUpdateEvent(static_cast(event)); + return true; + case QEvent::Gesture: + touchGestureEvent((QGestureEvent*)event); + return true; + case QEvent::Wheel: + wheelEvent(static_cast(event)); + return true; + case QEvent::Drop: + dropEvent(static_cast(event)); + return true; + + case QEvent::FileOpen: + if (handleFileOpenEvent(static_cast(event))) { + return true; + } + break; + + default: + break; + } + + return QApplication::event(event); +} + +bool Application::eventFilter(QObject* object, QEvent* event) { + + if (_aboutToQuit && event->type() != QEvent::DeferredDelete && event->type() != QEvent::Destroy) { + return true; + } + + if (event->type() == QEvent::Leave) { + getApplicationCompositor().handleLeaveEvent(); + } + + if (event->type() == QEvent::ShortcutOverride) { +#if !defined(DISABLE_QML) + if (getOffscreenUI()->shouldSwallowShortcut(event)) { + event->accept(); + return true; + } +#endif + + // Filter out captured keys before they're used for shortcut actions. + if (_controllerScriptingInterface->isKeyCaptured(static_cast(event))) { + event->accept(); + return true; + } + } + + return false; +} + +static bool _altPressed{ false }; + +void Application::keyPressEvent(QKeyEvent* event) { + _altPressed = event->key() == Qt::Key_Alt; + + if (!event->isAutoRepeat()) { + _keysPressed.insert(event->key(), *event); + } + + _controllerScriptingInterface->emitKeyPressEvent(event); // send events to any registered scripts + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isKeyCaptured(event) || isInterstitialMode()) { + return; + } + + if (hasFocus() && getLoginDialogPoppedUp()) { + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->keyReleaseEvent(event); + } + + bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); + bool isOption = event->modifiers().testFlag(Qt::AltModifier); + switch (event->key()) { + case Qt::Key_4: + case Qt::Key_5: + case Qt::Key_6: + case Qt::Key_7: + if (isMeta || isOption) { + unsigned int index = static_cast(event->key() - Qt::Key_1); + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + if (index < displayPlugins.size()) { + auto targetPlugin = displayPlugins.at(index); + QString targetName = targetPlugin->getName(); + auto menu = Menu::getInstance(); + QAction* action = menu->getActionForOption(targetName); + if (action && !action->isChecked()) { + action->trigger(); + } + } + } + break; + } + } else if (hasFocus()) { + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->keyPressEvent(event); + } + + bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); + bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); + bool isOption = event->modifiers().testFlag(Qt::AltModifier); + switch (event->key()) { + case Qt::Key_Enter: + case Qt::Key_Return: + if (isOption) { + if (_window->isFullScreen()) { + unsetFullscreen(); + } else { + setFullscreen(nullptr); + } + } + break; + + case Qt::Key_1: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::FirstPerson); + break; + } + case Qt::Key_2: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::FullscreenMirror); + break; + } + case Qt::Key_3: { + Menu* menu = Menu::getInstance(); + menu->triggerOption(MenuOption::ThirdPerson); + break; + } + case Qt::Key_4: + case Qt::Key_5: + case Qt::Key_6: + case Qt::Key_7: + if (isMeta || isOption) { + unsigned int index = static_cast(event->key() - Qt::Key_1); + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + if (index < displayPlugins.size()) { + auto targetPlugin = displayPlugins.at(index); + QString targetName = targetPlugin->getName(); + auto menu = Menu::getInstance(); + QAction* action = menu->getActionForOption(targetName); + if (action && !action->isChecked()) { + action->trigger(); + } + } + } + break; + + case Qt::Key_G: + if (isShifted && isMeta && Menu::getInstance() && Menu::getInstance()->getMenu("Developer")->isVisible()) { + static const QString HIFI_FRAMES_FOLDER_VAR = "HIFI_FRAMES_FOLDER"; + static const QString GPU_FRAME_FOLDER = QProcessEnvironment::systemEnvironment().contains(HIFI_FRAMES_FOLDER_VAR) + ? QProcessEnvironment::systemEnvironment().value(HIFI_FRAMES_FOLDER_VAR) + : "hifiFrames"; + static QString GPU_FRAME_TEMPLATE = GPU_FRAME_FOLDER + "/{DATE}_{TIME}"; + QString fullPath = FileUtils::computeDocumentPath(FileUtils::replaceDateTimeTokens(GPU_FRAME_TEMPLATE)); + if (FileUtils::canCreateFile(fullPath)) { + getActiveDisplayPlugin()->captureFrame(fullPath.toStdString()); + } + } + break; + case Qt::Key_X: + if (isShifted && isMeta) { + auto offscreenUi = getOffscreenUI(); + offscreenUi->togglePinned(); + //offscreenUi->getSurfaceContext()->engine()->clearComponentCache(); + //OffscreenUi::information("Debugging", "Component cache cleared"); + // placeholder for dialogs being converted to QML. + } + break; + + case Qt::Key_Y: + if (isShifted && isMeta) { + getActiveDisplayPlugin()->cycleDebugOutput(); + } + break; + + case Qt::Key_B: + if (isMeta) { + auto offscreenUi = getOffscreenUI(); + offscreenUi->load("Browser.qml"); + } else if (isOption) { + controller::InputRecorder* inputRecorder = controller::InputRecorder::getInstance(); + inputRecorder->stopPlayback(); + } + break; + + case Qt::Key_L: + if (isShifted && isMeta) { + Menu::getInstance()->triggerOption(MenuOption::Log); + } else if (isMeta) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->toggleAddressBar(); + } else if (isShifted) { + Menu::getInstance()->triggerOption(MenuOption::LodTools); + } + break; + + case Qt::Key_R: + if (isMeta && !event->isAutoRepeat()) { + DependencyManager::get()->reloadAllScripts(); + getOffscreenUI()->clearCache(); + } + break; + + case Qt::Key_Asterisk: + Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox); + break; + + case Qt::Key_M: + if (isMeta) { + auto audioClient = DependencyManager::get(); + audioClient->setMuted(!audioClient->isMuted()); + } + break; + + case Qt::Key_N: + if (!isOption && !isShifted && isMeta) { + DependencyManager::get()->toggleIgnoreRadius(); + } + break; + + case Qt::Key_S: + if (isShifted && isMeta && !isOption) { + Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); + } + break; + + case Qt::Key_P: { + if (!isShifted && !isMeta && !isOption && !event->isAutoRepeat()) { + AudioInjectorOptions options; + options.localOnly = true; + options.positionSet = false; // system sound + options.stereo = true; + + Setting::Handle notificationSounds{ MenuOption::NotificationSounds, true }; + Setting::Handle notificationSoundSnapshot{ MenuOption::NotificationSoundsSnapshot, true }; + if (notificationSounds.get() && notificationSoundSnapshot.get()) { + if (_snapshotSoundInjector) { + _snapshotSoundInjector->setOptions(options); + _snapshotSoundInjector->restart(); + } else { + _snapshotSoundInjector = AudioInjector::playSound(_snapshotSound, options); + } + } + takeSnapshot(true); + } + break; + } + + case Qt::Key_Apostrophe: { + if (isMeta) { + auto cursor = Cursor::Manager::instance().getCursor(); + auto curIcon = cursor->getIcon(); + if (curIcon == Cursor::Icon::DEFAULT) { + showCursor(Cursor::Icon::RETICLE); + } else if (curIcon == Cursor::Icon::RETICLE) { + showCursor(Cursor::Icon::SYSTEM); + } else if (curIcon == Cursor::Icon::SYSTEM) { + showCursor(Cursor::Icon::LINK); + } else { + showCursor(Cursor::Icon::DEFAULT); + } + } else if (!event->isAutoRepeat()){ + resetSensors(true); + } + break; + } + + case Qt::Key_Backslash: + Menu::getInstance()->triggerOption(MenuOption::Chat); + break; + + case Qt::Key_Slash: + Menu::getInstance()->triggerOption(MenuOption::Stats); + break; + + case Qt::Key_Plus: { + if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { + auto& cursorManager = Cursor::Manager::instance(); + cursorManager.setScale(cursorManager.getScale() * 1.1f); + } else { + getMyAvatar()->increaseSize(); + } + break; + } + + case Qt::Key_Minus: { + if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { + auto& cursorManager = Cursor::Manager::instance(); + cursorManager.setScale(cursorManager.getScale() / 1.1f); + } else { + getMyAvatar()->decreaseSize(); + } + break; + } + + case Qt::Key_Equal: + getMyAvatar()->resetSize(); + break; + case Qt::Key_Escape: { + getActiveDisplayPlugin()->abandonCalibration(); + break; + } + + default: + event->ignore(); + break; + } + } +} + +void Application::keyReleaseEvent(QKeyEvent* event) { + if (!event->isAutoRepeat()) { + _keysPressed.remove(event->key()); + } + +#if defined(Q_OS_ANDROID) + if (event->key() == Qt::Key_Back) { + event->accept(); + AndroidHelper::instance().requestActivity("Home", false); + } +#endif + _controllerScriptingInterface->emitKeyReleaseEvent(event); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isKeyCaptured(event)) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->keyReleaseEvent(event); + } +} + +void Application::focusOutEvent(QFocusEvent* event) { + auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); + foreach(auto inputPlugin, inputPlugins) { + if (inputPlugin->isActive()) { + inputPlugin->pluginFocusOutEvent(); + } + } + +// FIXME spacemouse code still needs cleanup +#if 0 + //SpacemouseDevice::getInstance().focusOutEvent(); + //SpacemouseManager::getInstance().getDevice()->focusOutEvent(); + SpacemouseManager::getInstance().ManagerFocusOutEvent(); +#endif + + synthesizeKeyReleasEvents(); +} + +void Application::synthesizeKeyReleasEvents() { + // synthesize events for keys currently pressed, since we may not get their release events + // Because our key event handlers may manipulate _keysPressed, lets swap the keys pressed into a local copy, + // clearing the existing list. + QHash keysPressed; + std::swap(keysPressed, _keysPressed); + for (auto& ev : keysPressed) { + QKeyEvent synthesizedEvent { QKeyEvent::KeyRelease, ev.key(), Qt::NoModifier, ev.text() }; + keyReleaseEvent(&synthesizedEvent); + } +} + +void Application::maybeToggleMenuVisible(QMouseEvent* event) const { +#ifndef Q_OS_MAC + // If in full screen, and our main windows menu bar is hidden, and we're close to the top of the QMainWindow + // then show the menubar. + if (_window->isFullScreen()) { + QMenuBar* menuBar = _window->menuBar(); + if (menuBar) { + static const int MENU_TOGGLE_AREA = 10; + if (!menuBar->isVisible()) { + if (event->pos().y() <= MENU_TOGGLE_AREA) { + menuBar->setVisible(true); + } + } else { + if (event->pos().y() > MENU_TOGGLE_AREA) { + menuBar->setVisible(false); + } + } + } + } +#endif +} + +void Application::mouseMoveEvent(QMouseEvent* event) { + PROFILE_RANGE(app_input_mouse, __FUNCTION__); + + maybeToggleMenuVisible(event); + + auto& compositor = getApplicationCompositor(); + // if this is a real mouse event, and we're in HMD mode, then we should use it to move the + // compositor reticle + // handleRealMouseMoveEvent() will return true, if we shouldn't process the event further + if (!compositor.fakeEventActive() && compositor.handleRealMouseMoveEvent()) { + return; // bail + } + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto eventPosition = compositor.getMouseEventPosition(event); + QPointF transformedPos = offscreenUi ? offscreenUi->mapToVirtualScreen(eventPosition) : QPointF(); +#else + QPointF transformedPos; +#endif + auto button = event->button(); + auto buttons = event->buttons(); + // Determine if the ReticleClick Action is 1 and if so, fake include the LeftMouseButton + if (_reticleClickPressed) { + if (button == Qt::NoButton) { + button = Qt::LeftButton; + } + buttons |= Qt::LeftButton; + } + + QMouseEvent mappedEvent(event->type(), + transformedPos, + event->screenPos(), button, + buttons, event->modifiers()); + + if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() || + getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_ENTITY_ID) { + getEntities()->mouseMoveEvent(&mappedEvent); + } + + _controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->mouseMoveEvent(event); + } +} + +void Application::mousePressEvent(QMouseEvent* event) { + // Inhibit the menu if the user is using alt-mouse dragging + _altPressed = false; + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + // If we get a mouse press event it means it wasn't consumed by the offscreen UI, + // hence, we should defocus all of the offscreen UI windows, in order to allow + // keyboard shortcuts not to be swallowed by them. In particular, WebEngineViews + // will consume all keyboard events. + offscreenUi->unfocusWindows(); + + auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); + QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); +#else + QPointF transformedPos; +#endif + + QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); + QUuid result = getEntities()->mousePressEvent(&mappedEvent); + setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID); + + _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + +#if defined(Q_OS_MAC) + // Fix for OSX right click dragging on window when coming from a native window + bool isFocussed = hasFocus(); + if (!isFocussed && event->button() == Qt::MouseButton::RightButton) { + setFocus(); + isFocussed = true; + } + + if (isFocussed) { +#else + if (hasFocus()) { +#endif + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->mousePressEvent(event); + } + } +} + +void Application::mouseDoublePressEvent(QMouseEvent* event) { +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); + QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); +#else + QPointF transformedPos; +#endif + QMouseEvent mappedEvent(event->type(), + transformedPos, + event->screenPos(), event->button(), + event->buttons(), event->modifiers()); + getEntities()->mouseDoublePressEvent(&mappedEvent); + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + + _controllerScriptingInterface->emitMouseDoublePressEvent(event); +} + +void Application::mouseReleaseEvent(QMouseEvent* event) { + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); + QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); +#else + QPointF transformedPos; +#endif + QMouseEvent mappedEvent(event->type(), + transformedPos, + event->screenPos(), event->button(), + event->buttons(), event->modifiers()); + + getEntities()->mouseReleaseEvent(&mappedEvent); + + _controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isMouseCaptured()) { + return; + } + + if (hasFocus()) { + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->mouseReleaseEvent(event); + } + } +} + +void Application::touchUpdateEvent(QTouchEvent* event) { + _altPressed = false; + + if (event->type() == QEvent::TouchUpdate) { + TouchEvent thisEvent(*event, _lastTouchEvent); + _controllerScriptingInterface->emitTouchUpdateEvent(thisEvent); // send events to any registered scripts + _lastTouchEvent = thisEvent; + } + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isTouchCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->touchUpdateEvent(event); + } + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchUpdateEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchUpdateEvent(event); + } +} + +void Application::touchBeginEvent(QTouchEvent* event) { + _altPressed = false; + TouchEvent thisEvent(*event); // on touch begin, we don't compare to last event + _controllerScriptingInterface->emitTouchBeginEvent(thisEvent); // send events to any registered scripts + + _lastTouchEvent = thisEvent; // and we reset our last event to this event before we call our update + touchUpdateEvent(event); + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isTouchCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->touchBeginEvent(event); + } + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchBeginEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchBeginEvent(event); + } + +} + +void Application::touchEndEvent(QTouchEvent* event) { + _altPressed = false; + TouchEvent thisEvent(*event, _lastTouchEvent); + _controllerScriptingInterface->emitTouchEndEvent(thisEvent); // send events to any registered scripts + _lastTouchEvent = thisEvent; + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isTouchCaptured()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->touchEndEvent(event); + } + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchEndEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchEndEvent(event); + } + // put any application specific touch behavior below here.. +} + +void Application::touchGestureEvent(QGestureEvent* event) { + if (_touchscreenDevice && _touchscreenDevice->isActive()) { + _touchscreenDevice->touchGestureEvent(event); + } + if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { + _touchscreenVirtualPadDevice->touchGestureEvent(event); + } +} + +void Application::wheelEvent(QWheelEvent* event) const { + _altPressed = false; + _controllerScriptingInterface->emitWheelEvent(event); // send events to any registered scripts + + // if one of our scripts have asked to capture this event, then stop processing it + if (_controllerScriptingInterface->isWheelCaptured() || getLoginDialogPoppedUp()) { + return; + } + + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->wheelEvent(event); + } +} + +void Application::dropEvent(QDropEvent *event) { + const QMimeData* mimeData = event->mimeData(); + for (auto& url : mimeData->urls()) { + QString urlString = url.toString(); + if (acceptURL(urlString, true)) { + event->acceptProposedAction(); + } + } +} + +void Application::dragEnterEvent(QDragEnterEvent* event) { + event->acceptProposedAction(); +} + +// This is currently not used, but could be invoked if the user wants to go to the place embedded in an +// Interface-taken snapshot. (It was developed for drag and drop, before we had asset-server loading or in-world browsers.) +bool Application::acceptSnapshot(const QString& urlString) { + QUrl url(urlString); + QString snapshotPath = url.toLocalFile(); + + SnapshotMetaData* snapshotData = DependencyManager::get()->parseSnapshotData(snapshotPath); + if (snapshotData) { + if (!snapshotData->getURL().toString().isEmpty()) { + DependencyManager::get()->handleLookupString(snapshotData->getURL().toString()); + } + } else { + OffscreenUi::asyncWarning("", "No location details were found in the file\n" + + snapshotPath + "\nTry dragging in an authentic Hifi snapshot."); + } + return true; +} + +#ifdef Q_OS_WIN +#include +#include +#include +#pragma comment(lib, "pdh.lib") +#pragma comment(lib, "ntdll.lib") + +extern "C" { + enum SYSTEM_INFORMATION_CLASS { + SystemBasicInformation = 0, + SystemProcessorPerformanceInformation = 8, + }; + + struct SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION { + LARGE_INTEGER IdleTime; + LARGE_INTEGER KernelTime; + LARGE_INTEGER UserTime; + LARGE_INTEGER DpcTime; + LARGE_INTEGER InterruptTime; + ULONG InterruptCount; + }; + + struct SYSTEM_BASIC_INFORMATION { + ULONG Reserved; + ULONG TimerResolution; + ULONG PageSize; + ULONG NumberOfPhysicalPages; + ULONG LowestPhysicalPageNumber; + ULONG HighestPhysicalPageNumber; + ULONG AllocationGranularity; + ULONG_PTR MinimumUserModeAddress; + ULONG_PTR MaximumUserModeAddress; + ULONG_PTR ActiveProcessorsAffinityMask; + CCHAR NumberOfProcessors; + }; + + NTSYSCALLAPI NTSTATUS NTAPI NtQuerySystemInformation( + _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass, + _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation, + _In_ ULONG SystemInformationLength, + _Out_opt_ PULONG ReturnLength + ); + +} +template +NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, T& t) { + return NtQuerySystemInformation(SystemInformationClass, &t, (ULONG)sizeof(T), nullptr); +} + +template +NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, std::vector& t) { + return NtQuerySystemInformation(SystemInformationClass, t.data(), (ULONG)(sizeof(T) * t.size()), nullptr); +} + + +template +void updateValueAndDelta(std::pair& pair, T newValue) { + auto& value = pair.first; + auto& delta = pair.second; + delta = (value != 0) ? newValue - value : 0; + value = newValue; +} + +struct MyCpuInfo { + using ValueAndDelta = std::pair; + std::string name; + ValueAndDelta kernel { 0, 0 }; + ValueAndDelta user { 0, 0 }; + ValueAndDelta idle { 0, 0 }; + float kernelUsage { 0.0f }; + float userUsage { 0.0f }; + + void update(const SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION& cpuInfo) { + updateValueAndDelta(kernel, cpuInfo.KernelTime.QuadPart); + updateValueAndDelta(user, cpuInfo.UserTime.QuadPart); + updateValueAndDelta(idle, cpuInfo.IdleTime.QuadPart); + auto totalTime = kernel.second + user.second + idle.second; + if (totalTime != 0) { + kernelUsage = (FLOAT)kernel.second / totalTime; + userUsage = (FLOAT)user.second / totalTime; + } else { + kernelUsage = userUsage = 0.0f; + } + } +}; + +void updateCpuInformation() { + static std::once_flag once; + static SYSTEM_BASIC_INFORMATION systemInfo {}; + static SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION cpuTotals; + static std::vector cpuInfos; + static std::vector myCpuInfos; + static MyCpuInfo myCpuTotals; + std::call_once(once, [&] { + NtQuerySystemInformation( SystemBasicInformation, systemInfo); + cpuInfos.resize(systemInfo.NumberOfProcessors); + myCpuInfos.resize(systemInfo.NumberOfProcessors); + for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { + myCpuInfos[i].name = "cpu." + std::to_string(i); + } + myCpuTotals.name = "cpu.total"; + }); + NtQuerySystemInformation(SystemProcessorPerformanceInformation, cpuInfos); + + // Zero the CPU totals. + memset(&cpuTotals, 0, sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); + for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { + auto& cpuInfo = cpuInfos[i]; + // KernelTime includes IdleTime. + cpuInfo.KernelTime.QuadPart -= cpuInfo.IdleTime.QuadPart; + + // Update totals + cpuTotals.IdleTime.QuadPart += cpuInfo.IdleTime.QuadPart; + cpuTotals.KernelTime.QuadPart += cpuInfo.KernelTime.QuadPart; + cpuTotals.UserTime.QuadPart += cpuInfo.UserTime.QuadPart; + + // Update friendly structure + auto& myCpuInfo = myCpuInfos[i]; + myCpuInfo.update(cpuInfo); + PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { + { "kernel", myCpuInfo.kernelUsage }, + { "user", myCpuInfo.userUsage } + }); + } + + myCpuTotals.update(cpuTotals); + PROFILE_COUNTER(app, myCpuTotals.name.c_str(), { + { "kernel", myCpuTotals.kernelUsage }, + { "user", myCpuTotals.userUsage } + }); +} + + +static ULARGE_INTEGER lastCPU, lastSysCPU, lastUserCPU; +static int numProcessors; +static HANDLE self; +static PDH_HQUERY cpuQuery; +static PDH_HCOUNTER cpuTotal; + +void initCpuUsage() { + SYSTEM_INFO sysInfo; + FILETIME ftime, fsys, fuser; + + GetSystemInfo(&sysInfo); + numProcessors = sysInfo.dwNumberOfProcessors; + + GetSystemTimeAsFileTime(&ftime); + memcpy(&lastCPU, &ftime, sizeof(FILETIME)); + + self = GetCurrentProcess(); + GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); + memcpy(&lastSysCPU, &fsys, sizeof(FILETIME)); + memcpy(&lastUserCPU, &fuser, sizeof(FILETIME)); + + PdhOpenQuery(NULL, NULL, &cpuQuery); + PdhAddCounter(cpuQuery, "\\Processor(_Total)\\% Processor Time", NULL, &cpuTotal); + PdhCollectQueryData(cpuQuery); +} + +void getCpuUsage(vec3& systemAndUser) { + FILETIME ftime, fsys, fuser; + ULARGE_INTEGER now, sys, user; + + GetSystemTimeAsFileTime(&ftime); + memcpy(&now, &ftime, sizeof(FILETIME)); + + GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); + memcpy(&sys, &fsys, sizeof(FILETIME)); + memcpy(&user, &fuser, sizeof(FILETIME)); + systemAndUser.x = (sys.QuadPart - lastSysCPU.QuadPart); + systemAndUser.y = (user.QuadPart - lastUserCPU.QuadPart); + systemAndUser /= (float)(now.QuadPart - lastCPU.QuadPart); + systemAndUser /= (float)numProcessors; + systemAndUser *= 100.0f; + lastCPU = now; + lastUserCPU = user; + lastSysCPU = sys; + + PDH_FMT_COUNTERVALUE counterVal; + PdhCollectQueryData(cpuQuery); + PdhGetFormattedCounterValue(cpuTotal, PDH_FMT_DOUBLE, NULL, &counterVal); + systemAndUser.z = (float)counterVal.doubleValue; +} + +void setupCpuMonitorThread() { + initCpuUsage(); + auto cpuMonitorThread = QThread::currentThread(); + + QTimer* timer = new QTimer(); + timer->setInterval(50); + QObject::connect(timer, &QTimer::timeout, [] { + updateCpuInformation(); + vec3 kernelUserAndSystem; + getCpuUsage(kernelUserAndSystem); + PROFILE_COUNTER(app, "cpuProcess", { { "system", kernelUserAndSystem.x }, { "user", kernelUserAndSystem.y } }); + PROFILE_COUNTER(app, "cpuSystem", { { "system", kernelUserAndSystem.z } }); + }); + QObject::connect(cpuMonitorThread, &QThread::finished, [=] { + timer->deleteLater(); + cpuMonitorThread->deleteLater(); + }); + timer->start(); +} + +#endif + +void Application::idle() { + PerformanceTimer perfTimer("idle"); + + // Update the deadlock watchdog + updateHeartbeat(); + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + + // These tasks need to be done on our first idle, because we don't want the showing of + // overlay subwindows to do a showDesktop() until after the first time through + static bool firstIdle = true; + if (firstIdle) { + firstIdle = false; + connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); + } +#endif + +#ifdef Q_OS_WIN + // If tracing is enabled then monitor the CPU in a separate thread + static std::once_flag once; + std::call_once(once, [&] { + if (trace_app().isDebugEnabled()) { + QThread* cpuMonitorThread = new QThread(qApp); + cpuMonitorThread->setObjectName("cpuMonitorThread"); + QObject::connect(cpuMonitorThread, &QThread::started, [this] { setupCpuMonitorThread(); }); + QObject::connect(qApp, &QCoreApplication::aboutToQuit, cpuMonitorThread, &QThread::quit); + cpuMonitorThread->start(); + } + }); +#endif + + auto displayPlugin = getActiveDisplayPlugin(); +#if !defined(DISABLE_QML) + if (displayPlugin) { + auto uiSize = displayPlugin->getRecommendedUiSize(); + // Bit of a hack since there's no device pixel ratio change event I can find. + if (offscreenUi->size() != fromGlm(uiSize)) { + qCDebug(interfaceapp) << "Device pixel ratio changed, triggering resize to " << uiSize; + offscreenUi->resize(fromGlm(uiSize)); + } + } +#endif + + if (displayPlugin) { + PROFILE_COUNTER_IF_CHANGED(app, "present", float, displayPlugin->presentRate()); + } + PROFILE_COUNTER_IF_CHANGED(app, "renderLoopRate", float, getRenderLoopRate()); + PROFILE_COUNTER_IF_CHANGED(app, "currentDownloads", uint32_t, ResourceCache::getLoadingRequests().length()); + PROFILE_COUNTER_IF_CHANGED(app, "pendingDownloads", uint32_t, ResourceCache::getPendingRequestCount()); + PROFILE_COUNTER_IF_CHANGED(app, "currentProcessing", int, DependencyManager::get()->getStat("Processing").toInt()); + PROFILE_COUNTER_IF_CHANGED(app, "pendingProcessing", int, DependencyManager::get()->getStat("PendingProcessing").toInt()); + auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); + PROFILE_COUNTER_IF_CHANGED(render, "gpuTime", float, (float)_graphicsEngine.getGPUContext()->getFrameTimerGPUAverage()); + + PROFILE_RANGE(app, __FUNCTION__); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + steamClient->runCallbacks(); + } + + if (auto oculusPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { + oculusPlugin->handleOVREvents(); + } + + float secondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_MSEC / MSECS_PER_SECOND; + _lastTimeUpdated.start(); + +#if !defined(DISABLE_QML) + // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. + if (offscreenUi && offscreenUi->getWindow()) { + auto activeFocusItem = offscreenUi->getWindow()->activeFocusItem(); + if (_keyboardDeviceHasFocus && activeFocusItem != offscreenUi->getRootItem()) { + _keyboardMouseDevice->pluginFocusOutEvent(); + _keyboardDeviceHasFocus = false; + synthesizeKeyReleasEvents(); + } else if (activeFocusItem == offscreenUi->getRootItem()) { + _keyboardDeviceHasFocus = true; + } + } +#endif + + checkChangeCursor(); + +#if !defined(DISABLE_QML) + auto stats = Stats::getInstance(); + if (stats) { + stats->updateStats(); + } + auto animStats = AnimStats::getInstance(); + if (animStats) { + animStats->updateStats(); + } +#endif + + // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing + // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing + // details normally. +#ifdef Q_OS_ANDROID + bool showWarnings = false; +#else + bool showWarnings = getLogger()->extraDebugging(); +#endif + PerformanceWarning warn(showWarnings, "idle()"); + + { + _gameWorkload.updateViews(_viewFrustum, getMyAvatar()->getHeadPosition()); + _gameWorkload._engine->run(); + } + { + PerformanceTimer perfTimer("update"); + PerformanceWarning warn(showWarnings, "Application::idle()... update()"); + static const float BIGGEST_DELTA_TIME_SECS = 0.25f; + update(glm::clamp(secondsSinceLastUpdate, 0.0f, BIGGEST_DELTA_TIME_SECS)); + } + + { // Update keyboard focus highlight + if (!_keyboardFocusedEntity.get().isInvalidID()) { + const quint64 LOSE_FOCUS_AFTER_ELAPSED_TIME = 30 * USECS_PER_SECOND; // if idle for 30 seconds, drop focus + quint64 elapsedSinceAcceptedKeyPress = usecTimestampNow() - _lastAcceptedKeyPress; + if (elapsedSinceAcceptedKeyPress > LOSE_FOCUS_AFTER_ELAPSED_TIME) { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + } else { + if (auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedEntity.get())) { + EntityItemProperties properties; + properties.setPosition(entity->getWorldPosition()); + properties.setRotation(entity->getWorldOrientation()); + properties.setDimensions(entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); + DependencyManager::get()->editEntity(_keyboardFocusHighlightID, properties); + } + } + } + } + + { + if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_keyboardFocusedEntity.get())) { + QUuid entityId = _keyboardFocusedEntity.get(); + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); + _keyboardFocusWaitingOnRenderable = false; + setKeyboardFocusEntity(entityId); + } + } + + { + PerformanceTimer perfTimer("pluginIdle"); + PerformanceWarning warn(showWarnings, "Application::idle()... pluginIdle()"); + getActiveDisplayPlugin()->idle(); + auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); + foreach(auto inputPlugin, inputPlugins) { + if (inputPlugin->isActive()) { + inputPlugin->idle(); + } + } + } + { + PerformanceTimer perfTimer("rest"); + PerformanceWarning warn(showWarnings, "Application::idle()... rest of it"); + _idleLoopStdev.addValue(secondsSinceLastUpdate); + + // Record standard deviation and reset counter if needed + const int STDEV_SAMPLES = 500; + if (_idleLoopStdev.getSamples() > STDEV_SAMPLES) { + _idleLoopMeasuredJitter = _idleLoopStdev.getStDev(); + _idleLoopStdev.reset(); + } + } + + _overlayConductor.update(secondsSinceLastUpdate); + + _gameLoopCounter.increment(); +} + +ivec2 Application::getMouse() const { + return getApplicationCompositor().getReticlePosition(); +} + +FaceTracker* Application::getActiveFaceTracker() { + auto dde = DependencyManager::get(); + + return dde->isActive() ? static_cast(dde.data()) : nullptr; +} + +FaceTracker* Application::getSelectedFaceTracker() { + FaceTracker* faceTracker = nullptr; +#ifdef HAVE_DDE + if (Menu::getInstance()->isOptionChecked(MenuOption::UseCamera)) { + faceTracker = DependencyManager::get().data(); + } +#endif + return faceTracker; +} + +void Application::setActiveFaceTracker() const { +#ifdef HAVE_DDE + bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); + bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera); + Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::CoupleEyelids)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::UseAudioForMouth)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::VelocityFilter)->setVisible(isUsingDDE); + Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setVisible(isUsingDDE); + auto ddeTracker = DependencyManager::get(); + ddeTracker->setIsMuted(isMuted); + ddeTracker->setEnabled(isUsingDDE && !isMuted); +#endif +} + +#ifdef HAVE_IVIEWHMD +void Application::setActiveEyeTracker() { + auto eyeTracker = DependencyManager::get(); + if (!eyeTracker->isInitialized()) { + return; + } + + bool isEyeTracking = Menu::getInstance()->isOptionChecked(MenuOption::SMIEyeTracking); + bool isSimulating = Menu::getInstance()->isOptionChecked(MenuOption::SimulateEyeTracking); + eyeTracker->setEnabled(isEyeTracking, isSimulating); + + Menu::getInstance()->getActionForOption(MenuOption::OnePointCalibration)->setEnabled(isEyeTracking && !isSimulating); + Menu::getInstance()->getActionForOption(MenuOption::ThreePointCalibration)->setEnabled(isEyeTracking && !isSimulating); + Menu::getInstance()->getActionForOption(MenuOption::FivePointCalibration)->setEnabled(isEyeTracking && !isSimulating); +} + +void Application::calibrateEyeTracker1Point() { + DependencyManager::get()->calibrate(1); +} + +void Application::calibrateEyeTracker3Points() { + DependencyManager::get()->calibrate(3); +} + +void Application::calibrateEyeTracker5Points() { + DependencyManager::get()->calibrate(5); +} +#endif + +bool Application::exportEntities(const QString& filename, + const QVector& entityIDs, + const glm::vec3* givenOffset) { + QHash entities; + + auto nodeList = DependencyManager::get(); + const QUuid myAvatarID = nodeList->getSessionUUID(); + + auto entityTree = getEntities()->getTree(); + auto exportTree = std::make_shared(); + exportTree->setMyAvatar(getMyAvatar()); + exportTree->createRootElement(); + glm::vec3 root(TREE_SCALE, TREE_SCALE, TREE_SCALE); + bool success = true; + entityTree->withReadLock([entityIDs, entityTree, givenOffset, myAvatarID, &root, &entities, &success, &exportTree] { + for (auto entityID : entityIDs) { // Gather entities and properties. + auto entityItem = entityTree->findEntityByEntityItemID(entityID); + if (!entityItem) { + qCWarning(interfaceapp) << "Skipping export of" << entityID << "that is not in scene."; + continue; + } + + if (!givenOffset) { + EntityItemID parentID = entityItem->getParentID(); + bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); + if (!parentIsAvatar && (parentID.isInvalidID() || + !entityIDs.contains(parentID) || + !entityTree->findEntityByEntityItemID(parentID))) { + // If parent wasn't selected, we want absolute position, which isn't in properties. + auto position = entityItem->getWorldPosition(); + root.x = glm::min(root.x, position.x); + root.y = glm::min(root.y, position.y); + root.z = glm::min(root.z, position.z); + } + } + entities[entityID] = entityItem; + } + + if (entities.size() == 0) { + success = false; + return; + } + + if (givenOffset) { + root = *givenOffset; + } + for (EntityItemPointer& entityDatum : entities) { + auto properties = entityDatum->getProperties(); + EntityItemID parentID = properties.getParentID(); + bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); + if (parentIsAvatar) { + properties.setParentID(AVATAR_SELF_ID); + } else { + if (parentID.isInvalidID()) { + properties.setPosition(properties.getPosition() - root); + } else if (!entities.contains(parentID)) { + entityDatum->globalizeProperties(properties, "Parent %3 of %2 %1 is not selected for export.", -root); + } // else valid parent -- don't offset + } + exportTree->addEntity(entityDatum->getEntityItemID(), properties); + } + }); + if (success) { + success = exportTree->writeToJSONFile(filename.toLocal8Bit().constData()); + + // restore the main window's active state + _window->activateWindow(); + } + return success; +} + +bool Application::exportEntities(const QString& filename, float x, float y, float z, float scale) { + glm::vec3 center(x, y, z); + glm::vec3 minCorner = center - vec3(scale); + float cubeSize = scale * 2; + AACube boundingCube(minCorner, cubeSize); + QVector entities; + auto entityTree = getEntities()->getTree(); + entityTree->withReadLock([&] { + entityTree->evalEntitiesInCube(boundingCube, PickFilter(), entities); + }); + return exportEntities(filename, entities, ¢er); +} + +void Application::loadSettings() { + + sessionRunTime.set(0); // Just clean living. We're about to saveSettings, which will update value. + DependencyManager::get()->loadSettings(); + DependencyManager::get()->loadSettings(); + + // DONT CHECK IN + //DependencyManager::get()->setAutomaticLODAdjust(false); + + auto menu = Menu::getInstance(); + menu->loadSettings(); + + // override the menu option show overlays to always be true on startup + menu->setIsOptionChecked(MenuOption::Overlays, true); + + // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. + auto pluginManager = PluginManager::getInstance(); + auto plugins = pluginManager->getPreferredDisplayPlugins(); + if (plugins.size() > 0) { + for (auto plugin : plugins) { + if (auto action = menu->getActionForOption(plugin->getName())) { + action->setChecked(true); + action->trigger(); + // Find and activated highest priority plugin, bail for the rest + break; + } + } + } + + bool isFirstPerson = false; + if (_firstRun.get()) { + // If this is our first run, and no preferred devices were set, default to + // an HMD device if available. + auto displayPlugins = pluginManager->getDisplayPlugins(); + for (auto& plugin : displayPlugins) { + if (plugin->isHmd()) { + if (auto action = menu->getActionForOption(plugin->getName())) { + action->setChecked(true); + action->trigger(); + break; + } + } + } + + isFirstPerson = (qApp->isHMDMode()); + + } else { + // if this is not the first run, the camera will be initialized differently depending on user settings + + if (qApp->isHMDMode()) { + // if the HMD is active, use first-person camera, unless the appropriate setting is checked + isFirstPerson = menu->isOptionChecked(MenuOption::FirstPersonHMD); + } else { + // if HMD is not active, only use first person if the menu option is checked + isFirstPerson = menu->isOptionChecked(MenuOption::FirstPerson); + } + } + + // finish initializing the camera, based on everything we checked above. Third person camera will be used if no settings + // dictated that we should be in first person + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, isFirstPerson); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !isFirstPerson); + _myCamera.setMode((isFirstPerson) ? CAMERA_MODE_FIRST_PERSON : CAMERA_MODE_THIRD_PERSON); + cameraMenuChanged(); + + auto inputs = pluginManager->getInputPlugins(); + for (auto plugin : inputs) { + if (!plugin->isActive()) { + plugin->activate(); + } + } + + getMyAvatar()->loadData(); + _settingsLoaded = true; +} + +void Application::saveSettings() const { + sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SECOND); + DependencyManager::get()->saveSettings(); + DependencyManager::get()->saveSettings(); + + Menu::getInstance()->saveSettings(); + getMyAvatar()->saveData(); + PluginManager::getInstance()->saveSettings(); +} + +bool Application::importEntities(const QString& urlOrFilename, const bool isObservable, const qint64 callerId) { + bool success = false; + _entityClipboard->withWriteLock([&] { + _entityClipboard->eraseAllOctreeElements(); + + success = _entityClipboard->readFromURL(urlOrFilename, isObservable, callerId); + if (success) { + _entityClipboard->reaverageOctreeElements(); + } + }); + return success; +} + +QVector Application::pasteEntities(float x, float y, float z) { + return _entityClipboard->sendEntities(&_entityEditSender, getEntities()->getTree(), x, y, z); +} + +void Application::init() { + // Make sure Login state is up to date +#if !defined(DISABLE_QML) + DependencyManager::get()->toggleLoginDialog(); +#endif + DependencyManager::get()->init(); + + _timerStart.start(); + _lastTimeUpdated.start(); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + // when +connect_lobby in command line, join steam lobby + const QString STEAM_LOBBY_COMMAND_LINE_KEY = "+connect_lobby"; + int lobbyIndex = arguments().indexOf(STEAM_LOBBY_COMMAND_LINE_KEY); + if (lobbyIndex != -1) { + QString lobbyId = arguments().value(lobbyIndex + 1); + steamClient->joinLobby(lobbyId); + } + } + + + qCDebug(interfaceapp) << "Loaded settings"; + + // fire off an immediate domain-server check in now that settings are loaded + if (!isServerlessMode()) { + DependencyManager::get()->sendDomainServerCheckIn(); + } + + // This allows collision to be set up properly for shape entities supported by GeometryCache. + // This is before entity setup to ensure that it's ready for whenever instance collision is initialized. + ShapeEntityItem::setShapeInfoCalulator(ShapeEntityItem::ShapeInfoCalculator(&shapeInfoCalculator)); + + getEntities()->init(); + getEntities()->setEntityLoadingPriorityFunction([this](const EntityItem& item) { + auto dims = item.getScaledDimensions(); + auto maxSize = glm::compMax(dims); + + if (maxSize <= 0.0f) { + return 0.0f; + } + + auto distance = glm::distance(getMyAvatar()->getWorldPosition(), item.getWorldPosition()); + return atan2(maxSize, distance); + }); + + ObjectMotionState::setShapeManager(&_shapeManager); + _physicsEngine->init(); + + EntityTreePointer tree = getEntities()->getTree(); + _entitySimulation->init(tree, _physicsEngine, &_entityEditSender); + tree->setSimulation(_entitySimulation); + + auto entityScriptingInterface = DependencyManager::get(); + + // connect the _entityCollisionSystem to our EntityTreeRenderer since that's what handles running entity scripts + connect(_entitySimulation.get(), &PhysicalEntitySimulation::entityCollisionWithEntity, + getEntities().data(), &EntityTreeRenderer::entityCollisionWithEntity); + + // connect the _entities (EntityTreeRenderer) to our script engine's EntityScriptingInterface for firing + // of events related clicking, hovering over, and entering entities + getEntities()->connectSignalsToSlots(entityScriptingInterface.data()); + + // Make sure any new sounds are loaded as soon as know about them. + connect(tree.get(), &EntityTree::newCollisionSoundURL, this, [this](QUrl newURL, EntityItemID id) { + getEntities()->setCollisionSound(id, DependencyManager::get()->getSound(newURL)); + }, Qt::QueuedConnection); + connect(getMyAvatar().get(), &MyAvatar::newCollisionSoundURL, this, [this](QUrl newURL) { + if (auto avatar = getMyAvatar()) { + auto sound = DependencyManager::get()->getSound(newURL); + avatar->setCollisionSound(sound); + } + }, Qt::QueuedConnection); + + _gameWorkload.startup(getEntities()->getWorkloadSpace(), _graphicsEngine.getRenderScene(), _entitySimulation); + _entitySimulation->setWorkloadSpace(getEntities()->getWorkloadSpace()); +} + +void Application::pauseUntilLoginDetermined() { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "pauseUntilLoginDetermined"); + return; + } + + auto myAvatar = getMyAvatar(); + _previousAvatarTargetScale = myAvatar->getTargetScale(); + _previousAvatarSkeletonModel = myAvatar->getSkeletonModelURL().toString(); + myAvatar->setTargetScale(1.0f); + myAvatar->setSkeletonModelURLFromScript(myAvatar->defaultFullAvatarModelUrl().toString()); + myAvatar->setEnableMeshVisible(false); + + _controllerScriptingInterface->disableMapping(STANDARD_TO_ACTION_MAPPING_NAME); + + { + auto userInputMapper = DependencyManager::get(); + if (userInputMapper->loadMapping(NO_MOVEMENT_MAPPING_JSON)) { + _controllerScriptingInterface->enableMapping(NO_MOVEMENT_MAPPING_NAME); + } + } + + const auto& nodeList = DependencyManager::get(); + // save interstitial mode setting until resuming. + _interstitialModeEnabled = nodeList->getDomainHandler().getInterstitialModeEnabled(); + nodeList->getDomainHandler().setInterstitialModeEnabled(false); + + auto menu = Menu::getInstance(); + menu->getMenu("Edit")->setVisible(false); + menu->getMenu("View")->setVisible(false); + menu->getMenu("Navigate")->setVisible(false); + menu->getMenu("Settings")->setVisible(false); + _developerMenuVisible = menu->getMenu("Developer")->isVisible(); + menu->setIsOptionChecked(MenuOption::Stats, false); + if (_developerMenuVisible) { + menu->getMenu("Developer")->setVisible(false); + } + _previousCameraMode = _myCamera.getMode(); + _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); + cameraModeChanged(); + + // disconnect domain handler. + nodeList->getDomainHandler().disconnect(); + +} + +void Application::resumeAfterLoginDialogActionTaken() { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "resumeAfterLoginDialogActionTaken"); + return; + } + + if (!isHMDMode() && getDesktopTabletBecomesToolbarSetting()) { + auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); + toolbar->writeProperty("visible", true); + } else { + getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(true); + getApplicationCompositor().getReticleInterface()->setVisible(true); + } + + updateSystemTabletMode(); + + { + auto userInputMapper = DependencyManager::get(); + userInputMapper->unloadMapping(NO_MOVEMENT_MAPPING_JSON); + _controllerScriptingInterface->disableMapping(NO_MOVEMENT_MAPPING_NAME); + } + + auto myAvatar = getMyAvatar(); + myAvatar->setTargetScale(_previousAvatarTargetScale); + myAvatar->setSkeletonModelURLFromScript(_previousAvatarSkeletonModel); + myAvatar->setEnableMeshVisible(true); + + _controllerScriptingInterface->enableMapping(STANDARD_TO_ACTION_MAPPING_NAME); + + const auto& nodeList = DependencyManager::get(); + nodeList->getDomainHandler().setInterstitialModeEnabled(_interstitialModeEnabled); + { + auto scriptEngines = DependencyManager::get().data(); + // this will force the model the look at the correct directory (weird order of operations issue) + scriptEngines->reloadLocalFiles(); + + // if the --scripts command-line argument was used. + if (!_defaultScriptsLocation.exists() && (arguments().indexOf(QString("--").append(SCRIPTS_SWITCH))) != -1) { + scriptEngines->loadDefaultScripts(); + scriptEngines->defaultScriptsLocationOverridden(true); + } else { + scriptEngines->loadScripts(); + } + } + + auto accountManager = DependencyManager::get(); + auto addressManager = DependencyManager::get(); + + // restart domain handler. + nodeList->getDomainHandler().resetting(); + + QVariant testProperty = property(hifi::properties::TEST); + if (testProperty.isValid()) { + const auto testScript = property(hifi::properties::TEST).toUrl(); + // Set last parameter to exit interface when the test script finishes, if so requested + DependencyManager::get()->loadScript(testScript, false, false, false, false, quitWhenFinished); + // This is done so we don't get a "connection time-out" message when we haven't passed in a URL. + if (arguments().contains("--url")) { + auto reply = SandboxUtils::getStatus(); + connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); + } + } else { + auto reply = SandboxUtils::getStatus(); + connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); + } + + auto menu = Menu::getInstance(); + menu->getMenu("Edit")->setVisible(true); + menu->getMenu("View")->setVisible(true); + menu->getMenu("Navigate")->setVisible(true); + menu->getMenu("Settings")->setVisible(true); + menu->getMenu("Developer")->setVisible(_developerMenuVisible); + _myCamera.setMode(_previousCameraMode); + cameraModeChanged(); +} + +void Application::loadAvatarScripts(const QVector& urls) { + auto scriptEngines = DependencyManager::get(); + auto runningScripts = scriptEngines->getRunningScripts(); + for (auto url : urls) { + int index = runningScripts.indexOf(url); + if (index < 0) { + auto scriptEnginePointer = scriptEngines->loadScript(url, false); + if (scriptEnginePointer) { + scriptEnginePointer->setType(ScriptEngine::Type::AVATAR); + } + } + } +} + +void Application::unloadAvatarScripts() { + auto scriptEngines = DependencyManager::get(); + auto urls = scriptEngines->getRunningScripts(); + for (auto url : urls) { + auto scriptEngine = scriptEngines->getScriptEngine(url); + if (scriptEngine->getType() == ScriptEngine::Type::AVATAR) { + scriptEngines->stopScript(url, false); + } + } +} + +void Application::updateLOD(float deltaTime) const { + PerformanceTimer perfTimer("LOD"); + // adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode + if (!isThrottleRendering()) { + float presentTime = getActiveDisplayPlugin()->getAveragePresentTime(); + float engineRunTime = (float)(_graphicsEngine.getRenderEngine()->getConfiguration().get()->getCPURunTime()); + float gpuTime = getGPUContext()->getFrameTimerGPUAverage(); + float batchTime = getGPUContext()->getFrameTimerBatchAverage(); + auto lodManager = DependencyManager::get(); + lodManager->setRenderTimes(presentTime, engineRunTime, batchTime, gpuTime); + lodManager->autoAdjustLOD(deltaTime); + } else { + DependencyManager::get()->resetLODAdjust(); + } +} + +void Application::pushPostUpdateLambda(void* key, const std::function& func) { + std::unique_lock guard(_postUpdateLambdasLock); + _postUpdateLambdas[key] = func; +} + +// Called during Application::update immediately before AvatarManager::updateMyAvatar, updating my data that is then sent to everyone. +// (Maybe this code should be moved there?) +// The principal result is to call updateLookAtTargetAvatar() and then setLookAtPosition(). +// Note that it is called BEFORE we update position or joints based on sensors, etc. +void Application::updateMyAvatarLookAtPosition() { + PerformanceTimer perfTimer("lookAt"); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()"); + + auto myAvatar = getMyAvatar(); + myAvatar->updateLookAtTargetAvatar(); + FaceTracker* faceTracker = getActiveFaceTracker(); + auto eyeTracker = DependencyManager::get(); + + bool isLookingAtSomeone = false; + bool isHMD = qApp->isHMDMode(); + glm::vec3 lookAtSpot; + if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { + // Look at the point that the user is looking at. + glm::vec3 lookAtPosition = eyeTracker->getLookAtPosition(); + if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { + lookAtPosition.x = -lookAtPosition.x; + } + if (isHMD) { + // TODO -- this code is probably wrong, getHeadPose() returns something in sensor frame, not avatar + glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); + glm::quat hmdRotation = glm::quat_cast(headPose); + lookAtSpot = _myCamera.getPosition() + myAvatar->getWorldOrientation() * (hmdRotation * lookAtPosition); + } else { + lookAtSpot = myAvatar->getHead()->getEyePosition() + + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * lookAtPosition); + } + } else { + AvatarSharedPointer lookingAt = myAvatar->getLookAtTargetAvatar().lock(); + bool haveLookAtCandidate = lookingAt && myAvatar.get() != lookingAt.get(); + auto avatar = static_pointer_cast(lookingAt); + bool mutualLookAtSnappingEnabled = avatar && avatar->getLookAtSnappingEnabled() && myAvatar->getLookAtSnappingEnabled(); + if (haveLookAtCandidate && mutualLookAtSnappingEnabled) { + // If I am looking at someone else, look directly at one of their eyes + isLookingAtSomeone = true; + auto lookingAtHead = avatar->getHead(); + + const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; + glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; + glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() + - lookingAtHead->getEyePosition()); + float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); + + if (faceAngle < MAXIMUM_FACE_ANGLE) { + // Randomly look back and forth between look targets + eyeContactTarget target = Menu::getInstance()->isOptionChecked(MenuOption::FixGaze) ? + LEFT_EYE : myAvatar->getEyeContactTarget(); + switch (target) { + case LEFT_EYE: + lookAtSpot = lookingAtHead->getLeftEyePosition(); + break; + case RIGHT_EYE: + lookAtSpot = lookingAtHead->getRightEyePosition(); + break; + case MOUTH: + lookAtSpot = lookingAtHead->getMouthPosition(); + break; + } + } else { + // Just look at their head (mid point between eyes) + lookAtSpot = lookingAtHead->getEyePosition(); + } + } else { + // I am not looking at anyone else, so just look forward + auto headPose = myAvatar->getControllerPoseInWorldFrame(controller::Action::HEAD); + if (headPose.isValid()) { + lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE)); + } else { + lookAtSpot = myAvatar->getHead()->getEyePosition() + + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); + } + } + + // Deflect the eyes a bit to match the detected gaze from the face tracker if active. + if (faceTracker && !faceTracker->isMuted()) { + float eyePitch = faceTracker->getEstimatedEyePitch(); + float eyeYaw = faceTracker->getEstimatedEyeYaw(); + const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f; + glm::vec3 origin = myAvatar->getHead()->getEyePosition(); + float deflection = faceTracker->getEyeDeflection(); + if (isLookingAtSomeone) { + deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; + } + lookAtSpot = origin + _myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( + eyePitch * deflection, eyeYaw * deflection, 0.0f))) * + glm::inverse(_myCamera.getOrientation()) * (lookAtSpot - origin); + } + } + + myAvatar->getHead()->setLookAtPosition(lookAtSpot); +} + +void Application::updateThreads(float deltaTime) { + PerformanceTimer perfTimer("updateThreads"); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::updateThreads()"); + + // parse voxel packets + if (!_enableProcessOctreeThread) { + _octreeProcessor.threadRoutine(); + _entityEditSender.threadRoutine(); + } +} + +void Application::toggleOverlays() { + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, !menu->isOptionChecked(MenuOption::Overlays)); +} + +void Application::setOverlaysVisible(bool visible) { + auto menu = Menu::getInstance(); + menu->setIsOptionChecked(MenuOption::Overlays, visible); +} + +void Application::centerUI() { + _overlayConductor.centerUI(); +} + +void Application::cycleCamera() { + auto menu = Menu::getInstance(); + if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { + + menu->setIsOptionChecked(MenuOption::FullscreenMirror, false); + menu->setIsOptionChecked(MenuOption::FirstPerson, true); + + } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { + + menu->setIsOptionChecked(MenuOption::FirstPerson, false); + menu->setIsOptionChecked(MenuOption::ThirdPerson, true); + + } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { + + menu->setIsOptionChecked(MenuOption::ThirdPerson, false); + menu->setIsOptionChecked(MenuOption::FullscreenMirror, true); + + } else if (menu->isOptionChecked(MenuOption::IndependentMode) || menu->isOptionChecked(MenuOption::CameraEntityMode)) { + // do nothing if in independent or camera entity modes + return; + } + cameraMenuChanged(); // handle the menu change +} + +void Application::cameraModeChanged() { + switch (_myCamera.getMode()) { + case CAMERA_MODE_FIRST_PERSON: + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); + break; + case CAMERA_MODE_THIRD_PERSON: + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + break; + case CAMERA_MODE_MIRROR: + Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, true); + break; + case CAMERA_MODE_INDEPENDENT: + Menu::getInstance()->setIsOptionChecked(MenuOption::IndependentMode, true); + break; + case CAMERA_MODE_ENTITY: + Menu::getInstance()->setIsOptionChecked(MenuOption::CameraEntityMode, true); + break; + default: + break; + } + cameraMenuChanged(); +} + +void Application::changeViewAsNeeded(float boomLength) { + // Switch between first and third person views as needed + // This is called when the boom length has changed + bool boomLengthGreaterThanMinimum = (boomLength > MyAvatar::ZOOM_MIN); + + if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON && boomLengthGreaterThanMinimum) { + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, false); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); + cameraMenuChanged(); + } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON && !boomLengthGreaterThanMinimum) { + Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); + Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, false); + cameraMenuChanged(); + } +} + +void Application::cameraMenuChanged() { + auto menu = Menu::getInstance(); + if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { + if (!isHMDMode() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { + _mirrorYawOffset = 0.0f; + _myCamera.setMode(CAMERA_MODE_MIRROR); + getMyAvatar()->reset(false, false, false); // to reset any active MyAvatar::FollowHelpers + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); + } + } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { + if (_myCamera.getMode() != CAMERA_MODE_FIRST_PERSON) { + _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); + } + } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { + if (_myCamera.getMode() != CAMERA_MODE_THIRD_PERSON) { + _myCamera.setMode(CAMERA_MODE_THIRD_PERSON); + if (getMyAvatar()->getBoomLength() == MyAvatar::ZOOM_MIN) { + getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); + } + } + } else if (menu->isOptionChecked(MenuOption::IndependentMode)) { + if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { + _myCamera.setMode(CAMERA_MODE_INDEPENDENT); + } + } else if (menu->isOptionChecked(MenuOption::CameraEntityMode)) { + if (_myCamera.getMode() != CAMERA_MODE_ENTITY) { + _myCamera.setMode(CAMERA_MODE_ENTITY); + } + } +} + +void Application::resetPhysicsReadyInformation() { + // we've changed domains or cleared out caches or something. we no longer know enough about the + // collision information of nearby entities to make running bullet be safe. + _fullSceneReceivedCounter = 0; + _fullSceneCounterAtLastPhysicsCheck = 0; + _gpuTextureMemSizeStabilityCount = 0; + _gpuTextureMemSizeAtLastCheck = 0; + _physicsEnabled = false; + _octreeProcessor.startEntitySequence(); +} + + +void Application::reloadResourceCaches() { + resetPhysicsReadyInformation(); + + // Query the octree to refresh everything in view + _queryExpiry = SteadyClock::now(); + _octreeQuery.incrementConnectionID(); + + queryOctree(NodeType::EntityServer, PacketType::EntityQuery); + + // Clear the entities and their renderables + getEntities()->clear(); + + DependencyManager::get()->clearCache(); + DependencyManager::get()->clearCache(); + + // Clear all the resource caches + DependencyManager::get()->clear(); + DependencyManager::get()->refreshAll(); + DependencyManager::get()->refreshAll(); + MaterialCache::instance().refreshAll(); + DependencyManager::get()->refreshAll(); + ShaderCache::instance().refreshAll(); + DependencyManager::get()->refreshAll(); + DependencyManager::get()->refreshAll(); + + DependencyManager::get()->reset(); // Force redownload of .fst models + + DependencyManager::get()->reloadAllScripts(); + getOffscreenUI()->clearCache(); + + DependencyManager::get()->createKeyboard(); + + getMyAvatar()->resetFullAvatarURL(); +} + +void Application::rotationModeChanged() const { + if (!Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { + getMyAvatar()->setHeadPitch(0); + } +} + +void Application::setKeyboardFocusHighlight(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions) { + if (qApp->getLoginDialogPoppedUp()) { + return; + } + + auto entityScriptingInterface = DependencyManager::get(); + if (_keyboardFocusHighlightID == UNKNOWN_ENTITY_ID || !entityScriptingInterface->isAddedEntity(_keyboardFocusHighlightID)) { + EntityItemProperties properties; + properties.setType(EntityTypes::Box); + properties.setAlpha(1.0f); + properties.setColor({ 0xFF, 0xEF, 0x00 }); + properties.setPrimitiveMode(PrimitiveMode::LINES); + properties.getPulse().setMin(0.5); + properties.getPulse().setMax(1.0f); + properties.getPulse().setColorMode(PulseMode::IN_PHASE); + properties.setIgnorePickIntersection(true); + _keyboardFocusHighlightID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); + } + + // Position focus + EntityItemProperties properties; + properties.setPosition(position); + properties.setRotation(rotation); + properties.setDimensions(dimensions); + properties.setVisible(true); + entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); +} + +QUuid Application::getKeyboardFocusEntity() const { + return _keyboardFocusedEntity.get(); +} + +void Application::setKeyboardFocusEntity(const QUuid& id) { + if (_keyboardFocusedEntity.get() != id) { + if (qApp->getLoginDialogPoppedUp() && !_loginDialogID.isNull()) { + if (id == _loginDialogID) { + emit loginDialogFocusEnabled(); + } else if (!_keyboardFocusWaitingOnRenderable) { + // that's the only entity we want in focus; + return; + } + } + + _keyboardFocusedEntity.set(id); + + auto entityScriptingInterface = DependencyManager::get(); + if (id != UNKNOWN_ENTITY_ID) { + EntityPropertyFlags desiredProperties; + desiredProperties += PROP_VISIBLE; + desiredProperties += PROP_SHOW_KEYBOARD_FOCUS_HIGHLIGHT; + auto properties = entityScriptingInterface->getEntityProperties(id); + if (properties.getVisible()) { + auto entities = getEntities(); + auto entityId = _keyboardFocusedEntity.get(); + auto entityItemRenderable = entities->renderableForEntityId(entityId); + if (!entityItemRenderable) { + _keyboardFocusWaitingOnRenderable = true; + } else if (entityItemRenderable->wantsKeyboardFocus()) { + entities->setProxyWindow(entityId, _window->windowHandle()); + if (_keyboardMouseDevice->isActive()) { + _keyboardMouseDevice->pluginFocusOutEvent(); + } + _lastAcceptedKeyPress = usecTimestampNow(); + + if (properties.getShowKeyboardFocusHighlight()) { + if (auto entity = entities->getEntity(entityId)) { + setKeyboardFocusHighlight(entity->getWorldPosition(), entity->getWorldOrientation(), + entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); + return; + } + } + } + } + } + + EntityItemProperties properties; + properties.setVisible(false); + entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); + } +} + +void Application::updateDialogs(float deltaTime) const { + PerformanceTimer perfTimer("updateDialogs"); + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::updateDialogs()"); + auto dialogsManager = DependencyManager::get(); + + QPointer octreeStatsDialog = dialogsManager->getOctreeStatsDialog(); + if (octreeStatsDialog) { + octreeStatsDialog->update(); + } +} + +void Application::updateSecondaryCameraViewFrustum() { + // TODO: Fix this by modeling the way the secondary camera works on how the main camera works + // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary + // camera should be. + + // Code based on SecondaryCameraJob + auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); + assert(renderConfig); + auto camera = dynamic_cast(renderConfig->getConfig("SecondaryCamera")); + + if (!camera || !camera->isEnabled()) { + return; + } + + ViewFrustum secondaryViewFrustum; + if (camera->portalProjection && !camera->attachedEntityId.isNull() && !camera->portalEntranceEntityId.isNull()) { + auto entityScriptingInterface = DependencyManager::get(); + EntityItemPointer portalEntrance = qApp->getEntities()->getTree()->findEntityByID(camera->portalEntranceEntityId); + EntityItemPointer portalExit = qApp->getEntities()->getTree()->findEntityByID(camera->attachedEntityId); + + glm::vec3 portalEntrancePropertiesPosition = portalEntrance->getWorldPosition(); + glm::quat portalEntrancePropertiesRotation = portalEntrance->getWorldOrientation(); + glm::mat4 worldFromPortalEntranceRotation = glm::mat4_cast(portalEntrancePropertiesRotation); + glm::mat4 worldFromPortalEntranceTranslation = glm::translate(portalEntrancePropertiesPosition); + glm::mat4 worldFromPortalEntrance = worldFromPortalEntranceTranslation * worldFromPortalEntranceRotation; + glm::mat4 portalEntranceFromWorld = glm::inverse(worldFromPortalEntrance); + + glm::vec3 portalExitPropertiesPosition = portalExit->getWorldPosition(); + glm::quat portalExitPropertiesRotation = portalExit->getWorldOrientation(); + glm::vec3 portalExitPropertiesDimensions = portalExit->getScaledDimensions(); + glm::vec3 halfPortalExitPropertiesDimensions = 0.5f * portalExitPropertiesDimensions; + + glm::mat4 worldFromPortalExitRotation = glm::mat4_cast(portalExitPropertiesRotation); + glm::mat4 worldFromPortalExitTranslation = glm::translate(portalExitPropertiesPosition); + glm::mat4 worldFromPortalExit = worldFromPortalExitTranslation * worldFromPortalExitRotation; + + glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); + glm::vec3 mainCameraPositionPortalEntrance = vec3(portalEntranceFromWorld * vec4(mainCameraPositionWorld, 1.0f)); + mainCameraPositionPortalEntrance = vec3(-mainCameraPositionPortalEntrance.x, mainCameraPositionPortalEntrance.y, + -mainCameraPositionPortalEntrance.z); + glm::vec3 portalExitCameraPositionWorld = vec3(worldFromPortalExit * vec4(mainCameraPositionPortalEntrance, 1.0f)); + + secondaryViewFrustum.setPosition(portalExitCameraPositionWorld); + secondaryViewFrustum.setOrientation(portalExitPropertiesRotation); + + float nearClip = mainCameraPositionPortalEntrance.z + portalExitPropertiesDimensions.z * 2.0f; + // `mainCameraPositionPortalEntrance` should technically be `mainCameraPositionPortalExit`, + // but the values are the same. + glm::vec3 upperRight = halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; + glm::vec3 bottomLeft = -halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; + glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); + secondaryViewFrustum.setProjection(frustum); + } else if (camera->mirrorProjection && !camera->attachedEntityId.isNull()) { + auto entityScriptingInterface = DependencyManager::get(); + auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); + glm::vec3 mirrorPropertiesPosition = entityProperties.getPosition(); + glm::quat mirrorPropertiesRotation = entityProperties.getRotation(); + glm::vec3 mirrorPropertiesDimensions = entityProperties.getDimensions(); + glm::vec3 halfMirrorPropertiesDimensions = 0.5f * mirrorPropertiesDimensions; + + // setup mirror from world as inverse of world from mirror transformation using inverted x and z for mirrored image + // TODO: we are assuming here that UP is world y-axis + glm::mat4 worldFromMirrorRotation = glm::mat4_cast(mirrorPropertiesRotation) * glm::scale(vec3(-1.0f, 1.0f, -1.0f)); + glm::mat4 worldFromMirrorTranslation = glm::translate(mirrorPropertiesPosition); + glm::mat4 worldFromMirror = worldFromMirrorTranslation * worldFromMirrorRotation; + glm::mat4 mirrorFromWorld = glm::inverse(worldFromMirror); + + // get mirror camera position by reflecting main camera position's z coordinate in mirror space + glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); + glm::vec3 mainCameraPositionMirror = vec3(mirrorFromWorld * vec4(mainCameraPositionWorld, 1.0f)); + glm::vec3 mirrorCameraPositionMirror = vec3(mainCameraPositionMirror.x, mainCameraPositionMirror.y, + -mainCameraPositionMirror.z); + glm::vec3 mirrorCameraPositionWorld = vec3(worldFromMirror * vec4(mirrorCameraPositionMirror, 1.0f)); + + // set frustum position to be mirrored camera and set orientation to mirror's adjusted rotation + glm::quat mirrorCameraOrientation = glm::quat_cast(worldFromMirrorRotation); + secondaryViewFrustum.setPosition(mirrorCameraPositionWorld); + secondaryViewFrustum.setOrientation(mirrorCameraOrientation); + + // build frustum using mirror space translation of mirrored camera + float nearClip = mirrorCameraPositionMirror.z + mirrorPropertiesDimensions.z * 2.0f; + glm::vec3 upperRight = halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; + glm::vec3 bottomLeft = -halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; + glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); + secondaryViewFrustum.setProjection(frustum); + } else { + if (!camera->attachedEntityId.isNull()) { + auto entityScriptingInterface = DependencyManager::get(); + auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); + secondaryViewFrustum.setPosition(entityProperties.getPosition()); + secondaryViewFrustum.setOrientation(entityProperties.getRotation()); + } else { + secondaryViewFrustum.setPosition(camera->position); + secondaryViewFrustum.setOrientation(camera->orientation); + } + + float aspectRatio = (float)camera->textureWidth / (float)camera->textureHeight; + secondaryViewFrustum.setProjection(camera->vFoV, + aspectRatio, + camera->nearClipPlaneDistance, + camera->farClipPlaneDistance); + } + // Without calculating the bound planes, the secondary camera will use the same culling frustum as the main camera, + // which is not what we want here. + secondaryViewFrustum.calculate(); + + _conicalViews.push_back(secondaryViewFrustum); +} + +static bool domainLoadingInProgress = false; + +void Application::update(float deltaTime) { + PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_graphicsEngine._renderFrameCount + 1); + + if (_aboutToQuit) { + return; + } + + + if (!_physicsEnabled) { + if (!domainLoadingInProgress) { + PROFILE_ASYNC_BEGIN(app, "Scene Loading", ""); + domainLoadingInProgress = true; + } + + // we haven't yet enabled physics. we wait until we think we have all the collision information + // for nearby entities before starting bullet up. + quint64 now = usecTimestampNow(); + if (isServerlessMode() || _octreeProcessor.isLoadSequenceComplete()) { + bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + + if (gpuTextureMemSizeStable() || !enableInterstitial) { + // we've received a new full-scene octree stats packet, or it's been long enough to try again anyway + _lastPhysicsCheckTime = now; + _fullSceneCounterAtLastPhysicsCheck = _fullSceneReceivedCounter; + _lastQueriedViews.clear(); // Force new view. + + // process octree stats packets are sent in between full sends of a scene (this isn't currently true). + // We keep physics disabled until we've received a full scene and everything near the avatar in that + // scene is ready to compute its collision shape. + if (getMyAvatar()->isReadyForPhysics()) { + _physicsEnabled = true; + setIsInterstitialMode(false); + getMyAvatar()->updateMotionBehaviorFromMenu(); + } + } + } + } else if (domainLoadingInProgress) { + domainLoadingInProgress = false; + PROFILE_ASYNC_END(app, "Scene Loading", ""); + } + + auto myAvatar = getMyAvatar(); + { + PerformanceTimer perfTimer("devices"); + + FaceTracker* tracker = getSelectedFaceTracker(); + if (tracker && Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking) != tracker->isMuted()) { + tracker->toggleMute(); + } + + tracker = getActiveFaceTracker(); + if (tracker && !tracker->isMuted()) { + tracker->update(deltaTime); + + // Auto-mute microphone after losing face tracking? + if (tracker->isTracking()) { + _lastFaceTrackerUpdate = usecTimestampNow(); + } else { + const quint64 MUTE_MICROPHONE_AFTER_USECS = 5000000; //5 secs + Menu* menu = Menu::getInstance(); + auto audioClient = DependencyManager::get(); + if (menu->isOptionChecked(MenuOption::AutoMuteAudio) && !audioClient->isMuted()) { + if (_lastFaceTrackerUpdate > 0 + && ((usecTimestampNow() - _lastFaceTrackerUpdate) > MUTE_MICROPHONE_AFTER_USECS)) { + audioClient->setMuted(true); + _lastFaceTrackerUpdate = 0; + } + } else { + _lastFaceTrackerUpdate = 0; + } + } + } else { + _lastFaceTrackerUpdate = 0; + } + + auto userInputMapper = DependencyManager::get(); + + controller::HmdAvatarAlignmentType hmdAvatarAlignmentType; + if (myAvatar->getHmdAvatarAlignmentType() == "eyes") { + hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Eyes; + } else { + hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Head; + } + + controller::InputCalibrationData calibrationData = { + myAvatar->getSensorToWorldMatrix(), + createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()), + myAvatar->getHMDSensorMatrix(), + myAvatar->getCenterEyeCalibrationMat(), + myAvatar->getHeadCalibrationMat(), + myAvatar->getSpine2CalibrationMat(), + myAvatar->getHipsCalibrationMat(), + myAvatar->getLeftFootCalibrationMat(), + myAvatar->getRightFootCalibrationMat(), + myAvatar->getRightArmCalibrationMat(), + myAvatar->getLeftArmCalibrationMat(), + myAvatar->getRightHandCalibrationMat(), + myAvatar->getLeftHandCalibrationMat(), + hmdAvatarAlignmentType + }; + + InputPluginPointer keyboardMousePlugin; + for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { + if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { + keyboardMousePlugin = inputPlugin; + } else if (inputPlugin->isActive()) { + inputPlugin->pluginUpdate(deltaTime, calibrationData); + } + } + + userInputMapper->setInputCalibrationData(calibrationData); + userInputMapper->update(deltaTime); + + if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { + keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); + } + // Transfer the user inputs to the driveKeys + // FIXME can we drop drive keys and just have the avatar read the action states directly? + myAvatar->clearDriveKeys(); + if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT && !isInterstitialMode()) { + if (!_controllerScriptingInterface->areActionsCaptured() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); + if (deltaTime > FLT_EPSILON) { + myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + myAvatar->setDriveKey(MyAvatar::DELTA_PITCH, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_PITCH)); + myAvatar->setDriveKey(MyAvatar::DELTA_YAW, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_YAW)); + myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + } + } + myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + } + + myAvatar->setSprintMode((bool)userInputMapper->getActionState(controller::Action::SPRINT)); + static const std::vector avatarControllerActions = { + controller::Action::LEFT_HAND, + controller::Action::RIGHT_HAND, + controller::Action::LEFT_FOOT, + controller::Action::RIGHT_FOOT, + controller::Action::HIPS, + controller::Action::SPINE2, + controller::Action::HEAD, + controller::Action::LEFT_HAND_THUMB1, + controller::Action::LEFT_HAND_THUMB2, + controller::Action::LEFT_HAND_THUMB3, + controller::Action::LEFT_HAND_THUMB4, + controller::Action::LEFT_HAND_INDEX1, + controller::Action::LEFT_HAND_INDEX2, + controller::Action::LEFT_HAND_INDEX3, + controller::Action::LEFT_HAND_INDEX4, + controller::Action::LEFT_HAND_MIDDLE1, + controller::Action::LEFT_HAND_MIDDLE2, + controller::Action::LEFT_HAND_MIDDLE3, + controller::Action::LEFT_HAND_MIDDLE4, + controller::Action::LEFT_HAND_RING1, + controller::Action::LEFT_HAND_RING2, + controller::Action::LEFT_HAND_RING3, + controller::Action::LEFT_HAND_RING4, + controller::Action::LEFT_HAND_PINKY1, + controller::Action::LEFT_HAND_PINKY2, + controller::Action::LEFT_HAND_PINKY3, + controller::Action::LEFT_HAND_PINKY4, + controller::Action::RIGHT_HAND_THUMB1, + controller::Action::RIGHT_HAND_THUMB2, + controller::Action::RIGHT_HAND_THUMB3, + controller::Action::RIGHT_HAND_THUMB4, + controller::Action::RIGHT_HAND_INDEX1, + controller::Action::RIGHT_HAND_INDEX2, + controller::Action::RIGHT_HAND_INDEX3, + controller::Action::RIGHT_HAND_INDEX4, + controller::Action::RIGHT_HAND_MIDDLE1, + controller::Action::RIGHT_HAND_MIDDLE2, + controller::Action::RIGHT_HAND_MIDDLE3, + controller::Action::RIGHT_HAND_MIDDLE4, + controller::Action::RIGHT_HAND_RING1, + controller::Action::RIGHT_HAND_RING2, + controller::Action::RIGHT_HAND_RING3, + controller::Action::RIGHT_HAND_RING4, + controller::Action::RIGHT_HAND_PINKY1, + controller::Action::RIGHT_HAND_PINKY2, + controller::Action::RIGHT_HAND_PINKY3, + controller::Action::RIGHT_HAND_PINKY4, + controller::Action::LEFT_ARM, + controller::Action::RIGHT_ARM, + controller::Action::LEFT_SHOULDER, + controller::Action::RIGHT_SHOULDER, + controller::Action::LEFT_FORE_ARM, + controller::Action::RIGHT_FORE_ARM, + controller::Action::LEFT_LEG, + controller::Action::RIGHT_LEG, + controller::Action::LEFT_UP_LEG, + controller::Action::RIGHT_UP_LEG, + controller::Action::LEFT_TOE_BASE, + controller::Action::RIGHT_TOE_BASE + }; + + // copy controller poses from userInputMapper to myAvatar. + glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()); + glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix()); + glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; + for (auto& action : avatarControllerActions) { + controller::Pose pose = userInputMapper->getPoseState(action); + myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); + } + + static const std::vector trackedObjectStringLiterals = { + QStringLiteral("_TrackedObject00"), QStringLiteral("_TrackedObject01"), QStringLiteral("_TrackedObject02"), QStringLiteral("_TrackedObject03"), + QStringLiteral("_TrackedObject04"), QStringLiteral("_TrackedObject05"), QStringLiteral("_TrackedObject06"), QStringLiteral("_TrackedObject07"), + QStringLiteral("_TrackedObject08"), QStringLiteral("_TrackedObject09"), QStringLiteral("_TrackedObject10"), QStringLiteral("_TrackedObject11"), + QStringLiteral("_TrackedObject12"), QStringLiteral("_TrackedObject13"), QStringLiteral("_TrackedObject14"), QStringLiteral("_TrackedObject15") + }; + + // Controlled by the Developer > Avatar > Show Tracked Objects menu. + if (_showTrackedObjects) { + static const std::vector trackedObjectActions = { + controller::Action::TRACKED_OBJECT_00, controller::Action::TRACKED_OBJECT_01, controller::Action::TRACKED_OBJECT_02, controller::Action::TRACKED_OBJECT_03, + controller::Action::TRACKED_OBJECT_04, controller::Action::TRACKED_OBJECT_05, controller::Action::TRACKED_OBJECT_06, controller::Action::TRACKED_OBJECT_07, + controller::Action::TRACKED_OBJECT_08, controller::Action::TRACKED_OBJECT_09, controller::Action::TRACKED_OBJECT_10, controller::Action::TRACKED_OBJECT_11, + controller::Action::TRACKED_OBJECT_12, controller::Action::TRACKED_OBJECT_13, controller::Action::TRACKED_OBJECT_14, controller::Action::TRACKED_OBJECT_15 + }; + + int i = 0; + glm::vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f); + for (auto& action : trackedObjectActions) { + controller::Pose pose = userInputMapper->getPoseState(action); + if (pose.valid) { + glm::vec3 pos = transformPoint(myAvatarMatrix, pose.translation); + glm::quat rot = glmExtractRotation(myAvatarMatrix) * pose.rotation; + DebugDraw::getInstance().addMarker(trackedObjectStringLiterals[i], rot, pos, BLUE); + } else { + DebugDraw::getInstance().removeMarker(trackedObjectStringLiterals[i]); + } + i++; + } + } else if (_prevShowTrackedObjects) { + for (auto& key : trackedObjectStringLiterals) { + DebugDraw::getInstance().removeMarker(key); + } + } + _prevShowTrackedObjects = _showTrackedObjects; + } + + updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... + updateDialogs(deltaTime); // update various stats dialogs if present + + auto grabManager = DependencyManager::get(); + grabManager->simulateGrabs(); + + // TODO: break these out into distinct perfTimers when they prove interesting + { + PROFILE_RANGE(app, "PickManager"); + PerformanceTimer perfTimer("pickManager"); + DependencyManager::get()->update(); + } + + { + PROFILE_RANGE(app, "PointerManager"); + PerformanceTimer perfTimer("pointerManager"); + DependencyManager::get()->update(); + } + + QSharedPointer avatarManager = DependencyManager::get(); + + { + PROFILE_RANGE(simulation_physics, "Simulation"); + PerformanceTimer perfTimer("simulation"); + + if (_physicsEnabled) { + auto t0 = std::chrono::high_resolution_clock::now(); + auto t1 = t0; + { + PROFILE_RANGE(simulation_physics, "PrePhysics"); + PerformanceTimer perfTimer("prePhysics)"); + { + PROFILE_RANGE(simulation_physics, "RemoveEntities"); + const VectorOfMotionStates& motionStates = _entitySimulation->getObjectsToRemoveFromPhysics(); + { + PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); + _physicsEngine->removeObjects(motionStates); + } + _entitySimulation->deleteObjectsRemovedFromPhysics(); + } + + { + PROFILE_RANGE(simulation_physics, "AddEntities"); + VectorOfMotionStates motionStates; + getEntities()->getTree()->withReadLock([&] { + _entitySimulation->getObjectsToAddToPhysics(motionStates); + PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); + _physicsEngine->addObjects(motionStates); + }); + } + { + VectorOfMotionStates motionStates; + PROFILE_RANGE(simulation_physics, "ChangeEntities"); + getEntities()->getTree()->withReadLock([&] { + _entitySimulation->getObjectsToChange(motionStates); + VectorOfMotionStates stillNeedChange = _physicsEngine->changeObjects(motionStates); + _entitySimulation->setObjectsToChange(stillNeedChange); + }); + } + + _entitySimulation->applyDynamicChanges(); + + t1 = std::chrono::high_resolution_clock::now(); + + { + PROFILE_RANGE(simulation_physics, "Avatars"); + PhysicsEngine::Transaction transaction; + avatarManager->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + avatarManager->handleProcessedPhysicsTransaction(transaction); + myAvatar->getCharacterController()->buildPhysicsTransaction(transaction); + _physicsEngine->processTransaction(transaction); + myAvatar->getCharacterController()->handleProcessedPhysicsTransaction(transaction); + myAvatar->prepareForPhysicsSimulation(); + _physicsEngine->enableGlobalContactAddedCallback(myAvatar->isFlying()); + } + + { + PROFILE_RANGE(simulation_physics, "PrepareActions"); + _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { + dynamic->prepareForPhysicsSimulation(); + }); + } + } + auto t2 = std::chrono::high_resolution_clock::now(); + { + PROFILE_RANGE(simulation_physics, "StepPhysics"); + PerformanceTimer perfTimer("stepPhysics"); + getEntities()->getTree()->withWriteLock([&] { + _physicsEngine->stepSimulation(); + }); + } + auto t3 = std::chrono::high_resolution_clock::now(); + { + if (_physicsEngine->hasOutgoingChanges()) { + { + PROFILE_RANGE(simulation_physics, "PostPhysics"); + PerformanceTimer perfTimer("postPhysics"); + // grab the collision events BEFORE handleChangedMotionStates() because at this point + // we have a better idea of which objects we own or should own. + auto& collisionEvents = _physicsEngine->getCollisionEvents(); + + getEntities()->getTree()->withWriteLock([&] { + PROFILE_RANGE(simulation_physics, "HandleChanges"); + PerformanceTimer perfTimer("handleChanges"); + + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); + _entitySimulation->handleChangedMotionStates(outgoingChanges); + avatarManager->handleChangedMotionStates(outgoingChanges); + + const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); + _entitySimulation->handleDeactivatedMotionStates(deactivations); + }); + + // handleCollisionEvents() AFTER handleChangedMotionStates() + { + PROFILE_RANGE(simulation_physics, "CollisionEvents"); + avatarManager->handleCollisionEvents(collisionEvents); + // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk + // deadlock.) + _entitySimulation->handleCollisionEvents(collisionEvents); + } + + { + PROFILE_RANGE(simulation_physics, "MyAvatar"); + myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); + } + + if (PerformanceTimer::isActive() && + Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && + Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsTiming)) { + _physicsEngine->harvestPerformanceStats(); + } + // NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework + _physicsEngine->dumpStatsIfNecessary(); + } + auto t4 = std::chrono::high_resolution_clock::now(); + + // NOTE: the getEntities()->update() call below will wait for lock + // and will provide non-physical entity motion + getEntities()->update(true); // update the models... + + auto t5 = std::chrono::high_resolution_clock::now(); + + workload::Timings timings(6); + timings[0] = t1 - t0; // prePhysics entities + timings[1] = t2 - t1; // prePhysics avatars + timings[2] = t3 - t2; // stepPhysics + timings[3] = t4 - t3; // postPhysics + timings[4] = t5 - t4; // non-physical kinematics + timings[5] = workload::Timing_ns((int32_t)(NSECS_PER_SECOND * deltaTime)); // game loop duration + _gameWorkload.updateSimulationTimings(timings); + } + } + } else { + // update the rendering without any simulation + getEntities()->update(false); + } + // remove recently dead avatarEntities + SetOfEntities deadAvatarEntities; + _entitySimulation->takeDeadAvatarEntities(deadAvatarEntities); + avatarManager->removeDeadAvatarEntities(deadAvatarEntities); + } + + // AvatarManager update + { + { + PROFILE_RANGE(simulation, "OtherAvatars"); + PerformanceTimer perfTimer("otherAvatars"); + avatarManager->updateOtherAvatars(deltaTime); + } + + { + PROFILE_RANGE(simulation, "MyAvatar"); + PerformanceTimer perfTimer("MyAvatar"); + qApp->updateMyAvatarLookAtPosition(); + avatarManager->updateMyAvatar(deltaTime); + } + } + + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::update()"); + + updateLOD(deltaTime); + + if (!_loginDialogID.isNull()) { + _loginStateManager.update(getMyAvatar()->getDominantHand(), _loginDialogID); + updateLoginDialogPosition(); + } + + { + PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("overlays"); + _overlays.update(deltaTime); + } + + // Update _viewFrustum with latest camera and view frustum data... + // NOTE: we get this from the view frustum, to make it simpler, since the + // loadViewFrumstum() method will get the correct details from the camera + // We could optimize this to not actually load the viewFrustum, since we don't + // actually need to calculate the view frustum planes to send these details + // to the server. + { + QMutexLocker viewLocker(&_viewMutex); + _myCamera.loadViewFrustum(_viewFrustum); + + _conicalViews.clear(); + _conicalViews.push_back(_viewFrustum); + // TODO: Fix this by modeling the way the secondary camera works on how the main camera works + // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary + // camera should be. + updateSecondaryCameraViewFrustum(); + } + + quint64 now = usecTimestampNow(); + + // Update my voxel servers with my current voxel query... + { + PROFILE_RANGE_EX(app, "QueryOctree", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("queryOctree"); + QMutexLocker viewLocker(&_viewMutex); + + bool viewIsDifferentEnough = false; + if (_conicalViews.size() == _lastQueriedViews.size()) { + for (size_t i = 0; i < _conicalViews.size(); ++i) { + if (!_conicalViews[i].isVerySimilar(_lastQueriedViews[i])) { + viewIsDifferentEnough = true; + break; + } + } + } else { + viewIsDifferentEnough = true; + } + + + // if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it + static const std::chrono::seconds MIN_PERIOD_BETWEEN_QUERIES { 3 }; + auto now = SteadyClock::now(); + if (now > _queryExpiry || viewIsDifferentEnough) { + if (DependencyManager::get()->shouldRenderEntities()) { + queryOctree(NodeType::EntityServer, PacketType::EntityQuery); + } + queryAvatars(); + + _lastQueriedViews = _conicalViews; + _queryExpiry = now + MIN_PERIOD_BETWEEN_QUERIES; + } + } + + // sent nack packets containing missing sequence numbers of received packets from nodes + { + quint64 sinceLastNack = now - _lastNackTime; + const quint64 TOO_LONG_SINCE_LAST_NACK = 1 * USECS_PER_SECOND; + if (sinceLastNack > TOO_LONG_SINCE_LAST_NACK) { + _lastNackTime = now; + sendNackPackets(); + } + } + + // send packet containing downstream audio stats to the AudioMixer + { + quint64 sinceLastNack = now - _lastSendDownstreamAudioStats; + if (sinceLastNack > TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS && !isInterstitialMode()) { + _lastSendDownstreamAudioStats = now; + + QMetaObject::invokeMethod(DependencyManager::get().data(), "sendDownstreamAudioStatsPacket", Qt::QueuedConnection); + } + } + + { + PerformanceTimer perfTimer("avatarManager/postUpdate"); + avatarManager->postUpdate(deltaTime, getMain3DScene()); + } + + { + PROFILE_RANGE_EX(app, "PostUpdateLambdas", 0xffff0000, (uint64_t)0); + PerformanceTimer perfTimer("postUpdateLambdas"); + std::unique_lock guard(_postUpdateLambdasLock); + for (auto& iter : _postUpdateLambdas) { + iter.second(); + } + _postUpdateLambdas.clear(); + } + + + updateRenderArgs(deltaTime); + + { + PerformanceTimer perfTimer("AnimDebugDraw"); + AnimDebugDraw::getInstance().update(); + } + + + { // Game loop is done, mark the end of the frame for the scene transactions and the render loop to take over + PerformanceTimer perfTimer("enqueueFrame"); + getMain3DScene()->enqueueFrame(); + } + + // If the display plugin is inactive then the frames won't be processed so process them here. + if (!getActiveDisplayPlugin()->isActive()) { + getMain3DScene()->processTransactionQueue(); + } +} + +void Application::updateRenderArgs(float deltaTime) { + _graphicsEngine.editRenderArgs([this, deltaTime](AppRenderArgs& appRenderArgs) { + PerformanceTimer perfTimer("editRenderArgs"); + appRenderArgs._headPose = getHMDSensorPose(); + + auto myAvatar = getMyAvatar(); + + // update the avatar with a fresh HMD pose + { + PROFILE_RANGE(render, "/updateAvatar"); + myAvatar->updateFromHMDSensorMatrix(appRenderArgs._headPose); + } + + auto lodManager = DependencyManager::get(); + + float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); + appRenderArgs._sensorToWorldScale = sensorToWorldScale; + appRenderArgs._sensorToWorld = getMyAvatar()->getSensorToWorldMatrix(); + { + PROFILE_RANGE(render, "/buildFrustrumAndArgs"); + { + QMutexLocker viewLocker(&_viewMutex); + // adjust near clip plane to account for sensor scaling. + auto adjustedProjection = glm::perspective(glm::radians(_fieldOfView.get()), + getActiveDisplayPlugin()->getRecommendedAspectRatio(), + DEFAULT_NEAR_CLIP * sensorToWorldScale, + DEFAULT_FAR_CLIP); + _viewFrustum.setProjection(adjustedProjection); + _viewFrustum.calculate(); + } + appRenderArgs._renderArgs = RenderArgs(_graphicsEngine.getGPUContext(), lodManager->getOctreeSizeScale(), + lodManager->getBoundaryLevelAdjust(), lodManager->getLODAngleHalfTan(), RenderArgs::DEFAULT_RENDER_MODE, + RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); + appRenderArgs._renderArgs._scene = getMain3DScene(); + + { + QMutexLocker viewLocker(&_viewMutex); + appRenderArgs._renderArgs.setViewFrustum(_viewFrustum); + } + } + { + PROFILE_RANGE(render, "/resizeGL"); + bool showWarnings = false; + bool suppressShortTimings = false; + auto menu = Menu::getInstance(); + if (menu) { + suppressShortTimings = menu->isOptionChecked(MenuOption::SuppressShortTimings); + showWarnings = menu->isOptionChecked(MenuOption::PipelineWarnings); + } + PerformanceWarning::setSuppressShortTimings(suppressShortTimings); + PerformanceWarning warn(showWarnings, "Application::paintGL()"); + resizeGL(); + } + + this->updateCamera(appRenderArgs._renderArgs, deltaTime); + appRenderArgs._eyeToWorld = _myCamera.getTransform(); + appRenderArgs._isStereo = false; + + { + auto hmdInterface = DependencyManager::get(); + float ipdScale = hmdInterface->getIPDScale(); + + // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. + ipdScale *= sensorToWorldScale; + + auto baseProjection = appRenderArgs._renderArgs.getViewFrustum().getProjection(); + + if (getActiveDisplayPlugin()->isStereo()) { + // Stereo modes will typically have a larger projection matrix overall, + // so we ask for the 'mono' projection matrix, which for stereo and HMD + // plugins will imply the combined projection for both eyes. + // + // This is properly implemented for the Oculus plugins, but for OpenVR + // and Stereo displays I'm not sure how to get / calculate it, so we're + // just relying on the left FOV in each case and hoping that the + // overall culling margin of error doesn't cause popping in the + // right eye. There are FIXMEs in the relevant plugins + _myCamera.setProjection(getActiveDisplayPlugin()->getCullingProjection(baseProjection)); + appRenderArgs._isStereo = true; + + auto& eyeOffsets = appRenderArgs._eyeOffsets; + auto& eyeProjections = appRenderArgs._eyeProjections; + + // FIXME we probably don't need to set the projection matrix every frame, + // only when the display plugin changes (or in non-HMD modes when the user + // changes the FOV manually, which right now I don't think they can. + for_each_eye([&](Eye eye) { + // For providing the stereo eye views, the HMD head pose has already been + // applied to the avatar, so we need to get the difference between the head + // pose applied to the avatar and the per eye pose, and use THAT as + // the per-eye stereo matrix adjustment. + mat4 eyeToHead = getActiveDisplayPlugin()->getEyeToHeadTransform(eye); + // Grab the translation + vec3 eyeOffset = glm::vec3(eyeToHead[3]); + // Apply IPD scaling + mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); + eyeOffsets[eye] = eyeOffsetTransform; + eyeProjections[eye] = getActiveDisplayPlugin()->getEyeProjection(eye, baseProjection); + }); + + // Configure the type of display / stereo + appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); + } + } + + { + QMutexLocker viewLocker(&_viewMutex); + _myCamera.loadViewFrustum(_displayViewFrustum); + appRenderArgs._view = glm::inverse(_displayViewFrustum.getView()); + } + + { + QMutexLocker viewLocker(&_viewMutex); + appRenderArgs._renderArgs.setViewFrustum(_displayViewFrustum); + } + + + // HACK + // load the view frustum + // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. + // Then we can move this logic into the Avatar::simulate call. + myAvatar->preDisplaySide(&appRenderArgs._renderArgs); + }); +} + +void Application::queryAvatars() { + if (!isInterstitialMode()) { + auto avatarPacket = NLPacket::create(PacketType::AvatarQuery); + auto destinationBuffer = reinterpret_cast(avatarPacket->getPayload()); + unsigned char* bufferStart = destinationBuffer; + + uint8_t numFrustums = (uint8_t)_conicalViews.size(); + memcpy(destinationBuffer, &numFrustums, sizeof(numFrustums)); + destinationBuffer += sizeof(numFrustums); + + for (const auto& view : _conicalViews) { + destinationBuffer += view.serialize(destinationBuffer); + } + + avatarPacket->setPayloadSize(destinationBuffer - bufferStart); + + DependencyManager::get()->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); + } +} + + +int Application::sendNackPackets() { + + // iterates through all nodes in NodeList + auto nodeList = DependencyManager::get(); + + int packetsSent = 0; + + nodeList->eachNode([&](const SharedNodePointer& node){ + + if (node->getActiveSocket() && node->getType() == NodeType::EntityServer) { + + auto nackPacketList = NLPacketList::create(PacketType::OctreeDataNack); + + QUuid nodeUUID = node->getUUID(); + + // if there are octree packets from this node that are waiting to be processed, + // don't send a NACK since the missing packets may be among those waiting packets. + if (_octreeProcessor.hasPacketsToProcessFrom(nodeUUID)) { + return; + } + + QSet missingSequenceNumbers; + _octreeServerSceneStats.withReadLock([&] { + // retrieve octree scene stats of this node + if (_octreeServerSceneStats.find(nodeUUID) == _octreeServerSceneStats.end()) { + return; + } + // get sequence number stats of node, prune its missing set, and make a copy of the missing set + SequenceNumberStats& sequenceNumberStats = _octreeServerSceneStats[nodeUUID].getIncomingOctreeSequenceNumberStats(); + sequenceNumberStats.pruneMissingSet(); + missingSequenceNumbers = sequenceNumberStats.getMissingSet(); + }); + + _isMissingSequenceNumbers = (missingSequenceNumbers.size() != 0); + + // construct nack packet(s) for this node + foreach(const OCTREE_PACKET_SEQUENCE& missingNumber, missingSequenceNumbers) { + nackPacketList->writePrimitive(missingNumber); + } + + if (nackPacketList->getNumPackets()) { + packetsSent += (int)nackPacketList->getNumPackets(); + + // send the packet list + nodeList->sendPacketList(std::move(nackPacketList), *node); + } + } + }); + + return packetsSent; +} + +void Application::queryOctree(NodeType_t serverType, PacketType packetType) { + + if (!_settingsLoaded) { + return; // bail early if settings are not loaded + } + + const bool isModifiedQuery = !_physicsEnabled; + if (isModifiedQuery) { + // Create modified view that is a simple sphere. + bool interstitialModeEnabled = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + + ConicalViewFrustum sphericalView; + sphericalView.setSimpleRadius(INITIAL_QUERY_RADIUS); + + if (interstitialModeEnabled) { + ConicalViewFrustum farView; + farView.set(_viewFrustum); + _octreeQuery.setConicalViews({ sphericalView, farView }); + } else { + _octreeQuery.setConicalViews({ sphericalView }); + } + + _octreeQuery.setOctreeSizeScale(DEFAULT_OCTREE_SIZE_SCALE); + static constexpr float MIN_LOD_ADJUST = -20.0f; + _octreeQuery.setBoundaryLevelAdjust(MIN_LOD_ADJUST); + } else { + _octreeQuery.setConicalViews(_conicalViews); + auto lodManager = DependencyManager::get(); + _octreeQuery.setOctreeSizeScale(lodManager->getOctreeSizeScale()); + _octreeQuery.setBoundaryLevelAdjust(lodManager->getBoundaryLevelAdjust()); + } + _octreeQuery.setReportInitialCompletion(isModifiedQuery); + + + auto nodeList = DependencyManager::get(); + + auto node = nodeList->soloNodeOfType(serverType); + if (node && node->getActiveSocket()) { + _octreeQuery.setMaxQueryPacketsPerSecond(getMaxOctreePacketsPerSecond()); + + auto queryPacket = NLPacket::create(packetType); + + // encode the query data + auto packetData = reinterpret_cast(queryPacket->getPayload()); + int packetSize = _octreeQuery.getBroadcastData(packetData); + queryPacket->setPayloadSize(packetSize); + + // make sure we still have an active socket + nodeList->sendUnreliablePacket(*queryPacket, *node); + } +} + + +bool Application::isHMDMode() const { + return getActiveDisplayPlugin()->isHmd(); +} + +float Application::getNumCollisionObjects() const { + return _physicsEngine ? _physicsEngine->getNumCollisionObjects() : 0; +} + +float Application::getTargetRenderFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); } + +QRect Application::getDesirableApplicationGeometry() const { + QRect applicationGeometry = getWindow()->geometry(); + + // If our parent window is on the HMD, then don't use its geometry, instead use + // the "main screen" geometry. + HMDToolsDialog* hmdTools = DependencyManager::get()->getHMDToolsDialog(); + if (hmdTools && hmdTools->hasHMDScreen()) { + QScreen* hmdScreen = hmdTools->getHMDScreen(); + QWindow* appWindow = getWindow()->windowHandle(); + QScreen* appScreen = appWindow->screen(); + + // if our app's screen is the hmd screen, we don't want to place the + // running scripts widget on it. So we need to pick a better screen. + // we will use the screen for the HMDTools since it's a guaranteed + // better screen. + if (appScreen == hmdScreen) { + QScreen* betterScreen = hmdTools->windowHandle()->screen(); + applicationGeometry = betterScreen->geometry(); + } + } + return applicationGeometry; +} + +PickRay Application::computePickRay(float x, float y) const { + vec2 pickPoint { x, y }; + PickRay result; + if (isHMDMode()) { + getApplicationCompositor().computeHmdPickRay(pickPoint, result.origin, result.direction); + } else { + pickPoint /= getCanvasSize(); + if (_myCamera.getMode() == CameraMode::CAMERA_MODE_MIRROR) { + pickPoint.x = 1.0f - pickPoint.x; + } + QMutexLocker viewLocker(&_viewMutex); + _viewFrustum.computePickRay(pickPoint.x, pickPoint.y, result.origin, result.direction); + } + return result; +} + +std::shared_ptr Application::getMyAvatar() const { + return DependencyManager::get()->getMyAvatar(); +} + +glm::vec3 Application::getAvatarPosition() const { + return getMyAvatar()->getWorldPosition(); +} + +void Application::copyViewFrustum(ViewFrustum& viewOut) const { + QMutexLocker viewLocker(&_viewMutex); + viewOut = _viewFrustum; +} + +void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const { + QMutexLocker viewLocker(&_viewMutex); + viewOut = _displayViewFrustum; +} + +// resentSensors() is a bit of vestigial feature. It used to be used for Oculus DK2 to recenter the view around +// the current head orientation. With the introduction of "room scale" tracking we no longer need that particular +// feature. However, we still use this to reset face trackers, eye trackers, audio and to optionally re-load the avatar +// rig and animations from scratch. +void Application::resetSensors(bool andReload) { + DependencyManager::get()->reset(); + DependencyManager::get()->reset(); + _overlayConductor.centerUI(); + getActiveDisplayPlugin()->resetSensors(); + getMyAvatar()->reset(true, andReload); + QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); +} + +void Application::hmdVisibleChanged(bool visible) { + // TODO + // calling start and stop will change audio input and ouput to default audio devices. + // we need to add a pause/unpause functionality to AudioClient for this to work properly +#if 0 + if (visible) { + QMetaObject::invokeMethod(DependencyManager::get().data(), "start", Qt::QueuedConnection); + } else { + QMetaObject::invokeMethod(DependencyManager::get().data(), "stop", Qt::QueuedConnection); + } +#endif +} + +void Application::updateWindowTitle() const { + + auto nodeList = DependencyManager::get(); + auto accountManager = DependencyManager::get(); + auto isInErrorState = nodeList->getDomainHandler().isInErrorState(); + + QString buildVersion = " - " + + (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) + + " " + applicationVersion(); + + QString loginStatus = accountManager->isLoggedIn() ? "" : " (NOT LOGGED IN)"; + + QString connectionStatus = isInErrorState ? " (ERROR CONNECTING)" : + nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED)"; + QString username = accountManager->getAccountInfo().getUsername(); + + setCrashAnnotation("username", username.toStdString()); + + QString currentPlaceName; + if (isServerlessMode()) { + if (isInErrorState) { + currentPlaceName = "serverless: " + nodeList->getDomainHandler().getErrorDomainURL().toString(); + } else { + currentPlaceName = "serverless: " + DependencyManager::get()->getDomainURL().toString(); + } + } else { + currentPlaceName = DependencyManager::get()->getDomainURL().host(); + if (currentPlaceName.isEmpty()) { + currentPlaceName = nodeList->getDomainHandler().getHostname(); + } + } + + QString title = QString() + (!username.isEmpty() ? username + " @ " : QString()) + + currentPlaceName + connectionStatus + loginStatus + buildVersion; + +#ifndef WIN32 + // crashes with vs2013/win32 + qCDebug(interfaceapp, "Application title set to: %s", title.toStdString().c_str()); +#endif + _window->setWindowTitle(title); + + // updateTitleWindow gets called whenever there's a change regarding the domain, so rather + // than placing this within domainURLChanged, it's placed here to cover the other potential cases. + DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", ""); +} + +void Application::clearDomainOctreeDetails(bool clearAll) { + // before we delete all entities get MyAvatar's AvatarEntityData ready + getMyAvatar()->prepareAvatarEntityDataForReload(); + + // if we're about to quit, we really don't need to do the rest of these things... + if (_aboutToQuit) { + return; + } + + qCDebug(interfaceapp) << "Clearing domain octree details..."; + + resetPhysicsReadyInformation(); + setIsInterstitialMode(true); + + _octreeServerSceneStats.withWriteLock([&] { + _octreeServerSceneStats.clear(); + }); + + // reset the model renderer + clearAll ? getEntities()->clear() : getEntities()->clearDomainAndNonOwnedEntities(); + + auto skyStage = DependencyManager::get()->getSkyStage(); + + skyStage->setBackgroundMode(graphics::SunSkyStage::SKY_DEFAULT); + + DependencyManager::get()->clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); + MaterialCache::instance().clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); + ShaderCache::instance().clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); + DependencyManager::get()->clearUnusedResources(); +} + +void Application::domainURLChanged(QUrl domainURL) { + // disable physics until we have enough information about our new location to not cause craziness. + resetPhysicsReadyInformation(); + setIsServerlessMode(domainURL.scheme() != URL_SCHEME_HIFI); + if (isServerlessMode()) { + loadServerlessDomain(domainURL); + } + updateWindowTitle(); +} + +void Application::goToErrorDomainURL(QUrl errorDomainURL) { + // disable physics until we have enough information about our new location to not cause craziness. + resetPhysicsReadyInformation(); + setIsServerlessMode(errorDomainURL.scheme() != URL_SCHEME_HIFI); + if (isServerlessMode()) { + loadErrorDomain(errorDomainURL); + } + updateWindowTitle(); +} + +void Application::resettingDomain() { + _notifiedPacketVersionMismatchThisDomain = false; + + clearDomainOctreeDetails(false); +} + +void Application::nodeAdded(SharedNodePointer node) const { + if (node->getType() == NodeType::EntityServer) { + if (!_failedToConnectToEntityServer) { + _entityServerConnectionTimer.stop(); + _entityServerConnectionTimer.setInterval(ENTITY_SERVER_CONNECTION_TIMEOUT); + _entityServerConnectionTimer.start(); + } + } +} + +void Application::nodeActivated(SharedNodePointer node) { + if (node->getType() == NodeType::AssetServer) { + // asset server just connected - check if we have the asset browser showing + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + + if (offscreenUi) { + auto nodeList = DependencyManager::get(); + + if (nodeList->getThisNodeCanWriteAssets()) { + // call reload on the shown asset browser dialog to get the mappings (if permissions allow) + auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; + if (assetDialog) { + QMetaObject::invokeMethod(assetDialog, "reload"); + } + } else { + // we switched to an Asset Server that we can't modify, hide the Asset Browser + offscreenUi->hide("AssetServer"); + } + } +#endif + } + + // If we get a new EntityServer activated, reset lastQueried time + // so we will do a proper query during update + if (node->getType() == NodeType::EntityServer) { + _queryExpiry = SteadyClock::now(); + _octreeQuery.incrementConnectionID(); + + if (!_failedToConnectToEntityServer) { + _entityServerConnectionTimer.stop(); + } + } + + if (node->getType() == NodeType::AudioMixer && !isInterstitialMode()) { + DependencyManager::get()->negotiateAudioFormat(); + } + + if (node->getType() == NodeType::AvatarMixer) { + _queryExpiry = SteadyClock::now(); + + // new avatar mixer, send off our identity packet on next update loop + // Reset skeletonModelUrl if the last server modified our choice. + // Override the avatar url (but not model name) here too. + if (_avatarOverrideUrl.isValid()) { + getMyAvatar()->useFullAvatarURL(_avatarOverrideUrl); + } + + if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->getSkeletonModelURL()) { + getMyAvatar()->resetFullAvatarURL(); + } + getMyAvatar()->markIdentityDataChanged(); + getMyAvatar()->resetLastSent(); + + if (!isInterstitialMode()) { + // transmit a "sendAll" packet to the AvatarMixer we just connected to. + getMyAvatar()->sendAvatarDataPacket(true); + } + } +} + +void Application::nodeKilled(SharedNodePointer node) { + // These are here because connecting NodeList::nodeKilled to OctreePacketProcessor::nodeKilled doesn't work: + // OctreePacketProcessor::nodeKilled is not being called when NodeList::nodeKilled is emitted. + // This may have to do with GenericThread::threadRoutine() blocking the QThread event loop + + _octreeProcessor.nodeKilled(node); + + _entityEditSender.nodeKilled(node); + + if (node->getType() == NodeType::AudioMixer) { + QMetaObject::invokeMethod(DependencyManager::get().data(), "audioMixerKilled"); + } else if (node->getType() == NodeType::EntityServer) { + // we lost an entity server, clear all of the domain octree details + clearDomainOctreeDetails(false); + } else if (node->getType() == NodeType::AssetServer) { + // asset server going away - check if we have the asset browser showing + +#if !defined(DISABLE_QML) + auto offscreenUi = getOffscreenUI(); + auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; + + if (assetDialog) { + // call reload on the shown asset browser dialog + QMetaObject::invokeMethod(assetDialog, "clear"); + } +#endif + } +} + +void Application::trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket) { + // Attempt to identify the sender from its address. + if (sendingNode) { + const QUuid& nodeUUID = sendingNode->getUUID(); + + // now that we know the node ID, let's add these stats to the stats for that node... + _octreeServerSceneStats.withWriteLock([&] { + if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { + OctreeSceneStats& stats = _octreeServerSceneStats[nodeUUID]; + stats.trackIncomingOctreePacket(message, wasStatsPacket, sendingNode->getClockSkewUsec()); + } + }); + } +} + +bool Application::gpuTextureMemSizeStable() { + auto renderConfig = qApp->getRenderEngine()->getConfiguration(); + auto renderStats = renderConfig->getConfig("Stats"); + + qint64 textureResourceGPUMemSize = renderStats->textureResourceGPUMemSize; + qint64 texturePopulatedGPUMemSize = renderStats->textureResourcePopulatedGPUMemSize; + qint64 textureTransferSize = renderStats->texturePendingGPUTransferSize; + + if (_gpuTextureMemSizeAtLastCheck == textureResourceGPUMemSize) { + _gpuTextureMemSizeStabilityCount++; + } else { + _gpuTextureMemSizeStabilityCount = 0; + } + _gpuTextureMemSizeAtLastCheck = textureResourceGPUMemSize; + + if (_gpuTextureMemSizeStabilityCount >= _minimumGPUTextureMemSizeStabilityCount) { + return (textureResourceGPUMemSize == texturePopulatedGPUMemSize) && (textureTransferSize == 0); + } + return false; +} + +int Application::processOctreeStats(ReceivedMessage& message, SharedNodePointer sendingNode) { + // parse the incoming stats datas stick it in a temporary object for now, while we + // determine which server it belongs to + int statsMessageLength = 0; + + const QUuid& nodeUUID = sendingNode->getUUID(); + + // now that we know the node ID, let's add these stats to the stats for that node... + _octreeServerSceneStats.withWriteLock([&] { + OctreeSceneStats& octreeStats = _octreeServerSceneStats[nodeUUID]; + statsMessageLength = octreeStats.unpackFromPacket(message); + + if (octreeStats.isFullScene()) { + _fullSceneReceivedCounter++; + } + }); + + return statsMessageLength; +} + +void Application::packetSent(quint64 length) { +} + +void Application::addingEntityWithCertificate(const QString& certificateID, const QString& placeName) { + auto ledger = DependencyManager::get(); + ledger->updateLocation(certificateID, placeName); +} + +void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointer scriptEngine) { + + scriptEngine->setEmitScriptUpdatesFunction([this]() { + SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); + return !entityServerNode || isPhysicsEnabled(); + }); + + // setup the packet sender of the script engine's scripting interfaces so + // we can use the same ones from the application. + auto entityScriptingInterface = DependencyManager::get(); + entityScriptingInterface->setPacketSender(&_entityEditSender); + entityScriptingInterface->setEntityTree(getEntities()->getTree()); + + if (property(hifi::properties::TEST).isValid()) { + scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance()); + } + + scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); + + // hook our avatar and avatar hash map object into this script engine + getMyAvatar()->registerMetaTypes(scriptEngine); + + scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("Camera", &_myCamera); + +#if defined(Q_OS_MAC) || defined(Q_OS_WIN) + scriptEngine->registerGlobalObject("SpeechRecognizer", DependencyManager::get().data()); +#endif + + ClipboardScriptingInterface* clipboardScriptable = new ClipboardScriptingInterface(); + scriptEngine->registerGlobalObject("Clipboard", clipboardScriptable); + connect(scriptEngine.data(), &ScriptEngine::finished, clipboardScriptable, &ClipboardScriptingInterface::deleteLater); + + scriptEngine->registerGlobalObject("Overlays", &_overlays); + qScriptRegisterMetaType(scriptEngine.data(), RayToOverlayIntersectionResultToScriptValue, + RayToOverlayIntersectionResultFromScriptValue); + +#if !defined(DISABLE_QML) + scriptEngine->registerGlobalObject("OffscreenFlags", getOffscreenUI()->getFlags()); + scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); +#endif + + qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); + qScriptRegisterMetaType(scriptEngine.data(), + wrapperToScriptValue, wrapperFromScriptValue); + scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); + + qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); + qScriptRegisterMetaType(scriptEngine.data(), + wrapperToScriptValue, wrapperFromScriptValue); + scriptEngine->registerGlobalObject("Tablet", DependencyManager::get().data()); + // FIXME remove these deprecated names for the tablet scripting interface + scriptEngine->registerGlobalObject("tabletInterface", DependencyManager::get().data()); + + auto toolbarScriptingInterface = DependencyManager::get().data(); + DependencyManager::get().data()->setToolbarScriptingInterface(toolbarScriptingInterface); + + scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); + scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, + LocationScriptingInterface::locationSetter, "Window"); + // register `location` on the global object. + scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, + LocationScriptingInterface::locationSetter); + + bool clientScript = scriptEngine->isClientScript(); + scriptEngine->registerFunction("OverlayWindow", clientScript ? QmlWindowClass::constructor : QmlWindowClass::restricted_constructor); +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) + scriptEngine->registerFunction("OverlayWebWindow", clientScript ? QmlWebWindowClass::constructor : QmlWebWindowClass::restricted_constructor); +#endif + scriptEngine->registerFunction("QmlFragment", clientScript ? QmlFragmentClass::constructor : QmlFragmentClass::restricted_constructor); + + scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("DesktopPreviewProvider", DependencyManager::get().data()); +#if !defined(DISABLE_QML) + scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); +#endif + scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); + scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); + scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AvatarBookmarks", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("LocationBookmarks", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("RayPick", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("LaserPointers", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Picks", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Pointers", DependencyManager::get().data()); + + // Caches + scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("TextureCache", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("ModelCache", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("DialogsManager", _dialogsManagerScriptingInterface); + + scriptEngine->registerGlobalObject("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + scriptEngine->registerGlobalObject("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED + scriptEngine->registerGlobalObject("AccountServices", AccountServicesScriptingInterface::getInstance()); + qScriptRegisterMetaType(scriptEngine.data(), DownloadInfoResultToScriptValue, DownloadInfoResultFromScriptValue); + + scriptEngine->registerGlobalObject("FaceTracker", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("LODManager", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("Keyboard", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("HMD", DependencyManager::get().data()); + scriptEngine->registerFunction("HMD", "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0); + scriptEngine->registerFunction("HMD", "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0); + + scriptEngine->registerGlobalObject("Scene", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); + scriptEngine->registerGlobalObject("Workload", _gameWorkload._engine->getConfiguration().get()); + + GraphicsScriptingInterface::registerMetaTypes(scriptEngine.data()); + scriptEngine->registerGlobalObject("Graphics", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); + + scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); + + scriptEngine->registerGlobalObject("GooglePoly", DependencyManager::get().data()); + + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { + scriptEngine->registerGlobalObject("Steam", new SteamScriptingInterface(scriptEngine.data(), steamClient.get())); + } + auto scriptingInterface = DependencyManager::get(); + scriptEngine->registerGlobalObject("Controller", scriptingInterface.data()); + UserInputMapper::registerControllerTypes(scriptEngine.data()); + + auto recordingInterface = DependencyManager::get(); + scriptEngine->registerGlobalObject("Recording", recordingInterface.data()); + + auto entityScriptServerLog = DependencyManager::get(); + scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data()); + scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); + scriptEngine->registerGlobalObject("Selection", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("WalletScriptingInterface", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); + scriptEngine->registerGlobalObject("ResourceRequestObserver", DependencyManager::get().data()); + + registerInteractiveWindowMetaType(scriptEngine.data()); + + auto pickScriptingInterface = DependencyManager::get(); + pickScriptingInterface->registerMetaTypes(scriptEngine.data()); + + // connect this script engines printedMessage signal to the global ScriptEngines these various messages + auto scriptEngines = DependencyManager::get().data(); + connect(scriptEngine.data(), &ScriptEngine::printedMessage, scriptEngines, &ScriptEngines::onPrintedMessage); + connect(scriptEngine.data(), &ScriptEngine::errorMessage, scriptEngines, &ScriptEngines::onErrorMessage); + connect(scriptEngine.data(), &ScriptEngine::warningMessage, scriptEngines, &ScriptEngines::onWarningMessage); + connect(scriptEngine.data(), &ScriptEngine::infoMessage, scriptEngines, &ScriptEngines::onInfoMessage); + connect(scriptEngine.data(), &ScriptEngine::clearDebugWindow, scriptEngines, &ScriptEngines::onClearDebugWindow); + +} + +bool Application::canAcceptURL(const QString& urlString) const { + QUrl url(urlString); + if (url.query().contains(WEB_VIEW_TAG)) { + return false; + } else if (urlString.startsWith(URL_SCHEME_HIFI)) { + return true; + } + QString lowerPath = url.path().toLower(); + for (auto& pair : _acceptedExtensions) { + if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { + return true; + } + } + return false; +} + +bool Application::acceptURL(const QString& urlString, bool defaultUpload) { + QUrl url(urlString); + + if (url.scheme() == URL_SCHEME_HIFI) { + // this is a hifi URL - have the AddressManager handle it + QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", + Qt::AutoConnection, Q_ARG(const QString&, urlString)); + return true; + } + + QString lowerPath = url.path().toLower(); + for (auto& pair : _acceptedExtensions) { + if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { + AcceptURLMethod method = pair.second; + return (this->*method)(urlString); + } + } + + if (defaultUpload && !url.fileName().isEmpty() && url.isLocalFile()) { + showAssetServerWidget(urlString); + } + return defaultUpload; +} + +void Application::setSessionUUID(const QUuid& sessionUUID) const { + Physics::setSessionUUID(sessionUUID); +} + +bool Application::askToSetAvatarUrl(const QString& url) { + QUrl realUrl(url); + if (realUrl.isLocalFile()) { + OffscreenUi::asyncWarning("", "You can not use local files for avatar components."); + return false; + } + + // Download the FST file, to attempt to determine its model type + QVariantHash fstMapping = FSTReader::downloadMapping(url); + + FSTReader::ModelType modelType = FSTReader::predictModelType(fstMapping); + + QString modelName = fstMapping["name"].toString(); + QString modelLicense = fstMapping["license"].toString(); + + bool agreeToLicense = true; // assume true + //create set avatar callback + auto setAvatar = [=] (QString url, QString modelName) { + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Set Avatar", + "Would you like to use '" + modelName + "' for your avatar?", + QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + + bool ok = (QMessageBox::Ok == static_cast(answer.toInt())); + if (ok) { + getMyAvatar()->useFullAvatarURL(url, modelName); + emit fullAvatarURLChanged(url, modelName); + } else { + qCDebug(interfaceapp) << "Declined to use the avatar"; + } + }); + }; + + if (!modelLicense.isEmpty()) { + // word wrap the license text to fit in a reasonable shaped message box. + const int MAX_CHARACTERS_PER_LINE = 90; + modelLicense = simpleWordWrap(modelLicense, MAX_CHARACTERS_PER_LINE); + + ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Avatar Usage License", + modelLicense + "\nDo you agree to these terms?", + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); + QObject::connect(dlg, &ModalDialogListener::response, this, [=, &agreeToLicense] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + + agreeToLicense = (static_cast(answer.toInt()) == QMessageBox::Yes); + if (agreeToLicense) { + switch (modelType) { + case FSTReader::HEAD_AND_BODY_MODEL: { + setAvatar(url, modelName); + break; + } + default: + OffscreenUi::asyncWarning("", modelName + "Does not support a head and body as required."); + break; + } + } else { + qCDebug(interfaceapp) << "Declined to agree to avatar license"; + } + + //auto offscreenUi = getOffscreenUI(); + }); + } else { + setAvatar(url, modelName); + } + + return true; +} + + +bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { + QString shortName = scriptFilenameOrURL; + + QUrl scriptURL { scriptFilenameOrURL }; + + if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + int startIndex = shortName.lastIndexOf('/') + 1; + int endIndex = shortName.lastIndexOf('?'); + shortName = shortName.mid(startIndex, endIndex - startIndex); + } + +#ifdef DISABLE_QML + DependencyManager::get()->loadScript(scriptFilenameOrURL); +#else + QString message = "Would you like to run this script:\n" + shortName; + ModalDialogListener* dlg = OffscreenUi::asyncQuestion(getWindow(), "Run Script", message, + QMessageBox::Yes | QMessageBox::No); + + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + const QString& fileName = scriptFilenameOrURL; + if (static_cast(answer.toInt()) == QMessageBox::Yes) { + qCDebug(interfaceapp) << "Chose to run the script: " << fileName; + DependencyManager::get()->loadScript(fileName); + } else { + qCDebug(interfaceapp) << "Declined to run the script"; + } + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + }); +#endif + return true; +} + +bool Application::askToWearAvatarAttachmentUrl(const QString& url) { + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkRequest networkRequest = QNetworkRequest(url); + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); + QNetworkReply* reply = networkAccessManager.get(networkRequest); + int requestNumber = ++_avatarAttachmentRequest; + connect(reply, &QNetworkReply::finished, [this, reply, url, requestNumber]() { + + if (requestNumber != _avatarAttachmentRequest) { + // this request has been superseded by another more recent request + reply->deleteLater(); + return; + } + + QNetworkReply::NetworkError networkError = reply->error(); + if (networkError == QNetworkReply::NoError) { + // download success + QByteArray contents = reply->readAll(); + + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(contents, &jsonError); + if (jsonError.error == QJsonParseError::NoError) { + + auto jsonObject = doc.object(); + + // retrieve optional name field from JSON + QString name = tr("Unnamed Attachment"); + auto nameValue = jsonObject.value("name"); + if (nameValue.isString()) { + name = nameValue.toString(); + } + + auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation"); + auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name); + ModalDialogListener* dlg = OffscreenUi::asyncQuestion(avatarAttachmentConfirmationTitle, + avatarAttachmentConfirmationMessage, + QMessageBox::Ok | QMessageBox::Cancel); + QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); + if (static_cast(answer.toInt()) == QMessageBox::Yes) { + // add attachment to avatar + auto myAvatar = getMyAvatar(); + assert(myAvatar); + auto attachmentDataVec = myAvatar->getAttachmentData(); + AttachmentData attachmentData; + attachmentData.fromJson(jsonObject); + attachmentDataVec.push_back(attachmentData); + myAvatar->setAttachmentData(attachmentDataVec); + } else { + qCDebug(interfaceapp) << "User declined to wear the avatar attachment"; + } + }); + } else { + // json parse error + auto avatarAttachmentParseErrorString = tr("Error parsing attachment JSON from url: \"%1\""); + displayAvatarAttachmentWarning(avatarAttachmentParseErrorString.arg(url)); + } + } else { + // download failure + auto avatarAttachmentDownloadErrorString = tr("Error downloading attachment JSON from url: \"%1\""); + displayAvatarAttachmentWarning(avatarAttachmentDownloadErrorString.arg(url)); + } + reply->deleteLater(); + }); + return true; +} + +void Application::replaceDomainContent(const QString& url) { + qCDebug(interfaceapp) << "Attempting to replace domain content"; + QByteArray urlData(url.toUtf8()); + auto limitedNodeList = DependencyManager::get(); + const auto& domainHandler = limitedNodeList->getDomainHandler(); + + auto octreeFilePacket = NLPacket::create(PacketType::DomainContentReplacementFromUrl, urlData.size(), true); + octreeFilePacket->write(urlData); + limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr()); + + auto addressManager = DependencyManager::get(); + addressManager->handleLookupString(DOMAIN_SPAWNING_POINT); + QString newHomeAddress = addressManager->getHost() + DOMAIN_SPAWNING_POINT; + qCDebug(interfaceapp) << "Setting new home bookmark to: " << newHomeAddress; + DependencyManager::get()->setHomeLocationToAddress(newHomeAddress); +} + +bool Application::askToReplaceDomainContent(const QString& url) { + QString methodDetails; + const int MAX_CHARACTERS_PER_LINE = 90; + if (DependencyManager::get()->getThisNodeCanReplaceContent()) { + QUrl originURL { url }; + if (originURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { + // Create a confirmation dialog when this call is made + static const QString infoText = simpleWordWrap("Your domain's content will be replaced with a new content set. " + "If you want to save what you have now, create a backup before proceeding. For more information about backing up " + "and restoring content, visit the documentation page at: ", MAX_CHARACTERS_PER_LINE) + + "\nhttps://docs.highfidelity.com/create-and-explore/start-working-in-your-sandbox/restoring-sandbox-content"; + + ModalDialogListener* dig = OffscreenUi::asyncQuestion("Are you sure you want to replace this domain's content set?", + infoText, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); + + QObject::connect(dig, &ModalDialogListener::response, this, [=] (QVariant answer) { + QString details; + if (static_cast(answer.toInt()) == QMessageBox::Yes) { + // Given confirmation, send request to domain server to replace content + replaceDomainContent(url); + details = "SuccessfulRequestToReplaceContent"; + } else { + details = "UserDeclinedToReplaceContent"; + } + QJsonObject messageProperties = { + { "status", details }, + { "content_set_url", url } + }; + UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); + QObject::disconnect(dig, &ModalDialogListener::response, this, nullptr); + }); + } else { + methodDetails = "ContentSetDidNotOriginateFromMarketplace"; + QJsonObject messageProperties = { + { "status", methodDetails }, + { "content_set_url", url } + }; + UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); + } + } else { + methodDetails = "UserDoesNotHavePermissionToReplaceContent"; + static const QString warningMessage = simpleWordWrap("The domain owner must enable 'Replace Content' " + "permissions for you in this domain's server settings before you can continue.", MAX_CHARACTERS_PER_LINE); + OffscreenUi::asyncWarning("You do not have permissions to replace domain content", warningMessage, + QMessageBox::Ok, QMessageBox::Ok); + + QJsonObject messageProperties = { + { "status", methodDetails }, + { "content_set_url", url } + }; + UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); + } + return true; +} + +void Application::displayAvatarAttachmentWarning(const QString& message) const { + auto avatarAttachmentWarningTitle = tr("Avatar Attachment Failure"); + OffscreenUi::asyncWarning(avatarAttachmentWarningTitle, message); +} + +void Application::showDialog(const QUrl& widgetUrl, const QUrl& tabletUrl, const QString& name) const { + auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); + auto hmd = DependencyManager::get(); + bool onTablet = false; + + if (!tablet->getToolbarMode()) { + onTablet = tablet->pushOntoStack(tabletUrl); + if (onTablet) { + toggleTabletUI(true); + } + } else { +#if !defined(DISABLE_QML) + getOffscreenUI()->show(widgetUrl, name); +#endif + } +} + +void Application::showScriptLogs() { + QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); + defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/debugging/debugWindow.js"); + DependencyManager::get()->loadScript(defaultScriptsLoc.toString()); +} + +void Application::showAssetServerWidget(QString filePath) { + if (!DependencyManager::get()->getThisNodeCanWriteAssets() || getLoginDialogPoppedUp()) { + return; + } + static const QUrl url { "hifi/AssetServer.qml" }; + + auto startUpload = [=](QQmlContext* context, QObject* newObject){ + if (!filePath.isEmpty()) { + emit uploadRequest(filePath); + } + }; + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + auto hmd = DependencyManager::get(); + if (tablet->getToolbarMode()) { + getOffscreenUI()->show(url, "AssetServer", startUpload); + } else { + if (!hmd->getShouldShowTablet() && !isHMDMode()) { + getOffscreenUI()->show(url, "AssetServer", startUpload); + } else { + static const QUrl url("hifi/dialogs/TabletAssetServer.qml"); + if (!tablet->isPathLoaded(url)) { + tablet->pushOntoStack(url); + } + } + } + + startUpload(nullptr, nullptr); +} + +void Application::addAssetToWorldFromURL(QString url) { + + QString filename; + if (url.contains("filename")) { + filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. + } + if (url.contains("poly.google.com/downloads")) { + filename = url.section('/', -1); + if (url.contains("noDownload")) { + filename.remove(".zip?noDownload=false"); + } else { + filename.remove(".zip"); + } + + } + + if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { + QString errorInfo = "You do not have permissions to write to the Asset Server."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filename, errorInfo); + return; + } + + addAssetToWorldInfo(filename, "Downloading model file " + filename + "."); + + auto request = DependencyManager::get()->createResourceRequest( + nullptr, QUrl(url), true, -1, "Application::addAssetToWorldFromURL"); + connect(request, &ResourceRequest::finished, this, &Application::addAssetToWorldFromURLRequestFinished); + request->send(); +} + +void Application::addAssetToWorldFromURLRequestFinished() { + auto request = qobject_cast(sender()); + auto url = request->getUrl().toString(); + auto result = request->getResult(); + + QString filename; + bool isBlocks = false; + + if (url.contains("filename")) { + filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. + } + if (url.contains("poly.google.com/downloads")) { + filename = url.section('/', -1); + if (url.contains("noDownload")) { + filename.remove(".zip?noDownload=false"); + } else { + filename.remove(".zip"); + } + isBlocks = true; + } + + if (result == ResourceRequest::Success) { + QTemporaryDir temporaryDir; + temporaryDir.setAutoRemove(false); + if (temporaryDir.isValid()) { + QString temporaryDirPath = temporaryDir.path(); + QString downloadPath = temporaryDirPath + "/" + filename; + + QFile tempFile(downloadPath); + if (tempFile.open(QIODevice::WriteOnly)) { + tempFile.write(request->getData()); + addAssetToWorldInfoClear(filename); // Remove message from list; next one added will have a different key. + tempFile.close(); + qApp->getFileDownloadInterface()->runUnzip(downloadPath, url, true, false, isBlocks); + } else { + QString errorInfo = "Couldn't open temporary file for download"; + qWarning(interfaceapp) << errorInfo; + addAssetToWorldError(filename, errorInfo); + } + } else { + QString errorInfo = "Couldn't create temporary directory for download"; + qWarning(interfaceapp) << errorInfo; + addAssetToWorldError(filename, errorInfo); + } + } else { + qWarning(interfaceapp) << "Error downloading" << url << ":" << request->getResultString(); + addAssetToWorldError(filename, "Error downloading " + filename + " : " + request->getResultString()); + } + + request->deleteLater(); +} + + +QString filenameFromPath(QString filePath) { + return filePath.right(filePath.length() - filePath.lastIndexOf("/") - 1); +} + +void Application::addAssetToWorldUnzipFailure(QString filePath) { + QString filename = filenameFromPath(QUrl(filePath).toLocalFile()); + qWarning(interfaceapp) << "Couldn't unzip file" << filePath; + addAssetToWorldError(filename, "Couldn't unzip file " + filename + "."); +} + +void Application::addAssetToWorld(QString path, QString zipFile, bool isZip, bool isBlocks) { + // Automatically upload and add asset to world as an alternative manual process initiated by showAssetServerWidget(). + QString mapping; + QString filename = filenameFromPath(path); + if (isZip || isBlocks) { + QString assetName = zipFile.section("/", -1).remove(QRegExp("[.]zip(.*)$")); + QString assetFolder = path.section("model_repo/", -1); + mapping = "/" + assetName + "/" + assetFolder; + } else { + mapping = "/" + filename; + } + + // Test repeated because possibly different code paths. + if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { + QString errorInfo = "You do not have permissions to write to the Asset Server."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filename, errorInfo); + return; + } + + addAssetToWorldInfo(filename, "Adding " + mapping.mid(1) + " to the Asset Server."); + + addAssetToWorldWithNewMapping(path, mapping, 0, isZip, isBlocks); +} + +void Application::addAssetToWorldWithNewMapping(QString filePath, QString mapping, int copy, bool isZip, bool isBlocks) { + auto request = DependencyManager::get()->createGetMappingRequest(mapping); + + QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { + const int MAX_COPY_COUNT = 100; // Limit number of duplicate assets; recursion guard. + auto result = request->getError(); + if (result == GetMappingRequest::NotFound) { + addAssetToWorldUpload(filePath, mapping, isZip, isBlocks); + } else if (result != GetMappingRequest::NoError) { + QString errorInfo = "Could not map asset name: " + + mapping.left(mapping.length() - QString::number(copy).length() - 1); + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else if (copy < MAX_COPY_COUNT - 1) { + if (copy > 0) { + mapping = mapping.remove(mapping.lastIndexOf("-"), QString::number(copy).length() + 1); + } + copy++; + mapping = mapping.insert(mapping.lastIndexOf("."), "-" + QString::number(copy)); + addAssetToWorldWithNewMapping(filePath, mapping, copy, isZip, isBlocks); + } else { + QString errorInfo = "Too many copies of asset name: " + + mapping.left(mapping.length() - QString::number(copy).length() - 1); + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } + request->deleteLater(); + }); + + request->start(); +} + +void Application::addAssetToWorldUpload(QString filePath, QString mapping, bool isZip, bool isBlocks) { + qInfo(interfaceapp) << "Uploading" << filePath << "to Asset Server as" << mapping; + auto upload = DependencyManager::get()->createUpload(filePath); + QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable { + if (upload->getError() != AssetUpload::NoError) { + QString errorInfo = "Could not upload model to the Asset Server."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else { + addAssetToWorldSetMapping(filePath, mapping, hash, isZip, isBlocks); + } + + // Remove temporary directory created by Clara.io market place download. + int index = filePath.lastIndexOf("/model_repo/"); + if (index > 0) { + QString tempDir = filePath.left(index); + qCDebug(interfaceapp) << "Removing temporary directory at: " + tempDir; + QDir(tempDir).removeRecursively(); + } + + upload->deleteLater(); + }); + + upload->start(); +} + +void Application::addAssetToWorldSetMapping(QString filePath, QString mapping, QString hash, bool isZip, bool isBlocks) { + auto request = DependencyManager::get()->createSetMappingRequest(mapping, hash); + connect(request, &SetMappingRequest::finished, this, [=](SetMappingRequest* request) mutable { + if (request->getError() != SetMappingRequest::NoError) { + QString errorInfo = "Could not set asset mapping."; + qWarning(interfaceapp) << "Error downloading model: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else { + // to prevent files that aren't models or texture files from being loaded into world automatically + if ((filePath.toLower().endsWith(OBJ_EXTENSION) || filePath.toLower().endsWith(FBX_EXTENSION)) || + ((filePath.toLower().endsWith(JPG_EXTENSION) || filePath.toLower().endsWith(PNG_EXTENSION)) && + ((!isBlocks) && (!isZip)))) { + addAssetToWorldAddEntity(filePath, mapping); + } else { + qCDebug(interfaceapp) << "Zipped contents are not supported entity files"; + addAssetToWorldInfoDone(filenameFromPath(filePath)); + } + } + request->deleteLater(); + }); + + request->start(); +} + +void Application::addAssetToWorldAddEntity(QString filePath, QString mapping) { + EntityItemProperties properties; + properties.setName(mapping.right(mapping.length() - 1)); + if (filePath.toLower().endsWith(PNG_EXTENSION) || filePath.toLower().endsWith(JPG_EXTENSION)) { + properties.setType(EntityTypes::Image); + properties.setImageURL(QString("atp:" + mapping)); + properties.setKeepAspectRatio(false); + } else { + properties.setType(EntityTypes::Model); + properties.setModelURL("atp:" + mapping); + properties.setShapeType(SHAPE_TYPE_SIMPLE_COMPOUND); + } + properties.setCollisionless(true); // Temporarily set so that doesn't collide with avatar. + properties.setVisible(false); // Temporarily set so that don't see at large unresized dimensions. + bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); + properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); + glm::vec3 positionOffset = getMyAvatar()->getWorldOrientation() * (getMyAvatar()->getSensorToWorldScale() * glm::vec3(0.0f, 0.0f, -2.0f)); + properties.setPosition(getMyAvatar()->getWorldPosition() + positionOffset); + properties.setRotation(getMyAvatar()->getWorldOrientation()); + properties.setGravity(glm::vec3(0.0f, 0.0f, 0.0f)); + auto entityID = DependencyManager::get()->addEntity(properties); + + // Note: Model dimensions are not available here; model is scaled per FBX mesh in RenderableModelEntityItem::update() later + // on. But FBX dimensions may be in cm, so we monitor for the dimension change and rescale again if warranted. + + if (entityID == QUuid()) { + QString errorInfo = "Could not add model " + mapping + " to world."; + qWarning(interfaceapp) << "Could not add model to world: " + errorInfo; + addAssetToWorldError(filenameFromPath(filePath), errorInfo); + } else { + // Monitor when asset is rendered in world so that can resize if necessary. + _addAssetToWorldResizeList.insert(entityID, 0); // List value is count of checks performed. + if (!_addAssetToWorldResizeTimer.isActive()) { + _addAssetToWorldResizeTimer.start(); + } + + // Close progress message box. + addAssetToWorldInfoDone(filenameFromPath(filePath)); + } +} + +void Application::addAssetToWorldCheckModelSize() { + if (_addAssetToWorldResizeList.size() == 0) { + return; + } + + auto item = _addAssetToWorldResizeList.begin(); + while (item != _addAssetToWorldResizeList.end()) { + auto entityID = item.key(); + + EntityPropertyFlags propertyFlags; + propertyFlags += PROP_NAME; + propertyFlags += PROP_DIMENSIONS; + auto entityScriptingInterface = DependencyManager::get(); + auto properties = entityScriptingInterface->getEntityProperties(entityID, propertyFlags); + auto name = properties.getName(); + auto dimensions = properties.getDimensions(); + + bool doResize = false; + + const glm::vec3 DEFAULT_DIMENSIONS = glm::vec3(0.1f, 0.1f, 0.1f); + if (dimensions != DEFAULT_DIMENSIONS) { + + // Scale model so that its maximum is exactly specific size. + const float MAXIMUM_DIMENSION = getMyAvatar()->getSensorToWorldScale(); + auto previousDimensions = dimensions; + auto scale = std::min(MAXIMUM_DIMENSION / dimensions.x, std::min(MAXIMUM_DIMENSION / dimensions.y, + MAXIMUM_DIMENSION / dimensions.z)); + dimensions *= scale; + qInfo(interfaceapp) << "Model" << name << "auto-resized from" << previousDimensions << " to " << dimensions; + doResize = true; + + item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. + } else { + // Increment count of checks done. + _addAssetToWorldResizeList[entityID]++; + + const int CHECK_MODEL_SIZE_MAX_CHECKS = 300; + if (_addAssetToWorldResizeList[entityID] > CHECK_MODEL_SIZE_MAX_CHECKS) { + // Have done enough checks; model was either the default size or something's gone wrong. + + // Rescale all dimensions. + const glm::vec3 UNIT_DIMENSIONS = glm::vec3(1.0f, 1.0f, 1.0f); + dimensions = UNIT_DIMENSIONS; + qInfo(interfaceapp) << "Model" << name << "auto-resize timed out; resized to " << dimensions; + doResize = true; + + item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. + } else { + // No action on this entity; advance to next. + ++item; + } + } + + if (doResize) { + EntityItemProperties properties; + properties.setDimensions(dimensions); + properties.setVisible(true); + if (!name.toLower().endsWith(PNG_EXTENSION) && !name.toLower().endsWith(JPG_EXTENSION)) { + properties.setCollisionless(false); + } + bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); + properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); + properties.setLastEdited(usecTimestampNow()); + entityScriptingInterface->editEntity(entityID, properties); + } + } + + // Stop timer if nothing in list to check. + if (_addAssetToWorldResizeList.size() == 0) { + _addAssetToWorldResizeTimer.stop(); + } +} + + +void Application::addAssetToWorldInfo(QString modelName, QString infoText) { + // Displays the most recent info message, subject to being overridden by error messages. + + if (_aboutToQuit) { + return; + } + + /* + Cancel info timer if running. + If list has an entry for modelName, delete it (just one). + Append modelName, infoText to list. + Display infoText in message box unless an error is being displayed (i.e., error timer is running). + Show message box if not already visible. + */ + + _addAssetToWorldInfoTimer.stop(); + + addAssetToWorldInfoClear(modelName); + + _addAssetToWorldInfoKeys.append(modelName); + _addAssetToWorldInfoMessages.append(infoText); + + if (!_addAssetToWorldErrorTimer.isActive()) { + if (!_addAssetToWorldMessageBox) { + _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, + "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); + connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); + } + + _addAssetToWorldMessageBox->setProperty("text", "\n" + infoText); + _addAssetToWorldMessageBox->setVisible(true); + } +} + +void Application::addAssetToWorldInfoClear(QString modelName) { + // Clears modelName entry from message list without affecting message currently displayed. + + if (_aboutToQuit) { + return; + } + + /* + Delete entry for modelName from list. + */ + + auto index = _addAssetToWorldInfoKeys.indexOf(modelName); + if (index > -1) { + _addAssetToWorldInfoKeys.removeAt(index); + _addAssetToWorldInfoMessages.removeAt(index); + } +} + +void Application::addAssetToWorldInfoDone(QString modelName) { + // Continues to display this message if the latest for a few seconds, then deletes it and displays the next latest. + + if (_aboutToQuit) { + return; + } + + /* + Delete entry for modelName from list. + (Re)start the info timer to update message box. ... onAddAssetToWorldInfoTimeout() + */ + + addAssetToWorldInfoClear(modelName); + _addAssetToWorldInfoTimer.start(); +} + +void Application::addAssetToWorldInfoTimeout() { + if (_aboutToQuit) { + return; + } + + /* + If list not empty, display last message in list (may already be displayed ) unless an error is being displayed. + If list empty, close the message box unless an error is being displayed. + */ + + if (!_addAssetToWorldErrorTimer.isActive() && _addAssetToWorldMessageBox) { + if (_addAssetToWorldInfoKeys.length() > 0) { + _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); + } else { + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox->setVisible(false); + _addAssetToWorldMessageBox->deleteLater(); + _addAssetToWorldMessageBox = nullptr; + } + } +} + +void Application::addAssetToWorldError(QString modelName, QString errorText) { + // Displays the most recent error message for a few seconds. + + if (_aboutToQuit) { + return; + } + + /* + If list has an entry for modelName, delete it. + Display errorText in message box. + Show message box if not already visible. + (Re)start error timer. ... onAddAssetToWorldErrorTimeout() + */ + + addAssetToWorldInfoClear(modelName); + + if (!_addAssetToWorldMessageBox) { + _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, + "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); + connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); + } + + _addAssetToWorldMessageBox->setProperty("text", "\n" + errorText); + _addAssetToWorldMessageBox->setVisible(true); + + _addAssetToWorldErrorTimer.start(); +} + +void Application::addAssetToWorldErrorTimeout() { + if (_aboutToQuit) { + return; + } + + /* + If list is not empty, display message from last entry. + If list is empty, close the message box. + */ + + if (_addAssetToWorldMessageBox) { + if (_addAssetToWorldInfoKeys.length() > 0) { + _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); + } else { + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox->setVisible(false); + _addAssetToWorldMessageBox->deleteLater(); + _addAssetToWorldMessageBox = nullptr; + } + } +} + + +void Application::addAssetToWorldMessageClose() { + // Clear messages, e.g., if Interface is being closed or domain changes. + + /* + Call if user manually closes message box. + Call if domain changes. + Call if application is shutting down. + + Stop timers. + Close the message box if open. + Clear lists. + */ + + _addAssetToWorldInfoTimer.stop(); + _addAssetToWorldErrorTimer.stop(); + + if (_addAssetToWorldMessageBox) { + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox->setVisible(false); + _addAssetToWorldMessageBox->deleteLater(); + _addAssetToWorldMessageBox = nullptr; + } + + _addAssetToWorldInfoKeys.clear(); + _addAssetToWorldInfoMessages.clear(); +} + +void Application::onAssetToWorldMessageBoxClosed() { + if (_addAssetToWorldMessageBox) { + // User manually closed message box; perhaps because it has become stuck, so reset all messages. + qInfo(interfaceapp) << "User manually closed download status message box"; + disconnect(_addAssetToWorldMessageBox); + _addAssetToWorldMessageBox = nullptr; + addAssetToWorldMessageClose(); + } +} + + +void Application::handleUnzip(QString zipFile, QStringList unzipFile, bool autoAdd, bool isZip, bool isBlocks) { + if (autoAdd) { + if (!unzipFile.isEmpty()) { + for (int i = 0; i < unzipFile.length(); i++) { + if (QFileInfo(unzipFile.at(i)).isFile()) { + qCDebug(interfaceapp) << "Preparing file for asset server: " << unzipFile.at(i); + addAssetToWorld(unzipFile.at(i), zipFile, isZip, isBlocks); + } + } + } else { + addAssetToWorldUnzipFailure(zipFile); + } + } else { + showAssetServerWidget(unzipFile.first()); + } +} + +void Application::packageModel() { + ModelPackager::package(); +} + +void Application::openUrl(const QUrl& url) const { + if (!url.isEmpty()) { + if (url.scheme() == URL_SCHEME_HIFI) { + DependencyManager::get()->handleLookupString(url.toString()); + } else if (url.scheme() == URL_SCHEME_HIFIAPP) { + DependencyManager::get()->openSystemApp(url.path()); + } else { + // address manager did not handle - ask QDesktopServices to handle + QDesktopServices::openUrl(url); + } + } +} + +void Application::loadDialog() { + ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"), + getPreviousScriptLocation(), + tr("JavaScript Files (*.js)")); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); + const QString& response = answer.toString(); + if (!response.isEmpty() && QFile(response).exists()) { + setPreviousScriptLocation(QFileInfo(response).absolutePath()); + DependencyManager::get()->loadScript(response, true, false, false, true); // Don't load from cache + } + }); +} + +QString Application::getPreviousScriptLocation() { + QString result = _previousScriptLocation.get(); + return result; +} + +void Application::setPreviousScriptLocation(const QString& location) { + _previousScriptLocation.set(location); +} + +void Application::loadScriptURLDialog() const { + ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); + connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { + disconnect(dlg, &ModalDialogListener::response, this, nullptr); + const QString& newScript = response.toString(); + if (QUrl(newScript).scheme() == "atp") { + OffscreenUi::asyncWarning("Error Loading Script", "Cannot load client script over ATP"); + } else if (!newScript.isEmpty()) { + DependencyManager::get()->loadScript(newScript.trimmed()); + } + }); +} + +SharedSoundPointer Application::getSampleSound() const { + return _sampleSound; +} + +void Application::loadLODToolsDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->lodTools(); + } else { + tablet->pushOntoStack("hifi/dialogs/TabletLODTools.qml"); + } +} + +void Application::loadEntityStatisticsDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->octreeStatsDetails(); + } else { + tablet->pushOntoStack("hifi/dialogs/TabletEntityStatistics.qml"); + } +} + +void Application::loadDomainConnectionDialog() { + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { + auto dialogsManager = DependencyManager::get(); + dialogsManager->showDomainConnectionDialog(); + } else { + tablet->pushOntoStack("hifi/dialogs/TabletDCDialog.qml"); + } +} + +void Application::toggleLogDialog() { +#ifndef ANDROID_APP_QUEST_INTERFACE + if (getLoginDialogPoppedUp()) { + return; + } + if (! _logDialog) { + + bool keepOnTop =_keepLogWindowOnTop.get(); +#ifdef Q_OS_WIN + _logDialog = new LogDialog(keepOnTop ? qApp->getWindow() : nullptr, getLogger()); +#elif !defined(Q_OS_ANDROID) + _logDialog = new LogDialog(nullptr, getLogger()); + + if (keepOnTop) { + Qt::WindowFlags flags = _logDialog->windowFlags() | Qt::Tool; + _logDialog->setWindowFlags(flags); + } +#endif + } + + if (_logDialog->isVisible()) { + _logDialog->hide(); + } else { + _logDialog->show(); + } +#endif +} + + void Application::recreateLogWindow(int keepOnTop) { + _keepLogWindowOnTop.set(keepOnTop != 0); + if (_logDialog) { + bool toggle = _logDialog->isVisible(); + _logDialog->close(); + _logDialog = nullptr; + + if (toggle) { + toggleLogDialog(); + } + } + } + +void Application::toggleEntityScriptServerLogDialog() { + if (! _entityScriptServerLogDialog) { + _entityScriptServerLogDialog = new EntityScriptServerLogDialog(nullptr); + } + + if (_entityScriptServerLogDialog->isVisible()) { + _entityScriptServerLogDialog->hide(); + } else { + _entityScriptServerLogDialog->show(); + } +} + +void Application::loadAddAvatarBookmarkDialog() const { + auto avatarBookmarks = DependencyManager::get(); +} + +void Application::loadAvatarBrowser() const { + auto tablet = dynamic_cast(DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + // construct the url to the marketplace item + QString url = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace?category=avatars"; + + QString MARKETPLACES_INJECT_SCRIPT_PATH = "file:///" + qApp->applicationDirPath() + "/scripts/system/html/js/marketplacesInject.js"; + tablet->gotoWebScreen(url, MARKETPLACES_INJECT_SCRIPT_PATH); + DependencyManager::get()->openTablet(); +} + +void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) { + postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] { + // Get a screenshot and save it + QString path = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, + TestScriptingInterface::getInstance()->getTestResultsLocation()); + + // If we're not doing an animated snapshot as well... + if (!includeAnimated) { + if (!path.isEmpty()) { + // Tell the dependency manager that the capture of the still snapshot has taken place. + emit DependencyManager::get()->stillSnapshotTaken(path, notify); + } + } else if (!SnapshotAnimated::isAlreadyTakingSnapshotAnimated()) { + // Get an animated GIF snapshot and save it + SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); + } + }); +} + +void Application::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) { + postLambdaEvent([notify, filename, this] { + QString snapshotPath = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, + TestScriptingInterface::getInstance()->getTestResultsLocation()); + + emit DependencyManager::get()->stillSnapshotTaken(snapshotPath, notify); + }); +} + +void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { + postLambdaEvent([notify, filename, cubemapOutputFormat, cameraPosition] { + DependencyManager::get()->save360Snapshot(cameraPosition, cubemapOutputFormat, notify, filename); + }); +} + +void Application::shareSnapshot(const QString& path, const QUrl& href) { + postLambdaEvent([path, href] { + // not much to do here, everything is done in snapshot code... + DependencyManager::get()->uploadSnapshot(path, href); + }); +} + +float Application::getRenderResolutionScale() const { + auto menu = Menu::getInstance(); + if (!menu) { + return 1.0f; + } + if (menu->isOptionChecked(MenuOption::RenderResolutionOne)) { + return 1.0f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionTwoThird)) { + return 0.666f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionHalf)) { + return 0.5f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionThird)) { + return 0.333f; + } else if (menu->isOptionChecked(MenuOption::RenderResolutionQuarter)) { + return 0.25f; + } else { + return 1.0f; + } +} + +void Application::notifyPacketVersionMismatch() { + if (!_notifiedPacketVersionMismatchThisDomain && !isInterstitialMode()) { + _notifiedPacketVersionMismatchThisDomain = true; + + QString message = "The location you are visiting is running an incompatible server version.\n"; + message += "Content may not display properly."; + + OffscreenUi::asyncWarning("", message); + } +} + +void Application::checkSkeleton() const { + if (getMyAvatar()->getSkeletonModel()->isActive() && !getMyAvatar()->getSkeletonModel()->hasSkeleton()) { + qCDebug(interfaceapp) << "MyAvatar model has no skeleton"; + + QString message = "Your selected avatar body has no skeleton.\n\nThe default body will be loaded..."; + OffscreenUi::asyncWarning("", message); + + getMyAvatar()->useFullAvatarURL(AvatarData::defaultFullAvatarModelUrl(), DEFAULT_FULL_AVATAR_MODEL_NAME); + } else { + _physicsEngine->setCharacterController(getMyAvatar()->getCharacterController()); + } +} + +void Application::activeChanged(Qt::ApplicationState state) { + switch (state) { + case Qt::ApplicationActive: + _isForeground = true; + break; + + case Qt::ApplicationSuspended: + case Qt::ApplicationHidden: + case Qt::ApplicationInactive: + default: + _isForeground = false; + break; + } +} + +void Application::windowMinimizedChanged(bool minimized) { + // initialize the _minimizedWindowTimer + static std::once_flag once; + std::call_once(once, [&] { + connect(&_minimizedWindowTimer, &QTimer::timeout, this, [] { + QCoreApplication::postEvent(QCoreApplication::instance(), new QEvent(static_cast(Idle)), Qt::HighEventPriority); + }); + }); + + // avoid rendering to the display plugin but continue posting Idle events, + // so that physics continues to simulate and the deadlock watchdog knows we're alive + if (!minimized && !getActiveDisplayPlugin()->isActive()) { + _minimizedWindowTimer.stop(); + getActiveDisplayPlugin()->activate(); + } else if (minimized && getActiveDisplayPlugin()->isActive()) { + getActiveDisplayPlugin()->deactivate(); + _minimizedWindowTimer.start(THROTTLED_SIM_FRAME_PERIOD_MS); + } +} + +void Application::postLambdaEvent(const std::function& f) { + if (this->thread() == QThread::currentThread()) { + f(); + } else { + QCoreApplication::postEvent(this, new LambdaEvent(f)); + } +} + +void Application::sendLambdaEvent(const std::function& f) { + if (this->thread() == QThread::currentThread()) { + f(); + } else { + LambdaEvent event(f); + QCoreApplication::sendEvent(this, &event); + } +} + +void Application::initPlugins(const QStringList& arguments) { + QCommandLineOption display("display", "Preferred displays", "displays"); + QCommandLineOption disableDisplays("disable-displays", "Displays to disable", "displays"); + QCommandLineOption disableInputs("disable-inputs", "Inputs to disable", "inputs"); + + QCommandLineParser parser; + parser.addOption(display); + parser.addOption(disableDisplays); + parser.addOption(disableInputs); + parser.parse(arguments); + + if (parser.isSet(display)) { + auto preferredDisplays = parser.value(display).split(',', QString::SkipEmptyParts); + qInfo() << "Setting prefered display plugins:" << preferredDisplays; + PluginManager::getInstance()->setPreferredDisplayPlugins(preferredDisplays); + } + + if (parser.isSet(disableDisplays)) { + auto disabledDisplays = parser.value(disableDisplays).split(',', QString::SkipEmptyParts); + qInfo() << "Disabling following display plugins:" << disabledDisplays; + PluginManager::getInstance()->disableDisplays(disabledDisplays); + } + + if (parser.isSet(disableInputs)) { + auto disabledInputs = parser.value(disableInputs).split(',', QString::SkipEmptyParts); + qInfo() << "Disabling following input plugins:" << disabledInputs; + PluginManager::getInstance()->disableInputs(disabledInputs); + } +} + +void Application::shutdownPlugins() { +} + +glm::uvec2 Application::getCanvasSize() const { + return glm::uvec2(_glWidget->width(), _glWidget->height()); +} + +QRect Application::getRenderingGeometry() const { + auto geometry = _glWidget->geometry(); + auto topLeft = geometry.topLeft(); + auto topLeftScreen = _glWidget->mapToGlobal(topLeft); + geometry.moveTopLeft(topLeftScreen); + return geometry; +} + +glm::uvec2 Application::getUiSize() const { + static const uint MIN_SIZE = 1; + glm::uvec2 result(MIN_SIZE); + if (_displayPlugin) { + result = getActiveDisplayPlugin()->getRecommendedUiSize(); + } + return result; +} + +QRect Application::getRecommendedHUDRect() const { + auto uiSize = getUiSize(); + QRect result(0, 0, uiSize.x, uiSize.y); + if (_displayPlugin) { + result = getActiveDisplayPlugin()->getRecommendedHUDRect(); + } + return result; +} + +glm::vec2 Application::getDeviceSize() const { + static const int MIN_SIZE = 1; + glm::vec2 result(MIN_SIZE); + if (_displayPlugin) { + result = getActiveDisplayPlugin()->getRecommendedRenderSize(); + } + return result; +} + +bool Application::isThrottleRendering() const { + if (_displayPlugin) { + return getActiveDisplayPlugin()->isThrottled(); + } + return false; +} + +bool Application::hasFocus() const { + bool result = (QApplication::activeWindow() != nullptr); +#if defined(Q_OS_WIN) + // On Windows, QWidget::activateWindow() - as called in setFocus() - makes the application's taskbar icon flash but doesn't + // take user focus away from their current window. So also check whether the application is the user's current foreground + // window. + result = result && (HWND)QApplication::activeWindow()->winId() == GetForegroundWindow(); +#endif + return result; +} + +void Application::setFocus() { + // Note: Windows doesn't allow a user focus to be taken away from another application. Instead, it changes the color of and + // flashes the taskbar icon. + auto window = qApp->getWindow(); + window->activateWindow(); +} + +void Application::raise() { + auto windowState = qApp->getWindow()->windowState(); + if (windowState & Qt::WindowMinimized) { + if (windowState & Qt::WindowMaximized) { + qApp->getWindow()->showMaximized(); + } else if (windowState & Qt::WindowFullScreen) { + qApp->getWindow()->showFullScreen(); + } else { + qApp->getWindow()->showNormal(); + } + } + qApp->getWindow()->raise(); +} + +void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) { + if (maxOctreePPS != _maxOctreePPS) { + _maxOctreePPS = maxOctreePPS; + maxOctreePacketsPerSecond.set(_maxOctreePPS); + } +} + +int Application::getMaxOctreePacketsPerSecond() const { + return _maxOctreePPS; +} + +qreal Application::getDevicePixelRatio() { + return (_window && _window->windowHandle()) ? _window->windowHandle()->devicePixelRatio() : 1.0; +} + +DisplayPluginPointer Application::getActiveDisplayPlugin() const { + if (QThread::currentThread() != thread()) { + std::unique_lock lock(_displayPluginLock); + return _displayPlugin; + } + + if (!_aboutToQuit && !_displayPlugin) { + const_cast(this)->updateDisplayMode(); + Q_ASSERT(_displayPlugin); + } + return _displayPlugin; +} + + +#if !defined(DISABLE_QML) +static const char* EXCLUSION_GROUP_KEY = "exclusionGroup"; + +static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active) { + auto menu = Menu::getInstance(); + QString name = displayPlugin->getName(); + auto grouping = displayPlugin->getGrouping(); + QString groupingMenu { "" }; + Q_ASSERT(!menu->menuItemExists(MenuOption::OutputMenu, name)); + + // assign the meny grouping based on plugin grouping + switch (grouping) { + case Plugin::ADVANCED: + groupingMenu = "Advanced"; + break; + case Plugin::DEVELOPER: + groupingMenu = "Developer"; + break; + default: + groupingMenu = "Standard"; + break; + } + + static QActionGroup* displayPluginGroup = nullptr; + if (!displayPluginGroup) { + displayPluginGroup = new QActionGroup(menu); + displayPluginGroup->setExclusive(true); + } + auto parent = menu->getMenu(MenuOption::OutputMenu); + auto action = menu->addActionToQMenuAndActionHash(parent, + name, QKeySequence(Qt::CTRL + (Qt::Key_0 + index)), qApp, + SLOT(updateDisplayMode()), + QAction::NoRole, Menu::UNSPECIFIED_POSITION, groupingMenu); + + action->setCheckable(true); + action->setChecked(active); + displayPluginGroup->addAction(action); + + action->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(displayPluginGroup)); + Q_ASSERT(menu->menuItemExists(MenuOption::OutputMenu, name)); +} +#endif + +void Application::updateDisplayMode() { + // Unsafe to call this method from anything but the main thread + if (QThread::currentThread() != thread()) { + qFatal("Attempted to switch display plugins from a non-main thread"); + } + + // Once time initialization code that depends on the UI being available + auto displayPlugins = getDisplayPlugins(); + + // Default to the first item on the list, in case none of the menu items match + DisplayPluginPointer newDisplayPlugin = displayPlugins.at(0); + auto menu = getPrimaryMenu(); + if (menu) { + foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { + QString name = displayPlugin->getName(); + QAction* action = menu->getActionForOption(name); + // Menu might have been removed if the display plugin lost + if (!action) { + continue; + } + if (action->isChecked()) { + newDisplayPlugin = displayPlugin; + break; + } + } + } + + if (newDisplayPlugin == _displayPlugin) { + return; + } + + setDisplayPlugin(newDisplayPlugin); +} + +void Application::setDisplayPlugin(DisplayPluginPointer newDisplayPlugin) { + if (newDisplayPlugin == _displayPlugin) { + return; + } + + // FIXME don't have the application directly set the state of the UI, + // instead emit a signal that the display plugin is changing and let + // the desktop lock itself. Reduces coupling between the UI and display + // plugins + auto offscreenUi = getOffscreenUI(); + auto desktop = offscreenUi ? offscreenUi->getDesktop() : nullptr; + auto menu = Menu::getInstance(); + + // Make the switch atomic from the perspective of other threads + { + std::unique_lock lock(_displayPluginLock); + bool wasRepositionLocked = false; + if (desktop) { + // Tell the desktop to no reposition (which requires plugin info), until we have set the new plugin, below. + wasRepositionLocked = desktop->property("repositionLocked").toBool(); + desktop->setProperty("repositionLocked", true); + } + + if (_displayPlugin) { + disconnect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent); + _displayPlugin->deactivate(); + } + + auto oldDisplayPlugin = _displayPlugin; + bool active = newDisplayPlugin->activate(); + + if (!active) { + auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); + + // If the new plugin fails to activate, fallback to last display + qWarning() << "Failed to activate display: " << newDisplayPlugin->getName(); + newDisplayPlugin = oldDisplayPlugin; + + if (newDisplayPlugin) { + qWarning() << "Falling back to last display: " << newDisplayPlugin->getName(); + active = newDisplayPlugin->activate(); + } + + // If there is no last display, or + // If the last display fails to activate, fallback to desktop + if (!active) { + newDisplayPlugin = displayPlugins.at(0); + qWarning() << "Falling back to display: " << newDisplayPlugin->getName(); + active = newDisplayPlugin->activate(); + } + + if (!active) { + qFatal("Failed to activate fallback plugin"); + } + } + + if (offscreenUi) { + offscreenUi->resize(fromGlm(newDisplayPlugin->getRecommendedUiSize())); + } + getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); + _displayPlugin = newDisplayPlugin; + connect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent, Qt::DirectConnection); + if (desktop) { + desktop->setProperty("repositionLocked", wasRepositionLocked); + } + } + + bool isHmd = _displayPlugin->isHmd(); + qCDebug(interfaceapp) << "Entering into" << (isHmd ? "HMD" : "Desktop") << "Mode"; + + // Only log/emit after a successful change + UserActivityLogger::getInstance().logAction("changed_display_mode", { + { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, + { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" }, + { "hmd", isHmd } + }); + emit activeDisplayPluginChanged(); + + // reset the avatar, to set head and hand palms back to a reasonable default pose. + getMyAvatar()->reset(false); + + // switch to first person if entering hmd and setting is checked + if (menu) { + QAction* action = menu->getActionForOption(newDisplayPlugin->getName()); + if (action) { + action->setChecked(true); + } + + if (isHmd && menu->isOptionChecked(MenuOption::FirstPersonHMD)) { + menu->setIsOptionChecked(MenuOption::FirstPerson, true); + cameraMenuChanged(); + } + + // Remove the mirror camera option from menu if in HMD mode + auto mirrorAction = menu->getActionForOption(MenuOption::FullscreenMirror); + mirrorAction->setVisible(!isHmd); + } + + Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); +} + +void Application::switchDisplayMode() { + if (!_autoSwitchDisplayModeSupportedHMDPlugin) { + return; + } + bool currentHMDWornStatus = _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); + if (currentHMDWornStatus != _previousHMDWornStatus) { + // Switch to respective mode as soon as currentHMDWornStatus changes + if (currentHMDWornStatus) { + qCDebug(interfaceapp) << "Switching from Desktop to HMD mode"; + endHMDSession(); + setActiveDisplayPlugin(_autoSwitchDisplayModeSupportedHMDPluginName); + } else { + qCDebug(interfaceapp) << "Switching from HMD to desktop mode"; + setActiveDisplayPlugin(DESKTOP_DISPLAY_PLUGIN_NAME); + startHMDStandBySession(); + } + } + _previousHMDWornStatus = currentHMDWornStatus; +} + +void Application::setShowBulletWireframe(bool value) { + _physicsEngine->setShowBulletWireframe(value); +} + +void Application::setShowBulletAABBs(bool value) { + _physicsEngine->setShowBulletAABBs(value); +} + +void Application::setShowBulletContactPoints(bool value) { + _physicsEngine->setShowBulletContactPoints(value); +} + +void Application::setShowBulletConstraints(bool value) { + _physicsEngine->setShowBulletConstraints(value); +} + +void Application::setShowBulletConstraintLimits(bool value) { + _physicsEngine->setShowBulletConstraintLimits(value); +} + +void Application::createLoginDialog() { + const glm::vec3 LOGIN_DIMENSIONS { 0.89f, 0.5f, 0.01f }; + const auto OFFSET = glm::vec2(0.7f, -0.1f); + auto cameraPosition = _myCamera.getPosition(); + auto cameraOrientation = _myCamera.getOrientation(); + auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; + auto headLookVec = (cameraOrientation * Vectors::FRONT); + // DEFAULT_DPI / tablet scale percentage + const float DPI = 31.0f / (75.0f / 100.0f); + auto offset = headLookVec * OFFSET.x; + auto position = (cameraPosition + offset) + (upVec * OFFSET.y); + + EntityItemProperties properties; + properties.setType(EntityTypes::Web); + properties.setName("LoginDialogEntity"); + properties.setSourceUrl(LOGIN_DIALOG.toString()); + properties.setPosition(position); + properties.setRotation(cameraOrientation); + properties.setDimensions(LOGIN_DIMENSIONS); + properties.setPrimitiveMode(PrimitiveMode::SOLID); + properties.getGrab().setGrabbable(false); + properties.setIgnorePickIntersection(false); + properties.setAlpha(1.0f); + properties.setDPI(DPI); + properties.setVisible(true); + + auto entityScriptingInterface = DependencyManager::get(); + _loginDialogID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); + + auto keyboard = DependencyManager::get().data(); + if (!keyboard->getAnchorID().isNull() && !_loginDialogID.isNull()) { + auto keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); + + EntityItemProperties properties; + properties.setPosition(position + keyboardLocalOffset); + properties.setRotation(cameraOrientation * Quaternions::Y_180); + + entityScriptingInterface->editEntity(keyboard->getAnchorID(), properties); + keyboard->setResetKeyboardPositionOnRaise(false); + } + setKeyboardFocusEntity(_loginDialogID); + emit loginDialogFocusEnabled(); + getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(false); + getApplicationCompositor().getReticleInterface()->setVisible(false); + if (!_loginStateManager.isSetUp()) { + _loginStateManager.setUp(); + } +} + +void Application::updateLoginDialogPosition() { + const float LOOK_AWAY_THRESHOLD_ANGLE = 70.0f; + const auto OFFSET = glm::vec2(0.7f, -0.1f); + + auto entityScriptingInterface = DependencyManager::get(); + EntityPropertyFlags desiredProperties; + desiredProperties += PROP_POSITION; + auto properties = entityScriptingInterface->getEntityProperties(_loginDialogID, desiredProperties); + auto positionVec = properties.getPosition(); + auto cameraPositionVec = _myCamera.getPosition(); + auto cameraOrientation = cancelOutRollAndPitch(_myCamera.getOrientation()); + auto headLookVec = (cameraOrientation * Vectors::FRONT); + auto entityToHeadVec = positionVec - cameraPositionVec; + auto pointAngle = (glm::acos(glm::dot(glm::normalize(entityToHeadVec), glm::normalize(headLookVec))) * 180.0f / PI); + auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; + auto offset = headLookVec * OFFSET.x; + auto newPositionVec = (cameraPositionVec + offset) + (upVec * OFFSET.y); + + bool outOfBounds = glm::distance(positionVec, cameraPositionVec) > 1.0f; + + if (pointAngle > LOOK_AWAY_THRESHOLD_ANGLE || outOfBounds) { + { + EntityItemProperties properties; + properties.setPosition(newPositionVec); + properties.setRotation(cameraOrientation); + entityScriptingInterface->editEntity(_loginDialogID, properties); + } + + { + glm::vec3 keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); + glm::quat keyboardOrientation = cameraOrientation * glm::quat(glm::radians(glm::vec3(-30.0f, 180.0f, 0.0f))); + + EntityItemProperties properties; + properties.setPosition(newPositionVec + keyboardLocalOffset); + properties.setRotation(keyboardOrientation); + entityScriptingInterface->editEntity(DependencyManager::get()->getAnchorID(), properties); + } + } +} + +bool Application::hasRiftControllers() { + return PluginUtils::isOculusTouchControllerAvailable(); +} + +bool Application::hasViveControllers() { + return PluginUtils::isViveControllerAvailable(); +} + +void Application::onDismissedLoginDialog() { + _loginDialogPoppedUp = false; + loginDialogPoppedUp.set(false); + auto keyboard = DependencyManager::get().data(); + keyboard->setResetKeyboardPositionOnRaise(true); + if (!_loginDialogID.isNull()) { + DependencyManager::get()->deleteEntity(_loginDialogID); + _loginDialogID = QUuid(); + _loginStateManager.tearDown(); + } + resumeAfterLoginDialogActionTaken(); +} + +void Application::setShowTrackedObjects(bool value) { + _showTrackedObjects = value; +} + +void Application::startHMDStandBySession() { + _autoSwitchDisplayModeSupportedHMDPlugin->startStandBySession(); +} + +void Application::endHMDSession() { + _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); +} + +mat4 Application::getEyeProjection(int eye) const { + QMutexLocker viewLocker(&_viewMutex); + if (isHMDMode()) { + return getActiveDisplayPlugin()->getEyeProjection((Eye)eye, _viewFrustum.getProjection()); + } + return _viewFrustum.getProjection(); +} + +mat4 Application::getEyeOffset(int eye) const { + // FIXME invert? + return getActiveDisplayPlugin()->getEyeToHeadTransform((Eye)eye); +} + +mat4 Application::getHMDSensorPose() const { + if (isHMDMode()) { + return getActiveDisplayPlugin()->getHeadPose(); + } + return mat4(); +} + +void Application::deadlockApplication() { + qCDebug(interfaceapp) << "Intentionally deadlocked Interface"; + // Using a loop that will *technically* eventually exit (in ~600 billion years) + // to avoid compiler warnings about a loop that will never exit + for (uint64_t i = 1; i != 0; ++i) { + QThread::sleep(1); + } +} + +// cause main thread to be unresponsive for 35 seconds +void Application::unresponsiveApplication() { + // to avoid compiler warnings about a loop that will never exit + uint64_t start = usecTimestampNow(); + uint64_t UNRESPONSIVE_FOR_SECONDS = 35; + uint64_t UNRESPONSIVE_FOR_USECS = UNRESPONSIVE_FOR_SECONDS * USECS_PER_SECOND; + qCDebug(interfaceapp) << "Intentionally cause Interface to be unresponsive for " << UNRESPONSIVE_FOR_SECONDS << " seconds"; + while (usecTimestampNow() - start < UNRESPONSIVE_FOR_USECS) { + QThread::sleep(1); + } +} + +void Application::setActiveDisplayPlugin(const QString& pluginName) { + DisplayPluginPointer newDisplayPlugin; + for (DisplayPluginPointer displayPlugin : PluginManager::getInstance()->getDisplayPlugins()) { + QString name = displayPlugin->getName(); + if (pluginName == name) { + newDisplayPlugin = displayPlugin; + break; + } + } + + if (newDisplayPlugin) { + setDisplayPlugin(newDisplayPlugin); + } +} + +void Application::handleLocalServerConnection() const { + auto server = qobject_cast(sender()); + + qCDebug(interfaceapp) << "Got connection on local server from additional instance - waiting for parameters"; + + auto socket = server->nextPendingConnection(); + + connect(socket, &QLocalSocket::readyRead, this, &Application::readArgumentsFromLocalSocket); + + qApp->getWindow()->raise(); + qApp->getWindow()->activateWindow(); +} + +void Application::readArgumentsFromLocalSocket() const { + auto socket = qobject_cast(sender()); + + auto message = socket->readAll(); + socket->deleteLater(); + + qCDebug(interfaceapp) << "Read from connection: " << message; + + // If we received a message, try to open it as a URL + if (message.length() > 0) { + qApp->openUrl(QString::fromUtf8(message)); + } +} + +void Application::showDesktop() { +} + +CompositorHelper& Application::getApplicationCompositor() const { + return *DependencyManager::get(); +} + + +// virtual functions required for PluginContainer +ui::Menu* Application::getPrimaryMenu() { + auto appMenu = _window->menuBar(); + auto uiMenu = dynamic_cast(appMenu); + return uiMenu; +} + +void Application::showDisplayPluginsTools(bool show) { + DependencyManager::get()->hmdTools(show); +} + +GLWidget* Application::getPrimaryWidget() { + return _glWidget; +} + +MainWindow* Application::getPrimaryWindow() { + return getWindow(); +} + +QOpenGLContext* Application::getPrimaryContext() { + return _glWidget->qglContext(); +} + +bool Application::makeRenderingContextCurrent() { + return true; +} + +bool Application::isForeground() const { + return _isForeground && !_window->isMinimized(); +} + +// FIXME? perhaps two, one for the main thread and one for the offscreen UI rendering thread? +static const int UI_RESERVED_THREADS = 1; +// Windows won't let you have all the cores +static const int OS_RESERVED_THREADS = 1; + +void Application::updateThreadPoolCount() const { + auto reservedThreads = UI_RESERVED_THREADS + OS_RESERVED_THREADS + _displayPlugin->getRequiredThreadCount(); + auto availableThreads = QThread::idealThreadCount() - reservedThreads; + auto threadPoolSize = std::max(MIN_PROCESSING_THREAD_POOL_SIZE, availableThreads); + qCDebug(interfaceapp) << "Ideal Thread Count " << QThread::idealThreadCount(); + qCDebug(interfaceapp) << "Reserved threads " << reservedThreads; + qCDebug(interfaceapp) << "Setting thread pool size to " << threadPoolSize; + QThreadPool::globalInstance()->setMaxThreadCount(threadPoolSize); +} + +void Application::updateSystemTabletMode() { + if (_settingsLoaded && !getLoginDialogPoppedUp()) { + qApp->setProperty(hifi::properties::HMD, isHMDMode()); + if (isHMDMode()) { + DependencyManager::get()->setToolbarMode(getHmdTabletBecomesToolbarSetting()); + } else { + DependencyManager::get()->setToolbarMode(getDesktopTabletBecomesToolbarSetting()); + } + } +} + +QUuid Application::getTabletScreenID() const { + auto HMD = DependencyManager::get(); + return HMD->getCurrentTabletScreenID(); +} + +QUuid Application::getTabletHomeButtonID() const { + auto HMD = DependencyManager::get(); + return HMD->getCurrentHomeButtonID(); +} + +QUuid Application::getTabletFrameID() const { + auto HMD = DependencyManager::get(); + return HMD->getCurrentTabletFrameID(); +} + +QVector Application::getTabletIDs() const { + // Most important first. + QVector result; + auto HMD = DependencyManager::get(); + result << HMD->getCurrentTabletScreenID(); + result << HMD->getCurrentHomeButtonID(); + result << HMD->getCurrentTabletFrameID(); + return result; +} + +void Application::setAvatarOverrideUrl(const QUrl& url, bool save) { + _avatarOverrideUrl = url; + _saveAvatarOverrideUrl = save; +} + +void Application::saveNextPhysicsStats(QString filename) { + _physicsEngine->saveNextPhysicsStats(filename); +} + +void Application::copyToClipboard(const QString& text) { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "copyToClipboard"); + return; + } + + // assume that the address is being copied because the user wants a shareable address + QApplication::clipboard()->setText(text); +} + +QString Application::getGraphicsCardType() { + return GPUIdent::getInstance()->getName(); +} + +#if defined(Q_OS_ANDROID) +void Application::beforeEnterBackground() { + auto nodeList = DependencyManager::get(); + nodeList->setSendDomainServerCheckInEnabled(false); + nodeList->reset(true); + clearDomainOctreeDetails(); +} + + + +void Application::enterBackground() { + QMetaObject::invokeMethod(DependencyManager::get().data(), + "stop", Qt::BlockingQueuedConnection); +// Quest only supports one plugin which can't be deactivated currently +#if !defined(ANDROID_APP_QUEST_INTERFACE) + if (getActiveDisplayPlugin()->isActive()) { + getActiveDisplayPlugin()->deactivate(); + } +#endif +} + +void Application::enterForeground() { + QMetaObject::invokeMethod(DependencyManager::get().data(), + "start", Qt::BlockingQueuedConnection); +// Quest only supports one plugin which can't be deactivated currently +#if !defined(ANDROID_APP_QUEST_INTERFACE) + if (!getActiveDisplayPlugin() || getActiveDisplayPlugin()->isActive() || !getActiveDisplayPlugin()->activate()) { + qWarning() << "Could not re-activate display plugin"; + } +#endif + auto nodeList = DependencyManager::get(); + nodeList->setSendDomainServerCheckInEnabled(true); +} + + +void Application::toggleAwayMode(){ + QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier); + QCoreApplication::sendEvent (this, &event); +} + + +#endif + + +#include "Application.moc" diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 852c4eb695..6d9a1823a1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1795,7 +1795,19 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { - resumeAfterLoginDialogActionTaken(); +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) + // Do not show login dialog if requested not to on the command line + QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); + int index = arguments().indexOf(hifiNoLoginCommandLineKey); + if (index != -1) { + resumeAfterLoginDialogActionTaken(); + return; + } + + showLoginScreen(); +#else + resumeAfterLoginDialogActionTaken(); +#endif }); // Make sure we don't time out during slow operations at startup From 4c7b292e1afc2961cef5599a800b70f2d06e2c87 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 10:25:40 -0700 Subject: [PATCH 07/63] Remove unneeded wait. --- tools/nitpick/src/TestCreator.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/nitpick/src/TestCreator.cpp b/tools/nitpick/src/TestCreator.cpp index 089e84904a..17a191315c 100644 --- a/tools/nitpick/src/TestCreator.cpp +++ b/tools/nitpick/src/TestCreator.cpp @@ -851,10 +851,7 @@ void TestCreator::createRecursiveScript(const QString& directory, bool interacti textStream << " nitpick = createNitpick(Script.resolvePath(\".\"));" << endl; textStream << " testsRootPath = nitpick.getTestsRootPath();" << endl << endl; textStream << " nitpick.enableRecursive();" << endl; - textStream << " nitpick.enableAuto();" << endl << endl; - textStream << " if (typeof Test !== 'undefined') {" << endl; - textStream << " Test.wait(10000);" << endl; - textStream << " }" << endl; + textStream << " nitpick.enableAuto();" << endl; textStream << "} else {" << endl; textStream << " depth++" << endl; textStream << "}" << endl << endl; From 858d80073faa16dac126566cfebb7f3a2bc43428 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 13:27:36 -0700 Subject: [PATCH 08/63] Change default snapshots folder. --- tools/nitpick/src/TestRunnerMobile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index eaebb6ca5a..ed48c37290 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -43,7 +43,7 @@ TestRunnerMobile::TestRunnerMobile( _installAPKPushbutton = installAPKPushbutton; _runInterfacePushbutton = runInterfacePushbutton; - folderLineEdit->setText("/sdcard/DCIM/TEST"); + folderLineEdit->setText("/sdcard/snapshots"); modelNames["SM_G955U1"] = "Samsung S8+ unlocked"; modelNames["SM_N960U1"] = "Samsung Note 9 unlocked"; From 643fbfd80538a95e3fc880c27f7b7877f7bd2829 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 14:06:45 -0700 Subject: [PATCH 09/63] TEST TEST TEST --- interface/src/Application.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index de4a6bb167..7dddaecadb 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1804,7 +1804,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) // Do not show login dialog if requested not to on the command line QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); int index = arguments().indexOf(hifiNoLoginCommandLineKey); @@ -1814,9 +1813,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } showLoginScreen(); -#else - resumeAfterLoginDialogActionTaken(); -#endif }); // Make sure we don't time out during slow operations at startup From 647b508b9d005584cd2806f87d69b7ff42955b7f Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 14:53:19 -0700 Subject: [PATCH 10/63] TEST TEST TEST --- interface/src/Application - Copy.cpp | 9191 -------------------------- 1 file changed, 9191 deletions(-) delete mode 100644 interface/src/Application - Copy.cpp diff --git a/interface/src/Application - Copy.cpp b/interface/src/Application - Copy.cpp deleted file mode 100644 index ca8883f660..0000000000 --- a/interface/src/Application - Copy.cpp +++ /dev/null @@ -1,9191 +0,0 @@ -// -// Application.cpp -// interface/src -// -// Created by Andrzej Kapolka on 5/10/13. -// Copyright 2013 High Fidelity, Inc. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - - -#include "Application.h" - -#include -#include - -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -#include - -#include -#include -#include - - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "ui/overlays/ContextOverlayInterface.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "LocationBookmarks.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "recording/ClipCache.h" - -#include "AudioClient.h" -#include "audio/AudioScope.h" -#include "avatar/AvatarManager.h" -#include "avatar/MyHead.h" -#include "avatar/AvatarPackager.h" -#include "avatar/MyCharacterController.h" -#include "CrashRecoveryHandler.h" -#include "CrashHandler.h" -#include "devices/DdeFaceTracker.h" -#include "DiscoverabilityManager.h" -#include "GLCanvas.h" -#include "InterfaceDynamicFactory.h" -#include "InterfaceLogging.h" -#include "LODManager.h" -#include "ModelPackager.h" -#include "scripting/Audio.h" -#include "networking/CloseEventSender.h" -#include "scripting/TestScriptingInterface.h" -#include "scripting/PlatformInfoScriptingInterface.h" -#include "scripting/AssetMappingsScriptingInterface.h" -#include "scripting/ClipboardScriptingInterface.h" -#include "scripting/DesktopScriptingInterface.h" -#include "scripting/AccountServicesScriptingInterface.h" -#include "scripting/HMDScriptingInterface.h" -#include "scripting/MenuScriptingInterface.h" -#include "graphics-scripting/GraphicsScriptingInterface.h" -#include "scripting/SettingsScriptingInterface.h" -#include "scripting/WindowScriptingInterface.h" -#include "scripting/ControllerScriptingInterface.h" -#include "scripting/RatesScriptingInterface.h" -#include "scripting/SelectionScriptingInterface.h" -#include "scripting/WalletScriptingInterface.h" -#include "scripting/TTSScriptingInterface.h" -#include "scripting/KeyboardScriptingInterface.h" - - - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) -#include "SpeechRecognizer.h" -#endif -#include "ui/ResourceImageItem.h" -#include "ui/AddressBarDialog.h" -#include "ui/AvatarInputs.h" -#include "ui/DialogsManager.h" -#include "ui/LoginDialog.h" -#include "ui/Snapshot.h" -#include "ui/SnapshotAnimated.h" -#include "ui/StandAloneJSConsole.h" -#include "ui/Stats.h" -#include "ui/AnimStats.h" -#include "ui/UpdateDialog.h" -#include "ui/DomainConnectionModel.h" -#include "ui/Keyboard.h" -#include "Util.h" -#include "InterfaceParentFinder.h" -#include "ui/OctreeStatsProvider.h" - -#include "avatar/GrabManager.h" - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include "commerce/Ledger.h" -#include "commerce/Wallet.h" -#include "commerce/QmlCommerce.h" -#include "commerce/QmlMarketplace.h" -#include "ResourceRequestObserver.h" - -#include "webbrowser/WebBrowserSuggestionsEngine.h" -#include - - -#include "AboutUtil.h" - -#if defined(Q_OS_WIN) -#include - -#ifdef DEBUG_EVENT_QUEUE -// This is a HACK that uses private headers included with the qt source distrubution. -// To use this feature you need to add these directores to your include path: -// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1/QtCore -// E:/Qt/5.10.1/Src/qtbase/include/QtCore/5.10.1 -#define QT_BOOTSTRAPPED -#include -#include -#undef QT_BOOTSTRAPPED -#endif - -// On Windows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU -// FIXME seems to be broken. -extern "C" { - _declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; -} -#endif - -#if defined(Q_OS_ANDROID) -#include -#include "AndroidHelper.h" -#endif - -#include "graphics/RenderEventHandler.h" - -Q_LOGGING_CATEGORY(trace_app_input_mouse, "trace.app.input.mouse") - -using namespace std; - -static QTimer locationUpdateTimer; -static QTimer identityPacketTimer; -static QTimer pingTimer; - -#if defined(Q_OS_ANDROID) -static bool DISABLE_WATCHDOG = true; -#else -static const QString DISABLE_WATCHDOG_FLAG{ "HIFI_DISABLE_WATCHDOG" }; -static bool DISABLE_WATCHDOG = nsightActive() || QProcessEnvironment::systemEnvironment().contains(DISABLE_WATCHDOG_FLAG); -#endif - -#if defined(USE_GLES) -static bool DISABLE_DEFERRED = true; -#else -static const QString RENDER_FORWARD{ "HIFI_RENDER_FORWARD" }; -static bool DISABLE_DEFERRED = QProcessEnvironment::systemEnvironment().contains(RENDER_FORWARD); -#endif - -#if !defined(Q_OS_ANDROID) -static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 16; -#else -static const uint32_t MAX_CONCURRENT_RESOURCE_DOWNLOADS = 4; -#endif - -// For processing on QThreadPool, we target a number of threads after reserving some -// based on how many are being consumed by the application and the display plugin. However, -// we will never drop below the 'min' value -static const int MIN_PROCESSING_THREAD_POOL_SIZE = 1; - -static const QString SNAPSHOT_EXTENSION = ".jpg"; -static const QString JPG_EXTENSION = ".jpg"; -static const QString PNG_EXTENSION = ".png"; -static const QString SVO_EXTENSION = ".svo"; -static const QString SVO_JSON_EXTENSION = ".svo.json"; -static const QString JSON_GZ_EXTENSION = ".json.gz"; -static const QString JSON_EXTENSION = ".json"; -static const QString JS_EXTENSION = ".js"; -static const QString FST_EXTENSION = ".fst"; -static const QString FBX_EXTENSION = ".fbx"; -static const QString OBJ_EXTENSION = ".obj"; -static const QString AVA_JSON_EXTENSION = ".ava.json"; -static const QString WEB_VIEW_TAG = "noDownload=true"; -static const QString ZIP_EXTENSION = ".zip"; -static const QString CONTENT_ZIP_EXTENSION = ".content.zip"; - -static const float MIRROR_FULLSCREEN_DISTANCE = 0.789f; - -static const quint64 TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS = 1 * USECS_PER_SECOND; - -static const QString INFO_EDIT_ENTITIES_PATH = "html/edit-commands.html"; -static const QString INFO_HELP_PATH = "html/tabletHelp.html"; - -static const unsigned int THROTTLED_SIM_FRAMERATE = 15; -static const int THROTTLED_SIM_FRAME_PERIOD_MS = MSECS_PER_SECOND / THROTTLED_SIM_FRAMERATE; -static const int ENTITY_SERVER_ADDED_TIMEOUT = 5000; -static const int ENTITY_SERVER_CONNECTION_TIMEOUT = 5000; - -static const float INITIAL_QUERY_RADIUS = 10.0f; // priority radius for entities before physics enabled - -static const QString DESKTOP_LOCATION = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); - -Setting::Handle maxOctreePacketsPerSecond{"maxOctreePPS", DEFAULT_MAX_OCTREE_PPS}; - -Setting::Handle loginDialogPoppedUp{"loginDialogPoppedUp", false}; - -static const QString STANDARD_TO_ACTION_MAPPING_NAME = "Standard to Action"; -static const QString NO_MOVEMENT_MAPPING_NAME = "Standard to Action (No Movement)"; -static const QString NO_MOVEMENT_MAPPING_JSON = PathUtils::resourcesPath() + "/controllers/standard_nomovement.json"; - -static const QString MARKETPLACE_CDN_HOSTNAME = "mpassets.highfidelity.com"; -static const int INTERVAL_TO_CHECK_HMD_WORN_STATUS = 500; // milliseconds -static const QString DESKTOP_DISPLAY_PLUGIN_NAME = "Desktop"; -static const QString ACTIVE_DISPLAY_PLUGIN_SETTING_NAME = "activeDisplayPlugin"; -static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; -static const QString KEEP_ME_LOGGED_IN_SETTING_NAME = "keepMeLoggedIn"; - -static const float FOCUS_HIGHLIGHT_EXPANSION_FACTOR = 1.05f; - -#if defined(Q_OS_ANDROID) -static const QString TESTER_FILE = "/sdcard/_hifi_test_device.txt"; -#endif -const std::vector> Application::_acceptedExtensions { - { SVO_EXTENSION, &Application::importSVOFromURL }, - { SVO_JSON_EXTENSION, &Application::importSVOFromURL }, - { AVA_JSON_EXTENSION, &Application::askToWearAvatarAttachmentUrl }, - { JSON_EXTENSION, &Application::importJSONFromURL }, - { JS_EXTENSION, &Application::askToLoadScript }, - { FST_EXTENSION, &Application::askToSetAvatarUrl }, - { JSON_GZ_EXTENSION, &Application::askToReplaceDomainContent }, - { CONTENT_ZIP_EXTENSION, &Application::askToReplaceDomainContent }, - { ZIP_EXTENSION, &Application::importFromZIP }, - { JPG_EXTENSION, &Application::importImage }, - { PNG_EXTENSION, &Application::importImage } -}; - -class DeadlockWatchdogThread : public QThread { -public: - static const unsigned long HEARTBEAT_UPDATE_INTERVAL_SECS = 1; - static const unsigned long MAX_HEARTBEAT_AGE_USECS = 120 * USECS_PER_SECOND; // 2 mins with no checkin probably a deadlock - static const int WARNING_ELAPSED_HEARTBEAT = 500 * USECS_PER_MSEC; // warn if elapsed heartbeat average is large - static const int HEARTBEAT_SAMPLES = 100000; // ~5 seconds worth of samples - - // Set the heartbeat on launch - DeadlockWatchdogThread() { - setObjectName("Deadlock Watchdog"); - // Give the heartbeat an initial value - _heartbeat = usecTimestampNow(); - _paused = false; - connect(qApp, &QCoreApplication::aboutToQuit, [this] { - _quit = true; - }); - } - - void setMainThreadID(Qt::HANDLE threadID) { - _mainThreadID = threadID; - } - - static void updateHeartbeat() { - auto now = usecTimestampNow(); - auto elapsed = now - _heartbeat; - _movingAverage.addSample(elapsed); - _heartbeat = now; - } - - void deadlockDetectionCrash() { - setCrashAnnotation("_mod_faulting_tid", std::to_string((uint64_t)_mainThreadID)); - setCrashAnnotation("deadlock", "1"); - uint32_t* crashTrigger = nullptr; - *crashTrigger = 0xDEAD10CC; - } - - static void withPause(const std::function& lambda) { - pause(); - lambda(); - resume(); - } - static void pause() { - _paused = true; - } - - static void resume() { - // Update the heartbeat BEFORE resuming the checks - updateHeartbeat(); - _paused = false; - } - - void run() override { - while (!_quit) { - QThread::sleep(HEARTBEAT_UPDATE_INTERVAL_SECS); - // Don't do heartbeat detection under nsight - if (_paused) { - continue; - } - uint64_t lastHeartbeat = _heartbeat; // sample atomic _heartbeat, because we could context switch away and have it updated on us - uint64_t now = usecTimestampNow(); - auto lastHeartbeatAge = (now > lastHeartbeat) ? now - lastHeartbeat : 0; - auto elapsedMovingAverage = _movingAverage.getAverage(); - - if (elapsedMovingAverage > _maxElapsedAverage) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" - << "lastHeartbeatAge:" << lastHeartbeatAge - << "elapsedMovingAverage:" << elapsedMovingAverage - << "maxElapsed:" << _maxElapsed - << "PREVIOUS maxElapsedAverage:" << _maxElapsedAverage - << "NEW maxElapsedAverage:" << elapsedMovingAverage << "** NEW MAX ELAPSED AVERAGE **" - << "samples:" << _movingAverage.getSamples(); - _maxElapsedAverage = elapsedMovingAverage; - } - if (lastHeartbeatAge > _maxElapsed) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" - << "lastHeartbeatAge:" << lastHeartbeatAge - << "elapsedMovingAverage:" << elapsedMovingAverage - << "PREVIOUS maxElapsed:" << _maxElapsed - << "NEW maxElapsed:" << lastHeartbeatAge << "** NEW MAX ELAPSED **" - << "maxElapsedAverage:" << _maxElapsedAverage - << "samples:" << _movingAverage.getSamples(); - _maxElapsed = lastHeartbeatAge; - } - if (elapsedMovingAverage > WARNING_ELAPSED_HEARTBEAT) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK WATCHDOG WARNING:" - << "lastHeartbeatAge:" << lastHeartbeatAge - << "elapsedMovingAverage:" << elapsedMovingAverage << "** OVER EXPECTED VALUE **" - << "maxElapsed:" << _maxElapsed - << "maxElapsedAverage:" << _maxElapsedAverage - << "samples:" << _movingAverage.getSamples(); - } - - if (lastHeartbeatAge > MAX_HEARTBEAT_AGE_USECS) { - qCDebug(interfaceapp_deadlock) << "DEADLOCK DETECTED -- " - << "lastHeartbeatAge:" << lastHeartbeatAge - << "[ lastHeartbeat :" << lastHeartbeat - << "now:" << now << " ]" - << "elapsedMovingAverage:" << elapsedMovingAverage - << "maxElapsed:" << _maxElapsed - << "maxElapsedAverage:" << _maxElapsedAverage - << "samples:" << _movingAverage.getSamples(); - - // Don't actually crash in debug builds, in case this apparent deadlock is simply from - // the developer actively debugging code - #ifdef NDEBUG - deadlockDetectionCrash(); - #endif - } - } - } - - static std::atomic _paused; - static std::atomic _heartbeat; - static std::atomic _maxElapsed; - static std::atomic _maxElapsedAverage; - static ThreadSafeMovingAverage _movingAverage; - - bool _quit { false }; - - Qt::HANDLE _mainThreadID = nullptr; -}; - -std::atomic DeadlockWatchdogThread::_paused; -std::atomic DeadlockWatchdogThread::_heartbeat; -std::atomic DeadlockWatchdogThread::_maxElapsed; -std::atomic DeadlockWatchdogThread::_maxElapsedAverage; -ThreadSafeMovingAverage DeadlockWatchdogThread::_movingAverage; - -bool isDomainURL(QUrl url) { - if (!url.isValid()) { - return false; - } - if (url.scheme() == URL_SCHEME_HIFI) { - return true; - } - if (url.scheme() != HIFI_URL_SCHEME_FILE) { - // TODO -- once Octree::readFromURL no-longer takes over the main event-loop, serverless-domain urls can - // be loaded over http(s) - // && url.scheme() != HIFI_URL_SCHEME_HTTP && - // url.scheme() != HIFI_URL_SCHEME_HTTPS - return false; - } - if (url.path().endsWith(".json", Qt::CaseInsensitive) || - url.path().endsWith(".json.gz", Qt::CaseInsensitive)) { - return true; - } - return false; -} - -#ifdef Q_OS_WIN -class MyNativeEventFilter : public QAbstractNativeEventFilter { -public: - static MyNativeEventFilter& getInstance() { - static MyNativeEventFilter staticInstance; - return staticInstance; - } - - bool nativeEventFilter(const QByteArray &eventType, void* msg, long* result) Q_DECL_OVERRIDE { - if (eventType == "windows_generic_MSG") { - MSG* message = (MSG*)msg; - - if (message->message == UWM_IDENTIFY_INSTANCES) { - *result = UWM_IDENTIFY_INSTANCES; - return true; - } - - if (message->message == UWM_SHOW_APPLICATION) { - MainWindow* applicationWindow = qApp->getWindow(); - if (applicationWindow->isMinimized()) { - applicationWindow->showNormal(); // Restores to windowed or maximized state appropriately. - } - qApp->setActiveWindow(applicationWindow); // Flashes the taskbar icon if not focus. - return true; - } - - if (message->message == WM_COPYDATA) { - COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)(message->lParam); - QUrl url = QUrl((const char*)(pcds->lpData)); - if (isDomainURL(url)) { - DependencyManager::get()->handleLookupString(url.toString()); - return true; - } - } - - if (message->message == WM_DEVICECHANGE) { - const float MIN_DELTA_SECONDS = 2.0f; // de-bounce signal - static float lastTriggerTime = 0.0f; - const float deltaSeconds = secTimestampNow() - lastTriggerTime; - lastTriggerTime = secTimestampNow(); - if (deltaSeconds > MIN_DELTA_SECONDS) { - Midi::USBchanged(); // re-scan the MIDI bus - } - } - } - return false; - } -}; -#endif - -class LambdaEvent : public QEvent { - std::function _fun; -public: - LambdaEvent(const std::function & fun) : - QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { - } - LambdaEvent(std::function && fun) : - QEvent(static_cast(ApplicationEvent::Lambda)), _fun(fun) { - } - void call() const { _fun(); } -}; - -void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& message) { - QString logMessage = LogHandler::getInstance().printMessage((LogMsgType) type, context, message); - - if (!logMessage.isEmpty()) { -#ifdef Q_OS_ANDROID - const char * local=logMessage.toStdString().c_str(); - switch (type) { - case QtDebugMsg: - __android_log_write(ANDROID_LOG_DEBUG,"Interface",local); - break; - case QtInfoMsg: - __android_log_write(ANDROID_LOG_INFO,"Interface",local); - break; - case QtWarningMsg: - __android_log_write(ANDROID_LOG_WARN,"Interface",local); - break; - case QtCriticalMsg: - __android_log_write(ANDROID_LOG_ERROR,"Interface",local); - break; - case QtFatalMsg: - default: - __android_log_write(ANDROID_LOG_FATAL,"Interface",local); - abort(); - } -#else - qApp->getLogger()->addMessage(qPrintable(logMessage)); -#endif - } -} - - -class ApplicationMeshProvider : public scriptable::ModelProviderFactory { -public: - virtual scriptable::ModelProviderPointer lookupModelProvider(const QUuid& uuid) override { - bool success; - if (auto nestable = DependencyManager::get()->find(uuid, success).lock()) { - auto type = nestable->getNestableType(); -#ifdef SCRIPTABLE_MESH_DEBUG - qCDebug(interfaceapp) << "ApplicationMeshProvider::lookupModelProvider" << uuid << SpatiallyNestable::nestableTypeToString(type); -#endif - switch (type) { - case NestableType::Entity: - return getEntityModelProvider(static_cast(uuid)); - case NestableType::Avatar: - return getAvatarModelProvider(uuid); - } - } - return nullptr; - } - -private: - scriptable::ModelProviderPointer getEntityModelProvider(EntityItemID entityID) { - scriptable::ModelProviderPointer provider; - auto entityTreeRenderer = qApp->getEntities(); - auto entityTree = entityTreeRenderer->getTree(); - if (auto entity = entityTree->findEntityByID(entityID)) { - if (auto renderer = entityTreeRenderer->renderableForEntityId(entityID)) { - provider = std::dynamic_pointer_cast(renderer); - provider->modelProviderType = NestableType::Entity; - } else { - qCWarning(interfaceapp) << "no renderer for entity ID" << entityID.toString(); - } - } - return provider; - } - - scriptable::ModelProviderPointer getAvatarModelProvider(QUuid sessionUUID) { - scriptable::ModelProviderPointer provider; - auto avatarManager = DependencyManager::get(); - if (auto avatar = avatarManager->getAvatarBySessionID(sessionUUID)) { - provider = std::dynamic_pointer_cast(avatar); - provider->modelProviderType = NestableType::Avatar; - } - return provider; - } -}; - -/**jsdoc - *

The Controller.Hardware.Application object has properties representing Interface's state. The property - * values are integer IDs, uniquely identifying each output. Read-only. These can be mapped to actions or functions or - * Controller.Standard items in a {@link RouteObject} mapping (e.g., using the {@link RouteObject#when} method). - * Each data value is either 1.0 for "true" or 0.0 for "false".

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
PropertyTypeDataDescription
CameraFirstPersonnumbernumberThe camera is in first-person mode. - *
CameraThirdPersonnumbernumberThe camera is in third-person mode. - *
CameraFSMnumbernumberThe camera is in full screen mirror mode.
CameraIndependentnumbernumberThe camera is in independent mode.
CameraEntitynumbernumberThe camera is in entity mode.
InHMDnumbernumberThe user is in HMD mode.
AdvancedMovementnumbernumberAdvanced movement controls are enabled. - *
SnapTurnnumbernumberSnap turn is enabled.
GroundednumbernumberThe user's avatar is on the ground.
NavigationFocusednumbernumberNot used.
- * @typedef {object} Controller.Hardware-Application - */ - -static const QString STATE_IN_HMD = "InHMD"; -static const QString STATE_CAMERA_FULL_SCREEN_MIRROR = "CameraFSM"; -static const QString STATE_CAMERA_FIRST_PERSON = "CameraFirstPerson"; -static const QString STATE_CAMERA_THIRD_PERSON = "CameraThirdPerson"; -static const QString STATE_CAMERA_ENTITY = "CameraEntity"; -static const QString STATE_CAMERA_INDEPENDENT = "CameraIndependent"; -static const QString STATE_SNAP_TURN = "SnapTurn"; -static const QString STATE_ADVANCED_MOVEMENT_CONTROLS = "AdvancedMovement"; -static const QString STATE_GROUNDED = "Grounded"; -static const QString STATE_NAV_FOCUSED = "NavigationFocused"; -static const QString STATE_PLATFORM_WINDOWS = "PlatformWindows"; -static const QString STATE_PLATFORM_MAC = "PlatformMac"; -static const QString STATE_PLATFORM_ANDROID = "PlatformAndroid"; - -// Statically provided display and input plugins -extern DisplayPluginList getDisplayPlugins(); -extern InputPluginList getInputPlugins(); -extern void saveInputPluginSettings(const InputPluginList& plugins); - -// Parameters used for running tests from teh command line -const QString TEST_SCRIPT_COMMAND{ "--testScript" }; -const QString TEST_QUIT_WHEN_FINISHED_OPTION{ "quitWhenFinished" }; -const QString TEST_RESULTS_LOCATION_COMMAND{ "--testResultsLocation" }; - -bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { - const char** constArgv = const_cast(argv); - - qInstallMessageHandler(messageHandler); - - // HRS: I could not figure out how to move these any earlier in startup, so when using this option, be sure to also supply - // --allowMultipleInstances - auto reportAndQuit = [&](const char* commandSwitch, std::function report) { - const char* reportfile = getCmdOption(argc, constArgv, commandSwitch); - // Reports to the specified file, because stdout is set up to be captured for logging. - if (reportfile) { - FILE* fp = fopen(reportfile, "w"); - if (fp) { - report(fp); - fclose(fp); - if (!runningMarkerExisted) { // don't leave ours around - RunningMarker runingMarker(RUNNING_MARKER_FILENAME); - runingMarker.deleteRunningMarkerFile(); // happens in deleter, but making the side-effect explicit. - } - _exit(0); - } - } - }; - reportAndQuit("--protocolVersion", [&](FILE* fp) { - auto version = protocolVersionsSignatureBase64(); - fputs(version.toLatin1().data(), fp); - }); - reportAndQuit("--version", [&](FILE* fp) { - fputs(BuildInfo::VERSION.toLatin1().data(), fp); - }); - - const char* portStr = getCmdOption(argc, constArgv, "--listenPort"); - const int listenPort = portStr ? atoi(portStr) : INVALID_PORT; - - static const auto SUPPRESS_SETTINGS_RESET = "--suppress-settings-reset"; - bool suppressPrompt = cmdOptionExists(argc, const_cast(argv), SUPPRESS_SETTINGS_RESET); - - // set the OCULUS_STORE property so the oculus plugin can know if we ran from the Oculus Store - static const auto OCULUS_STORE_ARG = "--oculus-store"; - bool isStore = cmdOptionExists(argc, const_cast(argv), OCULUS_STORE_ARG); - qApp->setProperty(hifi::properties::OCULUS_STORE, isStore); - - // Ignore any previous crashes if running from command line with a test script. - bool inTestMode { false }; - for (int i = 0; i < argc; ++i) { - QString parameter(argv[i]); - if (parameter == TEST_SCRIPT_COMMAND) { - inTestMode = true; - break; - } - } - - bool previousSessionCrashed { false }; - if (!inTestMode) { - previousSessionCrashed = CrashRecoveryHandler::checkForResetSettings(runningMarkerExisted, suppressPrompt); - } - - // get dir to use for cache - static const auto CACHE_SWITCH = "--cache"; - QString cacheDir = getCmdOption(argc, const_cast(argv), CACHE_SWITCH); - if (!cacheDir.isEmpty()) { - qApp->setProperty(hifi::properties::APP_LOCAL_DATA_PATH, cacheDir); - } - - { - const QString resourcesBinaryFile = PathUtils::getRccPath(); - if (!QFile::exists(resourcesBinaryFile)) { - throw std::runtime_error("Unable to find primary resources"); - } - if (!QResource::registerResource(resourcesBinaryFile)) { - throw std::runtime_error("Unable to load primary resources"); - } - } - - // Tell the plugin manager about our statically linked plugins - DependencyManager::set(); - auto pluginManager = PluginManager::getInstance(); - pluginManager->setInputPluginProvider([] { return getInputPlugins(); }); - pluginManager->setDisplayPluginProvider([] { return getDisplayPlugins(); }); - pluginManager->setInputPluginSettingsPersister([](const InputPluginList& plugins) { saveInputPluginSettings(plugins); }); - if (auto steamClient = pluginManager->getSteamClientPlugin()) { - steamClient->init(); - } - if (auto oculusPlatform = pluginManager->getOculusPlatformPlugin()) { - oculusPlatform->init(); - } - - PROFILE_SET_THREAD_NAME("Main Thread"); - -#if defined(Q_OS_WIN) - // Select appropriate audio DLL - QString audioDLLPath = QCoreApplication::applicationDirPath(); - if (IsWindows8OrGreater()) { - audioDLLPath += "/audioWin8"; - } else { - audioDLLPath += "/audioWin7"; - } - QCoreApplication::addLibraryPath(audioDLLPath); -#endif - - DependencyManager::registerInheritance(); - DependencyManager::registerInheritance(); - DependencyManager::registerInheritance(); - DependencyManager::registerInheritance(); - - // Set dependencies - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); -#if defined(Q_OS_ANDROID) - DependencyManager::set(); // use the default user agent getter -#else - DependencyManager::set(std::bind(&Application::getUserAgent, qApp)); -#endif - DependencyManager::set(); - DependencyManager::set(ScriptEngine::CLIENT_SCRIPT); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(NodeType::Agent, listenPort); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache constructor. - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(true); - DependencyManager::set(); - DependencyManager::registerInheritance(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - DependencyManager::set(); -#endif - DependencyManager::set(); - DependencyManager::set(); -#if !defined(DISABLE_QML) - DependencyManager::set(); -#endif - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - controller::StateController::setStateVariables({ { STATE_IN_HMD, STATE_CAMERA_FULL_SCREEN_MIRROR, - STATE_CAMERA_FIRST_PERSON, STATE_CAMERA_THIRD_PERSON, STATE_CAMERA_ENTITY, STATE_CAMERA_INDEPENDENT, - STATE_SNAP_TURN, STATE_ADVANCED_MOVEMENT_CONTROLS, STATE_GROUNDED, STATE_NAV_FOCUSED, - STATE_PLATFORM_WINDOWS, STATE_PLATFORM_MAC, STATE_PLATFORM_ANDROID } }); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(true, qApp, qApp); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); - - return previousSessionCrashed; -} - -// FIXME move to header, or better yet, design some kind of UI manager -// to take care of highlighting keyboard focused items, rather than -// continuing to overburden Application.cpp -QUuid _keyboardFocusHighlightID; - -OffscreenGLCanvas* _qmlShareContext { nullptr }; - -// FIXME hack access to the internal share context for the Chromium helper -// Normally we'd want to use QWebEngine::initialize(), but we can't because -// our primary context is a QGLWidget, which can't easily be initialized to share -// from a QOpenGLContext. -// -// So instead we create a new offscreen context to share with the QGLWidget, -// and manually set THAT to be the shared context for the Chromium helper -#if !defined(DISABLE_QML) -OffscreenGLCanvas* _chromiumShareContext { nullptr }; -#endif - -Q_GUI_EXPORT void qt_gl_set_global_share_context(QOpenGLContext *context); -Q_GUI_EXPORT QOpenGLContext *qt_gl_global_share_context(); - -Setting::Handle sessionRunTime{ "sessionRunTime", 0 }; - -const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 60.0f; -const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; -const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; -const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; -const bool DEFAULT_PREFER_STYLUS_OVER_LASER = false; -const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; -const QString DEFAULT_CURSOR_NAME = "DEFAULT"; -const bool DEFAULT_MINI_TABLET_ENABLED = true; - -QSharedPointer getOffscreenUI() { -#if !defined(DISABLE_QML) - return DependencyManager::get(); -#else - return nullptr; -#endif -} - -Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runningMarkerExisted) : - QApplication(argc, argv), - _window(new MainWindow(desktop())), - _sessionRunTimer(startupTimer), -#ifndef Q_OS_ANDROID - _logger(new FileLogger(this)), -#endif - _previousSessionCrashed(setupEssentials(argc, argv, runningMarkerExisted)), - _entitySimulation(new PhysicalEntitySimulation()), - _physicsEngine(new PhysicsEngine(Vectors::ZERO)), - _entityClipboard(new EntityTree()), - _previousScriptLocation("LastScriptLocation", DESKTOP_LOCATION), - _fieldOfView("fieldOfView", DEFAULT_FIELD_OF_VIEW_DEGREES), - _hmdTabletScale("hmdTabletScale", DEFAULT_HMD_TABLET_SCALE_PERCENT), - _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), - _firstRun(Settings::firstRun, true), - _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), - _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), - _preferStylusOverLaserSetting("preferStylusOverLaser", DEFAULT_PREFER_STYLUS_OVER_LASER), - _preferAvatarFingerOverStylusSetting("preferAvatarFingerOverStylus", DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS), - _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), - _preferredCursor("preferredCursor", DEFAULT_CURSOR_NAME), - _miniTabletEnabledSetting("miniTabletEnabled", DEFAULT_MINI_TABLET_ENABLED), - _scaleMirror(1.0f), - _mirrorYawOffset(0.0f), - _raiseMirror(0.0f), - _enableProcessOctreeThread(true), - _lastNackTime(usecTimestampNow()), - _lastSendDownstreamAudioStats(usecTimestampNow()), - _notifiedPacketVersionMismatchThisDomain(false), - _maxOctreePPS(maxOctreePacketsPerSecond.get()), - _lastFaceTrackerUpdate(0), - _snapshotSound(nullptr), - _sampleSound(nullptr) -{ - - auto steamClient = PluginManager::getInstance()->getSteamClientPlugin(); - setProperty(hifi::properties::STEAM, (steamClient && steamClient->isRunning())); - setProperty(hifi::properties::CRASHED, _previousSessionCrashed); - - { - const QStringList args = arguments(); - - for (int i = 0; i < args.size() - 1; ++i) { - if (args.at(i) == TEST_SCRIPT_COMMAND && (i + 1) < args.size()) { - QString testScriptPath = args.at(i + 1); - - // If the URL scheme is http(s) or ftp, then use as is, else - treat it as a local file - // This is done so as not break previous command line scripts - if (testScriptPath.left(HIFI_URL_SCHEME_HTTP.length()) == HIFI_URL_SCHEME_HTTP || - testScriptPath.left(HIFI_URL_SCHEME_FTP.length()) == HIFI_URL_SCHEME_FTP) { - - setProperty(hifi::properties::TEST, QUrl::fromUserInput(testScriptPath)); - } else if (QFileInfo(testScriptPath).exists()) { - setProperty(hifi::properties::TEST, QUrl::fromLocalFile(testScriptPath)); - } - - // quite when finished parameter must directly follow the test script - if ((i + 2) < args.size() && args.at(i + 2) == TEST_QUIT_WHEN_FINISHED_OPTION) { - quitWhenFinished = true; - } - } else if (args.at(i) == TEST_RESULTS_LOCATION_COMMAND) { - // Set test snapshot location only if it is a writeable directory - QString path(args.at(i + 1)); - - QFileInfo fileInfo(path); - if (fileInfo.isDir() && fileInfo.isWritable()) { - TestScriptingInterface::getInstance()->setTestResultsLocation(path); - } - } - } - } - - // make sure the debug draw singleton is initialized on the main thread. - DebugDraw::getInstance().removeMarker(""); - - PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care - PluginManager::getInstance()->setContainer(pluginContainer); - - QThreadPool::globalInstance()->setMaxThreadCount(MIN_PROCESSING_THREAD_POOL_SIZE); - thread()->setPriority(QThread::HighPriority); - thread()->setObjectName("Main Thread"); - - setInstance(this); - - auto controllerScriptingInterface = DependencyManager::get().data(); - _controllerScriptingInterface = dynamic_cast(controllerScriptingInterface); - connect(PluginManager::getInstance().data(), &PluginManager::inputDeviceRunningChanged, - controllerScriptingInterface, &controller::ScriptingInterface::updateRunningInputDevices); - - EntityTree::setEntityClicksCapturedOperator([this] { - return _controllerScriptingInterface->areEntityClicksCaptured(); - }); - - _entityClipboard->createRootElement(); - -#ifdef Q_OS_WIN - installNativeEventFilter(&MyNativeEventFilter::getInstance()); -#endif - - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/fontawesome-webfont.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/hifi-glyphs.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/AnonymousPro-Regular.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-Regular.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/FiraSans-SemiBold.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Light.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Regular.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/rawline-500.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-Bold.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Raleway-SemiBold.ttf"); - QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/Cairo-SemiBold.ttf"); - _window->setWindowTitle("High Fidelity Interface"); - - Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us - - auto nodeList = DependencyManager::get(); - nodeList->startThread(); - nodeList->setFlagTimeForConnectionStep(true); - - // move the AddressManager to the NodeList thread so that domain resets due to domain changes always occur - // before we tell MyAvatar to go to a new location in the new domain - auto addressManager = DependencyManager::get(); - addressManager->moveToThread(nodeList->thread()); - - const char** constArgv = const_cast(argv); - if (cmdOptionExists(argc, constArgv, "--disableWatchdog")) { - DISABLE_WATCHDOG = true; - } - // Set up a watchdog thread to intentionally crash the application on deadlocks - if (!DISABLE_WATCHDOG) { - auto deadlockWatchdogThread = new DeadlockWatchdogThread(); - deadlockWatchdogThread->setMainThreadID(QThread::currentThreadId()); - deadlockWatchdogThread->start(); - } - - // Set File Logger Session UUID - auto avatarManager = DependencyManager::get(); - auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - if (avatarManager) { - workload::SpacePointer space = getEntities()->getWorkloadSpace(); - avatarManager->setSpace(space); - } - auto accountManager = DependencyManager::get(); - -#ifndef Q_OS_ANDROID - _logger->setSessionID(accountManager->getSessionID()); -#endif - - setCrashAnnotation("metaverse_session_id", accountManager->getSessionID().toString().toStdString()); - setCrashAnnotation("main_thread_id", std::to_string((size_t)QThread::currentThreadId())); - - if (steamClient) { - qCDebug(interfaceapp) << "[VERSION] SteamVR buildID:" << steamClient->getSteamVRBuildID(); - } - setCrashAnnotation("steam", property(hifi::properties::STEAM).toBool() ? "1" : "0"); - - qCDebug(interfaceapp) << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); - qCDebug(interfaceapp) << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; - qCDebug(interfaceapp) << "[VERSION] VERSION:" << BuildInfo::VERSION; - qCDebug(interfaceapp) << "[VERSION] BUILD_TYPE_STRING:" << BuildInfo::BUILD_TYPE_STRING; - qCDebug(interfaceapp) << "[VERSION] BUILD_GLOBAL_SERVICES:" << BuildInfo::BUILD_GLOBAL_SERVICES; -#if USE_STABLE_GLOBAL_SERVICES - qCDebug(interfaceapp) << "[VERSION] We will use STABLE global services."; -#else - qCDebug(interfaceapp) << "[VERSION] We will use DEVELOPMENT global services."; -#endif - - bool isStore = property(hifi::properties::OCULUS_STORE).toBool(); - - DependencyManager::get()->setLimitedCommerce(isStore); // Or we could make it a separate arg, or if either arg is set, etc. And should this instead by a hifi::properties? - - updateHeartbeat(); - - // setup a timer for domain-server check ins - QTimer* domainCheckInTimer = new QTimer(this); - QWeakPointer nodeListWeak = nodeList; - connect(domainCheckInTimer, &QTimer::timeout, [this, nodeListWeak] { - auto nodeList = nodeListWeak.lock(); - if (!isServerlessMode() && nodeList) { - nodeList->sendDomainServerCheckIn(); - } - }); - domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); - connect(this, &QCoreApplication::aboutToQuit, [domainCheckInTimer] { - domainCheckInTimer->stop(); - domainCheckInTimer->deleteLater(); - }); - - { - auto audioIO = DependencyManager::get().data(); - audioIO->setPositionGetter([] { - auto avatarManager = DependencyManager::get(); - auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - - return myAvatar ? myAvatar->getPositionForAudio() : Vectors::ZERO; - }); - audioIO->setOrientationGetter([] { - auto avatarManager = DependencyManager::get(); - auto myAvatar = avatarManager ? avatarManager->getMyAvatar() : nullptr; - - return myAvatar ? myAvatar->getOrientationForAudio() : Quaternions::IDENTITY; - }); - - recording::Frame::registerFrameHandler(AudioConstants::getAudioFrameName(), [&audioIO](recording::Frame::ConstPointer frame) { - audioIO->handleRecordedAudioInput(frame->data); - }); - - connect(audioIO, &AudioClient::inputReceived, [](const QByteArray& audio) { - static auto recorder = DependencyManager::get(); - if (recorder->isRecording()) { - static const recording::FrameType AUDIO_FRAME_TYPE = recording::Frame::registerFrameType(AudioConstants::getAudioFrameName()); - recorder->recordFrame(AUDIO_FRAME_TYPE, audio); - } - }); - audioIO->startThread(); - } - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - // Setup MessagesClient - DependencyManager::get()->startThread(); - - const DomainHandler& domainHandler = nodeList->getDomainHandler(); - - connect(&domainHandler, SIGNAL(domainURLChanged(QUrl)), SLOT(domainURLChanged(QUrl))); - connect(&domainHandler, SIGNAL(redirectToErrorDomainURL(QUrl)), SLOT(goToErrorDomainURL(QUrl))); - connect(&domainHandler, &DomainHandler::domainURLChanged, [](QUrl domainURL){ - setCrashAnnotation("domain", domainURL.toString().toStdString()); - }); - connect(&domainHandler, SIGNAL(resetting()), SLOT(resettingDomain())); - connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle())); - connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); - connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { - auto tabletScriptingInterface = DependencyManager::get(); - if (tabletScriptingInterface) { - tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr); - } - auto entityScriptingInterface = DependencyManager::get(); - entityScriptingInterface->deleteEntity(getTabletScreenID()); - entityScriptingInterface->deleteEntity(getTabletHomeButtonID()); - entityScriptingInterface->deleteEntity(getTabletFrameID()); - _failedToConnectToEntityServer = false; - }); - - _entityServerConnectionTimer.setSingleShot(true); - connect(&_entityServerConnectionTimer, &QTimer::timeout, this, &Application::setFailedToConnectToEntityServer); - - connect(&domainHandler, &DomainHandler::connectedToDomain, this, [this]() { - if (!isServerlessMode()) { - _entityServerConnectionTimer.setInterval(ENTITY_SERVER_ADDED_TIMEOUT); - _entityServerConnectionTimer.start(); - _failedToConnectToEntityServer = false; - } - }); - connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); - - nodeList->getDomainHandler().setErrorDomainURL(QUrl(REDIRECT_HIFI_ADDRESS)); - - // We could clear ATP assets only when changing domains, but it's possible that the domain you are connected - // to has gone down and switched to a new content set, so when you reconnect the cached ATP assets will no longer be valid. - connect(&domainHandler, &DomainHandler::disconnectedFromDomain, DependencyManager::get().data(), &ScriptCache::clearATPScriptsFromCache); - - // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one - const qint64 DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS = 5 * MSECS_PER_SECOND; - - auto discoverabilityManager = DependencyManager::get(); - connect(&locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); - connect(&locationUpdateTimer, &QTimer::timeout, - DependencyManager::get().data(), &AddressManager::storeCurrentAddress); - locationUpdateTimer.start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS); - - // if we get a domain change, immediately attempt update location in metaverse server - connect(&nodeList->getDomainHandler(), &DomainHandler::connectedToDomain, - discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); - - // send a location update immediately - discoverabilityManager->updateLocation(); - - connect(nodeList.data(), &NodeList::nodeAdded, this, &Application::nodeAdded); - connect(nodeList.data(), &NodeList::nodeKilled, this, &Application::nodeKilled); - connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated); - connect(nodeList.data(), &NodeList::uuidChanged, myAvatar.get(), &MyAvatar::setSessionUUID); - connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID); - connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); - - // you might think we could just do this in NodeList but we only want this connection for Interface - connect(&nodeList->getDomainHandler(), SIGNAL(limitOfSilentDomainCheckInsReached()), - nodeList.data(), SLOT(reset())); - - auto dialogsManager = DependencyManager::get(); -#if defined(Q_OS_ANDROID) - connect(accountManager.data(), &AccountManager::authRequired, this, []() { - auto addressManager = DependencyManager::get(); - AndroidHelper::instance().showLoginDialog(addressManager->currentAddress()); - }); -#else - connect(accountManager.data(), &AccountManager::authRequired, dialogsManager.data(), &DialogsManager::showLoginDialog); -#endif - connect(accountManager.data(), &AccountManager::usernameChanged, this, &Application::updateWindowTitle); - - // set the account manager's root URL and trigger a login request if we don't have the access token - accountManager->setIsAgent(true); - accountManager->setAuthURL(NetworkingConstants::METAVERSE_SERVER_URL()); - - // use our MyAvatar position and quat for address manager path - addressManager->setPositionGetter([this]{ return getMyAvatar()->getWorldFeetPosition(); }); - addressManager->setOrientationGetter([this]{ return getMyAvatar()->getWorldOrientation(); }); - - connect(addressManager.data(), &AddressManager::hostChanged, this, &Application::updateWindowTitle); - connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); - - connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateThreadPoolCount); - connect(this, &Application::activeDisplayPluginChanged, this, [](){ - qApp->setProperty(hifi::properties::HMD, qApp->isHMDMode()); - auto displayPlugin = qApp->getActiveDisplayPlugin(); - setCrashAnnotation("display_plugin", displayPlugin->getName().toStdString()); - setCrashAnnotation("hmd", displayPlugin->isHmd() ? "1" : "0"); - }); - connect(this, &Application::activeDisplayPluginChanged, this, &Application::updateSystemTabletMode); - connect(this, &Application::activeDisplayPluginChanged, this, [&](){ - if (getLoginDialogPoppedUp()) { - auto dialogsManager = DependencyManager::get(); - auto keyboard = DependencyManager::get(); - if (_firstRun.get()) { - // display mode changed. Don't allow auto-switch to work after this session. - _firstRun.set(false); - } - if (isHMDMode()) { - emit loginDialogFocusDisabled(); - dialogsManager->hideLoginDialog(); - createLoginDialog(); - } else { - DependencyManager::get()->deleteEntity(_loginDialogID); - _loginDialogID = QUuid(); - _loginStateManager.tearDown(); - dialogsManager->showLoginDialog(); - emit loginDialogFocusEnabled(); - } - } - }); - - // Save avatar location immediately after a teleport. - connect(myAvatar.get(), &MyAvatar::positionGoneTo, - DependencyManager::get().data(), &AddressManager::storeCurrentAddress); - - connect(myAvatar.get(), &MyAvatar::skeletonModelURLChanged, [](){ - QUrl avatarURL = qApp->getMyAvatar()->getSkeletonModelURL(); - setCrashAnnotation("avatar", avatarURL.toString().toStdString()); - }); - - - // Inititalize sample before registering - _sampleSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/sample.wav")); - - { - auto scriptEngines = DependencyManager::get().data(); - scriptEngines->registerScriptInitializer([this](ScriptEnginePointer engine) { - registerScriptEngineWithApplicationServices(engine); - }); - - connect(scriptEngines, &ScriptEngines::scriptCountChanged, this, [this] { - auto scriptEngines = DependencyManager::get(); - if (scriptEngines->getRunningScripts().isEmpty()) { - getMyAvatar()->clearScriptableSettings(); - } - }, Qt::QueuedConnection); - - connect(scriptEngines, &ScriptEngines::scriptsReloading, this, [this] { - getEntities()->reloadEntityScripts(); - loadAvatarScripts(getMyAvatar()->getScriptUrls()); - }, Qt::QueuedConnection); - - connect(scriptEngines, &ScriptEngines::scriptLoadError, - this, [](const QString& filename, const QString& error) { - OffscreenUi::asyncWarning(nullptr, "Error Loading Script", filename + " failed to load."); - }, Qt::QueuedConnection); - } - -#ifdef _WIN32 - WSADATA WsaData; - int wsaresult = WSAStartup(MAKEWORD(2, 2), &WsaData); -#endif - - // tell the NodeList instance who to tell the domain server we care about - nodeList->addSetOfNodeTypesToNodeInterestSet(NodeSet() << NodeType::AudioMixer << NodeType::AvatarMixer - << NodeType::EntityServer << NodeType::AssetServer << NodeType::MessagesMixer << NodeType::EntityScriptServer); - - // connect to the packet sent signal of the _entityEditSender - connect(&_entityEditSender, &EntityEditPacketSender::packetSent, this, &Application::packetSent); - connect(&_entityEditSender, &EntityEditPacketSender::addingEntityWithCertificate, this, &Application::addingEntityWithCertificate); - - QString concurrentDownloadsStr = getCmdOption(argc, constArgv, "--concurrent-downloads"); - bool success; - uint32_t concurrentDownloads = concurrentDownloadsStr.toUInt(&success); - if (!success) { - concurrentDownloads = MAX_CONCURRENT_RESOURCE_DOWNLOADS; - } - ResourceCache::setRequestLimit(concurrentDownloads); - - // perhaps override the avatar url. Since we will test later for validity - // we don't need to do so here. - QString avatarURL = getCmdOption(argc, constArgv, "--avatarURL"); - _avatarOverrideUrl = QUrl::fromUserInput(avatarURL); - - // If someone specifies both --avatarURL and --replaceAvatarURL, - // the replaceAvatarURL wins. So only set the _overrideUrl if this - // does have a non-empty string. - QString replaceURL = getCmdOption(argc, constArgv, "--replaceAvatarURL"); - if (!replaceURL.isEmpty()) { - _avatarOverrideUrl = QUrl::fromUserInput(replaceURL); - _saveAvatarOverrideUrl = true; - } - - _glWidget = new GLCanvas(); - getApplicationCompositor().setRenderingWidget(_glWidget); - _window->setCentralWidget(_glWidget); - - _window->restoreGeometry(); - _window->setVisible(true); - - _glWidget->setFocusPolicy(Qt::StrongFocus); - _glWidget->setFocus(); - - if (cmdOptionExists(argc, constArgv, "--system-cursor")) { - _preferredCursor.set(Cursor::Manager::getIconName(Cursor::Icon::SYSTEM)); - } - showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); - - // enable mouse tracking; otherwise, we only get drag events - _glWidget->setMouseTracking(true); - // Make sure the window is set to the correct size by processing the pending events - QCoreApplication::processEvents(); - - // Create the main thread context, the GPU backend - initializeGL(); - qCDebug(interfaceapp, "Initialized GL"); - - // Initialize the display plugin architecture - initializeDisplayPlugins(); - qCDebug(interfaceapp, "Initialized Display"); - - // An audio device changed signal received before the display plugins are set up will cause a crash, - // so we defer the setup of the `scripting::Audio` class until this point - { - auto audioScriptingInterface = DependencyManager::set(); - auto audioIO = DependencyManager::get().data(); - connect(audioIO, &AudioClient::mutedByMixer, audioScriptingInterface.data(), &AudioScriptingInterface::mutedByMixer); - connect(audioIO, &AudioClient::receivedFirstPacket, audioScriptingInterface.data(), &AudioScriptingInterface::receivedFirstPacket); - connect(audioIO, &AudioClient::disconnected, audioScriptingInterface.data(), &AudioScriptingInterface::disconnected); - connect(audioIO, &AudioClient::muteEnvironmentRequested, [](glm::vec3 position, float radius) { - auto audioClient = DependencyManager::get(); - auto audioScriptingInterface = DependencyManager::get(); - auto myAvatarPosition = DependencyManager::get()->getMyAvatar()->getWorldPosition(); - float distance = glm::distance(myAvatarPosition, position); - - if (distance < radius) { - audioClient->setMuted(true); - audioScriptingInterface->environmentMuted(); - } - }); - connect(this, &Application::activeDisplayPluginChanged, - reinterpret_cast(audioScriptingInterface.data()), &scripting::Audio::onContextChanged); - } - - // Create the rendering engine. This can be slow on some machines due to lots of - // GPU pipeline creation. - initializeRenderEngine(); - qCDebug(interfaceapp, "Initialized Render Engine."); - - // Overlays need to exist before we set the ContextOverlayInterface dependency - _overlays.init(); // do this before scripts load - DependencyManager::set(); - - // Initialize the user interface and menu system - // Needs to happen AFTER the render engine initialization to access its configuration - initializeUi(); - - init(); - qCDebug(interfaceapp, "init() complete."); - - // create thread for parsing of octree data independent of the main network and rendering threads - _octreeProcessor.initialize(_enableProcessOctreeThread); - connect(&_octreeProcessor, &OctreePacketProcessor::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch); - _entityEditSender.initialize(_enableProcessOctreeThread); - - _idleLoopStdev.reset(); - - // update before the first render - update(0); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - static const QString TESTER = "HIFI_TESTER"; - bool isTester = false; -#if defined (Q_OS_ANDROID) - // Since we cannot set environment variables in Android we use a file presence - // to denote that this is a testing device - QFileInfo check_tester_file(TESTER_FILE); - isTester = check_tester_file.exists() && check_tester_file.isFile(); -#endif - - constexpr auto INSTALLER_INI_NAME = "installer.ini"; - auto iniPath = QDir(applicationDirPath()).filePath(INSTALLER_INI_NAME); - QFile installerFile { iniPath }; - std::unordered_map installerKeyValues; - if (installerFile.open(QIODevice::ReadOnly)) { - while (!installerFile.atEnd()) { - auto line = installerFile.readLine(); - if (!line.isEmpty()) { - auto index = line.indexOf("="); - if (index >= 0) { - installerKeyValues[line.mid(0, index).trimmed()] = line.mid(index + 1).trimmed(); - } - } - } - } - - // In practice we shouldn't run across installs that don't have a known installer type. - // Client or Client+Server installs should always have the installer.ini next to their - // respective interface.exe, and Steam installs will be detected as such. If a user were - // to delete the installer.ini, though, and as an example, we won't know the context of the - // original install. - constexpr auto INSTALLER_KEY_TYPE = "type"; - constexpr auto INSTALLER_KEY_CAMPAIGN = "campaign"; - constexpr auto INSTALLER_TYPE_UNKNOWN = "unknown"; - constexpr auto INSTALLER_TYPE_STEAM = "steam"; - - auto typeIt = installerKeyValues.find(INSTALLER_KEY_TYPE); - QString installerType = INSTALLER_TYPE_UNKNOWN; - if (typeIt == installerKeyValues.end()) { - if (property(hifi::properties::STEAM).toBool()) { - installerType = INSTALLER_TYPE_STEAM; - } - } else { - installerType = typeIt->second; - } - - auto campaignIt = installerKeyValues.find(INSTALLER_KEY_CAMPAIGN); - QString installerCampaign = campaignIt != installerKeyValues.end() ? campaignIt->second : ""; - - qDebug() << "Detected installer type:" << installerType; - qDebug() << "Detected installer campaign:" << installerCampaign; - - auto& userActivityLogger = UserActivityLogger::getInstance(); - if (userActivityLogger.isEnabled()) { - // sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value. - // The value will be 0 if the user blew away settings this session, which is both a feature and a bug. - static const QString TESTER = "HIFI_TESTER"; - auto gpuIdent = GPUIdent::getInstance(); - auto glContextData = getGLContextData(); - QJsonObject properties = { - { "version", applicationVersion() }, - { "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) || isTester }, - { "installer_campaign", installerCampaign }, - { "installer_type", installerType }, - { "build_type", BuildInfo::BUILD_TYPE_STRING }, - { "previousSessionCrashed", _previousSessionCrashed }, - { "previousSessionRuntime", sessionRunTime.get() }, - { "cpu_architecture", QSysInfo::currentCpuArchitecture() }, - { "kernel_type", QSysInfo::kernelType() }, - { "kernel_version", QSysInfo::kernelVersion() }, - { "os_type", QSysInfo::productType() }, - { "os_version", QSysInfo::productVersion() }, - { "gpu_name", gpuIdent->getName() }, - { "gpu_driver", gpuIdent->getDriver() }, - { "gpu_memory", static_cast(gpuIdent->getMemory()) }, - { "gl_version_int", glVersionToInteger(glContextData.value("version").toString()) }, - { "gl_version", glContextData["version"] }, - { "gl_vender", glContextData["vendor"] }, - { "gl_sl_version", glContextData["sl_version"] }, - { "gl_renderer", glContextData["renderer"] }, - { "ideal_thread_count", QThread::idealThreadCount() } - }; - auto macVersion = QSysInfo::macVersion(); - if (macVersion != QSysInfo::MV_None) { - properties["os_osx_version"] = QSysInfo::macVersion(); - } - auto windowsVersion = QSysInfo::windowsVersion(); - if (windowsVersion != QSysInfo::WV_None) { - properties["os_win_version"] = QSysInfo::windowsVersion(); - } - - ProcessorInfo procInfo; - if (getProcessorInfo(procInfo)) { - properties["processor_core_count"] = procInfo.numProcessorCores; - properties["logical_processor_count"] = procInfo.numLogicalProcessors; - properties["processor_l1_cache_count"] = procInfo.numProcessorCachesL1; - properties["processor_l2_cache_count"] = procInfo.numProcessorCachesL2; - properties["processor_l3_cache_count"] = procInfo.numProcessorCachesL3; - } - - properties["first_run"] = _firstRun.get(); - - // add the user's machine ID to the launch event - QString machineFingerPrint = uuidStringWithoutCurlyBraces(FingerprintUtils::getMachineFingerprint()); - properties["machine_fingerprint"] = machineFingerPrint; - - userActivityLogger.logAction("launch", properties); - } - - _entityEditSender.setMyAvatar(myAvatar.get()); - - // The entity octree will have to know about MyAvatar for the parentJointName import - getEntities()->getTree()->setMyAvatar(myAvatar); - _entityClipboard->setMyAvatar(myAvatar); - - // For now we're going to set the PPS for outbound packets to be super high, this is - // probably not the right long term solution. But for now, we're going to do this to - // allow you to move an entity around in your hand - _entityEditSender.setPacketsPerSecond(3000); // super high!! - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - connect(this, SIGNAL(aboutToQuit()), this, SLOT(onAboutToQuit())); - - // FIXME -- I'm a little concerned about this. - connect(myAvatar->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded, - this, &Application::checkSkeleton, Qt::QueuedConnection); - - // Setup the userInputMapper with the actions - auto userInputMapper = DependencyManager::get(); - connect(userInputMapper.data(), &UserInputMapper::actionEvent, [this](int action, float state) { - using namespace controller; - auto tabletScriptingInterface = DependencyManager::get(); - { - auto actionEnum = static_cast(action); - int key = Qt::Key_unknown; - static int lastKey = Qt::Key_unknown; - bool navAxis = false; - switch (actionEnum) { - case Action::UI_NAV_VERTICAL: - navAxis = true; - if (state > 0.0f) { - key = Qt::Key_Up; - } else if (state < 0.0f) { - key = Qt::Key_Down; - } - break; - - case Action::UI_NAV_LATERAL: - navAxis = true; - if (state > 0.0f) { - key = Qt::Key_Right; - } else if (state < 0.0f) { - key = Qt::Key_Left; - } - break; - - case Action::UI_NAV_GROUP: - navAxis = true; - if (state > 0.0f) { - key = Qt::Key_Tab; - } else if (state < 0.0f) { - key = Qt::Key_Backtab; - } - break; - - case Action::UI_NAV_BACK: - key = Qt::Key_Escape; - break; - - case Action::UI_NAV_SELECT: - key = Qt::Key_Return; - break; - default: - break; - } - - auto window = tabletScriptingInterface->getTabletWindow(); - if (navAxis && window) { - if (lastKey != Qt::Key_unknown) { - QKeyEvent event(QEvent::KeyRelease, lastKey, Qt::NoModifier); - sendEvent(window, &event); - lastKey = Qt::Key_unknown; - } - - if (key != Qt::Key_unknown) { - QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); - sendEvent(window, &event); - tabletScriptingInterface->processEvent(&event); - lastKey = key; - } - } else if (key != Qt::Key_unknown && window) { - if (state) { - QKeyEvent event(QEvent::KeyPress, key, Qt::NoModifier); - sendEvent(window, &event); - tabletScriptingInterface->processEvent(&event); - } else { - QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier); - sendEvent(window, &event); - } - return; - } - } - - if (action == controller::toInt(controller::Action::RETICLE_CLICK)) { - auto reticlePos = getApplicationCompositor().getReticlePosition(); - QPoint localPos(reticlePos.x, reticlePos.y); // both hmd and desktop already handle this in our coordinates. - if (state) { - QMouseEvent mousePress(QEvent::MouseButtonPress, localPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); - sendEvent(_glWidget, &mousePress); - _reticleClickPressed = true; - } else { - QMouseEvent mouseRelease(QEvent::MouseButtonRelease, localPos, Qt::LeftButton, Qt::NoButton, Qt::NoModifier); - sendEvent(_glWidget, &mouseRelease); - _reticleClickPressed = false; - } - return; // nothing else to do - } - - if (state) { - if (action == controller::toInt(controller::Action::TOGGLE_MUTE)) { - auto audioClient = DependencyManager::get(); - audioClient->setMuted(!audioClient->isMuted()); - } else if (action == controller::toInt(controller::Action::CYCLE_CAMERA)) { - cycleCamera(); - } else if (action == controller::toInt(controller::Action::CONTEXT_MENU) && !isInterstitialMode()) { - toggleTabletUI(); - } else if (action == controller::toInt(controller::Action::RETICLE_X)) { - auto oldPos = getApplicationCompositor().getReticlePosition(); - getApplicationCompositor().setReticlePosition({ oldPos.x + state, oldPos.y }); - } else if (action == controller::toInt(controller::Action::RETICLE_Y)) { - auto oldPos = getApplicationCompositor().getReticlePosition(); - getApplicationCompositor().setReticlePosition({ oldPos.x, oldPos.y + state }); - } else if (action == controller::toInt(controller::Action::TOGGLE_OVERLAY)) { - toggleOverlays(); - } - } - }); - - _applicationStateDevice = userInputMapper->getStateDevice(); - - _applicationStateDevice->setInputVariant(STATE_IN_HMD, []() -> float { - return qApp->isHMDMode() ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_FULL_SCREEN_MIRROR, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_MIRROR ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_FIRST_PERSON, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_FIRST_PERSON ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_THIRD_PERSON, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_THIRD_PERSON ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_ENTITY, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_ENTITY ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_CAMERA_INDEPENDENT, []() -> float { - return qApp->getCamera().getMode() == CAMERA_MODE_INDEPENDENT ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_SNAP_TURN, []() -> float { - return qApp->getMyAvatar()->getSnapTurn() ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_ADVANCED_MOVEMENT_CONTROLS, []() -> float { - return qApp->getMyAvatar()->useAdvancedMovementControls() ? 1 : 0; - }); - - _applicationStateDevice->setInputVariant(STATE_GROUNDED, []() -> float { - return qApp->getMyAvatar()->getCharacterController()->onGround() ? 1 : 0; - }); - _applicationStateDevice->setInputVariant(STATE_NAV_FOCUSED, []() -> float { - auto offscreenUi = getOffscreenUI(); - return offscreenUi ? (offscreenUi->navigationFocused() ? 1 : 0) : 0; - }); - _applicationStateDevice->setInputVariant(STATE_PLATFORM_WINDOWS, []() -> float { -#if defined(Q_OS_WIN) - return 1; -#else - return 0; -#endif - }); - _applicationStateDevice->setInputVariant(STATE_PLATFORM_MAC, []() -> float { -#if defined(Q_OS_MAC) - return 1; -#else - return 0; -#endif - }); - _applicationStateDevice->setInputVariant(STATE_PLATFORM_ANDROID, []() -> float { -#if defined(Q_OS_ANDROID) - return 1 ; -#else - return 0; -#endif - }); - - - // Setup the _keyboardMouseDevice, _touchscreenDevice, _touchscreenVirtualPadDevice and the user input mapper with the default bindings - userInputMapper->registerDevice(_keyboardMouseDevice->getInputDevice()); - // if the _touchscreenDevice is not supported it will not be registered - if (_touchscreenDevice) { - userInputMapper->registerDevice(_touchscreenDevice->getInputDevice()); - } - if (_touchscreenVirtualPadDevice) { - userInputMapper->registerDevice(_touchscreenVirtualPadDevice->getInputDevice()); - } - - QString scriptsSwitch = QString("--").append(SCRIPTS_SWITCH); - _defaultScriptsLocation = getCmdOption(argc, constArgv, scriptsSwitch.toStdString().c_str()); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - loadSettings(); - - updateVerboseLogging(); - - // Now that we've loaded the menu and thus switched to the previous display plugin - // we can unlock the desktop repositioning code, since all the positions will be - // relative to the desktop size for this plugin - auto offscreenUi = getOffscreenUI(); - connect(offscreenUi.data(), &OffscreenUi::desktopReady, []() { - auto offscreenUi = getOffscreenUI(); - auto desktop = offscreenUi->getDesktop(); - if (desktop) { - desktop->setProperty("repositionLocked", false); - } - }); - - connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) - // Do not show login dialog if requested not to on the command line - QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); - int index = arguments().indexOf(hifiNoLoginCommandLineKey); - if (index != -1) { - resumeAfterLoginDialogActionTaken(); - return; - } - - showLoginScreen(); -#else - resumeAfterLoginDialogActionTaken(); -#endif - }); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - QTimer* settingsTimer = new QTimer(); - moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{ - // This needs to run on the settings thread, so we need to pass the `settingsTimer` as the - // receiver object, otherwise it will run on the application thread and trigger a warning - // about trying to kill the timer on the main thread. - connect(qApp, &Application::beforeAboutToQuit, settingsTimer, [this, settingsTimer]{ - // Disconnect the signal from the save settings - QObject::disconnect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); - // Stop the settings timer - settingsTimer->stop(); - // Delete it (this will trigger the thread destruction - settingsTimer->deleteLater(); - // Mark the settings thread as finished, so we know we can safely save in the main application - // shutdown code - _settingsGuard.trigger(); - }); - - int SAVE_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND; // Let's save every seconds for now - settingsTimer->setSingleShot(false); - settingsTimer->setInterval(SAVE_SETTINGS_INTERVAL); // 10s, Qt::CoarseTimer acceptable - QObject::connect(settingsTimer, &QTimer::timeout, this, &Application::saveSettings); - settingsTimer->start(); - }, QThread::LowestPriority); - - if (Menu::getInstance()->isOptionChecked(MenuOption::FirstPerson)) { - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); // So that camera doesn't auto-switch to third person. - } else if (Menu::getInstance()->isOptionChecked(MenuOption::IndependentMode)) { - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); - } else if (Menu::getInstance()->isOptionChecked(MenuOption::CameraEntityMode)) { - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); - } - - { - auto audioIO = DependencyManager::get().data(); - // set the local loopback interface for local sounds - AudioInjector::setLocalAudioInterface(audioIO); - auto audioScriptingInterface = DependencyManager::get(); - audioScriptingInterface->setLocalAudioInterface(audioIO); - connect(audioIO, &AudioClient::noiseGateOpened, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateOpened); - connect(audioIO, &AudioClient::noiseGateClosed, audioScriptingInterface.data(), &AudioScriptingInterface::noiseGateClosed); - connect(audioIO, &AudioClient::inputReceived, audioScriptingInterface.data(), &AudioScriptingInterface::inputReceived); - } - - this->installEventFilter(this); - - - -#ifdef HAVE_DDE - auto ddeTracker = DependencyManager::get(); - ddeTracker->init(); - connect(ddeTracker.data(), &FaceTracker::muteToggled, this, &Application::faceTrackerMuteToggled); -#endif - -#ifdef HAVE_IVIEWHMD - auto eyeTracker = DependencyManager::get(); - eyeTracker->init(); - setActiveEyeTracker(); -#endif - - // If launched from Steam, let it handle updates - const QString HIFI_NO_UPDATER_COMMAND_LINE_KEY = "--no-updater"; - bool noUpdater = arguments().indexOf(HIFI_NO_UPDATER_COMMAND_LINE_KEY) != -1; - bool buildCanUpdate = BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable - || BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Master; - if (!noUpdater && buildCanUpdate) { - constexpr auto INSTALLER_TYPE_CLIENT_ONLY = "client_only"; - - auto applicationUpdater = DependencyManager::set(); - - AutoUpdater::InstallerType type = installerType == INSTALLER_TYPE_CLIENT_ONLY - ? AutoUpdater::InstallerType::CLIENT_ONLY : AutoUpdater::InstallerType::FULL; - - applicationUpdater->setInstallerType(type); - applicationUpdater->setInstallerCampaign(installerCampaign); - connect(applicationUpdater.data(), &AutoUpdater::newVersionIsAvailable, dialogsManager.data(), &DialogsManager::showUpdateDialog); - applicationUpdater->checkForUpdate(); - } - - Menu::getInstance()->setIsOptionChecked(MenuOption::ActionMotorControl, true); - -// FIXME spacemouse code still needs cleanup -#if 0 - // the 3Dconnexion device wants to be initialized after a window is displayed. - SpacemouseManager::getInstance().init(); -#endif - - // If the user clicks on an object, we will check that it's a web surface, and if so, set the focus to it - auto pointerManager = DependencyManager::get(); - auto keyboardFocusOperator = [this](const QUuid& id, const PointerEvent& event) { - if (event.shouldFocus()) { - auto keyboard = DependencyManager::get(); - if (getEntities()->wantsKeyboardFocus(id)) { - setKeyboardFocusEntity(id); - } else if (!keyboard->containsID(id)) { // FIXME: this is a hack to make the keyboard work for now, since the keys would otherwise steal focus - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - } - } - }; - connect(pointerManager.data(), &PointerManager::triggerBeginEntity, keyboardFocusOperator); - connect(pointerManager.data(), &PointerManager::triggerBeginOverlay, keyboardFocusOperator); - - auto entityScriptingInterface = DependencyManager::get(); - connect(entityScriptingInterface.data(), &EntityScriptingInterface::deletingEntity, this, [this](const EntityItemID& entityItemID) { - if (entityItemID == _keyboardFocusedEntity.get()) { - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - } - }, Qt::QueuedConnection); - - EntityTreeRenderer::setAddMaterialToEntityOperator([this](const QUuid& entityID, graphics::MaterialLayer material, const std::string& parentMaterialName) { - if (_aboutToQuit) { - return false; - } - - auto renderable = getEntities()->renderableForEntityId(entityID); - if (renderable) { - renderable->addMaterial(material, parentMaterialName); - return true; - } - - return false; - }); - EntityTreeRenderer::setRemoveMaterialFromEntityOperator([this](const QUuid& entityID, graphics::MaterialPointer material, const std::string& parentMaterialName) { - if (_aboutToQuit) { - return false; - } - - auto renderable = getEntities()->renderableForEntityId(entityID); - if (renderable) { - renderable->removeMaterial(material, parentMaterialName); - return true; - } - - return false; - }); - - EntityTreeRenderer::setAddMaterialToAvatarOperator([](const QUuid& avatarID, graphics::MaterialLayer material, const std::string& parentMaterialName) { - auto avatarManager = DependencyManager::get(); - auto avatar = avatarManager->getAvatarBySessionID(avatarID); - if (avatar) { - avatar->addMaterial(material, parentMaterialName); - return true; - } - return false; - }); - EntityTreeRenderer::setRemoveMaterialFromAvatarOperator([](const QUuid& avatarID, graphics::MaterialPointer material, const std::string& parentMaterialName) { - auto avatarManager = DependencyManager::get(); - auto avatar = avatarManager->getAvatarBySessionID(avatarID); - if (avatar) { - avatar->removeMaterial(material, parentMaterialName); - return true; - } - return false; - }); - - EntityTree::setGetEntityObjectOperator([this](const QUuid& id) -> QObject* { - auto entities = getEntities(); - if (auto entity = entities->renderableForEntityId(id)) { - return qobject_cast(entity.get()); - } - return nullptr; - }); - - EntityTree::setTextSizeOperator([this](const QUuid& id, const QString& text) { - auto entities = getEntities(); - if (auto entity = entities->renderableForEntityId(id)) { - if (auto renderable = std::dynamic_pointer_cast(entity)) { - return renderable->textSize(text); - } - } - return QSizeF(0.0f, 0.0f); - }); - - connect(this, &Application::aboutToQuit, [this]() { - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - }); - - // Add periodic checks to send user activity data - static int CHECK_NEARBY_AVATARS_INTERVAL_MS = 10000; - static int NEARBY_AVATAR_RADIUS_METERS = 10; - - // setup the stats interval depending on if the 1s faster hearbeat was requested - static const QString FAST_STATS_ARG = "--fast-heartbeat"; - static int SEND_STATS_INTERVAL_MS = arguments().indexOf(FAST_STATS_ARG) != -1 ? 1000 : 10000; - - static glm::vec3 lastAvatarPosition = myAvatar->getWorldPosition(); - static glm::mat4 lastHMDHeadPose = getHMDSensorPose(); - static controller::Pose lastLeftHandPose = myAvatar->getLeftHandPose(); - static controller::Pose lastRightHandPose = myAvatar->getRightHandPose(); - - // Periodically send fps as a user activity event - QTimer* sendStatsTimer = new QTimer(this); - sendStatsTimer->setInterval(SEND_STATS_INTERVAL_MS); // 10s, Qt::CoarseTimer acceptable - connect(sendStatsTimer, &QTimer::timeout, this, [this]() { - - QJsonObject properties = {}; - MemoryInfo memInfo; - if (getMemoryInfo(memInfo)) { - properties["system_memory_total"] = static_cast(memInfo.totalMemoryBytes); - properties["system_memory_used"] = static_cast(memInfo.usedMemoryBytes); - properties["process_memory_used"] = static_cast(memInfo.processUsedMemoryBytes); - } - - // content location and build info - useful for filtering stats - auto addressManager = DependencyManager::get(); - auto currentDomain = addressManager->currentShareableAddress(true).toString(); // domain only - auto currentPath = addressManager->currentPath(true); // with orientation - properties["current_domain"] = currentDomain; - properties["current_path"] = currentPath; - properties["build_version"] = BuildInfo::VERSION; - - auto displayPlugin = qApp->getActiveDisplayPlugin(); - - properties["render_rate"] = getRenderLoopRate(); - properties["target_render_rate"] = getTargetRenderFrameRate(); - properties["present_rate"] = displayPlugin->presentRate(); - properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); - properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); - properties["stutter_rate"] = displayPlugin->stutterRate(); - properties["game_rate"] = getGameLoopRate(); - properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); - properties["hardware_stats"] = displayPlugin->getHardwareStats(); - - // deadlock watchdog related stats - properties["deadlock_watchdog_maxElapsed"] = (int)DeadlockWatchdogThread::_maxElapsed; - properties["deadlock_watchdog_maxElapsedAverage"] = (int)DeadlockWatchdogThread::_maxElapsedAverage; - - auto nodeList = DependencyManager::get(); - properties["packet_rate_in"] = nodeList->getInboundPPS(); - properties["packet_rate_out"] = nodeList->getOutboundPPS(); - properties["kbps_in"] = nodeList->getInboundKbps(); - properties["kbps_out"] = nodeList->getOutboundKbps(); - - SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer); - SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer); - SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer); - SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer); - SharedNodePointer messagesMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer); - properties["entity_ping"] = entityServerNode ? entityServerNode->getPingMs() : -1; - properties["audio_ping"] = audioMixerNode ? audioMixerNode->getPingMs() : -1; - properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1; - properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1; - properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1; - properties["atp_in_kbps"] = assetServerNode ? assetServerNode->getInboundKbps() : 0.0f; - - auto loadingRequests = ResourceCache::getLoadingRequests(); - - QJsonArray loadingRequestsStats; - for (const auto& request : loadingRequests) { - QJsonObject requestStats; - requestStats["filename"] = request->getURL().fileName(); - requestStats["received"] = request->getBytesReceived(); - requestStats["total"] = request->getBytesTotal(); - requestStats["attempts"] = (int)request->getDownloadAttempts(); - loadingRequestsStats.append(requestStats); - } - - properties["active_downloads"] = loadingRequests.size(); - properties["pending_downloads"] = (int)ResourceCache::getPendingRequestCount(); - properties["active_downloads_details"] = loadingRequestsStats; - - auto statTracker = DependencyManager::get(); - - properties["processing_resources"] = statTracker->getStat("Processing").toInt(); - properties["pending_processing_resources"] = statTracker->getStat("PendingProcessing").toInt(); - - QJsonObject startedRequests; - startedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_STARTED).toInt(); - startedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_STARTED).toInt(); - startedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_STARTED).toInt(); - startedRequests["total"] = startedRequests["atp"].toInt() + startedRequests["http"].toInt() - + startedRequests["file"].toInt(); - properties["started_requests"] = startedRequests; - - QJsonObject successfulRequests; - successfulRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_SUCCESS).toInt(); - successfulRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_SUCCESS).toInt(); - successfulRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_SUCCESS).toInt(); - successfulRequests["total"] = successfulRequests["atp"].toInt() + successfulRequests["http"].toInt() - + successfulRequests["file"].toInt(); - properties["successful_requests"] = successfulRequests; - - QJsonObject failedRequests; - failedRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_FAILED).toInt(); - failedRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_FAILED).toInt(); - failedRequests["file"] = statTracker->getStat(STAT_FILE_REQUEST_FAILED).toInt(); - failedRequests["total"] = failedRequests["atp"].toInt() + failedRequests["http"].toInt() - + failedRequests["file"].toInt(); - properties["failed_requests"] = failedRequests; - - QJsonObject cacheRequests; - cacheRequests["atp"] = statTracker->getStat(STAT_ATP_REQUEST_CACHE).toInt(); - cacheRequests["http"] = statTracker->getStat(STAT_HTTP_REQUEST_CACHE).toInt(); - cacheRequests["total"] = cacheRequests["atp"].toInt() + cacheRequests["http"].toInt(); - properties["cache_requests"] = cacheRequests; - - QJsonObject atpMappingRequests; - atpMappingRequests["started"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_STARTED).toInt(); - atpMappingRequests["failed"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_FAILED).toInt(); - atpMappingRequests["successful"] = statTracker->getStat(STAT_ATP_MAPPING_REQUEST_SUCCESS).toInt(); - properties["atp_mapping_requests"] = atpMappingRequests; - - properties["throttled"] = _displayPlugin ? _displayPlugin->isThrottled() : false; - - QJsonObject bytesDownloaded; - auto atpBytes = statTracker->getStat(STAT_ATP_RESOURCE_TOTAL_BYTES).toLongLong(); - auto httpBytes = statTracker->getStat(STAT_HTTP_RESOURCE_TOTAL_BYTES).toLongLong(); - auto fileBytes = statTracker->getStat(STAT_FILE_RESOURCE_TOTAL_BYTES).toLongLong(); - bytesDownloaded["atp"] = atpBytes; - bytesDownloaded["http"] = httpBytes; - bytesDownloaded["file"] = fileBytes; - bytesDownloaded["total"] = atpBytes + httpBytes + fileBytes; - properties["bytes_downloaded"] = bytesDownloaded; - - auto myAvatar = getMyAvatar(); - glm::vec3 avatarPosition = myAvatar->getWorldPosition(); - properties["avatar_has_moved"] = lastAvatarPosition != avatarPosition; - lastAvatarPosition = avatarPosition; - - auto entityScriptingInterface = DependencyManager::get(); - auto entityActivityTracking = entityScriptingInterface->getActivityTracking(); - entityScriptingInterface->resetActivityTracking(); - properties["added_entity_cnt"] = entityActivityTracking.addedEntityCount; - properties["deleted_entity_cnt"] = entityActivityTracking.deletedEntityCount; - properties["edited_entity_cnt"] = entityActivityTracking.editedEntityCount; - - NodeToOctreeSceneStats* octreeServerSceneStats = getOcteeSceneStats(); - unsigned long totalServerOctreeElements = 0; - for (NodeToOctreeSceneStatsIterator i = octreeServerSceneStats->begin(); i != octreeServerSceneStats->end(); i++) { - totalServerOctreeElements += i->second.getTotalElements(); - } - - properties["local_octree_elements"] = (qint64) OctreeElement::getInternalNodeCount(); - properties["server_octree_elements"] = (qint64) totalServerOctreeElements; - - properties["active_display_plugin"] = getActiveDisplayPlugin()->getName(); - properties["using_hmd"] = isHMDMode(); - - _autoSwitchDisplayModeSupportedHMDPlugin = nullptr; - foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { - if (displayPlugin->isHmd() && - displayPlugin->getSupportsAutoSwitch()) { - _autoSwitchDisplayModeSupportedHMDPlugin = displayPlugin; - _autoSwitchDisplayModeSupportedHMDPluginName = - _autoSwitchDisplayModeSupportedHMDPlugin->getName(); - _previousHMDWornStatus = - _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); - break; - } - } - - if (_autoSwitchDisplayModeSupportedHMDPlugin) { - if (getActiveDisplayPlugin() != _autoSwitchDisplayModeSupportedHMDPlugin && - !_autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { - startHMDStandBySession(); - } - // Poll periodically to check whether the user has worn HMD or not. Switch Display mode accordingly. - // If the user wears HMD then switch to VR mode. If the user removes HMD then switch to Desktop mode. - QTimer* autoSwitchDisplayModeTimer = new QTimer(this); - connect(autoSwitchDisplayModeTimer, SIGNAL(timeout()), this, SLOT(switchDisplayMode())); - autoSwitchDisplayModeTimer->start(INTERVAL_TO_CHECK_HMD_WORN_STATUS); - } - - auto glInfo = getGLContextData(); - properties["gl_info"] = glInfo; - properties["gpu_used_memory"] = (int)BYTES_TO_MB(gpu::Context::getUsedGPUMemSize()); - properties["gpu_free_memory"] = (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemSize()); - properties["gpu_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerGPUAverage()); - properties["batch_frame_time"] = (float)(qApp->getGPUContext()->getFrameTimerBatchAverage()); - properties["ideal_thread_count"] = QThread::idealThreadCount(); - - auto hmdHeadPose = getHMDSensorPose(); - properties["hmd_head_pose_changed"] = isHMDMode() && (hmdHeadPose != lastHMDHeadPose); - lastHMDHeadPose = hmdHeadPose; - - auto leftHandPose = myAvatar->getLeftHandPose(); - auto rightHandPose = myAvatar->getRightHandPose(); - // controller::Pose considers two poses to be different if either are invalid. In our case, we actually - // want to consider the pose to be unchanged if it was invalid and still is invalid, so we check that first. - properties["hand_pose_changed"] = - ((leftHandPose.valid || lastLeftHandPose.valid) && (leftHandPose != lastLeftHandPose)) - || ((rightHandPose.valid || lastRightHandPose.valid) && (rightHandPose != lastRightHandPose)); - lastLeftHandPose = leftHandPose; - lastRightHandPose = rightHandPose; - - UserActivityLogger::getInstance().logAction("stats", properties); - }); - sendStatsTimer->start(); - - // Periodically check for count of nearby avatars - static int lastCountOfNearbyAvatars = -1; - QTimer* checkNearbyAvatarsTimer = new QTimer(this); - checkNearbyAvatarsTimer->setInterval(CHECK_NEARBY_AVATARS_INTERVAL_MS); // 10 seconds, Qt::CoarseTimer ok - connect(checkNearbyAvatarsTimer, &QTimer::timeout, this, []() { - auto avatarManager = DependencyManager::get(); - int nearbyAvatars = avatarManager->numberOfAvatarsInRange(avatarManager->getMyAvatar()->getWorldPosition(), - NEARBY_AVATAR_RADIUS_METERS) - 1; - if (nearbyAvatars != lastCountOfNearbyAvatars) { - lastCountOfNearbyAvatars = nearbyAvatars; - UserActivityLogger::getInstance().logAction("nearby_avatars", { { "count", nearbyAvatars } }); - } - }); - checkNearbyAvatarsTimer->start(); - - // Track user activity event when we receive a mute packet - auto onMutedByMixer = []() { - UserActivityLogger::getInstance().logAction("received_mute_packet"); - }; - connect(DependencyManager::get().data(), &AudioClient::mutedByMixer, this, onMutedByMixer); - - // Track when the address bar is opened - auto onAddressBarShown = [this]() { - // Record time - UserActivityLogger::getInstance().logAction("opened_address_bar", { { "uptime_ms", _sessionRunTimer.elapsed() } }); - }; - connect(DependencyManager::get().data(), &DialogsManager::addressBarShown, this, onAddressBarShown); - - // Make sure we don't time out during slow operations at startup - updateHeartbeat(); - - OctreeEditPacketSender* packetSender = entityScriptingInterface->getPacketSender(); - EntityEditPacketSender* entityPacketSender = static_cast(packetSender); - entityPacketSender->setMyAvatar(myAvatar.get()); - - connect(this, &Application::applicationStateChanged, this, &Application::activeChanged); - connect(_window, SIGNAL(windowMinimizedChanged(bool)), this, SLOT(windowMinimizedChanged(bool))); - qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0); - - EntityTreeRenderer::setEntitiesShouldFadeFunction([this]() { - SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); - return entityServerNode && !isPhysicsEnabled(); - }); - - _snapshotSound = DependencyManager::get()->getSound(PathUtils::resourcesUrl("sounds/snapshot/snap.wav")); - - // Monitor model assets (e.g., from Clara.io) added to the world that may need resizing. - static const int ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS = 1000; - _addAssetToWorldResizeTimer.setInterval(ADD_ASSET_TO_WORLD_TIMER_INTERVAL_MS); // 1s, Qt::CoarseTimer acceptable - connect(&_addAssetToWorldResizeTimer, &QTimer::timeout, this, &Application::addAssetToWorldCheckModelSize); - - // Auto-update and close adding asset to world info message box. - static const int ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS = 5000; - _addAssetToWorldInfoTimer.setInterval(ADD_ASSET_TO_WORLD_INFO_TIMEOUT_MS); // 5s, Qt::CoarseTimer acceptable - _addAssetToWorldInfoTimer.setSingleShot(true); - connect(&_addAssetToWorldInfoTimer, &QTimer::timeout, this, &Application::addAssetToWorldInfoTimeout); - static const int ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS = 8000; - _addAssetToWorldErrorTimer.setInterval(ADD_ASSET_TO_WORLD_ERROR_TIMEOUT_MS); // 8s, Qt::CoarseTimer acceptable - _addAssetToWorldErrorTimer.setSingleShot(true); - connect(&_addAssetToWorldErrorTimer, &QTimer::timeout, this, &Application::addAssetToWorldErrorTimeout); - - connect(this, &QCoreApplication::aboutToQuit, this, &Application::addAssetToWorldMessageClose); - connect(&domainHandler, &DomainHandler::domainURLChanged, this, &Application::addAssetToWorldMessageClose); - connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &Application::addAssetToWorldMessageClose); - - updateSystemTabletMode(); - - connect(&_myCamera, &Camera::modeUpdated, this, &Application::cameraModeChanged); - - DependencyManager::get()->setShouldPickHUDOperator([]() { return DependencyManager::get()->isHMDMode(); }); - DependencyManager::get()->setCalculatePos2DFromHUDOperator([this](const glm::vec3& intersection) { - const glm::vec2 MARGIN(25.0f); - glm::vec2 maxPos = _controllerScriptingInterface->getViewportDimensions() - MARGIN; - glm::vec2 pos2D = DependencyManager::get()->overlayFromWorldPoint(intersection); - return glm::max(MARGIN, glm::min(pos2D, maxPos)); - }); - - // Setup the mouse ray pick and related operators - { - auto mouseRayPick = std::make_shared(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_LOCAL_ENTITIES()), 0.0f, true); - mouseRayPick->parentTransform = std::make_shared(); - mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE); - auto mouseRayPickID = DependencyManager::get()->addPick(PickQuery::Ray, mouseRayPick); - DependencyManager::get()->setMouseRayPickID(mouseRayPickID); - } - DependencyManager::get()->setMouseRayPickResultOperator([](unsigned int rayPickID) { - RayToEntityIntersectionResult entityResult; - entityResult.intersects = false; - auto pickResult = DependencyManager::get()->getPrevPickResultTyped(rayPickID); - if (pickResult) { - entityResult.intersects = pickResult->type != IntersectionType::NONE; - if (entityResult.intersects) { - entityResult.intersection = pickResult->intersection; - entityResult.distance = pickResult->distance; - entityResult.surfaceNormal = pickResult->surfaceNormal; - entityResult.entityID = pickResult->objectID; - entityResult.extraInfo = pickResult->extraInfo; - } - } - return entityResult; - }); - DependencyManager::get()->setSetPrecisionPickingOperator([](unsigned int rayPickID, bool value) { - DependencyManager::get()->setPrecisionPicking(rayPickID, value); - }); - - EntityItem::setBillboardRotationOperator([this](const glm::vec3& position, const glm::quat& rotation, BillboardMode billboardMode, const glm::vec3& frustumPos) { - if (billboardMode == BillboardMode::YAW) { - //rotate about vertical to face the camera - glm::vec3 dPosition = frustumPos - position; - // If x and z are 0, atan(x, z) is undefined, so default to 0 degrees - float yawRotation = dPosition.x == 0.0f && dPosition.z == 0.0f ? 0.0f : glm::atan(dPosition.x, dPosition.z); - return glm::quat(glm::vec3(0.0f, yawRotation, 0.0f)); - } else if (billboardMode == BillboardMode::FULL) { - // use the referencial from the avatar, y isn't always up - glm::vec3 avatarUP = DependencyManager::get()->getMyAvatar()->getWorldOrientation() * Vectors::UP; - // check to see if glm::lookAt will work / using glm::lookAt variable name - glm::highp_vec3 s(glm::cross(position - frustumPos, avatarUP)); - - // make sure s is not NaN for any component - if (glm::length2(s) > 0.0f) { - return glm::conjugate(glm::toQuat(glm::lookAt(frustumPos, position, avatarUP))); - } - } - return rotation; - }); - EntityItem::setPrimaryViewFrustumPositionOperator([this]() { - ViewFrustum viewFrustum; - copyViewFrustum(viewFrustum); - return viewFrustum.getPosition(); - }); - - render::entities::WebEntityRenderer::setAcquireWebSurfaceOperator([this](const QString& url, bool htmlContent, QSharedPointer& webSurface, bool& cachedWebSurface) { - bool isTablet = url == TabletScriptingInterface::QML; - if (htmlContent) { - webSurface = DependencyManager::get()->acquire(render::entities::WebEntityRenderer::QML); - cachedWebSurface = true; - auto rootItemLoadedFunctor = [url, webSurface] { - webSurface->getRootItem()->setProperty(render::entities::WebEntityRenderer::URL_PROPERTY, url); - }; - if (webSurface->getRootItem()) { - rootItemLoadedFunctor(); - } else { - QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); - } - auto surfaceContext = webSurface->getSurfaceContext(); - surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); - } else { - // FIXME: the tablet should use the OffscreenQmlSurfaceCache - webSurface = QSharedPointer(new OffscreenQmlSurface(), [](OffscreenQmlSurface* webSurface) { - AbstractViewStateInterface::instance()->sendLambdaEvent([webSurface] { - // WebEngineView may run other threads (wasapi), so they must be deleted for a clean shutdown - // if the application has already stopped its event loop, delete must be explicit - delete webSurface; - }); - }); - auto rootItemLoadedFunctor = [webSurface, url, isTablet] { - Application::setupQmlSurface(webSurface->getSurfaceContext(), isTablet || url == LOGIN_DIALOG.toString()); - }; - if (webSurface->getRootItem()) { - rootItemLoadedFunctor(); - } else { - QObject::connect(webSurface.data(), &hifi::qml::OffscreenSurface::rootContextCreated, rootItemLoadedFunctor); - } - webSurface->load(url); - cachedWebSurface = false; - } - const uint8_t DEFAULT_MAX_FPS = 10; - const uint8_t TABLET_FPS = 90; - webSurface->setMaxFps(isTablet ? TABLET_FPS : DEFAULT_MAX_FPS); - }); - render::entities::WebEntityRenderer::setReleaseWebSurfaceOperator([this](QSharedPointer& webSurface, bool& cachedWebSurface, std::vector& connections) { - QQuickItem* rootItem = webSurface->getRootItem(); - - // Fix for crash in QtWebEngineCore when rapidly switching domains - // Call stop on the QWebEngineView before destroying OffscreenQMLSurface. - if (rootItem && !cachedWebSurface) { - // stop loading - QMetaObject::invokeMethod(rootItem, "stop"); - } - - webSurface->pause(); - - for (auto& connection : connections) { - QObject::disconnect(connection); - } - connections.clear(); - - // If the web surface was fetched out of the cache, release it back into the cache - if (cachedWebSurface) { - // If it's going back into the cache make sure to explicitly set the URL to a blank page - // in order to stop any resource consumption or audio related to the page. - if (rootItem) { - rootItem->setProperty("url", "about:blank"); - } - auto offscreenCache = DependencyManager::get(); - if (offscreenCache) { - offscreenCache->release(render::entities::WebEntityRenderer::QML, webSurface); - } - cachedWebSurface = false; - } - webSurface.reset(); - }); - - // Preload Tablet sounds - DependencyManager::get()->setEntityTree(qApp->getEntities()->getTree()); - DependencyManager::get()->preloadSounds(); - DependencyManager::get()->createKeyboard(); - - _pendingIdleEvent = false; - _graphicsEngine.startup(); - - qCDebug(interfaceapp) << "Metaverse session ID is" << uuidStringWithoutCurlyBraces(accountManager->getSessionID()); - -#if defined(Q_OS_ANDROID) - connect(&AndroidHelper::instance(), &AndroidHelper::beforeEnterBackground, this, &Application::beforeEnterBackground); - connect(&AndroidHelper::instance(), &AndroidHelper::enterBackground, this, &Application::enterBackground); - connect(&AndroidHelper::instance(), &AndroidHelper::enterForeground, this, &Application::enterForeground); - connect(&AndroidHelper::instance(), &AndroidHelper::toggleAwayMode, this, &Application::toggleAwayMode); - AndroidHelper::instance().notifyLoadComplete(); -#endif - pauseUntilLoginDetermined(); -} - -void Application::updateVerboseLogging() { - auto menu = Menu::getInstance(); - if (!menu) { - return; - } - bool enable = menu->isOptionChecked(MenuOption::VerboseLogging); - - QString rules = - "hifi.*.info=%1\n" - "hifi.audio-stream.debug=false\n" - "hifi.audio-stream.info=false"; - rules = rules.arg(enable ? "true" : "false"); - QLoggingCategory::setFilterRules(rules); -} - -void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCodeInt, const QString& extraInfo) { - DomainHandler::ConnectionRefusedReason reasonCode = static_cast(reasonCodeInt); - - if (reasonCode == DomainHandler::ConnectionRefusedReason::TooManyUsers && !extraInfo.isEmpty()) { - DependencyManager::get()->handleLookupString(extraInfo); - return; - } - - switch (reasonCode) { - case DomainHandler::ConnectionRefusedReason::ProtocolMismatch: - case DomainHandler::ConnectionRefusedReason::TooManyUsers: - case DomainHandler::ConnectionRefusedReason::Unknown: { - QString message = "Unable to connect to the location you are visiting.\n"; - message += reasonMessage; - OffscreenUi::asyncWarning("", message); - getMyAvatar()->setWorldVelocity(glm::vec3(0.0f)); - break; - } - default: - // nothing to do. - break; - } -} - -QString Application::getUserAgent() { - if (QThread::currentThread() != thread()) { - QString userAgent; - - BLOCKING_INVOKE_METHOD(this, "getUserAgent", Q_RETURN_ARG(QString, userAgent)); - - return userAgent; - } - - QString userAgent = "Mozilla/5.0 (HighFidelityInterface/" + BuildInfo::VERSION + "; " - + QSysInfo::productType() + " " + QSysInfo::productVersion() + ")"; - - auto formatPluginName = [](QString name) -> QString { return name.trimmed().replace(" ", "-"); }; - - // For each plugin, add to userAgent - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - for (auto& dp : displayPlugins) { - if (dp->isActive() && dp->isHmd()) { - userAgent += " " + formatPluginName(dp->getName()); - } - } - auto inputPlugins= PluginManager::getInstance()->getInputPlugins(); - for (auto& ip : inputPlugins) { - if (ip->isActive()) { - userAgent += " " + formatPluginName(ip->getName()); - } - } - // for codecs, we include all of them, even if not active - auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); - for (auto& cp : codecPlugins) { - userAgent += " " + formatPluginName(cp->getName()); - } - - return userAgent; -} - -void Application::toggleTabletUI(bool shouldOpen) const { - auto hmd = DependencyManager::get(); - if (!(shouldOpen && hmd->getShouldShowTablet())) { - auto HMD = DependencyManager::get(); - HMD->toggleShouldShowTablet(); - - if (!HMD->getShouldShowTablet()) { - DependencyManager::get()->setRaised(false); - _window->activateWindow(); - auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); - tablet->unfocus(); - } - } -} - -void Application::checkChangeCursor() { - QMutexLocker locker(&_changeCursorLock); - if (_cursorNeedsChanging) { -#ifdef Q_OS_MAC - auto cursorTarget = _window; // OSX doesn't seem to provide for hiding the cursor only on the GL widget -#else - // On windows and linux, hiding the top level cursor also means it's invisible when hovering over the - // window menu, which is a pain, so only hide it for the GL surface - auto cursorTarget = _glWidget; -#endif - cursorTarget->setCursor(_desiredCursor); - - _cursorNeedsChanging = false; - } -} - -void Application::showCursor(const Cursor::Icon& cursor) { - QMutexLocker locker(&_changeCursorLock); - - auto managedCursor = Cursor::Manager::instance().getCursor(); - auto curIcon = managedCursor->getIcon(); - if (curIcon != cursor) { - managedCursor->setIcon(cursor); - curIcon = cursor; - } - _desiredCursor = cursor == Cursor::Icon::SYSTEM ? Qt::ArrowCursor : Qt::BlankCursor; - _cursorNeedsChanging = true; -} - -void Application::updateHeartbeat() const { - DeadlockWatchdogThread::updateHeartbeat(); -} - -void Application::onAboutToQuit() { - // quickly save AvatarEntityData before the EntityTree is dismantled - getMyAvatar()->saveAvatarEntityDataToSettings(); - - emit beforeAboutToQuit(); - - if (getLoginDialogPoppedUp() && _firstRun.get()) { - _firstRun.set(false); - } - - foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { - if (inputPlugin->isActive()) { - inputPlugin->deactivate(); - } - } - - // The active display plugin needs to be loaded before the menu system is active, - // so its persisted explicitly here - Setting::Handle{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME }.set(getActiveDisplayPlugin()->getName()); - - loginDialogPoppedUp.set(false); - - getActiveDisplayPlugin()->deactivate(); - if (_autoSwitchDisplayModeSupportedHMDPlugin - && _autoSwitchDisplayModeSupportedHMDPlugin->isSessionActive()) { - _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); - } - // use the CloseEventSender via a QThread to send an event that says the user asked for the app to close - DependencyManager::get()->startThread(); - - // Hide Running Scripts dialog so that it gets destroyed in an orderly manner; prevents warnings at shutdown. -#if !defined(DISABLE_QML) - getOffscreenUI()->hide("RunningScripts"); -#endif - - _aboutToQuit = true; - - cleanupBeforeQuit(); -} - -void Application::cleanupBeforeQuit() { - // add a logline indicating if QTWEBENGINE_REMOTE_DEBUGGING is set or not - QString webengineRemoteDebugging = QProcessEnvironment::systemEnvironment().value("QTWEBENGINE_REMOTE_DEBUGGING", "false"); - qCDebug(interfaceapp) << "QTWEBENGINE_REMOTE_DEBUGGING =" << webengineRemoteDebugging; - - DependencyManager::prepareToExit(); - - if (tracing::enabled()) { - auto tracer = DependencyManager::get(); - tracer->stopTracing(); - auto outputFile = property(hifi::properties::TRACING).toString(); - tracer->serialize(outputFile); - } - - // Stop third party processes so that they're not left running in the event of a subsequent shutdown crash. -#ifdef HAVE_DDE - DependencyManager::get()->setEnabled(false); -#endif -#ifdef HAVE_IVIEWHMD - DependencyManager::get()->setEnabled(false, true); -#endif - AnimDebugDraw::getInstance().shutdown(); - - // FIXME: once we move to shared pointer for the INputDevice we shoud remove this naked delete: - _applicationStateDevice.reset(); - - { - if (_keyboardFocusHighlightID != UNKNOWN_ENTITY_ID) { - DependencyManager::get()->deleteEntity(_keyboardFocusHighlightID); - _keyboardFocusHighlightID = UNKNOWN_ENTITY_ID; - } - } - - { - auto nodeList = DependencyManager::get(); - - // send the domain a disconnect packet, force stoppage of domain-server check-ins - nodeList->getDomainHandler().disconnect(); - nodeList->setIsShuttingDown(true); - - // tell the packet receiver we're shutting down, so it can drop packets - nodeList->getPacketReceiver().setShouldDropPackets(true); - } - - getEntities()->shutdown(); // tell the entities system we're shutting down, so it will stop running scripts - - // Clear any queued processing (I/O, FBX/OBJ/Texture parsing) - QThreadPool::globalInstance()->clear(); - - DependencyManager::destroy(); - - // FIXME: Something is still holding on to the ScriptEnginePointers contained in ScriptEngines, and they hold backpointers to ScriptEngines, - // so this doesn't shut down properly - DependencyManager::get()->shutdownScripting(); // stop all currently running global scripts - // These classes hold ScriptEnginePointers, so they must be destroyed before ScriptEngines - // Must be done after shutdownScripting in case any scripts try to access these things - { - DependencyManager::destroy(); - EntityTreePointer tree = getEntities()->getTree(); - tree->setSimulation(nullptr); - DependencyManager::destroy(); - } - DependencyManager::destroy(); - - bool keepMeLoggedIn = Setting::Handle(KEEP_ME_LOGGED_IN_SETTING_NAME, false).get(); - if (!keepMeLoggedIn) { - DependencyManager::get()->removeAccountFromFile(); - } - - _displayPlugin.reset(); - PluginManager::getInstance()->shutdown(); - - // Cleanup all overlays after the scripts, as scripts might add more - _overlays.cleanupAllOverlays(); - - // first stop all timers directly or by invokeMethod - // depending on what thread they run in - locationUpdateTimer.stop(); - identityPacketTimer.stop(); - pingTimer.stop(); - - // Wait for the settings thread to shut down, and save the settings one last time when it's safe - if (_settingsGuard.wait()) { - // save state - saveSettings(); - } - - _window->saveGeometry(); - - // Destroy third party processes after scripts have finished using them. -#ifdef HAVE_DDE - DependencyManager::destroy(); -#endif -#ifdef HAVE_IVIEWHMD - DependencyManager::destroy(); -#endif - - DependencyManager::destroy(); // Must be destroyed before TabletScriptingInterface - - // stop QML - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - DependencyManager::destroy(); - - if (_snapshotSoundInjector != nullptr) { - _snapshotSoundInjector->stop(); - } - - // destroy Audio so it and its threads have a chance to go down safely - // this must happen after QML, as there are unexplained audio crashes originating in qtwebengine - QMetaObject::invokeMethod(DependencyManager::get().data(), "stop"); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - // The PointerManager must be destroyed before the PickManager because when a Pointer is deleted, - // it accesses the PickManager to delete its associated Pick - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; -} - -Application::~Application() { - // remove avatars from physics engine - auto avatarManager = DependencyManager::get(); - avatarManager->clearOtherAvatars(); - - PhysicsEngine::Transaction transaction; - avatarManager->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - avatarManager->handleProcessedPhysicsTransaction(transaction); - - avatarManager->deleteAllAvatars(); - - auto myCharacterController = getMyAvatar()->getCharacterController(); - myCharacterController->clearDetailedMotionStates(); - - myCharacterController->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - myCharacterController->handleProcessedPhysicsTransaction(transaction); - - _physicsEngine->setCharacterController(nullptr); - - // the _shapeManager should have zero references - _shapeManager.collectGarbage(); - assert(_shapeManager.getNumShapes() == 0); - - // shutdown graphics engine - _graphicsEngine.shutdown(); - - _gameWorkload.shutdown(); - - DependencyManager::destroy(); - - _entityClipboard->eraseAllOctreeElements(); - _entityClipboard.reset(); - - _octreeProcessor.terminate(); - _entityEditSender.terminate(); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - steamClient->shutdown(); - } - - if (auto oculusPlatform = PluginManager::getInstance()->getOculusPlatformPlugin()) { - oculusPlatform->shutdown(); - } - - DependencyManager::destroy(); - - DependencyManager::destroy(); // must be destroyed before the FramebufferCache - - DependencyManager::destroy(); - - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - DependencyManager::destroy(); - - DependencyManager::get()->cleanup(); - - // remove the NodeList from the DependencyManager - DependencyManager::destroy(); - -#if 0 - ConnexionClient::getInstance().destroy(); -#endif - // The window takes ownership of the menu, so this has the side effect of destroying it. - _window->setMenuBar(nullptr); - - _window->deleteLater(); - - // make sure that the quit event has finished sending before we take the application down - auto closeEventSender = DependencyManager::get(); - while (!closeEventSender->hasFinishedQuitEvent() && !closeEventSender->hasTimedOutQuitEvent()) { - // sleep a little so we're not spinning at 100% - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - // quit the thread used by the closure event sender - closeEventSender->thread()->quit(); - - // Can't log to file past this point, FileLogger about to be deleted - qInstallMessageHandler(LogHandler::verboseMessageHandler); -} - -void Application::initializeGL() { - qCDebug(interfaceapp) << "Created Display Window."; - -#ifdef DISABLE_QML - setAttribute(Qt::AA_DontCheckOpenGLContextThreadAffinity); -#endif - - // initialize glut for shape drawing; Qt apparently initializes it on OS X - if (_isGLInitialized) { - return; - } else { - _isGLInitialized = true; - } - - _glWidget->windowHandle()->setFormat(getDefaultOpenGLSurfaceFormat()); - - // When loading QtWebEngineWidgets, it creates a global share context on startup. - // We have to account for this possibility by checking here for an existing - // global share context - auto globalShareContext = qt_gl_global_share_context(); - -#if !defined(DISABLE_QML) - // Build a shared canvas / context for the Chromium processes - if (!globalShareContext) { - // Chromium rendering uses some GL functions that prevent nSight from capturing - // frames, so we only create the shared context if nsight is NOT active. - if (!nsightActive()) { - _chromiumShareContext = new OffscreenGLCanvas(); - _chromiumShareContext->setObjectName("ChromiumShareContext"); - auto format =QSurfaceFormat::defaultFormat(); -#ifdef Q_OS_MAC - // On mac, the primary shared OpenGL context must be a 3.2 core context, - // or chromium flips out and spews error spam (but renders fine) - format.setMajorVersion(3); - format.setMinorVersion(2); -#endif - _chromiumShareContext->setFormat(format); - _chromiumShareContext->create(); - if (!_chromiumShareContext->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make chromium shared context current"); - } - globalShareContext = _chromiumShareContext->getContext(); - qt_gl_set_global_share_context(globalShareContext); - _chromiumShareContext->doneCurrent(); - } - } -#endif - - - _glWidget->createContext(globalShareContext); - - if (!_glWidget->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make window context current"); - } - -#if !defined(DISABLE_QML) - // Disable signed distance field font rendering on ATI/AMD GPUs, due to - // https://highfidelity.manuscript.com/f/cases/13677/Text-showing-up-white-on-Marketplace-app - std::string vendor{ (const char*)glGetString(GL_VENDOR) }; - if ((vendor.find("AMD") != std::string::npos) || (vendor.find("ATI") != std::string::npos)) { - qputenv("QTWEBENGINE_CHROMIUM_FLAGS", QByteArray("--disable-distance-field-text")); - } -#endif - - if (!globalShareContext) { - globalShareContext = _glWidget->qglContext(); - qt_gl_set_global_share_context(globalShareContext); - } - - // Build a shared canvas / context for the QML rendering -#if !defined(DISABLE_QML) - { - _qmlShareContext = new OffscreenGLCanvas(); - _qmlShareContext->setObjectName("QmlShareContext"); - _qmlShareContext->create(globalShareContext); - if (!_qmlShareContext->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make QML shared context current"); - } - OffscreenQmlSurface::setSharedContext(_qmlShareContext->getContext()); - _qmlShareContext->doneCurrent(); - if (!_glWidget->makeCurrent()) { - qCWarning(interfaceapp, "Unable to make window context current"); - } - } -#endif - - - // Build an offscreen GL context for the main thread. - _glWidget->makeCurrent(); - glClearColor(0.2f, 0.2f, 0.2f, 1); - glClear(GL_COLOR_BUFFER_BIT); - _glWidget->swapBuffers(); - - _graphicsEngine.initializeGPU(_glWidget); -} - -void Application::initializeDisplayPlugins() { - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - Setting::Handle activeDisplayPluginSetting{ ACTIVE_DISPLAY_PLUGIN_SETTING_NAME, displayPlugins.at(0)->getName() }; - auto lastActiveDisplayPluginName = activeDisplayPluginSetting.get(); - - auto defaultDisplayPlugin = displayPlugins.at(0); - // Once time initialization code - DisplayPluginPointer targetDisplayPlugin; - foreach(auto displayPlugin, displayPlugins) { - displayPlugin->setContext(_graphicsEngine.getGPUContext()); - if (displayPlugin->getName() == lastActiveDisplayPluginName) { - targetDisplayPlugin = displayPlugin; - } - QObject::connect(displayPlugin.get(), &DisplayPlugin::recommendedFramebufferSizeChanged, - [this](const QSize& size) { resizeGL(); }); - QObject::connect(displayPlugin.get(), &DisplayPlugin::resetSensorsRequested, this, &Application::requestReset); - if (displayPlugin->isHmd()) { - auto hmdDisplayPlugin = dynamic_cast(displayPlugin.get()); - QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdMountedChanged, - DependencyManager::get().data(), &HMDScriptingInterface::mountedChanged); - QObject::connect(hmdDisplayPlugin, &HmdDisplayPlugin::hmdVisibleChanged, this, &Application::hmdVisibleChanged); - } - } - - // The default display plugin needs to be activated first, otherwise the display plugin thread - // may be launched by an external plugin, which is bad - setDisplayPlugin(defaultDisplayPlugin); - - // Now set the desired plugin if it's not the same as the default plugin - if (targetDisplayPlugin && (targetDisplayPlugin != defaultDisplayPlugin)) { - setDisplayPlugin(targetDisplayPlugin); - } - - // Submit a default frame to render until the engine starts up - updateRenderArgs(0.0f); -} - -void Application::initializeRenderEngine() { - // FIXME: on low end systems os the shaders take up to 1 minute to compile, so we pause the deadlock watchdog thread. - DeadlockWatchdogThread::withPause([&] { - _graphicsEngine.initializeRender(DISABLE_DEFERRED); - DependencyManager::get()->registerKeyboardHighlighting(); - }); -} - -extern void setupPreferences(); -#if !defined(DISABLE_QML) -static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active = false); -#endif - -void Application::showLoginScreen() { -#if !defined(DISABLE_QML) - auto accountManager = DependencyManager::get(); - auto dialogsManager = DependencyManager::get(); - if (!accountManager->isLoggedIn()) { - if (!isHMDMode()) { - auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); - toolbar->writeProperty("visible", false); - } - _loginDialogPoppedUp = true; - dialogsManager->showLoginDialog(); - emit loginDialogFocusEnabled(); - QJsonObject loginData = {}; - loginData["action"] = "login dialog popped up"; - UserActivityLogger::getInstance().logAction("encourageLoginDialog", loginData); - _window->setWindowTitle("High Fidelity Interface"); - } else { - resumeAfterLoginDialogActionTaken(); - } - _loginDialogPoppedUp = !accountManager->isLoggedIn(); - loginDialogPoppedUp.set(_loginDialogPoppedUp); -#else - resumeAfterLoginDialogActionTaken(); -#endif -} - -void Application::initializeUi() { - AddressBarDialog::registerType(); - ErrorDialog::registerType(); - LoginDialog::registerType(); - Tooltip::registerType(); - UpdateDialog::registerType(); - QmlContextCallback commerceCallback = [](QQmlContext* context) { - context->setContextProperty("Commerce", DependencyManager::get().data()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/commerce/checkout/Checkout.qml" }, - QUrl{ "hifi/commerce/common/CommerceLightbox.qml" }, - QUrl{ "hifi/commerce/common/EmulatedMarketplaceHeader.qml" }, - QUrl{ "hifi/commerce/common/FirstUseTutorial.qml" }, - QUrl{ "hifi/commerce/common/sendAsset/SendAsset.qml" }, - QUrl{ "hifi/commerce/common/SortableListModel.qml" }, - QUrl{ "hifi/commerce/inspectionCertificate/InspectionCertificate.qml" }, - QUrl{ "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"}, - QUrl{ "hifi/commerce/purchases/PurchasedItem.qml" }, - QUrl{ "hifi/commerce/purchases/Purchases.qml" }, - QUrl{ "hifi/commerce/wallet/Help.qml" }, - QUrl{ "hifi/commerce/wallet/NeedsLogIn.qml" }, - QUrl{ "hifi/commerce/wallet/PassphraseChange.qml" }, - QUrl{ "hifi/commerce/wallet/PassphraseModal.qml" }, - QUrl{ "hifi/commerce/wallet/PassphraseSelection.qml" }, - QUrl{ "hifi/commerce/wallet/Wallet.qml" }, - QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, - QUrl{ "hifi/commerce/wallet/WalletSetup.qml" }, - QUrl{ "hifi/dialogs/security/Security.qml" }, - QUrl{ "hifi/dialogs/security/SecurityImageChange.qml" }, - QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" }, - QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" }, - QUrl{ "hifi/tablet/TabletMenu.qml" }, - QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, - }, commerceCallback); - - QmlContextCallback marketplaceCallback = [](QQmlContext* context) { - context->setContextProperty("MarketplaceScriptingInterface", new QmlMarketplace()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, - }, marketplaceCallback); - - QmlContextCallback platformInfoCallback = [](QQmlContext* context) { - context->setContextProperty("PlatformInfo", new PlatformInfoScriptingInterface()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/commerce/marketplace/Marketplace.qml" }, - }, platformInfoCallback); - - QmlContextCallback ttsCallback = [](QQmlContext* context) { - context->setContextProperty("TextToSpeech", DependencyManager::get().data()); - }; - OffscreenQmlSurface::addWhitelistContextHandler({ - QUrl{ "hifi/tts/TTS.qml" } - }, ttsCallback); - qmlRegisterType("Hifi", 1, 0, "ResourceImageItem"); - qmlRegisterType("Hifi", 1, 0, "Preference"); - qmlRegisterType("HifiWeb", 1, 0, "WebBrowserSuggestionsEngine"); - - { - auto tabletScriptingInterface = DependencyManager::get(); - tabletScriptingInterface->getTablet(SYSTEM_TABLET); - } - - auto offscreenUi = getOffscreenUI(); - connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootContextCreated, - this, &Application::onDesktopRootContextCreated); - connect(offscreenUi.data(), &hifi::qml::OffscreenSurface::rootItemCreated, - this, &Application::onDesktopRootItemCreated); - -#if !defined(DISABLE_QML) - offscreenUi->setProxyWindow(_window->windowHandle()); - // OffscreenUi is a subclass of OffscreenQmlSurface specifically designed to - // support the window management and scripting proxies for VR use - DeadlockWatchdogThread::withPause([&] { - offscreenUi->createDesktop(PathUtils::qmlUrl("hifi/Desktop.qml")); - }); - // FIXME either expose so that dialogs can set this themselves or - // do better detection in the offscreen UI of what has focus - offscreenUi->setNavigationFocused(false); -#else - _window->setMenuBar(new Menu()); -#endif - - setupPreferences(); - -#if !defined(DISABLE_QML) - _glWidget->installEventFilter(offscreenUi.data()); - offscreenUi->setMouseTranslator([=](const QPointF& pt) { - QPointF result = pt; - auto displayPlugin = getActiveDisplayPlugin(); - if (displayPlugin->isHmd()) { - getApplicationCompositor().handleRealMouseMoveEvent(false); - auto resultVec = getApplicationCompositor().getReticlePosition(); - result = QPointF(resultVec.x, resultVec.y); - } - return result.toPoint(); - }); - offscreenUi->resume(); -#endif - connect(_window, &MainWindow::windowGeometryChanged, [this](const QRect& r){ - resizeGL(); - if (_touchscreenVirtualPadDevice) { - _touchscreenVirtualPadDevice->resize(); - } - }); - - // This will set up the input plugins UI - _activeInputPlugins.clear(); - foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) { - if (KeyboardMouseDevice::NAME == inputPlugin->getName()) { - _keyboardMouseDevice = std::dynamic_pointer_cast(inputPlugin); - } - if (TouchscreenDevice::NAME == inputPlugin->getName()) { - _touchscreenDevice = std::dynamic_pointer_cast(inputPlugin); - } - if (TouchscreenVirtualPadDevice::NAME == inputPlugin->getName()) { - _touchscreenVirtualPadDevice = std::dynamic_pointer_cast(inputPlugin); -#if defined(ANDROID_APP_INTERFACE) - auto& virtualPadManager = VirtualPad::Manager::instance(); - connect(&virtualPadManager, &VirtualPad::Manager::hapticFeedbackRequested, - this, [](int duration) { - AndroidHelper::instance().performHapticFeedback(duration); - }); -#endif - } - } - - auto compositorHelper = DependencyManager::get(); - connect(compositorHelper.data(), &CompositorHelper::allowMouseCaptureChanged, this, [=] { - if (isHMDMode()) { - auto compositorHelper = DependencyManager::get(); // don't capture outer smartpointer - showCursor(compositorHelper->getAllowMouseCapture() ? - Cursor::Manager::lookupIcon(_preferredCursor.get()) : - Cursor::Icon::SYSTEM); - } - }); - -#if !defined(DISABLE_QML) - // Pre-create a couple of offscreen surfaces to speed up tablet UI - auto offscreenSurfaceCache = DependencyManager::get(); - offscreenSurfaceCache->setOnRootContextCreated([&](const QString& rootObject, QQmlContext* surfaceContext) { - if (rootObject == TabletScriptingInterface::QML) { - // in Qt 5.10.0 there is already an "Audio" object in the QML context - // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" - surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); - surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - } - }); - - offscreenSurfaceCache->reserve(TabletScriptingInterface::QML, 1); - offscreenSurfaceCache->reserve(render::entities::WebEntityRenderer::QML, 2); -#endif - - flushMenuUpdates(); - -#if !defined(DISABLE_QML) - // Now that the menu is instantiated, ensure the display plugin menu is properly updated - { - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - // first sort the plugins into groupings: standard, advanced, developer - std::stable_sort(displayPlugins.begin(), displayPlugins.end(), - [](const DisplayPluginPointer& a, const DisplayPluginPointer& b) -> bool { return a->getGrouping() < b->getGrouping(); }); - int dpIndex = 1; - // concatenate the groupings into a single list in the order: standard, advanced, developer - for(const auto& displayPlugin : displayPlugins) { - addDisplayPluginToMenu(displayPlugin, dpIndex, _displayPlugin == displayPlugin); - dpIndex++; - } - - // after all plugins have been added to the menu, add a separator to the menu - auto parent = getPrimaryMenu()->getMenu(MenuOption::OutputMenu); - parent->addSeparator(); - } -#endif - - - // The display plugins are created before the menu now, so we need to do this here to hide the menu bar - // now that it exists - if (_window && _window->isFullScreen()) { - setFullscreen(nullptr, true); - } - - - setIsInterstitialMode(true); -} - - -void Application::onDesktopRootContextCreated(QQmlContext* surfaceContext) { - auto engine = surfaceContext->engine(); - // in Qt 5.10.0 there is already an "Audio" object in the QML context - // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" - surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); - - surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); - surfaceContext->setContextProperty("AudioScope", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); - surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); - _fileDownload = new FileScriptingInterface(engine); - surfaceContext->setContextProperty("File", _fileDownload); - connect(_fileDownload, &FileScriptingInterface::unzipResult, this, &Application::handleUnzip); - surfaceContext->setContextProperty("MyAvatar", getMyAvatar().get()); - surfaceContext->setContextProperty("Messages", DependencyManager::get().data()); - surfaceContext->setContextProperty("Recording", DependencyManager::get().data()); - surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); - surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("FrameTimings", &_graphicsEngine._frameTimingsScriptingInterface); - surfaceContext->setContextProperty("Rates", new RatesScriptingInterface(this)); - - surfaceContext->setContextProperty("TREE_SCALE", TREE_SCALE); - // FIXME Quat and Vec3 won't work with QJSEngine used by QML - surfaceContext->setContextProperty("Quat", new Quat()); - surfaceContext->setContextProperty("Vec3", new Vec3()); - surfaceContext->setContextProperty("Uuid", new ScriptUUID()); - surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); - surfaceContext->setContextProperty("Keyboard", DependencyManager::get().data()); - - surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); - surfaceContext->setContextProperty("Users", DependencyManager::get().data()); - - surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Camera", &_myCamera); - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - surfaceContext->setContextProperty("SpeechRecognizer", DependencyManager::get().data()); -#endif - - surfaceContext->setContextProperty("Overlays", &_overlays); - surfaceContext->setContextProperty("Window", DependencyManager::get().data()); - surfaceContext->setContextProperty("Desktop", DependencyManager::get().data()); - surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); - surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); - surfaceContext->setContextProperty("LocationBookmarks", DependencyManager::get().data()); - - // Caches - surfaceContext->setContextProperty("AnimationCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("TextureCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("ModelCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); - - surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); - - surfaceContext->setContextProperty("DialogsManager", _dialogsManagerScriptingInterface); - surfaceContext->setContextProperty("FaceTracker", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); - surfaceContext->setContextProperty("Scene", DependencyManager::get().data()); - surfaceContext->setContextProperty("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); - surfaceContext->setContextProperty("Workload", _gameWorkload._engine->getConfiguration().get()); - surfaceContext->setContextProperty("Reticle", getApplicationCompositor().getReticleInterface()); - surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); - - surfaceContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor()); - - surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); - surfaceContext->setContextProperty("Selection", DependencyManager::get().data()); - surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); - surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); - surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); - surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); - } - - _window->setMenuBar(new Menu()); -} - -void Application::onDesktopRootItemCreated(QQuickItem* rootItem) { - Stats::show(); - AnimStats::show(); - auto surfaceContext = getOffscreenUI()->getSurfaceContext(); - surfaceContext->setContextProperty("Stats", Stats::getInstance()); - surfaceContext->setContextProperty("AnimStats", AnimStats::getInstance()); - -#if !defined(Q_OS_ANDROID) - auto offscreenUi = getOffscreenUI(); - auto qml = PathUtils::qmlUrl("AvatarInputsBar.qml"); - offscreenUi->show(qml, "AvatarInputsBar"); -#endif -} - -void Application::setupQmlSurface(QQmlContext* surfaceContext, bool setAdditionalContextProperties) { - surfaceContext->setContextProperty("Users", DependencyManager::get().data()); - surfaceContext->setContextProperty("HMD", DependencyManager::get().data()); - surfaceContext->setContextProperty("UserActivityLogger", DependencyManager::get().data()); - surfaceContext->setContextProperty("Preferences", DependencyManager::get().data()); - surfaceContext->setContextProperty("Vec3", new Vec3()); - surfaceContext->setContextProperty("Quat", new Quat()); - surfaceContext->setContextProperty("MyAvatar", DependencyManager::get()->getMyAvatar().get()); - surfaceContext->setContextProperty("Entities", DependencyManager::get().data()); - surfaceContext->setContextProperty("Snapshot", DependencyManager::get().data()); - surfaceContext->setContextProperty("KeyboardScriptingInterface", DependencyManager::get().data()); - - if (setAdditionalContextProperties) { - auto tabletScriptingInterface = DependencyManager::get(); - auto flags = tabletScriptingInterface->getFlags(); - - surfaceContext->setContextProperty("offscreenFlags", flags); - surfaceContext->setContextProperty("AddressManager", DependencyManager::get().data()); - - surfaceContext->setContextProperty("Settings", SettingsScriptingInterface::getInstance()); - surfaceContext->setContextProperty("MenuInterface", MenuScriptingInterface::getInstance()); - - surfaceContext->setContextProperty("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - surfaceContext->setContextProperty("AccountServices", AccountServicesScriptingInterface::getInstance()); - - // in Qt 5.10.0 there is already an "Audio" object in the QML context - // though I failed to find it (from QtMultimedia??). So.. let it be "AudioScriptingInterface" - surfaceContext->setContextProperty("AudioScriptingInterface", DependencyManager::get().data()); - - surfaceContext->setContextProperty("AudioStats", DependencyManager::get()->getStats().data()); - surfaceContext->setContextProperty("fileDialogHelper", new FileDialogHelper()); - surfaceContext->setContextProperty("ScriptDiscoveryService", DependencyManager::get().data()); - surfaceContext->setContextProperty("Assets", DependencyManager::get().data()); - surfaceContext->setContextProperty("LODManager", DependencyManager::get().data()); - surfaceContext->setContextProperty("OctreeStats", DependencyManager::get().data()); - surfaceContext->setContextProperty("DCModel", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); - surfaceContext->setContextProperty("AvatarList", DependencyManager::get().data()); - surfaceContext->setContextProperty("DialogsManager", DialogsManagerScriptingInterface::getInstance()); - surfaceContext->setContextProperty("InputConfiguration", DependencyManager::get().data()); - surfaceContext->setContextProperty("SoundCache", DependencyManager::get().data()); - surfaceContext->setContextProperty("AvatarBookmarks", DependencyManager::get().data()); - surfaceContext->setContextProperty("Render", AbstractViewStateInterface::instance()->getRenderEngine()->getConfiguration().get()); - surfaceContext->setContextProperty("Workload", qApp->getGameWorkload()._engine->getConfiguration().get()); - surfaceContext->setContextProperty("Controller", DependencyManager::get().data()); - surfaceContext->setContextProperty("Pointers", DependencyManager::get().data()); - surfaceContext->setContextProperty("Window", DependencyManager::get().data()); - surfaceContext->setContextProperty("Reticle", qApp->getApplicationCompositor().getReticleInterface()); - surfaceContext->setContextProperty("HiFiAbout", AboutUtil::getInstance()); - surfaceContext->setContextProperty("WalletScriptingInterface", DependencyManager::get().data()); - surfaceContext->setContextProperty("ResourceRequestObserver", DependencyManager::get().data()); - } -} - -void Application::updateCamera(RenderArgs& renderArgs, float deltaTime) { - PROFILE_RANGE(render, __FUNCTION__); - PerformanceTimer perfTimer("updateCamera"); - - glm::vec3 boomOffset; - auto myAvatar = getMyAvatar(); - boomOffset = myAvatar->getModelScale() * myAvatar->getBoomLength() * -IDENTITY_FORWARD; - - // The render mode is default or mirror if the camera is in mirror mode, assigned further below - renderArgs._renderMode = RenderArgs::DEFAULT_RENDER_MODE; - - // Always use the default eye position, not the actual head eye position. - // Using the latter will cause the camera to wobble with idle animations, - // or with changes from the face tracker - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON) { - _thirdPersonHMDCameraBoomValid= false; - if (isHMDMode()) { - mat4 camMat = myAvatar->getSensorToWorldMatrix() * myAvatar->getHMDSensorMatrix(); - _myCamera.setPosition(extractTranslation(camMat)); - _myCamera.setOrientation(glmExtractRotation(camMat)); - } - else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition()); - _myCamera.setOrientation(myAvatar->getMyHead()->getHeadOrientation()); - } - } - else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON) { - if (isHMDMode()) { - - if (!_thirdPersonHMDCameraBoomValid) { - const glm::vec3 CAMERA_OFFSET = glm::vec3(0.0f, 0.0f, 0.7f); - _thirdPersonHMDCameraBoom = cancelOutRollAndPitch(myAvatar->getHMDSensorOrientation()) * CAMERA_OFFSET; - _thirdPersonHMDCameraBoomValid = true; - } - - glm::mat4 thirdPersonCameraSensorToWorldMatrix = myAvatar->getSensorToWorldMatrix(); - - const glm::vec3 cameraPos = myAvatar->getHMDSensorPosition() + _thirdPersonHMDCameraBoom * myAvatar->getBoomLength(); - glm::mat4 sensorCameraMat = createMatFromQuatAndPos(myAvatar->getHMDSensorOrientation(), cameraPos); - glm::mat4 worldCameraMat = thirdPersonCameraSensorToWorldMatrix * sensorCameraMat; - - _myCamera.setOrientation(glm::normalize(glmExtractRotation(worldCameraMat))); - _myCamera.setPosition(extractTranslation(worldCameraMat)); - } - else { - _thirdPersonHMDCameraBoomValid = false; - - _myCamera.setOrientation(myAvatar->getHead()->getOrientation()); - if (isOptionChecked(MenuOption::CenterPlayerInView)) { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + _myCamera.getOrientation() * boomOffset); - } - else { - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + myAvatar->getWorldOrientation() * boomOffset); - } - } - } - else if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - _thirdPersonHMDCameraBoomValid= false; - - if (isHMDMode()) { - auto mirrorBodyOrientation = myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f)); - - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD yaw and roll - glm::vec3 mirrorHmdEulers = glm::eulerAngles(hmdRotation); - mirrorHmdEulers.y = -mirrorHmdEulers.y; - mirrorHmdEulers.z = -mirrorHmdEulers.z; - glm::quat mirrorHmdRotation = glm::quat(mirrorHmdEulers); - - glm::quat worldMirrorRotation = mirrorBodyOrientation * mirrorHmdRotation; - - _myCamera.setOrientation(worldMirrorRotation); - - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - // Mirror HMD lateral offsets - hmdOffset.x = -hmdOffset.x; - - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + mirrorBodyOrientation * glm::vec3(0.0f, 0.0f, 1.0f) * MIRROR_FULLSCREEN_DISTANCE * _scaleMirror - + mirrorBodyOrientation * hmdOffset); - } - else { - auto userInputMapper = DependencyManager::get(); - const float YAW_SPEED = TWO_PI / 5.0f; - float deltaYaw = userInputMapper->getActionState(controller::Action::YAW) * YAW_SPEED * deltaTime; - _mirrorYawOffset += deltaYaw; - _myCamera.setOrientation(myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, PI + _mirrorYawOffset, 0.0f))); - _myCamera.setPosition(myAvatar->getDefaultEyePosition() - + glm::vec3(0, _raiseMirror * myAvatar->getModelScale(), 0) - + (myAvatar->getWorldOrientation() * glm::quat(glm::vec3(0.0f, _mirrorYawOffset, 0.0f))) * - glm::vec3(0.0f, 0.0f, -1.0f) * myAvatar->getBoomLength() * _scaleMirror); - } - renderArgs._renderMode = RenderArgs::MIRROR_RENDER_MODE; - } - else if (_myCamera.getMode() == CAMERA_MODE_ENTITY) { - _thirdPersonHMDCameraBoomValid= false; - EntityItemPointer cameraEntity = _myCamera.getCameraEntityPointer(); - if (cameraEntity != nullptr) { - if (isHMDMode()) { - glm::quat hmdRotation = extractRotation(myAvatar->getHMDSensorMatrix()); - _myCamera.setOrientation(cameraEntity->getWorldOrientation() * hmdRotation); - glm::vec3 hmdOffset = extractTranslation(myAvatar->getHMDSensorMatrix()); - _myCamera.setPosition(cameraEntity->getWorldPosition() + (hmdRotation * hmdOffset)); - } - else { - _myCamera.setOrientation(cameraEntity->getWorldOrientation()); - _myCamera.setPosition(cameraEntity->getWorldPosition()); - } - } - } - // Update camera position - if (!isHMDMode()) { - _myCamera.update(); - } - - renderArgs._cameraMode = (int8_t)_myCamera.getMode(); -} - -void Application::runTests() { - runTimingTests(); - runUnitTests(); -} - -void Application::faceTrackerMuteToggled() { - - QAction* muteAction = Menu::getInstance()->getActionForOption(MenuOption::MuteFaceTracking); - Q_CHECK_PTR(muteAction); - bool isMuted = getSelectedFaceTracker()->isMuted(); - muteAction->setChecked(isMuted); - getSelectedFaceTracker()->setEnabled(!isMuted); - Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setEnabled(!isMuted); -} - -void Application::setFieldOfView(float fov) { - if (fov != _fieldOfView.get()) { - _fieldOfView.set(fov); - resizeGL(); - } -} - -void Application::setHMDTabletScale(float hmdTabletScale) { - _hmdTabletScale.set(hmdTabletScale); -} - -void Application::setDesktopTabletScale(float desktopTabletScale) { - _desktopTabletScale.set(desktopTabletScale); -} - -void Application::setDesktopTabletBecomesToolbarSetting(bool value) { - _desktopTabletBecomesToolbarSetting.set(value); - updateSystemTabletMode(); -} - -void Application::setHmdTabletBecomesToolbarSetting(bool value) { - _hmdTabletBecomesToolbarSetting.set(value); - updateSystemTabletMode(); -} - -void Application::setPreferStylusOverLaser(bool value) { - _preferStylusOverLaserSetting.set(value); -} - -void Application::setPreferAvatarFingerOverStylus(bool value) { - _preferAvatarFingerOverStylusSetting.set(value); -} - -void Application::setPreferredCursor(const QString& cursorName) { - qCDebug(interfaceapp) << "setPreferredCursor" << cursorName; - _preferredCursor.set(cursorName.isEmpty() ? DEFAULT_CURSOR_NAME : cursorName); - showCursor(Cursor::Manager::lookupIcon(_preferredCursor.get())); -} - -void Application::setSettingConstrainToolbarPosition(bool setting) { - _constrainToolbarPosition.set(setting); - getOffscreenUI()->setConstrainToolbarToCenterX(setting); -} - -void Application::setMiniTabletEnabled(bool enabled) { - _miniTabletEnabledSetting.set(enabled); - emit miniTabletEnabledChanged(enabled); -} - -void Application::showHelp() { - static const QString HAND_CONTROLLER_NAME_VIVE = "vive"; - static const QString HAND_CONTROLLER_NAME_OCULUS_TOUCH = "oculus"; - static const QString HAND_CONTROLLER_NAME_WINDOWS_MR = "windowsMR"; - - static const QString VIVE_PLUGIN_NAME = "HTC Vive"; - static const QString OCULUS_RIFT_PLUGIN_NAME = "Oculus Rift"; - static const QString WINDOWS_MR_PLUGIN_NAME = "WindowsMR"; - - static const QString TAB_KEYBOARD_MOUSE = "kbm"; - static const QString TAB_GAMEPAD = "gamepad"; - static const QString TAB_HAND_CONTROLLERS = "handControllers"; - - QString handControllerName; - QString defaultTab = TAB_KEYBOARD_MOUSE; - - if (PluginUtils::isHMDAvailable(WINDOWS_MR_PLUGIN_NAME)) { - defaultTab = TAB_HAND_CONTROLLERS; - handControllerName = HAND_CONTROLLER_NAME_WINDOWS_MR; - } else if (PluginUtils::isHMDAvailable(VIVE_PLUGIN_NAME)) { - defaultTab = TAB_HAND_CONTROLLERS; - handControllerName = HAND_CONTROLLER_NAME_VIVE; - } else if (PluginUtils::isHMDAvailable(OCULUS_RIFT_PLUGIN_NAME)) { - if (PluginUtils::isOculusTouchControllerAvailable()) { - defaultTab = TAB_HAND_CONTROLLERS; - handControllerName = HAND_CONTROLLER_NAME_OCULUS_TOUCH; - } else if (PluginUtils::isXboxControllerAvailable()) { - defaultTab = TAB_GAMEPAD; - } else { - defaultTab = TAB_KEYBOARD_MOUSE; - } - } else if (PluginUtils::isXboxControllerAvailable()) { - defaultTab = TAB_GAMEPAD; - } else { - defaultTab = TAB_KEYBOARD_MOUSE; - } - - QUrlQuery queryString; - queryString.addQueryItem("handControllerName", handControllerName); - queryString.addQueryItem("defaultTab", defaultTab); - TabletProxy* tablet = dynamic_cast(DependencyManager::get()->getTablet(SYSTEM_TABLET)); - tablet->gotoWebScreen(PathUtils::resourcesUrl() + INFO_HELP_PATH + "?" + queryString.toString()); - DependencyManager::get()->openTablet(); - //InfoView::show(INFO_HELP_PATH, false, queryString.toString()); -} - -void Application::resizeEvent(QResizeEvent* event) { - resizeGL(); -} - -void Application::resizeGL() { - PROFILE_RANGE(render, __FUNCTION__); - if (nullptr == _displayPlugin) { - return; - } - - auto displayPlugin = getActiveDisplayPlugin(); - // Set the desired FBO texture size. If it hasn't changed, this does nothing. - // Otherwise, it must rebuild the FBOs - uvec2 framebufferSize = displayPlugin->getRecommendedRenderSize(); - uvec2 renderSize = uvec2(framebufferSize); - if (_renderResolution != renderSize) { - _renderResolution = renderSize; - DependencyManager::get()->setFrameBufferSize(fromGlm(renderSize)); - } - - auto renderResolutionScale = getRenderResolutionScale(); - if (displayPlugin->getRenderResolutionScale() != renderResolutionScale) { - auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); - assert(renderConfig); - auto mainView = renderConfig->getConfig("RenderMainView.RenderDeferredTask"); - // mainView can be null if we're rendering in forward mode - if (mainView) { - mainView->setProperty("resolutionScale", renderResolutionScale); - } - displayPlugin->setRenderResolutionScale(renderResolutionScale); - } - - // FIXME the aspect ratio for stereo displays is incorrect based on this. - float aspectRatio = displayPlugin->getRecommendedAspectRatio(); - _myCamera.setProjection(glm::perspective(glm::radians(_fieldOfView.get()), aspectRatio, - DEFAULT_NEAR_CLIP, DEFAULT_FAR_CLIP)); - // Possible change in aspect ratio - { - QMutexLocker viewLocker(&_viewMutex); - _myCamera.loadViewFrustum(_viewFrustum); - } - -#if !defined(DISABLE_QML) - getOffscreenUI()->resize(fromGlm(displayPlugin->getRecommendedUiSize())); -#endif -} - -void Application::handleSandboxStatus(QNetworkReply* reply) { - PROFILE_RANGE(render, __FUNCTION__); - - bool sandboxIsRunning = SandboxUtils::readStatus(reply->readAll()); - - enum HandControllerType { - Vive, - Oculus - }; - static const std::map MIN_CONTENT_VERSION = { - { Vive, 1 }, - { Oculus, 27 } - }; - - // Get sandbox content set version - auto acDirPath = PathUtils::getAppDataPath() + "../../" + BuildInfo::MODIFIED_ORGANIZATION + "/assignment-client/"; - auto contentVersionPath = acDirPath + "content-version.txt"; - qCDebug(interfaceapp) << "Checking " << contentVersionPath << " for content version"; - int contentVersion = 0; - QFile contentVersionFile(contentVersionPath); - if (contentVersionFile.open(QIODevice::ReadOnly | QIODevice::Text)) { - QString line = contentVersionFile.readAll(); - contentVersion = line.toInt(); // returns 0 if conversion fails - } - - // Get controller availability -#ifdef ANDROID_APP_QUEST_INTERFACE - bool hasHandControllers = true; -#else - bool hasHandControllers = false; - if (PluginUtils::isViveControllerAvailable() || PluginUtils::isOculusTouchControllerAvailable()) { - hasHandControllers = true; - } -#endif - - // Check HMD use (may be technically available without being in use) - bool hasHMD = PluginUtils::isHMDAvailable(); - bool isUsingHMD = _displayPlugin->isHmd(); - bool isUsingHMDAndHandControllers = hasHMD && hasHandControllers && isUsingHMD; - - qCDebug(interfaceapp) << "HMD:" << hasHMD << ", Hand Controllers: " << hasHandControllers << ", Using HMD: " << isUsingHMDAndHandControllers; - - // when --url in command line, teleport to location - const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; - int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); - QString addressLookupString; - if (urlIndex != -1) { - QUrl url(arguments().value(urlIndex + 1)); - if (url.scheme() == URL_SCHEME_HIFIAPP) { - Setting::Handle("startUpApp").set(url.path()); - } else { - addressLookupString = url.toString(); - } - } - - static const QString SENT_TO_PREVIOUS_LOCATION = "previous_location"; - static const QString SENT_TO_ENTRY = "entry"; - - QString sentTo; - - // If this is a first run we short-circuit the address passed in - if (_firstRun.get()) { -#if !defined(Q_OS_ANDROID) - DependencyManager::get()->goToEntry(); - sentTo = SENT_TO_ENTRY; -#endif - _firstRun.set(false); - - } else { -#if !defined(Q_OS_ANDROID) - QString goingTo = ""; - if (addressLookupString.isEmpty()) { - if (Menu::getInstance()->isOptionChecked(MenuOption::HomeLocation)) { - auto locationBookmarks = DependencyManager::get(); - addressLookupString = locationBookmarks->addressForBookmark(LocationBookmarks::HOME_BOOKMARK); - goingTo = "home location"; - } else { - goingTo = "previous location"; - } - } - qCDebug(interfaceapp) << "Not first run... going to" << qPrintable(!goingTo.isEmpty() ? goingTo : addressLookupString); - DependencyManager::get()->loadSettings(addressLookupString); - sentTo = SENT_TO_PREVIOUS_LOCATION; -#endif - } - - UserActivityLogger::getInstance().logAction("startup_sent_to", { - { "sent_to", sentTo }, - { "sandbox_is_running", sandboxIsRunning }, - { "has_hmd", hasHMD }, - { "has_hand_controllers", hasHandControllers }, - { "is_using_hmd", isUsingHMD }, - { "is_using_hmd_and_hand_controllers", isUsingHMDAndHandControllers }, - { "content_version", contentVersion } - }); - - _connectionMonitor.init(); -} - -bool Application::importJSONFromURL(const QString& urlString) { - // we only load files that terminate in just .json (not .svo.json and not .ava.json) - QUrl jsonURL { urlString }; - - emit svoImportRequested(urlString); - return true; -} - -bool Application::importSVOFromURL(const QString& urlString) { - emit svoImportRequested(urlString); - return true; -} - -bool Application::importFromZIP(const QString& filePath) { - qDebug() << "A zip file has been dropped in: " << filePath; - QUrl empty; - // handle Blocks download from Marketplace - if (filePath.contains("poly.google.com/downloads")) { - addAssetToWorldFromURL(filePath); - } else { - qApp->getFileDownloadInterface()->runUnzip(filePath, empty, true, true, false); - } - return true; -} - -bool Application::isServerlessMode() const { - auto tree = getEntities()->getTree(); - if (tree) { - return tree->isServerlessMode(); - } - return false; -} - -void Application::setIsInterstitialMode(bool interstitialMode) { - bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); - if (enableInterstitial) { - if (_interstitialMode != interstitialMode) { - _interstitialMode = interstitialMode; - emit interstitialModeChanged(_interstitialMode); - - DependencyManager::get()->setAudioPaused(_interstitialMode); - DependencyManager::get()->setMyAvatarDataPacketsPaused(_interstitialMode); - } - } -} - -void Application::setIsServerlessMode(bool serverlessDomain) { - auto tree = getEntities()->getTree(); - if (tree) { - tree->setIsServerlessMode(serverlessDomain); - } -} - -std::map Application::prepareServerlessDomainContents(QUrl domainURL) { - QUuid serverlessSessionID = QUuid::createUuid(); - getMyAvatar()->setSessionUUID(serverlessSessionID); - auto nodeList = DependencyManager::get(); - nodeList->setSessionUUID(serverlessSessionID); - - // there is no domain-server to tell us our permissions, so enable all - NodePermissions permissions; - permissions.setAll(true); - nodeList->setPermissions(permissions); - - // we can't import directly into the main tree because we would need to lock it, and - // Octree::readFromURL calls loop.exec which can run code which will also attempt to lock the tree. - EntityTreePointer tmpTree(new EntityTree()); - tmpTree->setIsServerlessMode(true); - tmpTree->createRootElement(); - auto myAvatar = getMyAvatar(); - tmpTree->setMyAvatar(myAvatar); - bool success = tmpTree->readFromURL(domainURL.toString()); - if (success) { - tmpTree->reaverageOctreeElements(); - tmpTree->sendEntities(&_entityEditSender, getEntities()->getTree(), 0, 0, 0); - } - std::map namedPaths = tmpTree->getNamedPaths(); - - // we must manually eraseAllOctreeElements(false) else the tmpTree will mem-leak - tmpTree->eraseAllOctreeElements(false); - - return namedPaths; -} - -void Application::loadServerlessDomain(QUrl domainURL) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadServerlessDomain", Q_ARG(QUrl, domainURL)); - return; - } - - if (domainURL.isEmpty()) { - return; - } - - auto namedPaths = prepareServerlessDomainContents(domainURL); - auto nodeList = DependencyManager::get(); - - nodeList->getDomainHandler().connectedToServerless(namedPaths); - - _fullSceneReceivedCounter++; -} - -void Application::loadErrorDomain(QUrl domainURL) { - if (QThread::currentThread() != thread()) { - QMetaObject::invokeMethod(this, "loadErrorDomain", Q_ARG(QUrl, domainURL)); - return; - } - - if (domainURL.isEmpty()) { - return; - } - - auto namedPaths = prepareServerlessDomainContents(domainURL); - auto nodeList = DependencyManager::get(); - - nodeList->getDomainHandler().loadedErrorDomain(namedPaths); - - _fullSceneReceivedCounter++; -} - -bool Application::importImage(const QString& urlString) { - qCDebug(interfaceapp) << "An image file has been dropped in"; - QString filepath(urlString); - filepath.remove("file:///"); - addAssetToWorld(filepath, "", false, false); - return true; -} - -// thread-safe -void Application::onPresent(quint32 frameCount) { - bool expected = false; - if (_pendingIdleEvent.compare_exchange_strong(expected, true)) { - postEvent(this, new QEvent((QEvent::Type)ApplicationEvent::Idle), Qt::HighEventPriority); - } - expected = false; - if (_graphicsEngine.checkPendingRenderEvent() && !isAboutToQuit()) { - postEvent(_graphicsEngine._renderEventHandler, new QEvent((QEvent::Type)ApplicationEvent::Render)); - } -} - -static inline bool isKeyEvent(QEvent::Type type) { - return type == QEvent::KeyPress || type == QEvent::KeyRelease; -} - -bool Application::handleKeyEventForFocusedEntity(QEvent* event) { - if (_keyboardFocusedEntity.get() != UNKNOWN_ENTITY_ID) { - switch (event->type()) { - case QEvent::KeyPress: - case QEvent::KeyRelease: - { - auto eventHandler = getEntities()->getEventHandler(_keyboardFocusedEntity.get()); - if (eventHandler) { - event->setAccepted(false); - QCoreApplication::sendEvent(eventHandler, event); - if (event->isAccepted()) { - _lastAcceptedKeyPress = usecTimestampNow(); - return true; - } - } - break; - } - default: - break; - } - } - - return false; -} - -bool Application::handleFileOpenEvent(QFileOpenEvent* fileEvent) { - QUrl url = fileEvent->url(); - if (!url.isEmpty()) { - QString urlString = url.toString(); - if (canAcceptURL(urlString)) { - return acceptURL(urlString); - } - } - return false; -} - -#ifdef DEBUG_EVENT_QUEUE -static int getEventQueueSize(QThread* thread) { - auto threadData = QThreadData::get2(thread); - QMutexLocker locker(&threadData->postEventList.mutex); - return threadData->postEventList.size(); -} - -static void dumpEventQueue(QThread* thread) { - auto threadData = QThreadData::get2(thread); - QMutexLocker locker(&threadData->postEventList.mutex); - qDebug() << "Event list, size =" << threadData->postEventList.size(); - for (auto& postEvent : threadData->postEventList) { - QEvent::Type type = (postEvent.event ? postEvent.event->type() : QEvent::None); - qDebug() << " " << type; - } -} -#endif // DEBUG_EVENT_QUEUE - -bool Application::event(QEvent* event) { - - if (_aboutToQuit) { - return false; - } - - if (!Menu::getInstance()) { - return false; - } - - // Allow focused Entities to handle keyboard input - if (isKeyEvent(event->type()) && handleKeyEventForFocusedEntity(event)) { - return true; - } - - int type = event->type(); - switch (type) { - case ApplicationEvent::Lambda: - static_cast(event)->call(); - return true; - - // Explicit idle keeps the idle running at a lower interval, but without any rendering - // see (windowMinimizedChanged) - case ApplicationEvent::Idle: - idle(); - -#ifdef DEBUG_EVENT_QUEUE - { - int count = getEventQueueSize(QThread::currentThread()); - if (count > 400) { - dumpEventQueue(QThread::currentThread()); - } - } -#endif // DEBUG_EVENT_QUEUE - - _pendingIdleEvent.store(false); - - return true; - - case QEvent::MouseMove: - mouseMoveEvent(static_cast(event)); - return true; - case QEvent::MouseButtonPress: - mousePressEvent(static_cast(event)); - return true; - case QEvent::MouseButtonDblClick: - mouseDoublePressEvent(static_cast(event)); - return true; - case QEvent::MouseButtonRelease: - mouseReleaseEvent(static_cast(event)); - return true; - case QEvent::KeyPress: - keyPressEvent(static_cast(event)); - return true; - case QEvent::KeyRelease: - keyReleaseEvent(static_cast(event)); - return true; - case QEvent::FocusOut: - focusOutEvent(static_cast(event)); - return true; - case QEvent::TouchBegin: - touchBeginEvent(static_cast(event)); - event->accept(); - return true; - case QEvent::TouchEnd: - touchEndEvent(static_cast(event)); - return true; - case QEvent::TouchUpdate: - touchUpdateEvent(static_cast(event)); - return true; - case QEvent::Gesture: - touchGestureEvent((QGestureEvent*)event); - return true; - case QEvent::Wheel: - wheelEvent(static_cast(event)); - return true; - case QEvent::Drop: - dropEvent(static_cast(event)); - return true; - - case QEvent::FileOpen: - if (handleFileOpenEvent(static_cast(event))) { - return true; - } - break; - - default: - break; - } - - return QApplication::event(event); -} - -bool Application::eventFilter(QObject* object, QEvent* event) { - - if (_aboutToQuit && event->type() != QEvent::DeferredDelete && event->type() != QEvent::Destroy) { - return true; - } - - if (event->type() == QEvent::Leave) { - getApplicationCompositor().handleLeaveEvent(); - } - - if (event->type() == QEvent::ShortcutOverride) { -#if !defined(DISABLE_QML) - if (getOffscreenUI()->shouldSwallowShortcut(event)) { - event->accept(); - return true; - } -#endif - - // Filter out captured keys before they're used for shortcut actions. - if (_controllerScriptingInterface->isKeyCaptured(static_cast(event))) { - event->accept(); - return true; - } - } - - return false; -} - -static bool _altPressed{ false }; - -void Application::keyPressEvent(QKeyEvent* event) { - _altPressed = event->key() == Qt::Key_Alt; - - if (!event->isAutoRepeat()) { - _keysPressed.insert(event->key(), *event); - } - - _controllerScriptingInterface->emitKeyPressEvent(event); // send events to any registered scripts - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isKeyCaptured(event) || isInterstitialMode()) { - return; - } - - if (hasFocus() && getLoginDialogPoppedUp()) { - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->keyReleaseEvent(event); - } - - bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); - bool isOption = event->modifiers().testFlag(Qt::AltModifier); - switch (event->key()) { - case Qt::Key_4: - case Qt::Key_5: - case Qt::Key_6: - case Qt::Key_7: - if (isMeta || isOption) { - unsigned int index = static_cast(event->key() - Qt::Key_1); - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - if (index < displayPlugins.size()) { - auto targetPlugin = displayPlugins.at(index); - QString targetName = targetPlugin->getName(); - auto menu = Menu::getInstance(); - QAction* action = menu->getActionForOption(targetName); - if (action && !action->isChecked()) { - action->trigger(); - } - } - } - break; - } - } else if (hasFocus()) { - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->keyPressEvent(event); - } - - bool isShifted = event->modifiers().testFlag(Qt::ShiftModifier); - bool isMeta = event->modifiers().testFlag(Qt::ControlModifier); - bool isOption = event->modifiers().testFlag(Qt::AltModifier); - switch (event->key()) { - case Qt::Key_Enter: - case Qt::Key_Return: - if (isOption) { - if (_window->isFullScreen()) { - unsetFullscreen(); - } else { - setFullscreen(nullptr); - } - } - break; - - case Qt::Key_1: { - Menu* menu = Menu::getInstance(); - menu->triggerOption(MenuOption::FirstPerson); - break; - } - case Qt::Key_2: { - Menu* menu = Menu::getInstance(); - menu->triggerOption(MenuOption::FullscreenMirror); - break; - } - case Qt::Key_3: { - Menu* menu = Menu::getInstance(); - menu->triggerOption(MenuOption::ThirdPerson); - break; - } - case Qt::Key_4: - case Qt::Key_5: - case Qt::Key_6: - case Qt::Key_7: - if (isMeta || isOption) { - unsigned int index = static_cast(event->key() - Qt::Key_1); - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - if (index < displayPlugins.size()) { - auto targetPlugin = displayPlugins.at(index); - QString targetName = targetPlugin->getName(); - auto menu = Menu::getInstance(); - QAction* action = menu->getActionForOption(targetName); - if (action && !action->isChecked()) { - action->trigger(); - } - } - } - break; - - case Qt::Key_G: - if (isShifted && isMeta && Menu::getInstance() && Menu::getInstance()->getMenu("Developer")->isVisible()) { - static const QString HIFI_FRAMES_FOLDER_VAR = "HIFI_FRAMES_FOLDER"; - static const QString GPU_FRAME_FOLDER = QProcessEnvironment::systemEnvironment().contains(HIFI_FRAMES_FOLDER_VAR) - ? QProcessEnvironment::systemEnvironment().value(HIFI_FRAMES_FOLDER_VAR) - : "hifiFrames"; - static QString GPU_FRAME_TEMPLATE = GPU_FRAME_FOLDER + "/{DATE}_{TIME}"; - QString fullPath = FileUtils::computeDocumentPath(FileUtils::replaceDateTimeTokens(GPU_FRAME_TEMPLATE)); - if (FileUtils::canCreateFile(fullPath)) { - getActiveDisplayPlugin()->captureFrame(fullPath.toStdString()); - } - } - break; - case Qt::Key_X: - if (isShifted && isMeta) { - auto offscreenUi = getOffscreenUI(); - offscreenUi->togglePinned(); - //offscreenUi->getSurfaceContext()->engine()->clearComponentCache(); - //OffscreenUi::information("Debugging", "Component cache cleared"); - // placeholder for dialogs being converted to QML. - } - break; - - case Qt::Key_Y: - if (isShifted && isMeta) { - getActiveDisplayPlugin()->cycleDebugOutput(); - } - break; - - case Qt::Key_B: - if (isMeta) { - auto offscreenUi = getOffscreenUI(); - offscreenUi->load("Browser.qml"); - } else if (isOption) { - controller::InputRecorder* inputRecorder = controller::InputRecorder::getInstance(); - inputRecorder->stopPlayback(); - } - break; - - case Qt::Key_L: - if (isShifted && isMeta) { - Menu::getInstance()->triggerOption(MenuOption::Log); - } else if (isMeta) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->toggleAddressBar(); - } else if (isShifted) { - Menu::getInstance()->triggerOption(MenuOption::LodTools); - } - break; - - case Qt::Key_R: - if (isMeta && !event->isAutoRepeat()) { - DependencyManager::get()->reloadAllScripts(); - getOffscreenUI()->clearCache(); - } - break; - - case Qt::Key_Asterisk: - Menu::getInstance()->triggerOption(MenuOption::DefaultSkybox); - break; - - case Qt::Key_M: - if (isMeta) { - auto audioClient = DependencyManager::get(); - audioClient->setMuted(!audioClient->isMuted()); - } - break; - - case Qt::Key_N: - if (!isOption && !isShifted && isMeta) { - DependencyManager::get()->toggleIgnoreRadius(); - } - break; - - case Qt::Key_S: - if (isShifted && isMeta && !isOption) { - Menu::getInstance()->triggerOption(MenuOption::SuppressShortTimings); - } - break; - - case Qt::Key_P: { - if (!isShifted && !isMeta && !isOption && !event->isAutoRepeat()) { - AudioInjectorOptions options; - options.localOnly = true; - options.positionSet = false; // system sound - options.stereo = true; - - Setting::Handle notificationSounds{ MenuOption::NotificationSounds, true }; - Setting::Handle notificationSoundSnapshot{ MenuOption::NotificationSoundsSnapshot, true }; - if (notificationSounds.get() && notificationSoundSnapshot.get()) { - if (_snapshotSoundInjector) { - _snapshotSoundInjector->setOptions(options); - _snapshotSoundInjector->restart(); - } else { - _snapshotSoundInjector = AudioInjector::playSound(_snapshotSound, options); - } - } - takeSnapshot(true); - } - break; - } - - case Qt::Key_Apostrophe: { - if (isMeta) { - auto cursor = Cursor::Manager::instance().getCursor(); - auto curIcon = cursor->getIcon(); - if (curIcon == Cursor::Icon::DEFAULT) { - showCursor(Cursor::Icon::RETICLE); - } else if (curIcon == Cursor::Icon::RETICLE) { - showCursor(Cursor::Icon::SYSTEM); - } else if (curIcon == Cursor::Icon::SYSTEM) { - showCursor(Cursor::Icon::LINK); - } else { - showCursor(Cursor::Icon::DEFAULT); - } - } else if (!event->isAutoRepeat()){ - resetSensors(true); - } - break; - } - - case Qt::Key_Backslash: - Menu::getInstance()->triggerOption(MenuOption::Chat); - break; - - case Qt::Key_Slash: - Menu::getInstance()->triggerOption(MenuOption::Stats); - break; - - case Qt::Key_Plus: { - if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { - auto& cursorManager = Cursor::Manager::instance(); - cursorManager.setScale(cursorManager.getScale() * 1.1f); - } else { - getMyAvatar()->increaseSize(); - } - break; - } - - case Qt::Key_Minus: { - if (isMeta && event->modifiers().testFlag(Qt::KeypadModifier)) { - auto& cursorManager = Cursor::Manager::instance(); - cursorManager.setScale(cursorManager.getScale() / 1.1f); - } else { - getMyAvatar()->decreaseSize(); - } - break; - } - - case Qt::Key_Equal: - getMyAvatar()->resetSize(); - break; - case Qt::Key_Escape: { - getActiveDisplayPlugin()->abandonCalibration(); - break; - } - - default: - event->ignore(); - break; - } - } -} - -void Application::keyReleaseEvent(QKeyEvent* event) { - if (!event->isAutoRepeat()) { - _keysPressed.remove(event->key()); - } - -#if defined(Q_OS_ANDROID) - if (event->key() == Qt::Key_Back) { - event->accept(); - AndroidHelper::instance().requestActivity("Home", false); - } -#endif - _controllerScriptingInterface->emitKeyReleaseEvent(event); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isKeyCaptured(event)) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->keyReleaseEvent(event); - } -} - -void Application::focusOutEvent(QFocusEvent* event) { - auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); - foreach(auto inputPlugin, inputPlugins) { - if (inputPlugin->isActive()) { - inputPlugin->pluginFocusOutEvent(); - } - } - -// FIXME spacemouse code still needs cleanup -#if 0 - //SpacemouseDevice::getInstance().focusOutEvent(); - //SpacemouseManager::getInstance().getDevice()->focusOutEvent(); - SpacemouseManager::getInstance().ManagerFocusOutEvent(); -#endif - - synthesizeKeyReleasEvents(); -} - -void Application::synthesizeKeyReleasEvents() { - // synthesize events for keys currently pressed, since we may not get their release events - // Because our key event handlers may manipulate _keysPressed, lets swap the keys pressed into a local copy, - // clearing the existing list. - QHash keysPressed; - std::swap(keysPressed, _keysPressed); - for (auto& ev : keysPressed) { - QKeyEvent synthesizedEvent { QKeyEvent::KeyRelease, ev.key(), Qt::NoModifier, ev.text() }; - keyReleaseEvent(&synthesizedEvent); - } -} - -void Application::maybeToggleMenuVisible(QMouseEvent* event) const { -#ifndef Q_OS_MAC - // If in full screen, and our main windows menu bar is hidden, and we're close to the top of the QMainWindow - // then show the menubar. - if (_window->isFullScreen()) { - QMenuBar* menuBar = _window->menuBar(); - if (menuBar) { - static const int MENU_TOGGLE_AREA = 10; - if (!menuBar->isVisible()) { - if (event->pos().y() <= MENU_TOGGLE_AREA) { - menuBar->setVisible(true); - } - } else { - if (event->pos().y() > MENU_TOGGLE_AREA) { - menuBar->setVisible(false); - } - } - } - } -#endif -} - -void Application::mouseMoveEvent(QMouseEvent* event) { - PROFILE_RANGE(app_input_mouse, __FUNCTION__); - - maybeToggleMenuVisible(event); - - auto& compositor = getApplicationCompositor(); - // if this is a real mouse event, and we're in HMD mode, then we should use it to move the - // compositor reticle - // handleRealMouseMoveEvent() will return true, if we shouldn't process the event further - if (!compositor.fakeEventActive() && compositor.handleRealMouseMoveEvent()) { - return; // bail - } - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto eventPosition = compositor.getMouseEventPosition(event); - QPointF transformedPos = offscreenUi ? offscreenUi->mapToVirtualScreen(eventPosition) : QPointF(); -#else - QPointF transformedPos; -#endif - auto button = event->button(); - auto buttons = event->buttons(); - // Determine if the ReticleClick Action is 1 and if so, fake include the LeftMouseButton - if (_reticleClickPressed) { - if (button == Qt::NoButton) { - button = Qt::LeftButton; - } - buttons |= Qt::LeftButton; - } - - QMouseEvent mappedEvent(event->type(), - transformedPos, - event->screenPos(), button, - buttons, event->modifiers()); - - if (compositor.getReticleVisible() || !isHMDMode() || !compositor.getReticleOverDesktop() || - getOverlays().getOverlayAtPoint(glm::vec2(transformedPos.x(), transformedPos.y())) != UNKNOWN_ENTITY_ID) { - getEntities()->mouseMoveEvent(&mappedEvent); - } - - _controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->mouseMoveEvent(event); - } -} - -void Application::mousePressEvent(QMouseEvent* event) { - // Inhibit the menu if the user is using alt-mouse dragging - _altPressed = false; - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - // If we get a mouse press event it means it wasn't consumed by the offscreen UI, - // hence, we should defocus all of the offscreen UI windows, in order to allow - // keyboard shortcuts not to be swallowed by them. In particular, WebEngineViews - // will consume all keyboard events. - offscreenUi->unfocusWindows(); - - auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); - QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); -#else - QPointF transformedPos; -#endif - - QMouseEvent mappedEvent(event->type(), transformedPos, event->screenPos(), event->button(), event->buttons(), event->modifiers()); - QUuid result = getEntities()->mousePressEvent(&mappedEvent); - setKeyboardFocusEntity(getEntities()->wantsKeyboardFocus(result) ? result : UNKNOWN_ENTITY_ID); - - _controllerScriptingInterface->emitMousePressEvent(&mappedEvent); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - -#if defined(Q_OS_MAC) - // Fix for OSX right click dragging on window when coming from a native window - bool isFocussed = hasFocus(); - if (!isFocussed && event->button() == Qt::MouseButton::RightButton) { - setFocus(); - isFocussed = true; - } - - if (isFocussed) { -#else - if (hasFocus()) { -#endif - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->mousePressEvent(event); - } - } -} - -void Application::mouseDoublePressEvent(QMouseEvent* event) { -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); - QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); -#else - QPointF transformedPos; -#endif - QMouseEvent mappedEvent(event->type(), - transformedPos, - event->screenPos(), event->button(), - event->buttons(), event->modifiers()); - getEntities()->mouseDoublePressEvent(&mappedEvent); - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - - _controllerScriptingInterface->emitMouseDoublePressEvent(event); -} - -void Application::mouseReleaseEvent(QMouseEvent* event) { - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto eventPosition = getApplicationCompositor().getMouseEventPosition(event); - QPointF transformedPos = offscreenUi->mapToVirtualScreen(eventPosition); -#else - QPointF transformedPos; -#endif - QMouseEvent mappedEvent(event->type(), - transformedPos, - event->screenPos(), event->button(), - event->buttons(), event->modifiers()); - - getEntities()->mouseReleaseEvent(&mappedEvent); - - _controllerScriptingInterface->emitMouseReleaseEvent(&mappedEvent); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isMouseCaptured()) { - return; - } - - if (hasFocus()) { - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->mouseReleaseEvent(event); - } - } -} - -void Application::touchUpdateEvent(QTouchEvent* event) { - _altPressed = false; - - if (event->type() == QEvent::TouchUpdate) { - TouchEvent thisEvent(*event, _lastTouchEvent); - _controllerScriptingInterface->emitTouchUpdateEvent(thisEvent); // send events to any registered scripts - _lastTouchEvent = thisEvent; - } - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isTouchCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->touchUpdateEvent(event); - } - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchUpdateEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchUpdateEvent(event); - } -} - -void Application::touchBeginEvent(QTouchEvent* event) { - _altPressed = false; - TouchEvent thisEvent(*event); // on touch begin, we don't compare to last event - _controllerScriptingInterface->emitTouchBeginEvent(thisEvent); // send events to any registered scripts - - _lastTouchEvent = thisEvent; // and we reset our last event to this event before we call our update - touchUpdateEvent(event); - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isTouchCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->touchBeginEvent(event); - } - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchBeginEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchBeginEvent(event); - } - -} - -void Application::touchEndEvent(QTouchEvent* event) { - _altPressed = false; - TouchEvent thisEvent(*event, _lastTouchEvent); - _controllerScriptingInterface->emitTouchEndEvent(thisEvent); // send events to any registered scripts - _lastTouchEvent = thisEvent; - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isTouchCaptured()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->touchEndEvent(event); - } - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchEndEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchEndEvent(event); - } - // put any application specific touch behavior below here.. -} - -void Application::touchGestureEvent(QGestureEvent* event) { - if (_touchscreenDevice && _touchscreenDevice->isActive()) { - _touchscreenDevice->touchGestureEvent(event); - } - if (_touchscreenVirtualPadDevice && _touchscreenVirtualPadDevice->isActive()) { - _touchscreenVirtualPadDevice->touchGestureEvent(event); - } -} - -void Application::wheelEvent(QWheelEvent* event) const { - _altPressed = false; - _controllerScriptingInterface->emitWheelEvent(event); // send events to any registered scripts - - // if one of our scripts have asked to capture this event, then stop processing it - if (_controllerScriptingInterface->isWheelCaptured() || getLoginDialogPoppedUp()) { - return; - } - - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->wheelEvent(event); - } -} - -void Application::dropEvent(QDropEvent *event) { - const QMimeData* mimeData = event->mimeData(); - for (auto& url : mimeData->urls()) { - QString urlString = url.toString(); - if (acceptURL(urlString, true)) { - event->acceptProposedAction(); - } - } -} - -void Application::dragEnterEvent(QDragEnterEvent* event) { - event->acceptProposedAction(); -} - -// This is currently not used, but could be invoked if the user wants to go to the place embedded in an -// Interface-taken snapshot. (It was developed for drag and drop, before we had asset-server loading or in-world browsers.) -bool Application::acceptSnapshot(const QString& urlString) { - QUrl url(urlString); - QString snapshotPath = url.toLocalFile(); - - SnapshotMetaData* snapshotData = DependencyManager::get()->parseSnapshotData(snapshotPath); - if (snapshotData) { - if (!snapshotData->getURL().toString().isEmpty()) { - DependencyManager::get()->handleLookupString(snapshotData->getURL().toString()); - } - } else { - OffscreenUi::asyncWarning("", "No location details were found in the file\n" + - snapshotPath + "\nTry dragging in an authentic Hifi snapshot."); - } - return true; -} - -#ifdef Q_OS_WIN -#include -#include -#include -#pragma comment(lib, "pdh.lib") -#pragma comment(lib, "ntdll.lib") - -extern "C" { - enum SYSTEM_INFORMATION_CLASS { - SystemBasicInformation = 0, - SystemProcessorPerformanceInformation = 8, - }; - - struct SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION { - LARGE_INTEGER IdleTime; - LARGE_INTEGER KernelTime; - LARGE_INTEGER UserTime; - LARGE_INTEGER DpcTime; - LARGE_INTEGER InterruptTime; - ULONG InterruptCount; - }; - - struct SYSTEM_BASIC_INFORMATION { - ULONG Reserved; - ULONG TimerResolution; - ULONG PageSize; - ULONG NumberOfPhysicalPages; - ULONG LowestPhysicalPageNumber; - ULONG HighestPhysicalPageNumber; - ULONG AllocationGranularity; - ULONG_PTR MinimumUserModeAddress; - ULONG_PTR MaximumUserModeAddress; - ULONG_PTR ActiveProcessorsAffinityMask; - CCHAR NumberOfProcessors; - }; - - NTSYSCALLAPI NTSTATUS NTAPI NtQuerySystemInformation( - _In_ SYSTEM_INFORMATION_CLASS SystemInformationClass, - _Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation, - _In_ ULONG SystemInformationLength, - _Out_opt_ PULONG ReturnLength - ); - -} -template -NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, T& t) { - return NtQuerySystemInformation(SystemInformationClass, &t, (ULONG)sizeof(T), nullptr); -} - -template -NTSTATUS NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS SystemInformationClass, std::vector& t) { - return NtQuerySystemInformation(SystemInformationClass, t.data(), (ULONG)(sizeof(T) * t.size()), nullptr); -} - - -template -void updateValueAndDelta(std::pair& pair, T newValue) { - auto& value = pair.first; - auto& delta = pair.second; - delta = (value != 0) ? newValue - value : 0; - value = newValue; -} - -struct MyCpuInfo { - using ValueAndDelta = std::pair; - std::string name; - ValueAndDelta kernel { 0, 0 }; - ValueAndDelta user { 0, 0 }; - ValueAndDelta idle { 0, 0 }; - float kernelUsage { 0.0f }; - float userUsage { 0.0f }; - - void update(const SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION& cpuInfo) { - updateValueAndDelta(kernel, cpuInfo.KernelTime.QuadPart); - updateValueAndDelta(user, cpuInfo.UserTime.QuadPart); - updateValueAndDelta(idle, cpuInfo.IdleTime.QuadPart); - auto totalTime = kernel.second + user.second + idle.second; - if (totalTime != 0) { - kernelUsage = (FLOAT)kernel.second / totalTime; - userUsage = (FLOAT)user.second / totalTime; - } else { - kernelUsage = userUsage = 0.0f; - } - } -}; - -void updateCpuInformation() { - static std::once_flag once; - static SYSTEM_BASIC_INFORMATION systemInfo {}; - static SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION cpuTotals; - static std::vector cpuInfos; - static std::vector myCpuInfos; - static MyCpuInfo myCpuTotals; - std::call_once(once, [&] { - NtQuerySystemInformation( SystemBasicInformation, systemInfo); - cpuInfos.resize(systemInfo.NumberOfProcessors); - myCpuInfos.resize(systemInfo.NumberOfProcessors); - for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { - myCpuInfos[i].name = "cpu." + std::to_string(i); - } - myCpuTotals.name = "cpu.total"; - }); - NtQuerySystemInformation(SystemProcessorPerformanceInformation, cpuInfos); - - // Zero the CPU totals. - memset(&cpuTotals, 0, sizeof(SYSTEM_PROCESSOR_PERFORMANCE_INFORMATION)); - for (size_t i = 0; i < systemInfo.NumberOfProcessors; ++i) { - auto& cpuInfo = cpuInfos[i]; - // KernelTime includes IdleTime. - cpuInfo.KernelTime.QuadPart -= cpuInfo.IdleTime.QuadPart; - - // Update totals - cpuTotals.IdleTime.QuadPart += cpuInfo.IdleTime.QuadPart; - cpuTotals.KernelTime.QuadPart += cpuInfo.KernelTime.QuadPart; - cpuTotals.UserTime.QuadPart += cpuInfo.UserTime.QuadPart; - - // Update friendly structure - auto& myCpuInfo = myCpuInfos[i]; - myCpuInfo.update(cpuInfo); - PROFILE_COUNTER(app, myCpuInfo.name.c_str(), { - { "kernel", myCpuInfo.kernelUsage }, - { "user", myCpuInfo.userUsage } - }); - } - - myCpuTotals.update(cpuTotals); - PROFILE_COUNTER(app, myCpuTotals.name.c_str(), { - { "kernel", myCpuTotals.kernelUsage }, - { "user", myCpuTotals.userUsage } - }); -} - - -static ULARGE_INTEGER lastCPU, lastSysCPU, lastUserCPU; -static int numProcessors; -static HANDLE self; -static PDH_HQUERY cpuQuery; -static PDH_HCOUNTER cpuTotal; - -void initCpuUsage() { - SYSTEM_INFO sysInfo; - FILETIME ftime, fsys, fuser; - - GetSystemInfo(&sysInfo); - numProcessors = sysInfo.dwNumberOfProcessors; - - GetSystemTimeAsFileTime(&ftime); - memcpy(&lastCPU, &ftime, sizeof(FILETIME)); - - self = GetCurrentProcess(); - GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); - memcpy(&lastSysCPU, &fsys, sizeof(FILETIME)); - memcpy(&lastUserCPU, &fuser, sizeof(FILETIME)); - - PdhOpenQuery(NULL, NULL, &cpuQuery); - PdhAddCounter(cpuQuery, "\\Processor(_Total)\\% Processor Time", NULL, &cpuTotal); - PdhCollectQueryData(cpuQuery); -} - -void getCpuUsage(vec3& systemAndUser) { - FILETIME ftime, fsys, fuser; - ULARGE_INTEGER now, sys, user; - - GetSystemTimeAsFileTime(&ftime); - memcpy(&now, &ftime, sizeof(FILETIME)); - - GetProcessTimes(self, &ftime, &ftime, &fsys, &fuser); - memcpy(&sys, &fsys, sizeof(FILETIME)); - memcpy(&user, &fuser, sizeof(FILETIME)); - systemAndUser.x = (sys.QuadPart - lastSysCPU.QuadPart); - systemAndUser.y = (user.QuadPart - lastUserCPU.QuadPart); - systemAndUser /= (float)(now.QuadPart - lastCPU.QuadPart); - systemAndUser /= (float)numProcessors; - systemAndUser *= 100.0f; - lastCPU = now; - lastUserCPU = user; - lastSysCPU = sys; - - PDH_FMT_COUNTERVALUE counterVal; - PdhCollectQueryData(cpuQuery); - PdhGetFormattedCounterValue(cpuTotal, PDH_FMT_DOUBLE, NULL, &counterVal); - systemAndUser.z = (float)counterVal.doubleValue; -} - -void setupCpuMonitorThread() { - initCpuUsage(); - auto cpuMonitorThread = QThread::currentThread(); - - QTimer* timer = new QTimer(); - timer->setInterval(50); - QObject::connect(timer, &QTimer::timeout, [] { - updateCpuInformation(); - vec3 kernelUserAndSystem; - getCpuUsage(kernelUserAndSystem); - PROFILE_COUNTER(app, "cpuProcess", { { "system", kernelUserAndSystem.x }, { "user", kernelUserAndSystem.y } }); - PROFILE_COUNTER(app, "cpuSystem", { { "system", kernelUserAndSystem.z } }); - }); - QObject::connect(cpuMonitorThread, &QThread::finished, [=] { - timer->deleteLater(); - cpuMonitorThread->deleteLater(); - }); - timer->start(); -} - -#endif - -void Application::idle() { - PerformanceTimer perfTimer("idle"); - - // Update the deadlock watchdog - updateHeartbeat(); - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - - // These tasks need to be done on our first idle, because we don't want the showing of - // overlay subwindows to do a showDesktop() until after the first time through - static bool firstIdle = true; - if (firstIdle) { - firstIdle = false; - connect(offscreenUi.data(), &OffscreenUi::showDesktop, this, &Application::showDesktop); - } -#endif - -#ifdef Q_OS_WIN - // If tracing is enabled then monitor the CPU in a separate thread - static std::once_flag once; - std::call_once(once, [&] { - if (trace_app().isDebugEnabled()) { - QThread* cpuMonitorThread = new QThread(qApp); - cpuMonitorThread->setObjectName("cpuMonitorThread"); - QObject::connect(cpuMonitorThread, &QThread::started, [this] { setupCpuMonitorThread(); }); - QObject::connect(qApp, &QCoreApplication::aboutToQuit, cpuMonitorThread, &QThread::quit); - cpuMonitorThread->start(); - } - }); -#endif - - auto displayPlugin = getActiveDisplayPlugin(); -#if !defined(DISABLE_QML) - if (displayPlugin) { - auto uiSize = displayPlugin->getRecommendedUiSize(); - // Bit of a hack since there's no device pixel ratio change event I can find. - if (offscreenUi->size() != fromGlm(uiSize)) { - qCDebug(interfaceapp) << "Device pixel ratio changed, triggering resize to " << uiSize; - offscreenUi->resize(fromGlm(uiSize)); - } - } -#endif - - if (displayPlugin) { - PROFILE_COUNTER_IF_CHANGED(app, "present", float, displayPlugin->presentRate()); - } - PROFILE_COUNTER_IF_CHANGED(app, "renderLoopRate", float, getRenderLoopRate()); - PROFILE_COUNTER_IF_CHANGED(app, "currentDownloads", uint32_t, ResourceCache::getLoadingRequests().length()); - PROFILE_COUNTER_IF_CHANGED(app, "pendingDownloads", uint32_t, ResourceCache::getPendingRequestCount()); - PROFILE_COUNTER_IF_CHANGED(app, "currentProcessing", int, DependencyManager::get()->getStat("Processing").toInt()); - PROFILE_COUNTER_IF_CHANGED(app, "pendingProcessing", int, DependencyManager::get()->getStat("PendingProcessing").toInt()); - auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); - PROFILE_COUNTER_IF_CHANGED(render, "gpuTime", float, (float)_graphicsEngine.getGPUContext()->getFrameTimerGPUAverage()); - - PROFILE_RANGE(app, __FUNCTION__); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - steamClient->runCallbacks(); - } - - if (auto oculusPlugin = PluginManager::getInstance()->getOculusPlatformPlugin()) { - oculusPlugin->handleOVREvents(); - } - - float secondsSinceLastUpdate = (float)_lastTimeUpdated.nsecsElapsed() / NSECS_PER_MSEC / MSECS_PER_SECOND; - _lastTimeUpdated.start(); - -#if !defined(DISABLE_QML) - // If the offscreen Ui has something active that is NOT the root, then assume it has keyboard focus. - if (offscreenUi && offscreenUi->getWindow()) { - auto activeFocusItem = offscreenUi->getWindow()->activeFocusItem(); - if (_keyboardDeviceHasFocus && activeFocusItem != offscreenUi->getRootItem()) { - _keyboardMouseDevice->pluginFocusOutEvent(); - _keyboardDeviceHasFocus = false; - synthesizeKeyReleasEvents(); - } else if (activeFocusItem == offscreenUi->getRootItem()) { - _keyboardDeviceHasFocus = true; - } - } -#endif - - checkChangeCursor(); - -#if !defined(DISABLE_QML) - auto stats = Stats::getInstance(); - if (stats) { - stats->updateStats(); - } - auto animStats = AnimStats::getInstance(); - if (animStats) { - animStats->updateStats(); - } -#endif - - // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing - // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing - // details normally. -#ifdef Q_OS_ANDROID - bool showWarnings = false; -#else - bool showWarnings = getLogger()->extraDebugging(); -#endif - PerformanceWarning warn(showWarnings, "idle()"); - - { - _gameWorkload.updateViews(_viewFrustum, getMyAvatar()->getHeadPosition()); - _gameWorkload._engine->run(); - } - { - PerformanceTimer perfTimer("update"); - PerformanceWarning warn(showWarnings, "Application::idle()... update()"); - static const float BIGGEST_DELTA_TIME_SECS = 0.25f; - update(glm::clamp(secondsSinceLastUpdate, 0.0f, BIGGEST_DELTA_TIME_SECS)); - } - - { // Update keyboard focus highlight - if (!_keyboardFocusedEntity.get().isInvalidID()) { - const quint64 LOSE_FOCUS_AFTER_ELAPSED_TIME = 30 * USECS_PER_SECOND; // if idle for 30 seconds, drop focus - quint64 elapsedSinceAcceptedKeyPress = usecTimestampNow() - _lastAcceptedKeyPress; - if (elapsedSinceAcceptedKeyPress > LOSE_FOCUS_AFTER_ELAPSED_TIME) { - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - } else { - if (auto entity = getEntities()->getTree()->findEntityByID(_keyboardFocusedEntity.get())) { - EntityItemProperties properties; - properties.setPosition(entity->getWorldPosition()); - properties.setRotation(entity->getWorldOrientation()); - properties.setDimensions(entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); - DependencyManager::get()->editEntity(_keyboardFocusHighlightID, properties); - } - } - } - } - - { - if (_keyboardFocusWaitingOnRenderable && getEntities()->renderableForEntityId(_keyboardFocusedEntity.get())) { - QUuid entityId = _keyboardFocusedEntity.get(); - setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); - _keyboardFocusWaitingOnRenderable = false; - setKeyboardFocusEntity(entityId); - } - } - - { - PerformanceTimer perfTimer("pluginIdle"); - PerformanceWarning warn(showWarnings, "Application::idle()... pluginIdle()"); - getActiveDisplayPlugin()->idle(); - auto inputPlugins = PluginManager::getInstance()->getInputPlugins(); - foreach(auto inputPlugin, inputPlugins) { - if (inputPlugin->isActive()) { - inputPlugin->idle(); - } - } - } - { - PerformanceTimer perfTimer("rest"); - PerformanceWarning warn(showWarnings, "Application::idle()... rest of it"); - _idleLoopStdev.addValue(secondsSinceLastUpdate); - - // Record standard deviation and reset counter if needed - const int STDEV_SAMPLES = 500; - if (_idleLoopStdev.getSamples() > STDEV_SAMPLES) { - _idleLoopMeasuredJitter = _idleLoopStdev.getStDev(); - _idleLoopStdev.reset(); - } - } - - _overlayConductor.update(secondsSinceLastUpdate); - - _gameLoopCounter.increment(); -} - -ivec2 Application::getMouse() const { - return getApplicationCompositor().getReticlePosition(); -} - -FaceTracker* Application::getActiveFaceTracker() { - auto dde = DependencyManager::get(); - - return dde->isActive() ? static_cast(dde.data()) : nullptr; -} - -FaceTracker* Application::getSelectedFaceTracker() { - FaceTracker* faceTracker = nullptr; -#ifdef HAVE_DDE - if (Menu::getInstance()->isOptionChecked(MenuOption::UseCamera)) { - faceTracker = DependencyManager::get().data(); - } -#endif - return faceTracker; -} - -void Application::setActiveFaceTracker() const { -#ifdef HAVE_DDE - bool isMuted = Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking); - bool isUsingDDE = Menu::getInstance()->isOptionChecked(MenuOption::UseCamera); - Menu::getInstance()->getActionForOption(MenuOption::BinaryEyelidControl)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::CoupleEyelids)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::UseAudioForMouth)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::VelocityFilter)->setVisible(isUsingDDE); - Menu::getInstance()->getActionForOption(MenuOption::CalibrateCamera)->setVisible(isUsingDDE); - auto ddeTracker = DependencyManager::get(); - ddeTracker->setIsMuted(isMuted); - ddeTracker->setEnabled(isUsingDDE && !isMuted); -#endif -} - -#ifdef HAVE_IVIEWHMD -void Application::setActiveEyeTracker() { - auto eyeTracker = DependencyManager::get(); - if (!eyeTracker->isInitialized()) { - return; - } - - bool isEyeTracking = Menu::getInstance()->isOptionChecked(MenuOption::SMIEyeTracking); - bool isSimulating = Menu::getInstance()->isOptionChecked(MenuOption::SimulateEyeTracking); - eyeTracker->setEnabled(isEyeTracking, isSimulating); - - Menu::getInstance()->getActionForOption(MenuOption::OnePointCalibration)->setEnabled(isEyeTracking && !isSimulating); - Menu::getInstance()->getActionForOption(MenuOption::ThreePointCalibration)->setEnabled(isEyeTracking && !isSimulating); - Menu::getInstance()->getActionForOption(MenuOption::FivePointCalibration)->setEnabled(isEyeTracking && !isSimulating); -} - -void Application::calibrateEyeTracker1Point() { - DependencyManager::get()->calibrate(1); -} - -void Application::calibrateEyeTracker3Points() { - DependencyManager::get()->calibrate(3); -} - -void Application::calibrateEyeTracker5Points() { - DependencyManager::get()->calibrate(5); -} -#endif - -bool Application::exportEntities(const QString& filename, - const QVector& entityIDs, - const glm::vec3* givenOffset) { - QHash entities; - - auto nodeList = DependencyManager::get(); - const QUuid myAvatarID = nodeList->getSessionUUID(); - - auto entityTree = getEntities()->getTree(); - auto exportTree = std::make_shared(); - exportTree->setMyAvatar(getMyAvatar()); - exportTree->createRootElement(); - glm::vec3 root(TREE_SCALE, TREE_SCALE, TREE_SCALE); - bool success = true; - entityTree->withReadLock([entityIDs, entityTree, givenOffset, myAvatarID, &root, &entities, &success, &exportTree] { - for (auto entityID : entityIDs) { // Gather entities and properties. - auto entityItem = entityTree->findEntityByEntityItemID(entityID); - if (!entityItem) { - qCWarning(interfaceapp) << "Skipping export of" << entityID << "that is not in scene."; - continue; - } - - if (!givenOffset) { - EntityItemID parentID = entityItem->getParentID(); - bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); - if (!parentIsAvatar && (parentID.isInvalidID() || - !entityIDs.contains(parentID) || - !entityTree->findEntityByEntityItemID(parentID))) { - // If parent wasn't selected, we want absolute position, which isn't in properties. - auto position = entityItem->getWorldPosition(); - root.x = glm::min(root.x, position.x); - root.y = glm::min(root.y, position.y); - root.z = glm::min(root.z, position.z); - } - } - entities[entityID] = entityItem; - } - - if (entities.size() == 0) { - success = false; - return; - } - - if (givenOffset) { - root = *givenOffset; - } - for (EntityItemPointer& entityDatum : entities) { - auto properties = entityDatum->getProperties(); - EntityItemID parentID = properties.getParentID(); - bool parentIsAvatar = (parentID == AVATAR_SELF_ID || parentID == myAvatarID); - if (parentIsAvatar) { - properties.setParentID(AVATAR_SELF_ID); - } else { - if (parentID.isInvalidID()) { - properties.setPosition(properties.getPosition() - root); - } else if (!entities.contains(parentID)) { - entityDatum->globalizeProperties(properties, "Parent %3 of %2 %1 is not selected for export.", -root); - } // else valid parent -- don't offset - } - exportTree->addEntity(entityDatum->getEntityItemID(), properties); - } - }); - if (success) { - success = exportTree->writeToJSONFile(filename.toLocal8Bit().constData()); - - // restore the main window's active state - _window->activateWindow(); - } - return success; -} - -bool Application::exportEntities(const QString& filename, float x, float y, float z, float scale) { - glm::vec3 center(x, y, z); - glm::vec3 minCorner = center - vec3(scale); - float cubeSize = scale * 2; - AACube boundingCube(minCorner, cubeSize); - QVector entities; - auto entityTree = getEntities()->getTree(); - entityTree->withReadLock([&] { - entityTree->evalEntitiesInCube(boundingCube, PickFilter(), entities); - }); - return exportEntities(filename, entities, ¢er); -} - -void Application::loadSettings() { - - sessionRunTime.set(0); // Just clean living. We're about to saveSettings, which will update value. - DependencyManager::get()->loadSettings(); - DependencyManager::get()->loadSettings(); - - // DONT CHECK IN - //DependencyManager::get()->setAutomaticLODAdjust(false); - - auto menu = Menu::getInstance(); - menu->loadSettings(); - - // override the menu option show overlays to always be true on startup - menu->setIsOptionChecked(MenuOption::Overlays, true); - - // If there is a preferred plugin, we probably messed it up with the menu settings, so fix it. - auto pluginManager = PluginManager::getInstance(); - auto plugins = pluginManager->getPreferredDisplayPlugins(); - if (plugins.size() > 0) { - for (auto plugin : plugins) { - if (auto action = menu->getActionForOption(plugin->getName())) { - action->setChecked(true); - action->trigger(); - // Find and activated highest priority plugin, bail for the rest - break; - } - } - } - - bool isFirstPerson = false; - if (_firstRun.get()) { - // If this is our first run, and no preferred devices were set, default to - // an HMD device if available. - auto displayPlugins = pluginManager->getDisplayPlugins(); - for (auto& plugin : displayPlugins) { - if (plugin->isHmd()) { - if (auto action = menu->getActionForOption(plugin->getName())) { - action->setChecked(true); - action->trigger(); - break; - } - } - } - - isFirstPerson = (qApp->isHMDMode()); - - } else { - // if this is not the first run, the camera will be initialized differently depending on user settings - - if (qApp->isHMDMode()) { - // if the HMD is active, use first-person camera, unless the appropriate setting is checked - isFirstPerson = menu->isOptionChecked(MenuOption::FirstPersonHMD); - } else { - // if HMD is not active, only use first person if the menu option is checked - isFirstPerson = menu->isOptionChecked(MenuOption::FirstPerson); - } - } - - // finish initializing the camera, based on everything we checked above. Third person camera will be used if no settings - // dictated that we should be in first person - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, isFirstPerson); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !isFirstPerson); - _myCamera.setMode((isFirstPerson) ? CAMERA_MODE_FIRST_PERSON : CAMERA_MODE_THIRD_PERSON); - cameraMenuChanged(); - - auto inputs = pluginManager->getInputPlugins(); - for (auto plugin : inputs) { - if (!plugin->isActive()) { - plugin->activate(); - } - } - - getMyAvatar()->loadData(); - _settingsLoaded = true; -} - -void Application::saveSettings() const { - sessionRunTime.set(_sessionRunTimer.elapsed() / MSECS_PER_SECOND); - DependencyManager::get()->saveSettings(); - DependencyManager::get()->saveSettings(); - - Menu::getInstance()->saveSettings(); - getMyAvatar()->saveData(); - PluginManager::getInstance()->saveSettings(); -} - -bool Application::importEntities(const QString& urlOrFilename, const bool isObservable, const qint64 callerId) { - bool success = false; - _entityClipboard->withWriteLock([&] { - _entityClipboard->eraseAllOctreeElements(); - - success = _entityClipboard->readFromURL(urlOrFilename, isObservable, callerId); - if (success) { - _entityClipboard->reaverageOctreeElements(); - } - }); - return success; -} - -QVector Application::pasteEntities(float x, float y, float z) { - return _entityClipboard->sendEntities(&_entityEditSender, getEntities()->getTree(), x, y, z); -} - -void Application::init() { - // Make sure Login state is up to date -#if !defined(DISABLE_QML) - DependencyManager::get()->toggleLoginDialog(); -#endif - DependencyManager::get()->init(); - - _timerStart.start(); - _lastTimeUpdated.start(); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - // when +connect_lobby in command line, join steam lobby - const QString STEAM_LOBBY_COMMAND_LINE_KEY = "+connect_lobby"; - int lobbyIndex = arguments().indexOf(STEAM_LOBBY_COMMAND_LINE_KEY); - if (lobbyIndex != -1) { - QString lobbyId = arguments().value(lobbyIndex + 1); - steamClient->joinLobby(lobbyId); - } - } - - - qCDebug(interfaceapp) << "Loaded settings"; - - // fire off an immediate domain-server check in now that settings are loaded - if (!isServerlessMode()) { - DependencyManager::get()->sendDomainServerCheckIn(); - } - - // This allows collision to be set up properly for shape entities supported by GeometryCache. - // This is before entity setup to ensure that it's ready for whenever instance collision is initialized. - ShapeEntityItem::setShapeInfoCalulator(ShapeEntityItem::ShapeInfoCalculator(&shapeInfoCalculator)); - - getEntities()->init(); - getEntities()->setEntityLoadingPriorityFunction([this](const EntityItem& item) { - auto dims = item.getScaledDimensions(); - auto maxSize = glm::compMax(dims); - - if (maxSize <= 0.0f) { - return 0.0f; - } - - auto distance = glm::distance(getMyAvatar()->getWorldPosition(), item.getWorldPosition()); - return atan2(maxSize, distance); - }); - - ObjectMotionState::setShapeManager(&_shapeManager); - _physicsEngine->init(); - - EntityTreePointer tree = getEntities()->getTree(); - _entitySimulation->init(tree, _physicsEngine, &_entityEditSender); - tree->setSimulation(_entitySimulation); - - auto entityScriptingInterface = DependencyManager::get(); - - // connect the _entityCollisionSystem to our EntityTreeRenderer since that's what handles running entity scripts - connect(_entitySimulation.get(), &PhysicalEntitySimulation::entityCollisionWithEntity, - getEntities().data(), &EntityTreeRenderer::entityCollisionWithEntity); - - // connect the _entities (EntityTreeRenderer) to our script engine's EntityScriptingInterface for firing - // of events related clicking, hovering over, and entering entities - getEntities()->connectSignalsToSlots(entityScriptingInterface.data()); - - // Make sure any new sounds are loaded as soon as know about them. - connect(tree.get(), &EntityTree::newCollisionSoundURL, this, [this](QUrl newURL, EntityItemID id) { - getEntities()->setCollisionSound(id, DependencyManager::get()->getSound(newURL)); - }, Qt::QueuedConnection); - connect(getMyAvatar().get(), &MyAvatar::newCollisionSoundURL, this, [this](QUrl newURL) { - if (auto avatar = getMyAvatar()) { - auto sound = DependencyManager::get()->getSound(newURL); - avatar->setCollisionSound(sound); - } - }, Qt::QueuedConnection); - - _gameWorkload.startup(getEntities()->getWorkloadSpace(), _graphicsEngine.getRenderScene(), _entitySimulation); - _entitySimulation->setWorkloadSpace(getEntities()->getWorkloadSpace()); -} - -void Application::pauseUntilLoginDetermined() { - if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "pauseUntilLoginDetermined"); - return; - } - - auto myAvatar = getMyAvatar(); - _previousAvatarTargetScale = myAvatar->getTargetScale(); - _previousAvatarSkeletonModel = myAvatar->getSkeletonModelURL().toString(); - myAvatar->setTargetScale(1.0f); - myAvatar->setSkeletonModelURLFromScript(myAvatar->defaultFullAvatarModelUrl().toString()); - myAvatar->setEnableMeshVisible(false); - - _controllerScriptingInterface->disableMapping(STANDARD_TO_ACTION_MAPPING_NAME); - - { - auto userInputMapper = DependencyManager::get(); - if (userInputMapper->loadMapping(NO_MOVEMENT_MAPPING_JSON)) { - _controllerScriptingInterface->enableMapping(NO_MOVEMENT_MAPPING_NAME); - } - } - - const auto& nodeList = DependencyManager::get(); - // save interstitial mode setting until resuming. - _interstitialModeEnabled = nodeList->getDomainHandler().getInterstitialModeEnabled(); - nodeList->getDomainHandler().setInterstitialModeEnabled(false); - - auto menu = Menu::getInstance(); - menu->getMenu("Edit")->setVisible(false); - menu->getMenu("View")->setVisible(false); - menu->getMenu("Navigate")->setVisible(false); - menu->getMenu("Settings")->setVisible(false); - _developerMenuVisible = menu->getMenu("Developer")->isVisible(); - menu->setIsOptionChecked(MenuOption::Stats, false); - if (_developerMenuVisible) { - menu->getMenu("Developer")->setVisible(false); - } - _previousCameraMode = _myCamera.getMode(); - _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - cameraModeChanged(); - - // disconnect domain handler. - nodeList->getDomainHandler().disconnect(); - -} - -void Application::resumeAfterLoginDialogActionTaken() { - if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "resumeAfterLoginDialogActionTaken"); - return; - } - - if (!isHMDMode() && getDesktopTabletBecomesToolbarSetting()) { - auto toolbar = DependencyManager::get()->getToolbar("com.highfidelity.interface.toolbar.system"); - toolbar->writeProperty("visible", true); - } else { - getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(true); - getApplicationCompositor().getReticleInterface()->setVisible(true); - } - - updateSystemTabletMode(); - - { - auto userInputMapper = DependencyManager::get(); - userInputMapper->unloadMapping(NO_MOVEMENT_MAPPING_JSON); - _controllerScriptingInterface->disableMapping(NO_MOVEMENT_MAPPING_NAME); - } - - auto myAvatar = getMyAvatar(); - myAvatar->setTargetScale(_previousAvatarTargetScale); - myAvatar->setSkeletonModelURLFromScript(_previousAvatarSkeletonModel); - myAvatar->setEnableMeshVisible(true); - - _controllerScriptingInterface->enableMapping(STANDARD_TO_ACTION_MAPPING_NAME); - - const auto& nodeList = DependencyManager::get(); - nodeList->getDomainHandler().setInterstitialModeEnabled(_interstitialModeEnabled); - { - auto scriptEngines = DependencyManager::get().data(); - // this will force the model the look at the correct directory (weird order of operations issue) - scriptEngines->reloadLocalFiles(); - - // if the --scripts command-line argument was used. - if (!_defaultScriptsLocation.exists() && (arguments().indexOf(QString("--").append(SCRIPTS_SWITCH))) != -1) { - scriptEngines->loadDefaultScripts(); - scriptEngines->defaultScriptsLocationOverridden(true); - } else { - scriptEngines->loadScripts(); - } - } - - auto accountManager = DependencyManager::get(); - auto addressManager = DependencyManager::get(); - - // restart domain handler. - nodeList->getDomainHandler().resetting(); - - QVariant testProperty = property(hifi::properties::TEST); - if (testProperty.isValid()) { - const auto testScript = property(hifi::properties::TEST).toUrl(); - // Set last parameter to exit interface when the test script finishes, if so requested - DependencyManager::get()->loadScript(testScript, false, false, false, false, quitWhenFinished); - // This is done so we don't get a "connection time-out" message when we haven't passed in a URL. - if (arguments().contains("--url")) { - auto reply = SandboxUtils::getStatus(); - connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); - } - } else { - auto reply = SandboxUtils::getStatus(); - connect(reply, &QNetworkReply::finished, this, [this, reply] { handleSandboxStatus(reply); }); - } - - auto menu = Menu::getInstance(); - menu->getMenu("Edit")->setVisible(true); - menu->getMenu("View")->setVisible(true); - menu->getMenu("Navigate")->setVisible(true); - menu->getMenu("Settings")->setVisible(true); - menu->getMenu("Developer")->setVisible(_developerMenuVisible); - _myCamera.setMode(_previousCameraMode); - cameraModeChanged(); -} - -void Application::loadAvatarScripts(const QVector& urls) { - auto scriptEngines = DependencyManager::get(); - auto runningScripts = scriptEngines->getRunningScripts(); - for (auto url : urls) { - int index = runningScripts.indexOf(url); - if (index < 0) { - auto scriptEnginePointer = scriptEngines->loadScript(url, false); - if (scriptEnginePointer) { - scriptEnginePointer->setType(ScriptEngine::Type::AVATAR); - } - } - } -} - -void Application::unloadAvatarScripts() { - auto scriptEngines = DependencyManager::get(); - auto urls = scriptEngines->getRunningScripts(); - for (auto url : urls) { - auto scriptEngine = scriptEngines->getScriptEngine(url); - if (scriptEngine->getType() == ScriptEngine::Type::AVATAR) { - scriptEngines->stopScript(url, false); - } - } -} - -void Application::updateLOD(float deltaTime) const { - PerformanceTimer perfTimer("LOD"); - // adjust it unless we were asked to disable this feature, or if we're currently in throttleRendering mode - if (!isThrottleRendering()) { - float presentTime = getActiveDisplayPlugin()->getAveragePresentTime(); - float engineRunTime = (float)(_graphicsEngine.getRenderEngine()->getConfiguration().get()->getCPURunTime()); - float gpuTime = getGPUContext()->getFrameTimerGPUAverage(); - float batchTime = getGPUContext()->getFrameTimerBatchAverage(); - auto lodManager = DependencyManager::get(); - lodManager->setRenderTimes(presentTime, engineRunTime, batchTime, gpuTime); - lodManager->autoAdjustLOD(deltaTime); - } else { - DependencyManager::get()->resetLODAdjust(); - } -} - -void Application::pushPostUpdateLambda(void* key, const std::function& func) { - std::unique_lock guard(_postUpdateLambdasLock); - _postUpdateLambdas[key] = func; -} - -// Called during Application::update immediately before AvatarManager::updateMyAvatar, updating my data that is then sent to everyone. -// (Maybe this code should be moved there?) -// The principal result is to call updateLookAtTargetAvatar() and then setLookAtPosition(). -// Note that it is called BEFORE we update position or joints based on sensors, etc. -void Application::updateMyAvatarLookAtPosition() { - PerformanceTimer perfTimer("lookAt"); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::updateMyAvatarLookAtPosition()"); - - auto myAvatar = getMyAvatar(); - myAvatar->updateLookAtTargetAvatar(); - FaceTracker* faceTracker = getActiveFaceTracker(); - auto eyeTracker = DependencyManager::get(); - - bool isLookingAtSomeone = false; - bool isHMD = qApp->isHMDMode(); - glm::vec3 lookAtSpot; - if (eyeTracker->isTracking() && (isHMD || eyeTracker->isSimulating())) { - // Look at the point that the user is looking at. - glm::vec3 lookAtPosition = eyeTracker->getLookAtPosition(); - if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { - lookAtPosition.x = -lookAtPosition.x; - } - if (isHMD) { - // TODO -- this code is probably wrong, getHeadPose() returns something in sensor frame, not avatar - glm::mat4 headPose = getActiveDisplayPlugin()->getHeadPose(); - glm::quat hmdRotation = glm::quat_cast(headPose); - lookAtSpot = _myCamera.getPosition() + myAvatar->getWorldOrientation() * (hmdRotation * lookAtPosition); - } else { - lookAtSpot = myAvatar->getHead()->getEyePosition() - + (myAvatar->getHead()->getFinalOrientationInWorldFrame() * lookAtPosition); - } - } else { - AvatarSharedPointer lookingAt = myAvatar->getLookAtTargetAvatar().lock(); - bool haveLookAtCandidate = lookingAt && myAvatar.get() != lookingAt.get(); - auto avatar = static_pointer_cast(lookingAt); - bool mutualLookAtSnappingEnabled = avatar && avatar->getLookAtSnappingEnabled() && myAvatar->getLookAtSnappingEnabled(); - if (haveLookAtCandidate && mutualLookAtSnappingEnabled) { - // If I am looking at someone else, look directly at one of their eyes - isLookingAtSomeone = true; - auto lookingAtHead = avatar->getHead(); - - const float MAXIMUM_FACE_ANGLE = 65.0f * RADIANS_PER_DEGREE; - glm::vec3 lookingAtFaceOrientation = lookingAtHead->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD; - glm::vec3 fromLookingAtToMe = glm::normalize(myAvatar->getHead()->getEyePosition() - - lookingAtHead->getEyePosition()); - float faceAngle = glm::angle(lookingAtFaceOrientation, fromLookingAtToMe); - - if (faceAngle < MAXIMUM_FACE_ANGLE) { - // Randomly look back and forth between look targets - eyeContactTarget target = Menu::getInstance()->isOptionChecked(MenuOption::FixGaze) ? - LEFT_EYE : myAvatar->getEyeContactTarget(); - switch (target) { - case LEFT_EYE: - lookAtSpot = lookingAtHead->getLeftEyePosition(); - break; - case RIGHT_EYE: - lookAtSpot = lookingAtHead->getRightEyePosition(); - break; - case MOUTH: - lookAtSpot = lookingAtHead->getMouthPosition(); - break; - } - } else { - // Just look at their head (mid point between eyes) - lookAtSpot = lookingAtHead->getEyePosition(); - } - } else { - // I am not looking at anyone else, so just look forward - auto headPose = myAvatar->getControllerPoseInWorldFrame(controller::Action::HEAD); - if (headPose.isValid()) { - lookAtSpot = transformPoint(headPose.getMatrix(), glm::vec3(0.0f, 0.0f, TREE_SCALE)); - } else { - lookAtSpot = myAvatar->getHead()->getEyePosition() + - (myAvatar->getHead()->getFinalOrientationInWorldFrame() * glm::vec3(0.0f, 0.0f, -TREE_SCALE)); - } - } - - // Deflect the eyes a bit to match the detected gaze from the face tracker if active. - if (faceTracker && !faceTracker->isMuted()) { - float eyePitch = faceTracker->getEstimatedEyePitch(); - float eyeYaw = faceTracker->getEstimatedEyeYaw(); - const float GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT = 0.1f; - glm::vec3 origin = myAvatar->getHead()->getEyePosition(); - float deflection = faceTracker->getEyeDeflection(); - if (isLookingAtSomeone) { - deflection *= GAZE_DEFLECTION_REDUCTION_DURING_EYE_CONTACT; - } - lookAtSpot = origin + _myCamera.getOrientation() * glm::quat(glm::radians(glm::vec3( - eyePitch * deflection, eyeYaw * deflection, 0.0f))) * - glm::inverse(_myCamera.getOrientation()) * (lookAtSpot - origin); - } - } - - myAvatar->getHead()->setLookAtPosition(lookAtSpot); -} - -void Application::updateThreads(float deltaTime) { - PerformanceTimer perfTimer("updateThreads"); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::updateThreads()"); - - // parse voxel packets - if (!_enableProcessOctreeThread) { - _octreeProcessor.threadRoutine(); - _entityEditSender.threadRoutine(); - } -} - -void Application::toggleOverlays() { - auto menu = Menu::getInstance(); - menu->setIsOptionChecked(MenuOption::Overlays, !menu->isOptionChecked(MenuOption::Overlays)); -} - -void Application::setOverlaysVisible(bool visible) { - auto menu = Menu::getInstance(); - menu->setIsOptionChecked(MenuOption::Overlays, visible); -} - -void Application::centerUI() { - _overlayConductor.centerUI(); -} - -void Application::cycleCamera() { - auto menu = Menu::getInstance(); - if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { - - menu->setIsOptionChecked(MenuOption::FullscreenMirror, false); - menu->setIsOptionChecked(MenuOption::FirstPerson, true); - - } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { - - menu->setIsOptionChecked(MenuOption::FirstPerson, false); - menu->setIsOptionChecked(MenuOption::ThirdPerson, true); - - } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { - - menu->setIsOptionChecked(MenuOption::ThirdPerson, false); - menu->setIsOptionChecked(MenuOption::FullscreenMirror, true); - - } else if (menu->isOptionChecked(MenuOption::IndependentMode) || menu->isOptionChecked(MenuOption::CameraEntityMode)) { - // do nothing if in independent or camera entity modes - return; - } - cameraMenuChanged(); // handle the menu change -} - -void Application::cameraModeChanged() { - switch (_myCamera.getMode()) { - case CAMERA_MODE_FIRST_PERSON: - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); - break; - case CAMERA_MODE_THIRD_PERSON: - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - break; - case CAMERA_MODE_MIRROR: - Menu::getInstance()->setIsOptionChecked(MenuOption::FullscreenMirror, true); - break; - case CAMERA_MODE_INDEPENDENT: - Menu::getInstance()->setIsOptionChecked(MenuOption::IndependentMode, true); - break; - case CAMERA_MODE_ENTITY: - Menu::getInstance()->setIsOptionChecked(MenuOption::CameraEntityMode, true); - break; - default: - break; - } - cameraMenuChanged(); -} - -void Application::changeViewAsNeeded(float boomLength) { - // Switch between first and third person views as needed - // This is called when the boom length has changed - bool boomLengthGreaterThanMinimum = (boomLength > MyAvatar::ZOOM_MIN); - - if (_myCamera.getMode() == CAMERA_MODE_FIRST_PERSON && boomLengthGreaterThanMinimum) { - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, false); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, true); - cameraMenuChanged(); - } else if (_myCamera.getMode() == CAMERA_MODE_THIRD_PERSON && !boomLengthGreaterThanMinimum) { - Menu::getInstance()->setIsOptionChecked(MenuOption::FirstPerson, true); - Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, false); - cameraMenuChanged(); - } -} - -void Application::cameraMenuChanged() { - auto menu = Menu::getInstance(); - if (menu->isOptionChecked(MenuOption::FullscreenMirror)) { - if (!isHMDMode() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { - _mirrorYawOffset = 0.0f; - _myCamera.setMode(CAMERA_MODE_MIRROR); - getMyAvatar()->reset(false, false, false); // to reset any active MyAvatar::FollowHelpers - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); - } - } else if (menu->isOptionChecked(MenuOption::FirstPerson)) { - if (_myCamera.getMode() != CAMERA_MODE_FIRST_PERSON) { - _myCamera.setMode(CAMERA_MODE_FIRST_PERSON); - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_MIN); - } - } else if (menu->isOptionChecked(MenuOption::ThirdPerson)) { - if (_myCamera.getMode() != CAMERA_MODE_THIRD_PERSON) { - _myCamera.setMode(CAMERA_MODE_THIRD_PERSON); - if (getMyAvatar()->getBoomLength() == MyAvatar::ZOOM_MIN) { - getMyAvatar()->setBoomLength(MyAvatar::ZOOM_DEFAULT); - } - } - } else if (menu->isOptionChecked(MenuOption::IndependentMode)) { - if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { - _myCamera.setMode(CAMERA_MODE_INDEPENDENT); - } - } else if (menu->isOptionChecked(MenuOption::CameraEntityMode)) { - if (_myCamera.getMode() != CAMERA_MODE_ENTITY) { - _myCamera.setMode(CAMERA_MODE_ENTITY); - } - } -} - -void Application::resetPhysicsReadyInformation() { - // we've changed domains or cleared out caches or something. we no longer know enough about the - // collision information of nearby entities to make running bullet be safe. - _fullSceneReceivedCounter = 0; - _fullSceneCounterAtLastPhysicsCheck = 0; - _gpuTextureMemSizeStabilityCount = 0; - _gpuTextureMemSizeAtLastCheck = 0; - _physicsEnabled = false; - _octreeProcessor.startEntitySequence(); -} - - -void Application::reloadResourceCaches() { - resetPhysicsReadyInformation(); - - // Query the octree to refresh everything in view - _queryExpiry = SteadyClock::now(); - _octreeQuery.incrementConnectionID(); - - queryOctree(NodeType::EntityServer, PacketType::EntityQuery); - - // Clear the entities and their renderables - getEntities()->clear(); - - DependencyManager::get()->clearCache(); - DependencyManager::get()->clearCache(); - - // Clear all the resource caches - DependencyManager::get()->clear(); - DependencyManager::get()->refreshAll(); - DependencyManager::get()->refreshAll(); - MaterialCache::instance().refreshAll(); - DependencyManager::get()->refreshAll(); - ShaderCache::instance().refreshAll(); - DependencyManager::get()->refreshAll(); - DependencyManager::get()->refreshAll(); - - DependencyManager::get()->reset(); // Force redownload of .fst models - - DependencyManager::get()->reloadAllScripts(); - getOffscreenUI()->clearCache(); - - DependencyManager::get()->createKeyboard(); - - getMyAvatar()->resetFullAvatarURL(); -} - -void Application::rotationModeChanged() const { - if (!Menu::getInstance()->isOptionChecked(MenuOption::CenterPlayerInView)) { - getMyAvatar()->setHeadPitch(0); - } -} - -void Application::setKeyboardFocusHighlight(const glm::vec3& position, const glm::quat& rotation, const glm::vec3& dimensions) { - if (qApp->getLoginDialogPoppedUp()) { - return; - } - - auto entityScriptingInterface = DependencyManager::get(); - if (_keyboardFocusHighlightID == UNKNOWN_ENTITY_ID || !entityScriptingInterface->isAddedEntity(_keyboardFocusHighlightID)) { - EntityItemProperties properties; - properties.setType(EntityTypes::Box); - properties.setAlpha(1.0f); - properties.setColor({ 0xFF, 0xEF, 0x00 }); - properties.setPrimitiveMode(PrimitiveMode::LINES); - properties.getPulse().setMin(0.5); - properties.getPulse().setMax(1.0f); - properties.getPulse().setColorMode(PulseMode::IN_PHASE); - properties.setIgnorePickIntersection(true); - _keyboardFocusHighlightID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); - } - - // Position focus - EntityItemProperties properties; - properties.setPosition(position); - properties.setRotation(rotation); - properties.setDimensions(dimensions); - properties.setVisible(true); - entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); -} - -QUuid Application::getKeyboardFocusEntity() const { - return _keyboardFocusedEntity.get(); -} - -void Application::setKeyboardFocusEntity(const QUuid& id) { - if (_keyboardFocusedEntity.get() != id) { - if (qApp->getLoginDialogPoppedUp() && !_loginDialogID.isNull()) { - if (id == _loginDialogID) { - emit loginDialogFocusEnabled(); - } else if (!_keyboardFocusWaitingOnRenderable) { - // that's the only entity we want in focus; - return; - } - } - - _keyboardFocusedEntity.set(id); - - auto entityScriptingInterface = DependencyManager::get(); - if (id != UNKNOWN_ENTITY_ID) { - EntityPropertyFlags desiredProperties; - desiredProperties += PROP_VISIBLE; - desiredProperties += PROP_SHOW_KEYBOARD_FOCUS_HIGHLIGHT; - auto properties = entityScriptingInterface->getEntityProperties(id); - if (properties.getVisible()) { - auto entities = getEntities(); - auto entityId = _keyboardFocusedEntity.get(); - auto entityItemRenderable = entities->renderableForEntityId(entityId); - if (!entityItemRenderable) { - _keyboardFocusWaitingOnRenderable = true; - } else if (entityItemRenderable->wantsKeyboardFocus()) { - entities->setProxyWindow(entityId, _window->windowHandle()); - if (_keyboardMouseDevice->isActive()) { - _keyboardMouseDevice->pluginFocusOutEvent(); - } - _lastAcceptedKeyPress = usecTimestampNow(); - - if (properties.getShowKeyboardFocusHighlight()) { - if (auto entity = entities->getEntity(entityId)) { - setKeyboardFocusHighlight(entity->getWorldPosition(), entity->getWorldOrientation(), - entity->getScaledDimensions() * FOCUS_HIGHLIGHT_EXPANSION_FACTOR); - return; - } - } - } - } - } - - EntityItemProperties properties; - properties.setVisible(false); - entityScriptingInterface->editEntity(_keyboardFocusHighlightID, properties); - } -} - -void Application::updateDialogs(float deltaTime) const { - PerformanceTimer perfTimer("updateDialogs"); - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::updateDialogs()"); - auto dialogsManager = DependencyManager::get(); - - QPointer octreeStatsDialog = dialogsManager->getOctreeStatsDialog(); - if (octreeStatsDialog) { - octreeStatsDialog->update(); - } -} - -void Application::updateSecondaryCameraViewFrustum() { - // TODO: Fix this by modeling the way the secondary camera works on how the main camera works - // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary - // camera should be. - - // Code based on SecondaryCameraJob - auto renderConfig = _graphicsEngine.getRenderEngine()->getConfiguration(); - assert(renderConfig); - auto camera = dynamic_cast(renderConfig->getConfig("SecondaryCamera")); - - if (!camera || !camera->isEnabled()) { - return; - } - - ViewFrustum secondaryViewFrustum; - if (camera->portalProjection && !camera->attachedEntityId.isNull() && !camera->portalEntranceEntityId.isNull()) { - auto entityScriptingInterface = DependencyManager::get(); - EntityItemPointer portalEntrance = qApp->getEntities()->getTree()->findEntityByID(camera->portalEntranceEntityId); - EntityItemPointer portalExit = qApp->getEntities()->getTree()->findEntityByID(camera->attachedEntityId); - - glm::vec3 portalEntrancePropertiesPosition = portalEntrance->getWorldPosition(); - glm::quat portalEntrancePropertiesRotation = portalEntrance->getWorldOrientation(); - glm::mat4 worldFromPortalEntranceRotation = glm::mat4_cast(portalEntrancePropertiesRotation); - glm::mat4 worldFromPortalEntranceTranslation = glm::translate(portalEntrancePropertiesPosition); - glm::mat4 worldFromPortalEntrance = worldFromPortalEntranceTranslation * worldFromPortalEntranceRotation; - glm::mat4 portalEntranceFromWorld = glm::inverse(worldFromPortalEntrance); - - glm::vec3 portalExitPropertiesPosition = portalExit->getWorldPosition(); - glm::quat portalExitPropertiesRotation = portalExit->getWorldOrientation(); - glm::vec3 portalExitPropertiesDimensions = portalExit->getScaledDimensions(); - glm::vec3 halfPortalExitPropertiesDimensions = 0.5f * portalExitPropertiesDimensions; - - glm::mat4 worldFromPortalExitRotation = glm::mat4_cast(portalExitPropertiesRotation); - glm::mat4 worldFromPortalExitTranslation = glm::translate(portalExitPropertiesPosition); - glm::mat4 worldFromPortalExit = worldFromPortalExitTranslation * worldFromPortalExitRotation; - - glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); - glm::vec3 mainCameraPositionPortalEntrance = vec3(portalEntranceFromWorld * vec4(mainCameraPositionWorld, 1.0f)); - mainCameraPositionPortalEntrance = vec3(-mainCameraPositionPortalEntrance.x, mainCameraPositionPortalEntrance.y, - -mainCameraPositionPortalEntrance.z); - glm::vec3 portalExitCameraPositionWorld = vec3(worldFromPortalExit * vec4(mainCameraPositionPortalEntrance, 1.0f)); - - secondaryViewFrustum.setPosition(portalExitCameraPositionWorld); - secondaryViewFrustum.setOrientation(portalExitPropertiesRotation); - - float nearClip = mainCameraPositionPortalEntrance.z + portalExitPropertiesDimensions.z * 2.0f; - // `mainCameraPositionPortalEntrance` should technically be `mainCameraPositionPortalExit`, - // but the values are the same. - glm::vec3 upperRight = halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; - glm::vec3 bottomLeft = -halfPortalExitPropertiesDimensions - mainCameraPositionPortalEntrance; - glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); - secondaryViewFrustum.setProjection(frustum); - } else if (camera->mirrorProjection && !camera->attachedEntityId.isNull()) { - auto entityScriptingInterface = DependencyManager::get(); - auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); - glm::vec3 mirrorPropertiesPosition = entityProperties.getPosition(); - glm::quat mirrorPropertiesRotation = entityProperties.getRotation(); - glm::vec3 mirrorPropertiesDimensions = entityProperties.getDimensions(); - glm::vec3 halfMirrorPropertiesDimensions = 0.5f * mirrorPropertiesDimensions; - - // setup mirror from world as inverse of world from mirror transformation using inverted x and z for mirrored image - // TODO: we are assuming here that UP is world y-axis - glm::mat4 worldFromMirrorRotation = glm::mat4_cast(mirrorPropertiesRotation) * glm::scale(vec3(-1.0f, 1.0f, -1.0f)); - glm::mat4 worldFromMirrorTranslation = glm::translate(mirrorPropertiesPosition); - glm::mat4 worldFromMirror = worldFromMirrorTranslation * worldFromMirrorRotation; - glm::mat4 mirrorFromWorld = glm::inverse(worldFromMirror); - - // get mirror camera position by reflecting main camera position's z coordinate in mirror space - glm::vec3 mainCameraPositionWorld = getCamera().getPosition(); - glm::vec3 mainCameraPositionMirror = vec3(mirrorFromWorld * vec4(mainCameraPositionWorld, 1.0f)); - glm::vec3 mirrorCameraPositionMirror = vec3(mainCameraPositionMirror.x, mainCameraPositionMirror.y, - -mainCameraPositionMirror.z); - glm::vec3 mirrorCameraPositionWorld = vec3(worldFromMirror * vec4(mirrorCameraPositionMirror, 1.0f)); - - // set frustum position to be mirrored camera and set orientation to mirror's adjusted rotation - glm::quat mirrorCameraOrientation = glm::quat_cast(worldFromMirrorRotation); - secondaryViewFrustum.setPosition(mirrorCameraPositionWorld); - secondaryViewFrustum.setOrientation(mirrorCameraOrientation); - - // build frustum using mirror space translation of mirrored camera - float nearClip = mirrorCameraPositionMirror.z + mirrorPropertiesDimensions.z * 2.0f; - glm::vec3 upperRight = halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; - glm::vec3 bottomLeft = -halfMirrorPropertiesDimensions - mirrorCameraPositionMirror; - glm::mat4 frustum = glm::frustum(bottomLeft.x, upperRight.x, bottomLeft.y, upperRight.y, nearClip, camera->farClipPlaneDistance); - secondaryViewFrustum.setProjection(frustum); - } else { - if (!camera->attachedEntityId.isNull()) { - auto entityScriptingInterface = DependencyManager::get(); - auto entityProperties = entityScriptingInterface->getEntityProperties(camera->attachedEntityId); - secondaryViewFrustum.setPosition(entityProperties.getPosition()); - secondaryViewFrustum.setOrientation(entityProperties.getRotation()); - } else { - secondaryViewFrustum.setPosition(camera->position); - secondaryViewFrustum.setOrientation(camera->orientation); - } - - float aspectRatio = (float)camera->textureWidth / (float)camera->textureHeight; - secondaryViewFrustum.setProjection(camera->vFoV, - aspectRatio, - camera->nearClipPlaneDistance, - camera->farClipPlaneDistance); - } - // Without calculating the bound planes, the secondary camera will use the same culling frustum as the main camera, - // which is not what we want here. - secondaryViewFrustum.calculate(); - - _conicalViews.push_back(secondaryViewFrustum); -} - -static bool domainLoadingInProgress = false; - -void Application::update(float deltaTime) { - PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_graphicsEngine._renderFrameCount + 1); - - if (_aboutToQuit) { - return; - } - - - if (!_physicsEnabled) { - if (!domainLoadingInProgress) { - PROFILE_ASYNC_BEGIN(app, "Scene Loading", ""); - domainLoadingInProgress = true; - } - - // we haven't yet enabled physics. we wait until we think we have all the collision information - // for nearby entities before starting bullet up. - quint64 now = usecTimestampNow(); - if (isServerlessMode() || _octreeProcessor.isLoadSequenceComplete()) { - bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); - - if (gpuTextureMemSizeStable() || !enableInterstitial) { - // we've received a new full-scene octree stats packet, or it's been long enough to try again anyway - _lastPhysicsCheckTime = now; - _fullSceneCounterAtLastPhysicsCheck = _fullSceneReceivedCounter; - _lastQueriedViews.clear(); // Force new view. - - // process octree stats packets are sent in between full sends of a scene (this isn't currently true). - // We keep physics disabled until we've received a full scene and everything near the avatar in that - // scene is ready to compute its collision shape. - if (getMyAvatar()->isReadyForPhysics()) { - _physicsEnabled = true; - setIsInterstitialMode(false); - getMyAvatar()->updateMotionBehaviorFromMenu(); - } - } - } - } else if (domainLoadingInProgress) { - domainLoadingInProgress = false; - PROFILE_ASYNC_END(app, "Scene Loading", ""); - } - - auto myAvatar = getMyAvatar(); - { - PerformanceTimer perfTimer("devices"); - - FaceTracker* tracker = getSelectedFaceTracker(); - if (tracker && Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking) != tracker->isMuted()) { - tracker->toggleMute(); - } - - tracker = getActiveFaceTracker(); - if (tracker && !tracker->isMuted()) { - tracker->update(deltaTime); - - // Auto-mute microphone after losing face tracking? - if (tracker->isTracking()) { - _lastFaceTrackerUpdate = usecTimestampNow(); - } else { - const quint64 MUTE_MICROPHONE_AFTER_USECS = 5000000; //5 secs - Menu* menu = Menu::getInstance(); - auto audioClient = DependencyManager::get(); - if (menu->isOptionChecked(MenuOption::AutoMuteAudio) && !audioClient->isMuted()) { - if (_lastFaceTrackerUpdate > 0 - && ((usecTimestampNow() - _lastFaceTrackerUpdate) > MUTE_MICROPHONE_AFTER_USECS)) { - audioClient->setMuted(true); - _lastFaceTrackerUpdate = 0; - } - } else { - _lastFaceTrackerUpdate = 0; - } - } - } else { - _lastFaceTrackerUpdate = 0; - } - - auto userInputMapper = DependencyManager::get(); - - controller::HmdAvatarAlignmentType hmdAvatarAlignmentType; - if (myAvatar->getHmdAvatarAlignmentType() == "eyes") { - hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Eyes; - } else { - hmdAvatarAlignmentType = controller::HmdAvatarAlignmentType::Head; - } - - controller::InputCalibrationData calibrationData = { - myAvatar->getSensorToWorldMatrix(), - createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()), - myAvatar->getHMDSensorMatrix(), - myAvatar->getCenterEyeCalibrationMat(), - myAvatar->getHeadCalibrationMat(), - myAvatar->getSpine2CalibrationMat(), - myAvatar->getHipsCalibrationMat(), - myAvatar->getLeftFootCalibrationMat(), - myAvatar->getRightFootCalibrationMat(), - myAvatar->getRightArmCalibrationMat(), - myAvatar->getLeftArmCalibrationMat(), - myAvatar->getRightHandCalibrationMat(), - myAvatar->getLeftHandCalibrationMat(), - hmdAvatarAlignmentType - }; - - InputPluginPointer keyboardMousePlugin; - for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { - if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { - keyboardMousePlugin = inputPlugin; - } else if (inputPlugin->isActive()) { - inputPlugin->pluginUpdate(deltaTime, calibrationData); - } - } - - userInputMapper->setInputCalibrationData(calibrationData); - userInputMapper->update(deltaTime); - - if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { - keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); - } - // Transfer the user inputs to the driveKeys - // FIXME can we drop drive keys and just have the avatar read the action states directly? - myAvatar->clearDriveKeys(); - if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT && !isInterstitialMode()) { - if (!_controllerScriptingInterface->areActionsCaptured() && _myCamera.getMode() != CAMERA_MODE_MIRROR) { - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); - if (deltaTime > FLT_EPSILON) { - myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); - myAvatar->setDriveKey(MyAvatar::DELTA_PITCH, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_PITCH)); - myAvatar->setDriveKey(MyAvatar::DELTA_YAW, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_YAW)); - myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); - } - } - myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); - } - - myAvatar->setSprintMode((bool)userInputMapper->getActionState(controller::Action::SPRINT)); - static const std::vector avatarControllerActions = { - controller::Action::LEFT_HAND, - controller::Action::RIGHT_HAND, - controller::Action::LEFT_FOOT, - controller::Action::RIGHT_FOOT, - controller::Action::HIPS, - controller::Action::SPINE2, - controller::Action::HEAD, - controller::Action::LEFT_HAND_THUMB1, - controller::Action::LEFT_HAND_THUMB2, - controller::Action::LEFT_HAND_THUMB3, - controller::Action::LEFT_HAND_THUMB4, - controller::Action::LEFT_HAND_INDEX1, - controller::Action::LEFT_HAND_INDEX2, - controller::Action::LEFT_HAND_INDEX3, - controller::Action::LEFT_HAND_INDEX4, - controller::Action::LEFT_HAND_MIDDLE1, - controller::Action::LEFT_HAND_MIDDLE2, - controller::Action::LEFT_HAND_MIDDLE3, - controller::Action::LEFT_HAND_MIDDLE4, - controller::Action::LEFT_HAND_RING1, - controller::Action::LEFT_HAND_RING2, - controller::Action::LEFT_HAND_RING3, - controller::Action::LEFT_HAND_RING4, - controller::Action::LEFT_HAND_PINKY1, - controller::Action::LEFT_HAND_PINKY2, - controller::Action::LEFT_HAND_PINKY3, - controller::Action::LEFT_HAND_PINKY4, - controller::Action::RIGHT_HAND_THUMB1, - controller::Action::RIGHT_HAND_THUMB2, - controller::Action::RIGHT_HAND_THUMB3, - controller::Action::RIGHT_HAND_THUMB4, - controller::Action::RIGHT_HAND_INDEX1, - controller::Action::RIGHT_HAND_INDEX2, - controller::Action::RIGHT_HAND_INDEX3, - controller::Action::RIGHT_HAND_INDEX4, - controller::Action::RIGHT_HAND_MIDDLE1, - controller::Action::RIGHT_HAND_MIDDLE2, - controller::Action::RIGHT_HAND_MIDDLE3, - controller::Action::RIGHT_HAND_MIDDLE4, - controller::Action::RIGHT_HAND_RING1, - controller::Action::RIGHT_HAND_RING2, - controller::Action::RIGHT_HAND_RING3, - controller::Action::RIGHT_HAND_RING4, - controller::Action::RIGHT_HAND_PINKY1, - controller::Action::RIGHT_HAND_PINKY2, - controller::Action::RIGHT_HAND_PINKY3, - controller::Action::RIGHT_HAND_PINKY4, - controller::Action::LEFT_ARM, - controller::Action::RIGHT_ARM, - controller::Action::LEFT_SHOULDER, - controller::Action::RIGHT_SHOULDER, - controller::Action::LEFT_FORE_ARM, - controller::Action::RIGHT_FORE_ARM, - controller::Action::LEFT_LEG, - controller::Action::RIGHT_LEG, - controller::Action::LEFT_UP_LEG, - controller::Action::RIGHT_UP_LEG, - controller::Action::LEFT_TOE_BASE, - controller::Action::RIGHT_TOE_BASE - }; - - // copy controller poses from userInputMapper to myAvatar. - glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getWorldOrientation(), myAvatar->getWorldPosition()); - glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix()); - glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; - for (auto& action : avatarControllerActions) { - controller::Pose pose = userInputMapper->getPoseState(action); - myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); - } - - static const std::vector trackedObjectStringLiterals = { - QStringLiteral("_TrackedObject00"), QStringLiteral("_TrackedObject01"), QStringLiteral("_TrackedObject02"), QStringLiteral("_TrackedObject03"), - QStringLiteral("_TrackedObject04"), QStringLiteral("_TrackedObject05"), QStringLiteral("_TrackedObject06"), QStringLiteral("_TrackedObject07"), - QStringLiteral("_TrackedObject08"), QStringLiteral("_TrackedObject09"), QStringLiteral("_TrackedObject10"), QStringLiteral("_TrackedObject11"), - QStringLiteral("_TrackedObject12"), QStringLiteral("_TrackedObject13"), QStringLiteral("_TrackedObject14"), QStringLiteral("_TrackedObject15") - }; - - // Controlled by the Developer > Avatar > Show Tracked Objects menu. - if (_showTrackedObjects) { - static const std::vector trackedObjectActions = { - controller::Action::TRACKED_OBJECT_00, controller::Action::TRACKED_OBJECT_01, controller::Action::TRACKED_OBJECT_02, controller::Action::TRACKED_OBJECT_03, - controller::Action::TRACKED_OBJECT_04, controller::Action::TRACKED_OBJECT_05, controller::Action::TRACKED_OBJECT_06, controller::Action::TRACKED_OBJECT_07, - controller::Action::TRACKED_OBJECT_08, controller::Action::TRACKED_OBJECT_09, controller::Action::TRACKED_OBJECT_10, controller::Action::TRACKED_OBJECT_11, - controller::Action::TRACKED_OBJECT_12, controller::Action::TRACKED_OBJECT_13, controller::Action::TRACKED_OBJECT_14, controller::Action::TRACKED_OBJECT_15 - }; - - int i = 0; - glm::vec4 BLUE(0.0f, 0.0f, 1.0f, 1.0f); - for (auto& action : trackedObjectActions) { - controller::Pose pose = userInputMapper->getPoseState(action); - if (pose.valid) { - glm::vec3 pos = transformPoint(myAvatarMatrix, pose.translation); - glm::quat rot = glmExtractRotation(myAvatarMatrix) * pose.rotation; - DebugDraw::getInstance().addMarker(trackedObjectStringLiterals[i], rot, pos, BLUE); - } else { - DebugDraw::getInstance().removeMarker(trackedObjectStringLiterals[i]); - } - i++; - } - } else if (_prevShowTrackedObjects) { - for (auto& key : trackedObjectStringLiterals) { - DebugDraw::getInstance().removeMarker(key); - } - } - _prevShowTrackedObjects = _showTrackedObjects; - } - - updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... - updateDialogs(deltaTime); // update various stats dialogs if present - - auto grabManager = DependencyManager::get(); - grabManager->simulateGrabs(); - - // TODO: break these out into distinct perfTimers when they prove interesting - { - PROFILE_RANGE(app, "PickManager"); - PerformanceTimer perfTimer("pickManager"); - DependencyManager::get()->update(); - } - - { - PROFILE_RANGE(app, "PointerManager"); - PerformanceTimer perfTimer("pointerManager"); - DependencyManager::get()->update(); - } - - QSharedPointer avatarManager = DependencyManager::get(); - - { - PROFILE_RANGE(simulation_physics, "Simulation"); - PerformanceTimer perfTimer("simulation"); - - if (_physicsEnabled) { - auto t0 = std::chrono::high_resolution_clock::now(); - auto t1 = t0; - { - PROFILE_RANGE(simulation_physics, "PrePhysics"); - PerformanceTimer perfTimer("prePhysics)"); - { - PROFILE_RANGE(simulation_physics, "RemoveEntities"); - const VectorOfMotionStates& motionStates = _entitySimulation->getObjectsToRemoveFromPhysics(); - { - PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); - _physicsEngine->removeObjects(motionStates); - } - _entitySimulation->deleteObjectsRemovedFromPhysics(); - } - - { - PROFILE_RANGE(simulation_physics, "AddEntities"); - VectorOfMotionStates motionStates; - getEntities()->getTree()->withReadLock([&] { - _entitySimulation->getObjectsToAddToPhysics(motionStates); - PROFILE_RANGE_EX(simulation_physics, "NumObjs", 0xffff0000, (uint64_t)motionStates.size()); - _physicsEngine->addObjects(motionStates); - }); - } - { - VectorOfMotionStates motionStates; - PROFILE_RANGE(simulation_physics, "ChangeEntities"); - getEntities()->getTree()->withReadLock([&] { - _entitySimulation->getObjectsToChange(motionStates); - VectorOfMotionStates stillNeedChange = _physicsEngine->changeObjects(motionStates); - _entitySimulation->setObjectsToChange(stillNeedChange); - }); - } - - _entitySimulation->applyDynamicChanges(); - - t1 = std::chrono::high_resolution_clock::now(); - - { - PROFILE_RANGE(simulation_physics, "Avatars"); - PhysicsEngine::Transaction transaction; - avatarManager->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - avatarManager->handleProcessedPhysicsTransaction(transaction); - myAvatar->getCharacterController()->buildPhysicsTransaction(transaction); - _physicsEngine->processTransaction(transaction); - myAvatar->getCharacterController()->handleProcessedPhysicsTransaction(transaction); - myAvatar->prepareForPhysicsSimulation(); - _physicsEngine->enableGlobalContactAddedCallback(myAvatar->isFlying()); - } - - { - PROFILE_RANGE(simulation_physics, "PrepareActions"); - _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { - dynamic->prepareForPhysicsSimulation(); - }); - } - } - auto t2 = std::chrono::high_resolution_clock::now(); - { - PROFILE_RANGE(simulation_physics, "StepPhysics"); - PerformanceTimer perfTimer("stepPhysics"); - getEntities()->getTree()->withWriteLock([&] { - _physicsEngine->stepSimulation(); - }); - } - auto t3 = std::chrono::high_resolution_clock::now(); - { - if (_physicsEngine->hasOutgoingChanges()) { - { - PROFILE_RANGE(simulation_physics, "PostPhysics"); - PerformanceTimer perfTimer("postPhysics"); - // grab the collision events BEFORE handleChangedMotionStates() because at this point - // we have a better idea of which objects we own or should own. - auto& collisionEvents = _physicsEngine->getCollisionEvents(); - - getEntities()->getTree()->withWriteLock([&] { - PROFILE_RANGE(simulation_physics, "HandleChanges"); - PerformanceTimer perfTimer("handleChanges"); - - const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); - _entitySimulation->handleChangedMotionStates(outgoingChanges); - avatarManager->handleChangedMotionStates(outgoingChanges); - - const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); - _entitySimulation->handleDeactivatedMotionStates(deactivations); - }); - - // handleCollisionEvents() AFTER handleChangedMotionStates() - { - PROFILE_RANGE(simulation_physics, "CollisionEvents"); - avatarManager->handleCollisionEvents(collisionEvents); - // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk - // deadlock.) - _entitySimulation->handleCollisionEvents(collisionEvents); - } - - { - PROFILE_RANGE(simulation_physics, "MyAvatar"); - myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); - } - - if (PerformanceTimer::isActive() && - Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && - Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsTiming)) { - _physicsEngine->harvestPerformanceStats(); - } - // NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework - _physicsEngine->dumpStatsIfNecessary(); - } - auto t4 = std::chrono::high_resolution_clock::now(); - - // NOTE: the getEntities()->update() call below will wait for lock - // and will provide non-physical entity motion - getEntities()->update(true); // update the models... - - auto t5 = std::chrono::high_resolution_clock::now(); - - workload::Timings timings(6); - timings[0] = t1 - t0; // prePhysics entities - timings[1] = t2 - t1; // prePhysics avatars - timings[2] = t3 - t2; // stepPhysics - timings[3] = t4 - t3; // postPhysics - timings[4] = t5 - t4; // non-physical kinematics - timings[5] = workload::Timing_ns((int32_t)(NSECS_PER_SECOND * deltaTime)); // game loop duration - _gameWorkload.updateSimulationTimings(timings); - } - } - } else { - // update the rendering without any simulation - getEntities()->update(false); - } - // remove recently dead avatarEntities - SetOfEntities deadAvatarEntities; - _entitySimulation->takeDeadAvatarEntities(deadAvatarEntities); - avatarManager->removeDeadAvatarEntities(deadAvatarEntities); - } - - // AvatarManager update - { - { - PROFILE_RANGE(simulation, "OtherAvatars"); - PerformanceTimer perfTimer("otherAvatars"); - avatarManager->updateOtherAvatars(deltaTime); - } - - { - PROFILE_RANGE(simulation, "MyAvatar"); - PerformanceTimer perfTimer("MyAvatar"); - qApp->updateMyAvatarLookAtPosition(); - avatarManager->updateMyAvatar(deltaTime); - } - } - - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::update()"); - - updateLOD(deltaTime); - - if (!_loginDialogID.isNull()) { - _loginStateManager.update(getMyAvatar()->getDominantHand(), _loginDialogID); - updateLoginDialogPosition(); - } - - { - PROFILE_RANGE_EX(app, "Overlays", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("overlays"); - _overlays.update(deltaTime); - } - - // Update _viewFrustum with latest camera and view frustum data... - // NOTE: we get this from the view frustum, to make it simpler, since the - // loadViewFrumstum() method will get the correct details from the camera - // We could optimize this to not actually load the viewFrustum, since we don't - // actually need to calculate the view frustum planes to send these details - // to the server. - { - QMutexLocker viewLocker(&_viewMutex); - _myCamera.loadViewFrustum(_viewFrustum); - - _conicalViews.clear(); - _conicalViews.push_back(_viewFrustum); - // TODO: Fix this by modeling the way the secondary camera works on how the main camera works - // ie. Use a camera object stored in the game logic and informs the Engine on where the secondary - // camera should be. - updateSecondaryCameraViewFrustum(); - } - - quint64 now = usecTimestampNow(); - - // Update my voxel servers with my current voxel query... - { - PROFILE_RANGE_EX(app, "QueryOctree", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("queryOctree"); - QMutexLocker viewLocker(&_viewMutex); - - bool viewIsDifferentEnough = false; - if (_conicalViews.size() == _lastQueriedViews.size()) { - for (size_t i = 0; i < _conicalViews.size(); ++i) { - if (!_conicalViews[i].isVerySimilar(_lastQueriedViews[i])) { - viewIsDifferentEnough = true; - break; - } - } - } else { - viewIsDifferentEnough = true; - } - - - // if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it - static const std::chrono::seconds MIN_PERIOD_BETWEEN_QUERIES { 3 }; - auto now = SteadyClock::now(); - if (now > _queryExpiry || viewIsDifferentEnough) { - if (DependencyManager::get()->shouldRenderEntities()) { - queryOctree(NodeType::EntityServer, PacketType::EntityQuery); - } - queryAvatars(); - - _lastQueriedViews = _conicalViews; - _queryExpiry = now + MIN_PERIOD_BETWEEN_QUERIES; - } - } - - // sent nack packets containing missing sequence numbers of received packets from nodes - { - quint64 sinceLastNack = now - _lastNackTime; - const quint64 TOO_LONG_SINCE_LAST_NACK = 1 * USECS_PER_SECOND; - if (sinceLastNack > TOO_LONG_SINCE_LAST_NACK) { - _lastNackTime = now; - sendNackPackets(); - } - } - - // send packet containing downstream audio stats to the AudioMixer - { - quint64 sinceLastNack = now - _lastSendDownstreamAudioStats; - if (sinceLastNack > TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS && !isInterstitialMode()) { - _lastSendDownstreamAudioStats = now; - - QMetaObject::invokeMethod(DependencyManager::get().data(), "sendDownstreamAudioStatsPacket", Qt::QueuedConnection); - } - } - - { - PerformanceTimer perfTimer("avatarManager/postUpdate"); - avatarManager->postUpdate(deltaTime, getMain3DScene()); - } - - { - PROFILE_RANGE_EX(app, "PostUpdateLambdas", 0xffff0000, (uint64_t)0); - PerformanceTimer perfTimer("postUpdateLambdas"); - std::unique_lock guard(_postUpdateLambdasLock); - for (auto& iter : _postUpdateLambdas) { - iter.second(); - } - _postUpdateLambdas.clear(); - } - - - updateRenderArgs(deltaTime); - - { - PerformanceTimer perfTimer("AnimDebugDraw"); - AnimDebugDraw::getInstance().update(); - } - - - { // Game loop is done, mark the end of the frame for the scene transactions and the render loop to take over - PerformanceTimer perfTimer("enqueueFrame"); - getMain3DScene()->enqueueFrame(); - } - - // If the display plugin is inactive then the frames won't be processed so process them here. - if (!getActiveDisplayPlugin()->isActive()) { - getMain3DScene()->processTransactionQueue(); - } -} - -void Application::updateRenderArgs(float deltaTime) { - _graphicsEngine.editRenderArgs([this, deltaTime](AppRenderArgs& appRenderArgs) { - PerformanceTimer perfTimer("editRenderArgs"); - appRenderArgs._headPose = getHMDSensorPose(); - - auto myAvatar = getMyAvatar(); - - // update the avatar with a fresh HMD pose - { - PROFILE_RANGE(render, "/updateAvatar"); - myAvatar->updateFromHMDSensorMatrix(appRenderArgs._headPose); - } - - auto lodManager = DependencyManager::get(); - - float sensorToWorldScale = getMyAvatar()->getSensorToWorldScale(); - appRenderArgs._sensorToWorldScale = sensorToWorldScale; - appRenderArgs._sensorToWorld = getMyAvatar()->getSensorToWorldMatrix(); - { - PROFILE_RANGE(render, "/buildFrustrumAndArgs"); - { - QMutexLocker viewLocker(&_viewMutex); - // adjust near clip plane to account for sensor scaling. - auto adjustedProjection = glm::perspective(glm::radians(_fieldOfView.get()), - getActiveDisplayPlugin()->getRecommendedAspectRatio(), - DEFAULT_NEAR_CLIP * sensorToWorldScale, - DEFAULT_FAR_CLIP); - _viewFrustum.setProjection(adjustedProjection); - _viewFrustum.calculate(); - } - appRenderArgs._renderArgs = RenderArgs(_graphicsEngine.getGPUContext(), lodManager->getOctreeSizeScale(), - lodManager->getBoundaryLevelAdjust(), lodManager->getLODAngleHalfTan(), RenderArgs::DEFAULT_RENDER_MODE, - RenderArgs::MONO, RenderArgs::RENDER_DEBUG_NONE); - appRenderArgs._renderArgs._scene = getMain3DScene(); - - { - QMutexLocker viewLocker(&_viewMutex); - appRenderArgs._renderArgs.setViewFrustum(_viewFrustum); - } - } - { - PROFILE_RANGE(render, "/resizeGL"); - bool showWarnings = false; - bool suppressShortTimings = false; - auto menu = Menu::getInstance(); - if (menu) { - suppressShortTimings = menu->isOptionChecked(MenuOption::SuppressShortTimings); - showWarnings = menu->isOptionChecked(MenuOption::PipelineWarnings); - } - PerformanceWarning::setSuppressShortTimings(suppressShortTimings); - PerformanceWarning warn(showWarnings, "Application::paintGL()"); - resizeGL(); - } - - this->updateCamera(appRenderArgs._renderArgs, deltaTime); - appRenderArgs._eyeToWorld = _myCamera.getTransform(); - appRenderArgs._isStereo = false; - - { - auto hmdInterface = DependencyManager::get(); - float ipdScale = hmdInterface->getIPDScale(); - - // scale IPD by sensorToWorldScale, to make the world seem larger or smaller accordingly. - ipdScale *= sensorToWorldScale; - - auto baseProjection = appRenderArgs._renderArgs.getViewFrustum().getProjection(); - - if (getActiveDisplayPlugin()->isStereo()) { - // Stereo modes will typically have a larger projection matrix overall, - // so we ask for the 'mono' projection matrix, which for stereo and HMD - // plugins will imply the combined projection for both eyes. - // - // This is properly implemented for the Oculus plugins, but for OpenVR - // and Stereo displays I'm not sure how to get / calculate it, so we're - // just relying on the left FOV in each case and hoping that the - // overall culling margin of error doesn't cause popping in the - // right eye. There are FIXMEs in the relevant plugins - _myCamera.setProjection(getActiveDisplayPlugin()->getCullingProjection(baseProjection)); - appRenderArgs._isStereo = true; - - auto& eyeOffsets = appRenderArgs._eyeOffsets; - auto& eyeProjections = appRenderArgs._eyeProjections; - - // FIXME we probably don't need to set the projection matrix every frame, - // only when the display plugin changes (or in non-HMD modes when the user - // changes the FOV manually, which right now I don't think they can. - for_each_eye([&](Eye eye) { - // For providing the stereo eye views, the HMD head pose has already been - // applied to the avatar, so we need to get the difference between the head - // pose applied to the avatar and the per eye pose, and use THAT as - // the per-eye stereo matrix adjustment. - mat4 eyeToHead = getActiveDisplayPlugin()->getEyeToHeadTransform(eye); - // Grab the translation - vec3 eyeOffset = glm::vec3(eyeToHead[3]); - // Apply IPD scaling - mat4 eyeOffsetTransform = glm::translate(mat4(), eyeOffset * -1.0f * ipdScale); - eyeOffsets[eye] = eyeOffsetTransform; - eyeProjections[eye] = getActiveDisplayPlugin()->getEyeProjection(eye, baseProjection); - }); - - // Configure the type of display / stereo - appRenderArgs._renderArgs._displayMode = (isHMDMode() ? RenderArgs::STEREO_HMD : RenderArgs::STEREO_MONITOR); - } - } - - { - QMutexLocker viewLocker(&_viewMutex); - _myCamera.loadViewFrustum(_displayViewFrustum); - appRenderArgs._view = glm::inverse(_displayViewFrustum.getView()); - } - - { - QMutexLocker viewLocker(&_viewMutex); - appRenderArgs._renderArgs.setViewFrustum(_displayViewFrustum); - } - - - // HACK - // load the view frustum - // FIXME: This preDisplayRender call is temporary until we create a separate render::scene for the mirror rendering. - // Then we can move this logic into the Avatar::simulate call. - myAvatar->preDisplaySide(&appRenderArgs._renderArgs); - }); -} - -void Application::queryAvatars() { - if (!isInterstitialMode()) { - auto avatarPacket = NLPacket::create(PacketType::AvatarQuery); - auto destinationBuffer = reinterpret_cast(avatarPacket->getPayload()); - unsigned char* bufferStart = destinationBuffer; - - uint8_t numFrustums = (uint8_t)_conicalViews.size(); - memcpy(destinationBuffer, &numFrustums, sizeof(numFrustums)); - destinationBuffer += sizeof(numFrustums); - - for (const auto& view : _conicalViews) { - destinationBuffer += view.serialize(destinationBuffer); - } - - avatarPacket->setPayloadSize(destinationBuffer - bufferStart); - - DependencyManager::get()->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); - } -} - - -int Application::sendNackPackets() { - - // iterates through all nodes in NodeList - auto nodeList = DependencyManager::get(); - - int packetsSent = 0; - - nodeList->eachNode([&](const SharedNodePointer& node){ - - if (node->getActiveSocket() && node->getType() == NodeType::EntityServer) { - - auto nackPacketList = NLPacketList::create(PacketType::OctreeDataNack); - - QUuid nodeUUID = node->getUUID(); - - // if there are octree packets from this node that are waiting to be processed, - // don't send a NACK since the missing packets may be among those waiting packets. - if (_octreeProcessor.hasPacketsToProcessFrom(nodeUUID)) { - return; - } - - QSet missingSequenceNumbers; - _octreeServerSceneStats.withReadLock([&] { - // retrieve octree scene stats of this node - if (_octreeServerSceneStats.find(nodeUUID) == _octreeServerSceneStats.end()) { - return; - } - // get sequence number stats of node, prune its missing set, and make a copy of the missing set - SequenceNumberStats& sequenceNumberStats = _octreeServerSceneStats[nodeUUID].getIncomingOctreeSequenceNumberStats(); - sequenceNumberStats.pruneMissingSet(); - missingSequenceNumbers = sequenceNumberStats.getMissingSet(); - }); - - _isMissingSequenceNumbers = (missingSequenceNumbers.size() != 0); - - // construct nack packet(s) for this node - foreach(const OCTREE_PACKET_SEQUENCE& missingNumber, missingSequenceNumbers) { - nackPacketList->writePrimitive(missingNumber); - } - - if (nackPacketList->getNumPackets()) { - packetsSent += (int)nackPacketList->getNumPackets(); - - // send the packet list - nodeList->sendPacketList(std::move(nackPacketList), *node); - } - } - }); - - return packetsSent; -} - -void Application::queryOctree(NodeType_t serverType, PacketType packetType) { - - if (!_settingsLoaded) { - return; // bail early if settings are not loaded - } - - const bool isModifiedQuery = !_physicsEnabled; - if (isModifiedQuery) { - // Create modified view that is a simple sphere. - bool interstitialModeEnabled = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); - - ConicalViewFrustum sphericalView; - sphericalView.setSimpleRadius(INITIAL_QUERY_RADIUS); - - if (interstitialModeEnabled) { - ConicalViewFrustum farView; - farView.set(_viewFrustum); - _octreeQuery.setConicalViews({ sphericalView, farView }); - } else { - _octreeQuery.setConicalViews({ sphericalView }); - } - - _octreeQuery.setOctreeSizeScale(DEFAULT_OCTREE_SIZE_SCALE); - static constexpr float MIN_LOD_ADJUST = -20.0f; - _octreeQuery.setBoundaryLevelAdjust(MIN_LOD_ADJUST); - } else { - _octreeQuery.setConicalViews(_conicalViews); - auto lodManager = DependencyManager::get(); - _octreeQuery.setOctreeSizeScale(lodManager->getOctreeSizeScale()); - _octreeQuery.setBoundaryLevelAdjust(lodManager->getBoundaryLevelAdjust()); - } - _octreeQuery.setReportInitialCompletion(isModifiedQuery); - - - auto nodeList = DependencyManager::get(); - - auto node = nodeList->soloNodeOfType(serverType); - if (node && node->getActiveSocket()) { - _octreeQuery.setMaxQueryPacketsPerSecond(getMaxOctreePacketsPerSecond()); - - auto queryPacket = NLPacket::create(packetType); - - // encode the query data - auto packetData = reinterpret_cast(queryPacket->getPayload()); - int packetSize = _octreeQuery.getBroadcastData(packetData); - queryPacket->setPayloadSize(packetSize); - - // make sure we still have an active socket - nodeList->sendUnreliablePacket(*queryPacket, *node); - } -} - - -bool Application::isHMDMode() const { - return getActiveDisplayPlugin()->isHmd(); -} - -float Application::getNumCollisionObjects() const { - return _physicsEngine ? _physicsEngine->getNumCollisionObjects() : 0; -} - -float Application::getTargetRenderFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); } - -QRect Application::getDesirableApplicationGeometry() const { - QRect applicationGeometry = getWindow()->geometry(); - - // If our parent window is on the HMD, then don't use its geometry, instead use - // the "main screen" geometry. - HMDToolsDialog* hmdTools = DependencyManager::get()->getHMDToolsDialog(); - if (hmdTools && hmdTools->hasHMDScreen()) { - QScreen* hmdScreen = hmdTools->getHMDScreen(); - QWindow* appWindow = getWindow()->windowHandle(); - QScreen* appScreen = appWindow->screen(); - - // if our app's screen is the hmd screen, we don't want to place the - // running scripts widget on it. So we need to pick a better screen. - // we will use the screen for the HMDTools since it's a guaranteed - // better screen. - if (appScreen == hmdScreen) { - QScreen* betterScreen = hmdTools->windowHandle()->screen(); - applicationGeometry = betterScreen->geometry(); - } - } - return applicationGeometry; -} - -PickRay Application::computePickRay(float x, float y) const { - vec2 pickPoint { x, y }; - PickRay result; - if (isHMDMode()) { - getApplicationCompositor().computeHmdPickRay(pickPoint, result.origin, result.direction); - } else { - pickPoint /= getCanvasSize(); - if (_myCamera.getMode() == CameraMode::CAMERA_MODE_MIRROR) { - pickPoint.x = 1.0f - pickPoint.x; - } - QMutexLocker viewLocker(&_viewMutex); - _viewFrustum.computePickRay(pickPoint.x, pickPoint.y, result.origin, result.direction); - } - return result; -} - -std::shared_ptr Application::getMyAvatar() const { - return DependencyManager::get()->getMyAvatar(); -} - -glm::vec3 Application::getAvatarPosition() const { - return getMyAvatar()->getWorldPosition(); -} - -void Application::copyViewFrustum(ViewFrustum& viewOut) const { - QMutexLocker viewLocker(&_viewMutex); - viewOut = _viewFrustum; -} - -void Application::copyDisplayViewFrustum(ViewFrustum& viewOut) const { - QMutexLocker viewLocker(&_viewMutex); - viewOut = _displayViewFrustum; -} - -// resentSensors() is a bit of vestigial feature. It used to be used for Oculus DK2 to recenter the view around -// the current head orientation. With the introduction of "room scale" tracking we no longer need that particular -// feature. However, we still use this to reset face trackers, eye trackers, audio and to optionally re-load the avatar -// rig and animations from scratch. -void Application::resetSensors(bool andReload) { - DependencyManager::get()->reset(); - DependencyManager::get()->reset(); - _overlayConductor.centerUI(); - getActiveDisplayPlugin()->resetSensors(); - getMyAvatar()->reset(true, andReload); - QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); -} - -void Application::hmdVisibleChanged(bool visible) { - // TODO - // calling start and stop will change audio input and ouput to default audio devices. - // we need to add a pause/unpause functionality to AudioClient for this to work properly -#if 0 - if (visible) { - QMetaObject::invokeMethod(DependencyManager::get().data(), "start", Qt::QueuedConnection); - } else { - QMetaObject::invokeMethod(DependencyManager::get().data(), "stop", Qt::QueuedConnection); - } -#endif -} - -void Application::updateWindowTitle() const { - - auto nodeList = DependencyManager::get(); - auto accountManager = DependencyManager::get(); - auto isInErrorState = nodeList->getDomainHandler().isInErrorState(); - - QString buildVersion = " - " - + (BuildInfo::BUILD_TYPE == BuildInfo::BuildType::Stable ? QString("Version") : QString("Build")) - + " " + applicationVersion(); - - QString loginStatus = accountManager->isLoggedIn() ? "" : " (NOT LOGGED IN)"; - - QString connectionStatus = isInErrorState ? " (ERROR CONNECTING)" : - nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED)"; - QString username = accountManager->getAccountInfo().getUsername(); - - setCrashAnnotation("username", username.toStdString()); - - QString currentPlaceName; - if (isServerlessMode()) { - if (isInErrorState) { - currentPlaceName = "serverless: " + nodeList->getDomainHandler().getErrorDomainURL().toString(); - } else { - currentPlaceName = "serverless: " + DependencyManager::get()->getDomainURL().toString(); - } - } else { - currentPlaceName = DependencyManager::get()->getDomainURL().host(); - if (currentPlaceName.isEmpty()) { - currentPlaceName = nodeList->getDomainHandler().getHostname(); - } - } - - QString title = QString() + (!username.isEmpty() ? username + " @ " : QString()) - + currentPlaceName + connectionStatus + loginStatus + buildVersion; - -#ifndef WIN32 - // crashes with vs2013/win32 - qCDebug(interfaceapp, "Application title set to: %s", title.toStdString().c_str()); -#endif - _window->setWindowTitle(title); - - // updateTitleWindow gets called whenever there's a change regarding the domain, so rather - // than placing this within domainURLChanged, it's placed here to cover the other potential cases. - DependencyManager::get< MessagesClient >()->sendLocalMessage("Toolbar-DomainChanged", ""); -} - -void Application::clearDomainOctreeDetails(bool clearAll) { - // before we delete all entities get MyAvatar's AvatarEntityData ready - getMyAvatar()->prepareAvatarEntityDataForReload(); - - // if we're about to quit, we really don't need to do the rest of these things... - if (_aboutToQuit) { - return; - } - - qCDebug(interfaceapp) << "Clearing domain octree details..."; - - resetPhysicsReadyInformation(); - setIsInterstitialMode(true); - - _octreeServerSceneStats.withWriteLock([&] { - _octreeServerSceneStats.clear(); - }); - - // reset the model renderer - clearAll ? getEntities()->clear() : getEntities()->clearDomainAndNonOwnedEntities(); - - auto skyStage = DependencyManager::get()->getSkyStage(); - - skyStage->setBackgroundMode(graphics::SunSkyStage::SKY_DEFAULT); - - DependencyManager::get()->clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); - MaterialCache::instance().clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); - ShaderCache::instance().clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); - DependencyManager::get()->clearUnusedResources(); -} - -void Application::domainURLChanged(QUrl domainURL) { - // disable physics until we have enough information about our new location to not cause craziness. - resetPhysicsReadyInformation(); - setIsServerlessMode(domainURL.scheme() != URL_SCHEME_HIFI); - if (isServerlessMode()) { - loadServerlessDomain(domainURL); - } - updateWindowTitle(); -} - -void Application::goToErrorDomainURL(QUrl errorDomainURL) { - // disable physics until we have enough information about our new location to not cause craziness. - resetPhysicsReadyInformation(); - setIsServerlessMode(errorDomainURL.scheme() != URL_SCHEME_HIFI); - if (isServerlessMode()) { - loadErrorDomain(errorDomainURL); - } - updateWindowTitle(); -} - -void Application::resettingDomain() { - _notifiedPacketVersionMismatchThisDomain = false; - - clearDomainOctreeDetails(false); -} - -void Application::nodeAdded(SharedNodePointer node) const { - if (node->getType() == NodeType::EntityServer) { - if (!_failedToConnectToEntityServer) { - _entityServerConnectionTimer.stop(); - _entityServerConnectionTimer.setInterval(ENTITY_SERVER_CONNECTION_TIMEOUT); - _entityServerConnectionTimer.start(); - } - } -} - -void Application::nodeActivated(SharedNodePointer node) { - if (node->getType() == NodeType::AssetServer) { - // asset server just connected - check if we have the asset browser showing - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - - if (offscreenUi) { - auto nodeList = DependencyManager::get(); - - if (nodeList->getThisNodeCanWriteAssets()) { - // call reload on the shown asset browser dialog to get the mappings (if permissions allow) - auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; - if (assetDialog) { - QMetaObject::invokeMethod(assetDialog, "reload"); - } - } else { - // we switched to an Asset Server that we can't modify, hide the Asset Browser - offscreenUi->hide("AssetServer"); - } - } -#endif - } - - // If we get a new EntityServer activated, reset lastQueried time - // so we will do a proper query during update - if (node->getType() == NodeType::EntityServer) { - _queryExpiry = SteadyClock::now(); - _octreeQuery.incrementConnectionID(); - - if (!_failedToConnectToEntityServer) { - _entityServerConnectionTimer.stop(); - } - } - - if (node->getType() == NodeType::AudioMixer && !isInterstitialMode()) { - DependencyManager::get()->negotiateAudioFormat(); - } - - if (node->getType() == NodeType::AvatarMixer) { - _queryExpiry = SteadyClock::now(); - - // new avatar mixer, send off our identity packet on next update loop - // Reset skeletonModelUrl if the last server modified our choice. - // Override the avatar url (but not model name) here too. - if (_avatarOverrideUrl.isValid()) { - getMyAvatar()->useFullAvatarURL(_avatarOverrideUrl); - } - - if (getMyAvatar()->getFullAvatarURLFromPreferences() != getMyAvatar()->getSkeletonModelURL()) { - getMyAvatar()->resetFullAvatarURL(); - } - getMyAvatar()->markIdentityDataChanged(); - getMyAvatar()->resetLastSent(); - - if (!isInterstitialMode()) { - // transmit a "sendAll" packet to the AvatarMixer we just connected to. - getMyAvatar()->sendAvatarDataPacket(true); - } - } -} - -void Application::nodeKilled(SharedNodePointer node) { - // These are here because connecting NodeList::nodeKilled to OctreePacketProcessor::nodeKilled doesn't work: - // OctreePacketProcessor::nodeKilled is not being called when NodeList::nodeKilled is emitted. - // This may have to do with GenericThread::threadRoutine() blocking the QThread event loop - - _octreeProcessor.nodeKilled(node); - - _entityEditSender.nodeKilled(node); - - if (node->getType() == NodeType::AudioMixer) { - QMetaObject::invokeMethod(DependencyManager::get().data(), "audioMixerKilled"); - } else if (node->getType() == NodeType::EntityServer) { - // we lost an entity server, clear all of the domain octree details - clearDomainOctreeDetails(false); - } else if (node->getType() == NodeType::AssetServer) { - // asset server going away - check if we have the asset browser showing - -#if !defined(DISABLE_QML) - auto offscreenUi = getOffscreenUI(); - auto assetDialog = offscreenUi ? offscreenUi->getRootItem()->findChild("AssetServer") : nullptr; - - if (assetDialog) { - // call reload on the shown asset browser dialog - QMetaObject::invokeMethod(assetDialog, "clear"); - } -#endif - } -} - -void Application::trackIncomingOctreePacket(ReceivedMessage& message, SharedNodePointer sendingNode, bool wasStatsPacket) { - // Attempt to identify the sender from its address. - if (sendingNode) { - const QUuid& nodeUUID = sendingNode->getUUID(); - - // now that we know the node ID, let's add these stats to the stats for that node... - _octreeServerSceneStats.withWriteLock([&] { - if (_octreeServerSceneStats.find(nodeUUID) != _octreeServerSceneStats.end()) { - OctreeSceneStats& stats = _octreeServerSceneStats[nodeUUID]; - stats.trackIncomingOctreePacket(message, wasStatsPacket, sendingNode->getClockSkewUsec()); - } - }); - } -} - -bool Application::gpuTextureMemSizeStable() { - auto renderConfig = qApp->getRenderEngine()->getConfiguration(); - auto renderStats = renderConfig->getConfig("Stats"); - - qint64 textureResourceGPUMemSize = renderStats->textureResourceGPUMemSize; - qint64 texturePopulatedGPUMemSize = renderStats->textureResourcePopulatedGPUMemSize; - qint64 textureTransferSize = renderStats->texturePendingGPUTransferSize; - - if (_gpuTextureMemSizeAtLastCheck == textureResourceGPUMemSize) { - _gpuTextureMemSizeStabilityCount++; - } else { - _gpuTextureMemSizeStabilityCount = 0; - } - _gpuTextureMemSizeAtLastCheck = textureResourceGPUMemSize; - - if (_gpuTextureMemSizeStabilityCount >= _minimumGPUTextureMemSizeStabilityCount) { - return (textureResourceGPUMemSize == texturePopulatedGPUMemSize) && (textureTransferSize == 0); - } - return false; -} - -int Application::processOctreeStats(ReceivedMessage& message, SharedNodePointer sendingNode) { - // parse the incoming stats datas stick it in a temporary object for now, while we - // determine which server it belongs to - int statsMessageLength = 0; - - const QUuid& nodeUUID = sendingNode->getUUID(); - - // now that we know the node ID, let's add these stats to the stats for that node... - _octreeServerSceneStats.withWriteLock([&] { - OctreeSceneStats& octreeStats = _octreeServerSceneStats[nodeUUID]; - statsMessageLength = octreeStats.unpackFromPacket(message); - - if (octreeStats.isFullScene()) { - _fullSceneReceivedCounter++; - } - }); - - return statsMessageLength; -} - -void Application::packetSent(quint64 length) { -} - -void Application::addingEntityWithCertificate(const QString& certificateID, const QString& placeName) { - auto ledger = DependencyManager::get(); - ledger->updateLocation(certificateID, placeName); -} - -void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointer scriptEngine) { - - scriptEngine->setEmitScriptUpdatesFunction([this]() { - SharedNodePointer entityServerNode = DependencyManager::get()->soloNodeOfType(NodeType::EntityServer); - return !entityServerNode || isPhysicsEnabled(); - }); - - // setup the packet sender of the script engine's scripting interfaces so - // we can use the same ones from the application. - auto entityScriptingInterface = DependencyManager::get(); - entityScriptingInterface->setPacketSender(&_entityEditSender); - entityScriptingInterface->setEntityTree(getEntities()->getTree()); - - if (property(hifi::properties::TEST).isValid()) { - scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance()); - } - - scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance()); - scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this)); - - // hook our avatar and avatar hash map object into this script engine - getMyAvatar()->registerMetaTypes(scriptEngine); - - scriptEngine->registerGlobalObject("AvatarList", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("Camera", &_myCamera); - -#if defined(Q_OS_MAC) || defined(Q_OS_WIN) - scriptEngine->registerGlobalObject("SpeechRecognizer", DependencyManager::get().data()); -#endif - - ClipboardScriptingInterface* clipboardScriptable = new ClipboardScriptingInterface(); - scriptEngine->registerGlobalObject("Clipboard", clipboardScriptable); - connect(scriptEngine.data(), &ScriptEngine::finished, clipboardScriptable, &ClipboardScriptingInterface::deleteLater); - - scriptEngine->registerGlobalObject("Overlays", &_overlays); - qScriptRegisterMetaType(scriptEngine.data(), RayToOverlayIntersectionResultToScriptValue, - RayToOverlayIntersectionResultFromScriptValue); - -#if !defined(DISABLE_QML) - scriptEngine->registerGlobalObject("OffscreenFlags", getOffscreenUI()->getFlags()); - scriptEngine->registerGlobalObject("Desktop", DependencyManager::get().data()); -#endif - - qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); - qScriptRegisterMetaType(scriptEngine.data(), - wrapperToScriptValue, wrapperFromScriptValue); - scriptEngine->registerGlobalObject("Toolbars", DependencyManager::get().data()); - - qScriptRegisterMetaType(scriptEngine.data(), wrapperToScriptValue, wrapperFromScriptValue); - qScriptRegisterMetaType(scriptEngine.data(), - wrapperToScriptValue, wrapperFromScriptValue); - scriptEngine->registerGlobalObject("Tablet", DependencyManager::get().data()); - // FIXME remove these deprecated names for the tablet scripting interface - scriptEngine->registerGlobalObject("tabletInterface", DependencyManager::get().data()); - - auto toolbarScriptingInterface = DependencyManager::get().data(); - DependencyManager::get().data()->setToolbarScriptingInterface(toolbarScriptingInterface); - - scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); - scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, - LocationScriptingInterface::locationSetter, "Window"); - // register `location` on the global object. - scriptEngine->registerGetterSetter("location", LocationScriptingInterface::locationGetter, - LocationScriptingInterface::locationSetter); - - bool clientScript = scriptEngine->isClientScript(); - scriptEngine->registerFunction("OverlayWindow", clientScript ? QmlWindowClass::constructor : QmlWindowClass::restricted_constructor); -#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) - scriptEngine->registerFunction("OverlayWebWindow", clientScript ? QmlWebWindowClass::constructor : QmlWebWindowClass::restricted_constructor); -#endif - scriptEngine->registerFunction("QmlFragment", clientScript ? QmlFragmentClass::constructor : QmlFragmentClass::restricted_constructor); - - scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); - scriptEngine->registerGlobalObject("DesktopPreviewProvider", DependencyManager::get().data()); -#if !defined(DISABLE_QML) - scriptEngine->registerGlobalObject("Stats", Stats::getInstance()); -#endif - scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); - scriptEngine->registerGlobalObject("Snapshot", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("AudioStats", DependencyManager::get()->getStats().data()); - scriptEngine->registerGlobalObject("AudioScope", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("AvatarBookmarks", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("LocationBookmarks", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("RayPick", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("LaserPointers", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Picks", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Pointers", DependencyManager::get().data()); - - // Caches - scriptEngine->registerGlobalObject("AnimationCache", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("TextureCache", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("ModelCache", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("SoundCache", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("DialogsManager", _dialogsManagerScriptingInterface); - - scriptEngine->registerGlobalObject("Account", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - scriptEngine->registerGlobalObject("GlobalServices", AccountServicesScriptingInterface::getInstance()); // DEPRECATED - TO BE REMOVED - scriptEngine->registerGlobalObject("AccountServices", AccountServicesScriptingInterface::getInstance()); - qScriptRegisterMetaType(scriptEngine.data(), DownloadInfoResultToScriptValue, DownloadInfoResultFromScriptValue); - - scriptEngine->registerGlobalObject("FaceTracker", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("LODManager", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("Keyboard", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("HMD", DependencyManager::get().data()); - scriptEngine->registerFunction("HMD", "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0); - scriptEngine->registerFunction("HMD", "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0); - - scriptEngine->registerGlobalObject("Scene", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Render", _graphicsEngine.getRenderEngine()->getConfiguration().get()); - scriptEngine->registerGlobalObject("Workload", _gameWorkload._engine->getConfiguration().get()); - - GraphicsScriptingInterface::registerMetaTypes(scriptEngine.data()); - scriptEngine->registerGlobalObject("Graphics", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("ScriptDiscoveryService", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); - - scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); - - scriptEngine->registerGlobalObject("GooglePoly", DependencyManager::get().data()); - - if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { - scriptEngine->registerGlobalObject("Steam", new SteamScriptingInterface(scriptEngine.data(), steamClient.get())); - } - auto scriptingInterface = DependencyManager::get(); - scriptEngine->registerGlobalObject("Controller", scriptingInterface.data()); - UserInputMapper::registerControllerTypes(scriptEngine.data()); - - auto recordingInterface = DependencyManager::get(); - scriptEngine->registerGlobalObject("Recording", recordingInterface.data()); - - auto entityScriptServerLog = DependencyManager::get(); - scriptEngine->registerGlobalObject("EntityScriptServerLog", entityScriptServerLog.data()); - scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); - scriptEngine->registerGlobalObject("Selection", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("WalletScriptingInterface", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("AddressManager", DependencyManager::get().data()); - scriptEngine->registerGlobalObject("HifiAbout", AboutUtil::getInstance()); - scriptEngine->registerGlobalObject("ResourceRequestObserver", DependencyManager::get().data()); - - registerInteractiveWindowMetaType(scriptEngine.data()); - - auto pickScriptingInterface = DependencyManager::get(); - pickScriptingInterface->registerMetaTypes(scriptEngine.data()); - - // connect this script engines printedMessage signal to the global ScriptEngines these various messages - auto scriptEngines = DependencyManager::get().data(); - connect(scriptEngine.data(), &ScriptEngine::printedMessage, scriptEngines, &ScriptEngines::onPrintedMessage); - connect(scriptEngine.data(), &ScriptEngine::errorMessage, scriptEngines, &ScriptEngines::onErrorMessage); - connect(scriptEngine.data(), &ScriptEngine::warningMessage, scriptEngines, &ScriptEngines::onWarningMessage); - connect(scriptEngine.data(), &ScriptEngine::infoMessage, scriptEngines, &ScriptEngines::onInfoMessage); - connect(scriptEngine.data(), &ScriptEngine::clearDebugWindow, scriptEngines, &ScriptEngines::onClearDebugWindow); - -} - -bool Application::canAcceptURL(const QString& urlString) const { - QUrl url(urlString); - if (url.query().contains(WEB_VIEW_TAG)) { - return false; - } else if (urlString.startsWith(URL_SCHEME_HIFI)) { - return true; - } - QString lowerPath = url.path().toLower(); - for (auto& pair : _acceptedExtensions) { - if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { - return true; - } - } - return false; -} - -bool Application::acceptURL(const QString& urlString, bool defaultUpload) { - QUrl url(urlString); - - if (url.scheme() == URL_SCHEME_HIFI) { - // this is a hifi URL - have the AddressManager handle it - QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", - Qt::AutoConnection, Q_ARG(const QString&, urlString)); - return true; - } - - QString lowerPath = url.path().toLower(); - for (auto& pair : _acceptedExtensions) { - if (lowerPath.endsWith(pair.first, Qt::CaseInsensitive)) { - AcceptURLMethod method = pair.second; - return (this->*method)(urlString); - } - } - - if (defaultUpload && !url.fileName().isEmpty() && url.isLocalFile()) { - showAssetServerWidget(urlString); - } - return defaultUpload; -} - -void Application::setSessionUUID(const QUuid& sessionUUID) const { - Physics::setSessionUUID(sessionUUID); -} - -bool Application::askToSetAvatarUrl(const QString& url) { - QUrl realUrl(url); - if (realUrl.isLocalFile()) { - OffscreenUi::asyncWarning("", "You can not use local files for avatar components."); - return false; - } - - // Download the FST file, to attempt to determine its model type - QVariantHash fstMapping = FSTReader::downloadMapping(url); - - FSTReader::ModelType modelType = FSTReader::predictModelType(fstMapping); - - QString modelName = fstMapping["name"].toString(); - QString modelLicense = fstMapping["license"].toString(); - - bool agreeToLicense = true; // assume true - //create set avatar callback - auto setAvatar = [=] (QString url, QString modelName) { - ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Set Avatar", - "Would you like to use '" + modelName + "' for your avatar?", - QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); - QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - - bool ok = (QMessageBox::Ok == static_cast(answer.toInt())); - if (ok) { - getMyAvatar()->useFullAvatarURL(url, modelName); - emit fullAvatarURLChanged(url, modelName); - } else { - qCDebug(interfaceapp) << "Declined to use the avatar"; - } - }); - }; - - if (!modelLicense.isEmpty()) { - // word wrap the license text to fit in a reasonable shaped message box. - const int MAX_CHARACTERS_PER_LINE = 90; - modelLicense = simpleWordWrap(modelLicense, MAX_CHARACTERS_PER_LINE); - - ModalDialogListener* dlg = OffscreenUi::asyncQuestion("Avatar Usage License", - modelLicense + "\nDo you agree to these terms?", - QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); - QObject::connect(dlg, &ModalDialogListener::response, this, [=, &agreeToLicense] (QVariant answer) { - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - - agreeToLicense = (static_cast(answer.toInt()) == QMessageBox::Yes); - if (agreeToLicense) { - switch (modelType) { - case FSTReader::HEAD_AND_BODY_MODEL: { - setAvatar(url, modelName); - break; - } - default: - OffscreenUi::asyncWarning("", modelName + "Does not support a head and body as required."); - break; - } - } else { - qCDebug(interfaceapp) << "Declined to agree to avatar license"; - } - - //auto offscreenUi = getOffscreenUI(); - }); - } else { - setAvatar(url, modelName); - } - - return true; -} - - -bool Application::askToLoadScript(const QString& scriptFilenameOrURL) { - QString shortName = scriptFilenameOrURL; - - QUrl scriptURL { scriptFilenameOrURL }; - - if (scriptURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { - int startIndex = shortName.lastIndexOf('/') + 1; - int endIndex = shortName.lastIndexOf('?'); - shortName = shortName.mid(startIndex, endIndex - startIndex); - } - -#ifdef DISABLE_QML - DependencyManager::get()->loadScript(scriptFilenameOrURL); -#else - QString message = "Would you like to run this script:\n" + shortName; - ModalDialogListener* dlg = OffscreenUi::asyncQuestion(getWindow(), "Run Script", message, - QMessageBox::Yes | QMessageBox::No); - - QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - const QString& fileName = scriptFilenameOrURL; - if (static_cast(answer.toInt()) == QMessageBox::Yes) { - qCDebug(interfaceapp) << "Chose to run the script: " << fileName; - DependencyManager::get()->loadScript(fileName); - } else { - qCDebug(interfaceapp) << "Declined to run the script"; - } - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - }); -#endif - return true; -} - -bool Application::askToWearAvatarAttachmentUrl(const QString& url) { - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest = QNetworkRequest(url); - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT); - QNetworkReply* reply = networkAccessManager.get(networkRequest); - int requestNumber = ++_avatarAttachmentRequest; - connect(reply, &QNetworkReply::finished, [this, reply, url, requestNumber]() { - - if (requestNumber != _avatarAttachmentRequest) { - // this request has been superseded by another more recent request - reply->deleteLater(); - return; - } - - QNetworkReply::NetworkError networkError = reply->error(); - if (networkError == QNetworkReply::NoError) { - // download success - QByteArray contents = reply->readAll(); - - QJsonParseError jsonError; - auto doc = QJsonDocument::fromJson(contents, &jsonError); - if (jsonError.error == QJsonParseError::NoError) { - - auto jsonObject = doc.object(); - - // retrieve optional name field from JSON - QString name = tr("Unnamed Attachment"); - auto nameValue = jsonObject.value("name"); - if (nameValue.isString()) { - name = nameValue.toString(); - } - - auto avatarAttachmentConfirmationTitle = tr("Avatar Attachment Confirmation"); - auto avatarAttachmentConfirmationMessage = tr("Would you like to wear '%1' on your avatar?").arg(name); - ModalDialogListener* dlg = OffscreenUi::asyncQuestion(avatarAttachmentConfirmationTitle, - avatarAttachmentConfirmationMessage, - QMessageBox::Ok | QMessageBox::Cancel); - QObject::connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - QObject::disconnect(dlg, &ModalDialogListener::response, this, nullptr); - if (static_cast(answer.toInt()) == QMessageBox::Yes) { - // add attachment to avatar - auto myAvatar = getMyAvatar(); - assert(myAvatar); - auto attachmentDataVec = myAvatar->getAttachmentData(); - AttachmentData attachmentData; - attachmentData.fromJson(jsonObject); - attachmentDataVec.push_back(attachmentData); - myAvatar->setAttachmentData(attachmentDataVec); - } else { - qCDebug(interfaceapp) << "User declined to wear the avatar attachment"; - } - }); - } else { - // json parse error - auto avatarAttachmentParseErrorString = tr("Error parsing attachment JSON from url: \"%1\""); - displayAvatarAttachmentWarning(avatarAttachmentParseErrorString.arg(url)); - } - } else { - // download failure - auto avatarAttachmentDownloadErrorString = tr("Error downloading attachment JSON from url: \"%1\""); - displayAvatarAttachmentWarning(avatarAttachmentDownloadErrorString.arg(url)); - } - reply->deleteLater(); - }); - return true; -} - -void Application::replaceDomainContent(const QString& url) { - qCDebug(interfaceapp) << "Attempting to replace domain content"; - QByteArray urlData(url.toUtf8()); - auto limitedNodeList = DependencyManager::get(); - const auto& domainHandler = limitedNodeList->getDomainHandler(); - - auto octreeFilePacket = NLPacket::create(PacketType::DomainContentReplacementFromUrl, urlData.size(), true); - octreeFilePacket->write(urlData); - limitedNodeList->sendPacket(std::move(octreeFilePacket), domainHandler.getSockAddr()); - - auto addressManager = DependencyManager::get(); - addressManager->handleLookupString(DOMAIN_SPAWNING_POINT); - QString newHomeAddress = addressManager->getHost() + DOMAIN_SPAWNING_POINT; - qCDebug(interfaceapp) << "Setting new home bookmark to: " << newHomeAddress; - DependencyManager::get()->setHomeLocationToAddress(newHomeAddress); -} - -bool Application::askToReplaceDomainContent(const QString& url) { - QString methodDetails; - const int MAX_CHARACTERS_PER_LINE = 90; - if (DependencyManager::get()->getThisNodeCanReplaceContent()) { - QUrl originURL { url }; - if (originURL.host().endsWith(MARKETPLACE_CDN_HOSTNAME)) { - // Create a confirmation dialog when this call is made - static const QString infoText = simpleWordWrap("Your domain's content will be replaced with a new content set. " - "If you want to save what you have now, create a backup before proceeding. For more information about backing up " - "and restoring content, visit the documentation page at: ", MAX_CHARACTERS_PER_LINE) + - "\nhttps://docs.highfidelity.com/create-and-explore/start-working-in-your-sandbox/restoring-sandbox-content"; - - ModalDialogListener* dig = OffscreenUi::asyncQuestion("Are you sure you want to replace this domain's content set?", - infoText, QMessageBox::Yes | QMessageBox::No, QMessageBox::No); - - QObject::connect(dig, &ModalDialogListener::response, this, [=] (QVariant answer) { - QString details; - if (static_cast(answer.toInt()) == QMessageBox::Yes) { - // Given confirmation, send request to domain server to replace content - replaceDomainContent(url); - details = "SuccessfulRequestToReplaceContent"; - } else { - details = "UserDeclinedToReplaceContent"; - } - QJsonObject messageProperties = { - { "status", details }, - { "content_set_url", url } - }; - UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); - QObject::disconnect(dig, &ModalDialogListener::response, this, nullptr); - }); - } else { - methodDetails = "ContentSetDidNotOriginateFromMarketplace"; - QJsonObject messageProperties = { - { "status", methodDetails }, - { "content_set_url", url } - }; - UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); - } - } else { - methodDetails = "UserDoesNotHavePermissionToReplaceContent"; - static const QString warningMessage = simpleWordWrap("The domain owner must enable 'Replace Content' " - "permissions for you in this domain's server settings before you can continue.", MAX_CHARACTERS_PER_LINE); - OffscreenUi::asyncWarning("You do not have permissions to replace domain content", warningMessage, - QMessageBox::Ok, QMessageBox::Ok); - - QJsonObject messageProperties = { - { "status", methodDetails }, - { "content_set_url", url } - }; - UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); - } - return true; -} - -void Application::displayAvatarAttachmentWarning(const QString& message) const { - auto avatarAttachmentWarningTitle = tr("Avatar Attachment Failure"); - OffscreenUi::asyncWarning(avatarAttachmentWarningTitle, message); -} - -void Application::showDialog(const QUrl& widgetUrl, const QUrl& tabletUrl, const QString& name) const { - auto tablet = DependencyManager::get()->getTablet(SYSTEM_TABLET); - auto hmd = DependencyManager::get(); - bool onTablet = false; - - if (!tablet->getToolbarMode()) { - onTablet = tablet->pushOntoStack(tabletUrl); - if (onTablet) { - toggleTabletUI(true); - } - } else { -#if !defined(DISABLE_QML) - getOffscreenUI()->show(widgetUrl, name); -#endif - } -} - -void Application::showScriptLogs() { - QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); - defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/debugging/debugWindow.js"); - DependencyManager::get()->loadScript(defaultScriptsLoc.toString()); -} - -void Application::showAssetServerWidget(QString filePath) { - if (!DependencyManager::get()->getThisNodeCanWriteAssets() || getLoginDialogPoppedUp()) { - return; - } - static const QUrl url { "hifi/AssetServer.qml" }; - - auto startUpload = [=](QQmlContext* context, QObject* newObject){ - if (!filePath.isEmpty()) { - emit uploadRequest(filePath); - } - }; - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - auto hmd = DependencyManager::get(); - if (tablet->getToolbarMode()) { - getOffscreenUI()->show(url, "AssetServer", startUpload); - } else { - if (!hmd->getShouldShowTablet() && !isHMDMode()) { - getOffscreenUI()->show(url, "AssetServer", startUpload); - } else { - static const QUrl url("hifi/dialogs/TabletAssetServer.qml"); - if (!tablet->isPathLoaded(url)) { - tablet->pushOntoStack(url); - } - } - } - - startUpload(nullptr, nullptr); -} - -void Application::addAssetToWorldFromURL(QString url) { - - QString filename; - if (url.contains("filename")) { - filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. - } - if (url.contains("poly.google.com/downloads")) { - filename = url.section('/', -1); - if (url.contains("noDownload")) { - filename.remove(".zip?noDownload=false"); - } else { - filename.remove(".zip"); - } - - } - - if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { - QString errorInfo = "You do not have permissions to write to the Asset Server."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filename, errorInfo); - return; - } - - addAssetToWorldInfo(filename, "Downloading model file " + filename + "."); - - auto request = DependencyManager::get()->createResourceRequest( - nullptr, QUrl(url), true, -1, "Application::addAssetToWorldFromURL"); - connect(request, &ResourceRequest::finished, this, &Application::addAssetToWorldFromURLRequestFinished); - request->send(); -} - -void Application::addAssetToWorldFromURLRequestFinished() { - auto request = qobject_cast(sender()); - auto url = request->getUrl().toString(); - auto result = request->getResult(); - - QString filename; - bool isBlocks = false; - - if (url.contains("filename")) { - filename = url.section("filename=", 1, 1); // Filename is in "?filename=" parameter at end of URL. - } - if (url.contains("poly.google.com/downloads")) { - filename = url.section('/', -1); - if (url.contains("noDownload")) { - filename.remove(".zip?noDownload=false"); - } else { - filename.remove(".zip"); - } - isBlocks = true; - } - - if (result == ResourceRequest::Success) { - QTemporaryDir temporaryDir; - temporaryDir.setAutoRemove(false); - if (temporaryDir.isValid()) { - QString temporaryDirPath = temporaryDir.path(); - QString downloadPath = temporaryDirPath + "/" + filename; - - QFile tempFile(downloadPath); - if (tempFile.open(QIODevice::WriteOnly)) { - tempFile.write(request->getData()); - addAssetToWorldInfoClear(filename); // Remove message from list; next one added will have a different key. - tempFile.close(); - qApp->getFileDownloadInterface()->runUnzip(downloadPath, url, true, false, isBlocks); - } else { - QString errorInfo = "Couldn't open temporary file for download"; - qWarning(interfaceapp) << errorInfo; - addAssetToWorldError(filename, errorInfo); - } - } else { - QString errorInfo = "Couldn't create temporary directory for download"; - qWarning(interfaceapp) << errorInfo; - addAssetToWorldError(filename, errorInfo); - } - } else { - qWarning(interfaceapp) << "Error downloading" << url << ":" << request->getResultString(); - addAssetToWorldError(filename, "Error downloading " + filename + " : " + request->getResultString()); - } - - request->deleteLater(); -} - - -QString filenameFromPath(QString filePath) { - return filePath.right(filePath.length() - filePath.lastIndexOf("/") - 1); -} - -void Application::addAssetToWorldUnzipFailure(QString filePath) { - QString filename = filenameFromPath(QUrl(filePath).toLocalFile()); - qWarning(interfaceapp) << "Couldn't unzip file" << filePath; - addAssetToWorldError(filename, "Couldn't unzip file " + filename + "."); -} - -void Application::addAssetToWorld(QString path, QString zipFile, bool isZip, bool isBlocks) { - // Automatically upload and add asset to world as an alternative manual process initiated by showAssetServerWidget(). - QString mapping; - QString filename = filenameFromPath(path); - if (isZip || isBlocks) { - QString assetName = zipFile.section("/", -1).remove(QRegExp("[.]zip(.*)$")); - QString assetFolder = path.section("model_repo/", -1); - mapping = "/" + assetName + "/" + assetFolder; - } else { - mapping = "/" + filename; - } - - // Test repeated because possibly different code paths. - if (!DependencyManager::get()->getThisNodeCanWriteAssets()) { - QString errorInfo = "You do not have permissions to write to the Asset Server."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filename, errorInfo); - return; - } - - addAssetToWorldInfo(filename, "Adding " + mapping.mid(1) + " to the Asset Server."); - - addAssetToWorldWithNewMapping(path, mapping, 0, isZip, isBlocks); -} - -void Application::addAssetToWorldWithNewMapping(QString filePath, QString mapping, int copy, bool isZip, bool isBlocks) { - auto request = DependencyManager::get()->createGetMappingRequest(mapping); - - QObject::connect(request, &GetMappingRequest::finished, this, [=](GetMappingRequest* request) mutable { - const int MAX_COPY_COUNT = 100; // Limit number of duplicate assets; recursion guard. - auto result = request->getError(); - if (result == GetMappingRequest::NotFound) { - addAssetToWorldUpload(filePath, mapping, isZip, isBlocks); - } else if (result != GetMappingRequest::NoError) { - QString errorInfo = "Could not map asset name: " - + mapping.left(mapping.length() - QString::number(copy).length() - 1); - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else if (copy < MAX_COPY_COUNT - 1) { - if (copy > 0) { - mapping = mapping.remove(mapping.lastIndexOf("-"), QString::number(copy).length() + 1); - } - copy++; - mapping = mapping.insert(mapping.lastIndexOf("."), "-" + QString::number(copy)); - addAssetToWorldWithNewMapping(filePath, mapping, copy, isZip, isBlocks); - } else { - QString errorInfo = "Too many copies of asset name: " - + mapping.left(mapping.length() - QString::number(copy).length() - 1); - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } - request->deleteLater(); - }); - - request->start(); -} - -void Application::addAssetToWorldUpload(QString filePath, QString mapping, bool isZip, bool isBlocks) { - qInfo(interfaceapp) << "Uploading" << filePath << "to Asset Server as" << mapping; - auto upload = DependencyManager::get()->createUpload(filePath); - QObject::connect(upload, &AssetUpload::finished, this, [=](AssetUpload* upload, const QString& hash) mutable { - if (upload->getError() != AssetUpload::NoError) { - QString errorInfo = "Could not upload model to the Asset Server."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else { - addAssetToWorldSetMapping(filePath, mapping, hash, isZip, isBlocks); - } - - // Remove temporary directory created by Clara.io market place download. - int index = filePath.lastIndexOf("/model_repo/"); - if (index > 0) { - QString tempDir = filePath.left(index); - qCDebug(interfaceapp) << "Removing temporary directory at: " + tempDir; - QDir(tempDir).removeRecursively(); - } - - upload->deleteLater(); - }); - - upload->start(); -} - -void Application::addAssetToWorldSetMapping(QString filePath, QString mapping, QString hash, bool isZip, bool isBlocks) { - auto request = DependencyManager::get()->createSetMappingRequest(mapping, hash); - connect(request, &SetMappingRequest::finished, this, [=](SetMappingRequest* request) mutable { - if (request->getError() != SetMappingRequest::NoError) { - QString errorInfo = "Could not set asset mapping."; - qWarning(interfaceapp) << "Error downloading model: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else { - // to prevent files that aren't models or texture files from being loaded into world automatically - if ((filePath.toLower().endsWith(OBJ_EXTENSION) || filePath.toLower().endsWith(FBX_EXTENSION)) || - ((filePath.toLower().endsWith(JPG_EXTENSION) || filePath.toLower().endsWith(PNG_EXTENSION)) && - ((!isBlocks) && (!isZip)))) { - addAssetToWorldAddEntity(filePath, mapping); - } else { - qCDebug(interfaceapp) << "Zipped contents are not supported entity files"; - addAssetToWorldInfoDone(filenameFromPath(filePath)); - } - } - request->deleteLater(); - }); - - request->start(); -} - -void Application::addAssetToWorldAddEntity(QString filePath, QString mapping) { - EntityItemProperties properties; - properties.setName(mapping.right(mapping.length() - 1)); - if (filePath.toLower().endsWith(PNG_EXTENSION) || filePath.toLower().endsWith(JPG_EXTENSION)) { - properties.setType(EntityTypes::Image); - properties.setImageURL(QString("atp:" + mapping)); - properties.setKeepAspectRatio(false); - } else { - properties.setType(EntityTypes::Model); - properties.setModelURL("atp:" + mapping); - properties.setShapeType(SHAPE_TYPE_SIMPLE_COMPOUND); - } - properties.setCollisionless(true); // Temporarily set so that doesn't collide with avatar. - properties.setVisible(false); // Temporarily set so that don't see at large unresized dimensions. - bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); - properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); - glm::vec3 positionOffset = getMyAvatar()->getWorldOrientation() * (getMyAvatar()->getSensorToWorldScale() * glm::vec3(0.0f, 0.0f, -2.0f)); - properties.setPosition(getMyAvatar()->getWorldPosition() + positionOffset); - properties.setRotation(getMyAvatar()->getWorldOrientation()); - properties.setGravity(glm::vec3(0.0f, 0.0f, 0.0f)); - auto entityID = DependencyManager::get()->addEntity(properties); - - // Note: Model dimensions are not available here; model is scaled per FBX mesh in RenderableModelEntityItem::update() later - // on. But FBX dimensions may be in cm, so we monitor for the dimension change and rescale again if warranted. - - if (entityID == QUuid()) { - QString errorInfo = "Could not add model " + mapping + " to world."; - qWarning(interfaceapp) << "Could not add model to world: " + errorInfo; - addAssetToWorldError(filenameFromPath(filePath), errorInfo); - } else { - // Monitor when asset is rendered in world so that can resize if necessary. - _addAssetToWorldResizeList.insert(entityID, 0); // List value is count of checks performed. - if (!_addAssetToWorldResizeTimer.isActive()) { - _addAssetToWorldResizeTimer.start(); - } - - // Close progress message box. - addAssetToWorldInfoDone(filenameFromPath(filePath)); - } -} - -void Application::addAssetToWorldCheckModelSize() { - if (_addAssetToWorldResizeList.size() == 0) { - return; - } - - auto item = _addAssetToWorldResizeList.begin(); - while (item != _addAssetToWorldResizeList.end()) { - auto entityID = item.key(); - - EntityPropertyFlags propertyFlags; - propertyFlags += PROP_NAME; - propertyFlags += PROP_DIMENSIONS; - auto entityScriptingInterface = DependencyManager::get(); - auto properties = entityScriptingInterface->getEntityProperties(entityID, propertyFlags); - auto name = properties.getName(); - auto dimensions = properties.getDimensions(); - - bool doResize = false; - - const glm::vec3 DEFAULT_DIMENSIONS = glm::vec3(0.1f, 0.1f, 0.1f); - if (dimensions != DEFAULT_DIMENSIONS) { - - // Scale model so that its maximum is exactly specific size. - const float MAXIMUM_DIMENSION = getMyAvatar()->getSensorToWorldScale(); - auto previousDimensions = dimensions; - auto scale = std::min(MAXIMUM_DIMENSION / dimensions.x, std::min(MAXIMUM_DIMENSION / dimensions.y, - MAXIMUM_DIMENSION / dimensions.z)); - dimensions *= scale; - qInfo(interfaceapp) << "Model" << name << "auto-resized from" << previousDimensions << " to " << dimensions; - doResize = true; - - item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. - } else { - // Increment count of checks done. - _addAssetToWorldResizeList[entityID]++; - - const int CHECK_MODEL_SIZE_MAX_CHECKS = 300; - if (_addAssetToWorldResizeList[entityID] > CHECK_MODEL_SIZE_MAX_CHECKS) { - // Have done enough checks; model was either the default size or something's gone wrong. - - // Rescale all dimensions. - const glm::vec3 UNIT_DIMENSIONS = glm::vec3(1.0f, 1.0f, 1.0f); - dimensions = UNIT_DIMENSIONS; - qInfo(interfaceapp) << "Model" << name << "auto-resize timed out; resized to " << dimensions; - doResize = true; - - item = _addAssetToWorldResizeList.erase(item); // Finished with this entity; advance to next. - } else { - // No action on this entity; advance to next. - ++item; - } - } - - if (doResize) { - EntityItemProperties properties; - properties.setDimensions(dimensions); - properties.setVisible(true); - if (!name.toLower().endsWith(PNG_EXTENSION) && !name.toLower().endsWith(JPG_EXTENSION)) { - properties.setCollisionless(false); - } - bool grabbable = (Menu::getInstance()->isOptionChecked(MenuOption::CreateEntitiesGrabbable)); - properties.setUserData(grabbable ? GRABBABLE_USER_DATA : NOT_GRABBABLE_USER_DATA); - properties.setLastEdited(usecTimestampNow()); - entityScriptingInterface->editEntity(entityID, properties); - } - } - - // Stop timer if nothing in list to check. - if (_addAssetToWorldResizeList.size() == 0) { - _addAssetToWorldResizeTimer.stop(); - } -} - - -void Application::addAssetToWorldInfo(QString modelName, QString infoText) { - // Displays the most recent info message, subject to being overridden by error messages. - - if (_aboutToQuit) { - return; - } - - /* - Cancel info timer if running. - If list has an entry for modelName, delete it (just one). - Append modelName, infoText to list. - Display infoText in message box unless an error is being displayed (i.e., error timer is running). - Show message box if not already visible. - */ - - _addAssetToWorldInfoTimer.stop(); - - addAssetToWorldInfoClear(modelName); - - _addAssetToWorldInfoKeys.append(modelName); - _addAssetToWorldInfoMessages.append(infoText); - - if (!_addAssetToWorldErrorTimer.isActive()) { - if (!_addAssetToWorldMessageBox) { - _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, - "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); - connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); - } - - _addAssetToWorldMessageBox->setProperty("text", "\n" + infoText); - _addAssetToWorldMessageBox->setVisible(true); - } -} - -void Application::addAssetToWorldInfoClear(QString modelName) { - // Clears modelName entry from message list without affecting message currently displayed. - - if (_aboutToQuit) { - return; - } - - /* - Delete entry for modelName from list. - */ - - auto index = _addAssetToWorldInfoKeys.indexOf(modelName); - if (index > -1) { - _addAssetToWorldInfoKeys.removeAt(index); - _addAssetToWorldInfoMessages.removeAt(index); - } -} - -void Application::addAssetToWorldInfoDone(QString modelName) { - // Continues to display this message if the latest for a few seconds, then deletes it and displays the next latest. - - if (_aboutToQuit) { - return; - } - - /* - Delete entry for modelName from list. - (Re)start the info timer to update message box. ... onAddAssetToWorldInfoTimeout() - */ - - addAssetToWorldInfoClear(modelName); - _addAssetToWorldInfoTimer.start(); -} - -void Application::addAssetToWorldInfoTimeout() { - if (_aboutToQuit) { - return; - } - - /* - If list not empty, display last message in list (may already be displayed ) unless an error is being displayed. - If list empty, close the message box unless an error is being displayed. - */ - - if (!_addAssetToWorldErrorTimer.isActive() && _addAssetToWorldMessageBox) { - if (_addAssetToWorldInfoKeys.length() > 0) { - _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); - } else { - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox->setVisible(false); - _addAssetToWorldMessageBox->deleteLater(); - _addAssetToWorldMessageBox = nullptr; - } - } -} - -void Application::addAssetToWorldError(QString modelName, QString errorText) { - // Displays the most recent error message for a few seconds. - - if (_aboutToQuit) { - return; - } - - /* - If list has an entry for modelName, delete it. - Display errorText in message box. - Show message box if not already visible. - (Re)start error timer. ... onAddAssetToWorldErrorTimeout() - */ - - addAssetToWorldInfoClear(modelName); - - if (!_addAssetToWorldMessageBox) { - _addAssetToWorldMessageBox = getOffscreenUI()->createMessageBox(OffscreenUi::ICON_INFORMATION, - "Downloading Model", "", QMessageBox::NoButton, QMessageBox::NoButton); - connect(_addAssetToWorldMessageBox, SIGNAL(destroyed()), this, SLOT(onAssetToWorldMessageBoxClosed())); - } - - _addAssetToWorldMessageBox->setProperty("text", "\n" + errorText); - _addAssetToWorldMessageBox->setVisible(true); - - _addAssetToWorldErrorTimer.start(); -} - -void Application::addAssetToWorldErrorTimeout() { - if (_aboutToQuit) { - return; - } - - /* - If list is not empty, display message from last entry. - If list is empty, close the message box. - */ - - if (_addAssetToWorldMessageBox) { - if (_addAssetToWorldInfoKeys.length() > 0) { - _addAssetToWorldMessageBox->setProperty("text", "\n" + _addAssetToWorldInfoMessages.last()); - } else { - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox->setVisible(false); - _addAssetToWorldMessageBox->deleteLater(); - _addAssetToWorldMessageBox = nullptr; - } - } -} - - -void Application::addAssetToWorldMessageClose() { - // Clear messages, e.g., if Interface is being closed or domain changes. - - /* - Call if user manually closes message box. - Call if domain changes. - Call if application is shutting down. - - Stop timers. - Close the message box if open. - Clear lists. - */ - - _addAssetToWorldInfoTimer.stop(); - _addAssetToWorldErrorTimer.stop(); - - if (_addAssetToWorldMessageBox) { - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox->setVisible(false); - _addAssetToWorldMessageBox->deleteLater(); - _addAssetToWorldMessageBox = nullptr; - } - - _addAssetToWorldInfoKeys.clear(); - _addAssetToWorldInfoMessages.clear(); -} - -void Application::onAssetToWorldMessageBoxClosed() { - if (_addAssetToWorldMessageBox) { - // User manually closed message box; perhaps because it has become stuck, so reset all messages. - qInfo(interfaceapp) << "User manually closed download status message box"; - disconnect(_addAssetToWorldMessageBox); - _addAssetToWorldMessageBox = nullptr; - addAssetToWorldMessageClose(); - } -} - - -void Application::handleUnzip(QString zipFile, QStringList unzipFile, bool autoAdd, bool isZip, bool isBlocks) { - if (autoAdd) { - if (!unzipFile.isEmpty()) { - for (int i = 0; i < unzipFile.length(); i++) { - if (QFileInfo(unzipFile.at(i)).isFile()) { - qCDebug(interfaceapp) << "Preparing file for asset server: " << unzipFile.at(i); - addAssetToWorld(unzipFile.at(i), zipFile, isZip, isBlocks); - } - } - } else { - addAssetToWorldUnzipFailure(zipFile); - } - } else { - showAssetServerWidget(unzipFile.first()); - } -} - -void Application::packageModel() { - ModelPackager::package(); -} - -void Application::openUrl(const QUrl& url) const { - if (!url.isEmpty()) { - if (url.scheme() == URL_SCHEME_HIFI) { - DependencyManager::get()->handleLookupString(url.toString()); - } else if (url.scheme() == URL_SCHEME_HIFIAPP) { - DependencyManager::get()->openSystemApp(url.path()); - } else { - // address manager did not handle - ask QDesktopServices to handle - QDesktopServices::openUrl(url); - } - } -} - -void Application::loadDialog() { - ModalDialogListener* dlg = OffscreenUi::getOpenFileNameAsync(_glWidget, tr("Open Script"), - getPreviousScriptLocation(), - tr("JavaScript Files (*.js)")); - connect(dlg, &ModalDialogListener::response, this, [=] (QVariant answer) { - disconnect(dlg, &ModalDialogListener::response, this, nullptr); - const QString& response = answer.toString(); - if (!response.isEmpty() && QFile(response).exists()) { - setPreviousScriptLocation(QFileInfo(response).absolutePath()); - DependencyManager::get()->loadScript(response, true, false, false, true); // Don't load from cache - } - }); -} - -QString Application::getPreviousScriptLocation() { - QString result = _previousScriptLocation.get(); - return result; -} - -void Application::setPreviousScriptLocation(const QString& location) { - _previousScriptLocation.set(location); -} - -void Application::loadScriptURLDialog() const { - ModalDialogListener* dlg = OffscreenUi::getTextAsync(OffscreenUi::ICON_NONE, "Open and Run Script", "Script URL"); - connect(dlg, &ModalDialogListener::response, this, [=] (QVariant response) { - disconnect(dlg, &ModalDialogListener::response, this, nullptr); - const QString& newScript = response.toString(); - if (QUrl(newScript).scheme() == "atp") { - OffscreenUi::asyncWarning("Error Loading Script", "Cannot load client script over ATP"); - } else if (!newScript.isEmpty()) { - DependencyManager::get()->loadScript(newScript.trimmed()); - } - }); -} - -SharedSoundPointer Application::getSampleSound() const { - return _sampleSound; -} - -void Application::loadLODToolsDialog() { - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->lodTools(); - } else { - tablet->pushOntoStack("hifi/dialogs/TabletLODTools.qml"); - } -} - -void Application::loadEntityStatisticsDialog() { - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->octreeStatsDetails(); - } else { - tablet->pushOntoStack("hifi/dialogs/TabletEntityStatistics.qml"); - } -} - -void Application::loadDomainConnectionDialog() { - auto tabletScriptingInterface = DependencyManager::get(); - auto tablet = dynamic_cast(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !isHMDMode())) { - auto dialogsManager = DependencyManager::get(); - dialogsManager->showDomainConnectionDialog(); - } else { - tablet->pushOntoStack("hifi/dialogs/TabletDCDialog.qml"); - } -} - -void Application::toggleLogDialog() { -#ifndef ANDROID_APP_QUEST_INTERFACE - if (getLoginDialogPoppedUp()) { - return; - } - if (! _logDialog) { - - bool keepOnTop =_keepLogWindowOnTop.get(); -#ifdef Q_OS_WIN - _logDialog = new LogDialog(keepOnTop ? qApp->getWindow() : nullptr, getLogger()); -#elif !defined(Q_OS_ANDROID) - _logDialog = new LogDialog(nullptr, getLogger()); - - if (keepOnTop) { - Qt::WindowFlags flags = _logDialog->windowFlags() | Qt::Tool; - _logDialog->setWindowFlags(flags); - } -#endif - } - - if (_logDialog->isVisible()) { - _logDialog->hide(); - } else { - _logDialog->show(); - } -#endif -} - - void Application::recreateLogWindow(int keepOnTop) { - _keepLogWindowOnTop.set(keepOnTop != 0); - if (_logDialog) { - bool toggle = _logDialog->isVisible(); - _logDialog->close(); - _logDialog = nullptr; - - if (toggle) { - toggleLogDialog(); - } - } - } - -void Application::toggleEntityScriptServerLogDialog() { - if (! _entityScriptServerLogDialog) { - _entityScriptServerLogDialog = new EntityScriptServerLogDialog(nullptr); - } - - if (_entityScriptServerLogDialog->isVisible()) { - _entityScriptServerLogDialog->hide(); - } else { - _entityScriptServerLogDialog->show(); - } -} - -void Application::loadAddAvatarBookmarkDialog() const { - auto avatarBookmarks = DependencyManager::get(); -} - -void Application::loadAvatarBrowser() const { - auto tablet = dynamic_cast(DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); - // construct the url to the marketplace item - QString url = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace?category=avatars"; - - QString MARKETPLACES_INJECT_SCRIPT_PATH = "file:///" + qApp->applicationDirPath() + "/scripts/system/html/js/marketplacesInject.js"; - tablet->gotoWebScreen(url, MARKETPLACES_INJECT_SCRIPT_PATH); - DependencyManager::get()->openTablet(); -} - -void Application::takeSnapshot(bool notify, bool includeAnimated, float aspectRatio, const QString& filename) { - postLambdaEvent([notify, includeAnimated, aspectRatio, filename, this] { - // Get a screenshot and save it - QString path = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getScreenshot(aspectRatio), filename, - TestScriptingInterface::getInstance()->getTestResultsLocation()); - - // If we're not doing an animated snapshot as well... - if (!includeAnimated) { - if (!path.isEmpty()) { - // Tell the dependency manager that the capture of the still snapshot has taken place. - emit DependencyManager::get()->stillSnapshotTaken(path, notify); - } - } else if (!SnapshotAnimated::isAlreadyTakingSnapshotAnimated()) { - // Get an animated GIF snapshot and save it - SnapshotAnimated::saveSnapshotAnimated(path, aspectRatio, qApp, DependencyManager::get()); - } - }); -} - -void Application::takeSecondaryCameraSnapshot(const bool& notify, const QString& filename) { - postLambdaEvent([notify, filename, this] { - QString snapshotPath = DependencyManager::get()->saveSnapshot(getActiveDisplayPlugin()->getSecondaryCameraScreenshot(), filename, - TestScriptingInterface::getInstance()->getTestResultsLocation()); - - emit DependencyManager::get()->stillSnapshotTaken(snapshotPath, notify); - }); -} - -void Application::takeSecondaryCamera360Snapshot(const glm::vec3& cameraPosition, const bool& cubemapOutputFormat, const bool& notify, const QString& filename) { - postLambdaEvent([notify, filename, cubemapOutputFormat, cameraPosition] { - DependencyManager::get()->save360Snapshot(cameraPosition, cubemapOutputFormat, notify, filename); - }); -} - -void Application::shareSnapshot(const QString& path, const QUrl& href) { - postLambdaEvent([path, href] { - // not much to do here, everything is done in snapshot code... - DependencyManager::get()->uploadSnapshot(path, href); - }); -} - -float Application::getRenderResolutionScale() const { - auto menu = Menu::getInstance(); - if (!menu) { - return 1.0f; - } - if (menu->isOptionChecked(MenuOption::RenderResolutionOne)) { - return 1.0f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionTwoThird)) { - return 0.666f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionHalf)) { - return 0.5f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionThird)) { - return 0.333f; - } else if (menu->isOptionChecked(MenuOption::RenderResolutionQuarter)) { - return 0.25f; - } else { - return 1.0f; - } -} - -void Application::notifyPacketVersionMismatch() { - if (!_notifiedPacketVersionMismatchThisDomain && !isInterstitialMode()) { - _notifiedPacketVersionMismatchThisDomain = true; - - QString message = "The location you are visiting is running an incompatible server version.\n"; - message += "Content may not display properly."; - - OffscreenUi::asyncWarning("", message); - } -} - -void Application::checkSkeleton() const { - if (getMyAvatar()->getSkeletonModel()->isActive() && !getMyAvatar()->getSkeletonModel()->hasSkeleton()) { - qCDebug(interfaceapp) << "MyAvatar model has no skeleton"; - - QString message = "Your selected avatar body has no skeleton.\n\nThe default body will be loaded..."; - OffscreenUi::asyncWarning("", message); - - getMyAvatar()->useFullAvatarURL(AvatarData::defaultFullAvatarModelUrl(), DEFAULT_FULL_AVATAR_MODEL_NAME); - } else { - _physicsEngine->setCharacterController(getMyAvatar()->getCharacterController()); - } -} - -void Application::activeChanged(Qt::ApplicationState state) { - switch (state) { - case Qt::ApplicationActive: - _isForeground = true; - break; - - case Qt::ApplicationSuspended: - case Qt::ApplicationHidden: - case Qt::ApplicationInactive: - default: - _isForeground = false; - break; - } -} - -void Application::windowMinimizedChanged(bool minimized) { - // initialize the _minimizedWindowTimer - static std::once_flag once; - std::call_once(once, [&] { - connect(&_minimizedWindowTimer, &QTimer::timeout, this, [] { - QCoreApplication::postEvent(QCoreApplication::instance(), new QEvent(static_cast(Idle)), Qt::HighEventPriority); - }); - }); - - // avoid rendering to the display plugin but continue posting Idle events, - // so that physics continues to simulate and the deadlock watchdog knows we're alive - if (!minimized && !getActiveDisplayPlugin()->isActive()) { - _minimizedWindowTimer.stop(); - getActiveDisplayPlugin()->activate(); - } else if (minimized && getActiveDisplayPlugin()->isActive()) { - getActiveDisplayPlugin()->deactivate(); - _minimizedWindowTimer.start(THROTTLED_SIM_FRAME_PERIOD_MS); - } -} - -void Application::postLambdaEvent(const std::function& f) { - if (this->thread() == QThread::currentThread()) { - f(); - } else { - QCoreApplication::postEvent(this, new LambdaEvent(f)); - } -} - -void Application::sendLambdaEvent(const std::function& f) { - if (this->thread() == QThread::currentThread()) { - f(); - } else { - LambdaEvent event(f); - QCoreApplication::sendEvent(this, &event); - } -} - -void Application::initPlugins(const QStringList& arguments) { - QCommandLineOption display("display", "Preferred displays", "displays"); - QCommandLineOption disableDisplays("disable-displays", "Displays to disable", "displays"); - QCommandLineOption disableInputs("disable-inputs", "Inputs to disable", "inputs"); - - QCommandLineParser parser; - parser.addOption(display); - parser.addOption(disableDisplays); - parser.addOption(disableInputs); - parser.parse(arguments); - - if (parser.isSet(display)) { - auto preferredDisplays = parser.value(display).split(',', QString::SkipEmptyParts); - qInfo() << "Setting prefered display plugins:" << preferredDisplays; - PluginManager::getInstance()->setPreferredDisplayPlugins(preferredDisplays); - } - - if (parser.isSet(disableDisplays)) { - auto disabledDisplays = parser.value(disableDisplays).split(',', QString::SkipEmptyParts); - qInfo() << "Disabling following display plugins:" << disabledDisplays; - PluginManager::getInstance()->disableDisplays(disabledDisplays); - } - - if (parser.isSet(disableInputs)) { - auto disabledInputs = parser.value(disableInputs).split(',', QString::SkipEmptyParts); - qInfo() << "Disabling following input plugins:" << disabledInputs; - PluginManager::getInstance()->disableInputs(disabledInputs); - } -} - -void Application::shutdownPlugins() { -} - -glm::uvec2 Application::getCanvasSize() const { - return glm::uvec2(_glWidget->width(), _glWidget->height()); -} - -QRect Application::getRenderingGeometry() const { - auto geometry = _glWidget->geometry(); - auto topLeft = geometry.topLeft(); - auto topLeftScreen = _glWidget->mapToGlobal(topLeft); - geometry.moveTopLeft(topLeftScreen); - return geometry; -} - -glm::uvec2 Application::getUiSize() const { - static const uint MIN_SIZE = 1; - glm::uvec2 result(MIN_SIZE); - if (_displayPlugin) { - result = getActiveDisplayPlugin()->getRecommendedUiSize(); - } - return result; -} - -QRect Application::getRecommendedHUDRect() const { - auto uiSize = getUiSize(); - QRect result(0, 0, uiSize.x, uiSize.y); - if (_displayPlugin) { - result = getActiveDisplayPlugin()->getRecommendedHUDRect(); - } - return result; -} - -glm::vec2 Application::getDeviceSize() const { - static const int MIN_SIZE = 1; - glm::vec2 result(MIN_SIZE); - if (_displayPlugin) { - result = getActiveDisplayPlugin()->getRecommendedRenderSize(); - } - return result; -} - -bool Application::isThrottleRendering() const { - if (_displayPlugin) { - return getActiveDisplayPlugin()->isThrottled(); - } - return false; -} - -bool Application::hasFocus() const { - bool result = (QApplication::activeWindow() != nullptr); -#if defined(Q_OS_WIN) - // On Windows, QWidget::activateWindow() - as called in setFocus() - makes the application's taskbar icon flash but doesn't - // take user focus away from their current window. So also check whether the application is the user's current foreground - // window. - result = result && (HWND)QApplication::activeWindow()->winId() == GetForegroundWindow(); -#endif - return result; -} - -void Application::setFocus() { - // Note: Windows doesn't allow a user focus to be taken away from another application. Instead, it changes the color of and - // flashes the taskbar icon. - auto window = qApp->getWindow(); - window->activateWindow(); -} - -void Application::raise() { - auto windowState = qApp->getWindow()->windowState(); - if (windowState & Qt::WindowMinimized) { - if (windowState & Qt::WindowMaximized) { - qApp->getWindow()->showMaximized(); - } else if (windowState & Qt::WindowFullScreen) { - qApp->getWindow()->showFullScreen(); - } else { - qApp->getWindow()->showNormal(); - } - } - qApp->getWindow()->raise(); -} - -void Application::setMaxOctreePacketsPerSecond(int maxOctreePPS) { - if (maxOctreePPS != _maxOctreePPS) { - _maxOctreePPS = maxOctreePPS; - maxOctreePacketsPerSecond.set(_maxOctreePPS); - } -} - -int Application::getMaxOctreePacketsPerSecond() const { - return _maxOctreePPS; -} - -qreal Application::getDevicePixelRatio() { - return (_window && _window->windowHandle()) ? _window->windowHandle()->devicePixelRatio() : 1.0; -} - -DisplayPluginPointer Application::getActiveDisplayPlugin() const { - if (QThread::currentThread() != thread()) { - std::unique_lock lock(_displayPluginLock); - return _displayPlugin; - } - - if (!_aboutToQuit && !_displayPlugin) { - const_cast(this)->updateDisplayMode(); - Q_ASSERT(_displayPlugin); - } - return _displayPlugin; -} - - -#if !defined(DISABLE_QML) -static const char* EXCLUSION_GROUP_KEY = "exclusionGroup"; - -static void addDisplayPluginToMenu(const DisplayPluginPointer& displayPlugin, int index, bool active) { - auto menu = Menu::getInstance(); - QString name = displayPlugin->getName(); - auto grouping = displayPlugin->getGrouping(); - QString groupingMenu { "" }; - Q_ASSERT(!menu->menuItemExists(MenuOption::OutputMenu, name)); - - // assign the meny grouping based on plugin grouping - switch (grouping) { - case Plugin::ADVANCED: - groupingMenu = "Advanced"; - break; - case Plugin::DEVELOPER: - groupingMenu = "Developer"; - break; - default: - groupingMenu = "Standard"; - break; - } - - static QActionGroup* displayPluginGroup = nullptr; - if (!displayPluginGroup) { - displayPluginGroup = new QActionGroup(menu); - displayPluginGroup->setExclusive(true); - } - auto parent = menu->getMenu(MenuOption::OutputMenu); - auto action = menu->addActionToQMenuAndActionHash(parent, - name, QKeySequence(Qt::CTRL + (Qt::Key_0 + index)), qApp, - SLOT(updateDisplayMode()), - QAction::NoRole, Menu::UNSPECIFIED_POSITION, groupingMenu); - - action->setCheckable(true); - action->setChecked(active); - displayPluginGroup->addAction(action); - - action->setProperty(EXCLUSION_GROUP_KEY, QVariant::fromValue(displayPluginGroup)); - Q_ASSERT(menu->menuItemExists(MenuOption::OutputMenu, name)); -} -#endif - -void Application::updateDisplayMode() { - // Unsafe to call this method from anything but the main thread - if (QThread::currentThread() != thread()) { - qFatal("Attempted to switch display plugins from a non-main thread"); - } - - // Once time initialization code that depends on the UI being available - auto displayPlugins = getDisplayPlugins(); - - // Default to the first item on the list, in case none of the menu items match - DisplayPluginPointer newDisplayPlugin = displayPlugins.at(0); - auto menu = getPrimaryMenu(); - if (menu) { - foreach(DisplayPluginPointer displayPlugin, PluginManager::getInstance()->getDisplayPlugins()) { - QString name = displayPlugin->getName(); - QAction* action = menu->getActionForOption(name); - // Menu might have been removed if the display plugin lost - if (!action) { - continue; - } - if (action->isChecked()) { - newDisplayPlugin = displayPlugin; - break; - } - } - } - - if (newDisplayPlugin == _displayPlugin) { - return; - } - - setDisplayPlugin(newDisplayPlugin); -} - -void Application::setDisplayPlugin(DisplayPluginPointer newDisplayPlugin) { - if (newDisplayPlugin == _displayPlugin) { - return; - } - - // FIXME don't have the application directly set the state of the UI, - // instead emit a signal that the display plugin is changing and let - // the desktop lock itself. Reduces coupling between the UI and display - // plugins - auto offscreenUi = getOffscreenUI(); - auto desktop = offscreenUi ? offscreenUi->getDesktop() : nullptr; - auto menu = Menu::getInstance(); - - // Make the switch atomic from the perspective of other threads - { - std::unique_lock lock(_displayPluginLock); - bool wasRepositionLocked = false; - if (desktop) { - // Tell the desktop to no reposition (which requires plugin info), until we have set the new plugin, below. - wasRepositionLocked = desktop->property("repositionLocked").toBool(); - desktop->setProperty("repositionLocked", true); - } - - if (_displayPlugin) { - disconnect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent); - _displayPlugin->deactivate(); - } - - auto oldDisplayPlugin = _displayPlugin; - bool active = newDisplayPlugin->activate(); - - if (!active) { - auto displayPlugins = PluginManager::getInstance()->getDisplayPlugins(); - - // If the new plugin fails to activate, fallback to last display - qWarning() << "Failed to activate display: " << newDisplayPlugin->getName(); - newDisplayPlugin = oldDisplayPlugin; - - if (newDisplayPlugin) { - qWarning() << "Falling back to last display: " << newDisplayPlugin->getName(); - active = newDisplayPlugin->activate(); - } - - // If there is no last display, or - // If the last display fails to activate, fallback to desktop - if (!active) { - newDisplayPlugin = displayPlugins.at(0); - qWarning() << "Falling back to display: " << newDisplayPlugin->getName(); - active = newDisplayPlugin->activate(); - } - - if (!active) { - qFatal("Failed to activate fallback plugin"); - } - } - - if (offscreenUi) { - offscreenUi->resize(fromGlm(newDisplayPlugin->getRecommendedUiSize())); - } - getApplicationCompositor().setDisplayPlugin(newDisplayPlugin); - _displayPlugin = newDisplayPlugin; - connect(_displayPlugin.get(), &DisplayPlugin::presented, this, &Application::onPresent, Qt::DirectConnection); - if (desktop) { - desktop->setProperty("repositionLocked", wasRepositionLocked); - } - } - - bool isHmd = _displayPlugin->isHmd(); - qCDebug(interfaceapp) << "Entering into" << (isHmd ? "HMD" : "Desktop") << "Mode"; - - // Only log/emit after a successful change - UserActivityLogger::getInstance().logAction("changed_display_mode", { - { "previous_display_mode", _displayPlugin ? _displayPlugin->getName() : "" }, - { "display_mode", newDisplayPlugin ? newDisplayPlugin->getName() : "" }, - { "hmd", isHmd } - }); - emit activeDisplayPluginChanged(); - - // reset the avatar, to set head and hand palms back to a reasonable default pose. - getMyAvatar()->reset(false); - - // switch to first person if entering hmd and setting is checked - if (menu) { - QAction* action = menu->getActionForOption(newDisplayPlugin->getName()); - if (action) { - action->setChecked(true); - } - - if (isHmd && menu->isOptionChecked(MenuOption::FirstPersonHMD)) { - menu->setIsOptionChecked(MenuOption::FirstPerson, true); - cameraMenuChanged(); - } - - // Remove the mirror camera option from menu if in HMD mode - auto mirrorAction = menu->getActionForOption(MenuOption::FullscreenMirror); - mirrorAction->setVisible(!isHmd); - } - - Q_ASSERT_X(_displayPlugin, "Application::updateDisplayMode", "could not find an activated display plugin"); -} - -void Application::switchDisplayMode() { - if (!_autoSwitchDisplayModeSupportedHMDPlugin) { - return; - } - bool currentHMDWornStatus = _autoSwitchDisplayModeSupportedHMDPlugin->isDisplayVisible(); - if (currentHMDWornStatus != _previousHMDWornStatus) { - // Switch to respective mode as soon as currentHMDWornStatus changes - if (currentHMDWornStatus) { - qCDebug(interfaceapp) << "Switching from Desktop to HMD mode"; - endHMDSession(); - setActiveDisplayPlugin(_autoSwitchDisplayModeSupportedHMDPluginName); - } else { - qCDebug(interfaceapp) << "Switching from HMD to desktop mode"; - setActiveDisplayPlugin(DESKTOP_DISPLAY_PLUGIN_NAME); - startHMDStandBySession(); - } - } - _previousHMDWornStatus = currentHMDWornStatus; -} - -void Application::setShowBulletWireframe(bool value) { - _physicsEngine->setShowBulletWireframe(value); -} - -void Application::setShowBulletAABBs(bool value) { - _physicsEngine->setShowBulletAABBs(value); -} - -void Application::setShowBulletContactPoints(bool value) { - _physicsEngine->setShowBulletContactPoints(value); -} - -void Application::setShowBulletConstraints(bool value) { - _physicsEngine->setShowBulletConstraints(value); -} - -void Application::setShowBulletConstraintLimits(bool value) { - _physicsEngine->setShowBulletConstraintLimits(value); -} - -void Application::createLoginDialog() { - const glm::vec3 LOGIN_DIMENSIONS { 0.89f, 0.5f, 0.01f }; - const auto OFFSET = glm::vec2(0.7f, -0.1f); - auto cameraPosition = _myCamera.getPosition(); - auto cameraOrientation = _myCamera.getOrientation(); - auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; - auto headLookVec = (cameraOrientation * Vectors::FRONT); - // DEFAULT_DPI / tablet scale percentage - const float DPI = 31.0f / (75.0f / 100.0f); - auto offset = headLookVec * OFFSET.x; - auto position = (cameraPosition + offset) + (upVec * OFFSET.y); - - EntityItemProperties properties; - properties.setType(EntityTypes::Web); - properties.setName("LoginDialogEntity"); - properties.setSourceUrl(LOGIN_DIALOG.toString()); - properties.setPosition(position); - properties.setRotation(cameraOrientation); - properties.setDimensions(LOGIN_DIMENSIONS); - properties.setPrimitiveMode(PrimitiveMode::SOLID); - properties.getGrab().setGrabbable(false); - properties.setIgnorePickIntersection(false); - properties.setAlpha(1.0f); - properties.setDPI(DPI); - properties.setVisible(true); - - auto entityScriptingInterface = DependencyManager::get(); - _loginDialogID = entityScriptingInterface->addEntityInternal(properties, entity::HostType::LOCAL); - - auto keyboard = DependencyManager::get().data(); - if (!keyboard->getAnchorID().isNull() && !_loginDialogID.isNull()) { - auto keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); - - EntityItemProperties properties; - properties.setPosition(position + keyboardLocalOffset); - properties.setRotation(cameraOrientation * Quaternions::Y_180); - - entityScriptingInterface->editEntity(keyboard->getAnchorID(), properties); - keyboard->setResetKeyboardPositionOnRaise(false); - } - setKeyboardFocusEntity(_loginDialogID); - emit loginDialogFocusEnabled(); - getApplicationCompositor().getReticleInterface()->setAllowMouseCapture(false); - getApplicationCompositor().getReticleInterface()->setVisible(false); - if (!_loginStateManager.isSetUp()) { - _loginStateManager.setUp(); - } -} - -void Application::updateLoginDialogPosition() { - const float LOOK_AWAY_THRESHOLD_ANGLE = 70.0f; - const auto OFFSET = glm::vec2(0.7f, -0.1f); - - auto entityScriptingInterface = DependencyManager::get(); - EntityPropertyFlags desiredProperties; - desiredProperties += PROP_POSITION; - auto properties = entityScriptingInterface->getEntityProperties(_loginDialogID, desiredProperties); - auto positionVec = properties.getPosition(); - auto cameraPositionVec = _myCamera.getPosition(); - auto cameraOrientation = cancelOutRollAndPitch(_myCamera.getOrientation()); - auto headLookVec = (cameraOrientation * Vectors::FRONT); - auto entityToHeadVec = positionVec - cameraPositionVec; - auto pointAngle = (glm::acos(glm::dot(glm::normalize(entityToHeadVec), glm::normalize(headLookVec))) * 180.0f / PI); - auto upVec = getMyAvatar()->getWorldOrientation() * Vectors::UNIT_Y; - auto offset = headLookVec * OFFSET.x; - auto newPositionVec = (cameraPositionVec + offset) + (upVec * OFFSET.y); - - bool outOfBounds = glm::distance(positionVec, cameraPositionVec) > 1.0f; - - if (pointAngle > LOOK_AWAY_THRESHOLD_ANGLE || outOfBounds) { - { - EntityItemProperties properties; - properties.setPosition(newPositionVec); - properties.setRotation(cameraOrientation); - entityScriptingInterface->editEntity(_loginDialogID, properties); - } - - { - glm::vec3 keyboardLocalOffset = cameraOrientation * glm::vec3(-0.4f * getMyAvatar()->getSensorToWorldScale(), -0.3f, 0.2f); - glm::quat keyboardOrientation = cameraOrientation * glm::quat(glm::radians(glm::vec3(-30.0f, 180.0f, 0.0f))); - - EntityItemProperties properties; - properties.setPosition(newPositionVec + keyboardLocalOffset); - properties.setRotation(keyboardOrientation); - entityScriptingInterface->editEntity(DependencyManager::get()->getAnchorID(), properties); - } - } -} - -bool Application::hasRiftControllers() { - return PluginUtils::isOculusTouchControllerAvailable(); -} - -bool Application::hasViveControllers() { - return PluginUtils::isViveControllerAvailable(); -} - -void Application::onDismissedLoginDialog() { - _loginDialogPoppedUp = false; - loginDialogPoppedUp.set(false); - auto keyboard = DependencyManager::get().data(); - keyboard->setResetKeyboardPositionOnRaise(true); - if (!_loginDialogID.isNull()) { - DependencyManager::get()->deleteEntity(_loginDialogID); - _loginDialogID = QUuid(); - _loginStateManager.tearDown(); - } - resumeAfterLoginDialogActionTaken(); -} - -void Application::setShowTrackedObjects(bool value) { - _showTrackedObjects = value; -} - -void Application::startHMDStandBySession() { - _autoSwitchDisplayModeSupportedHMDPlugin->startStandBySession(); -} - -void Application::endHMDSession() { - _autoSwitchDisplayModeSupportedHMDPlugin->endSession(); -} - -mat4 Application::getEyeProjection(int eye) const { - QMutexLocker viewLocker(&_viewMutex); - if (isHMDMode()) { - return getActiveDisplayPlugin()->getEyeProjection((Eye)eye, _viewFrustum.getProjection()); - } - return _viewFrustum.getProjection(); -} - -mat4 Application::getEyeOffset(int eye) const { - // FIXME invert? - return getActiveDisplayPlugin()->getEyeToHeadTransform((Eye)eye); -} - -mat4 Application::getHMDSensorPose() const { - if (isHMDMode()) { - return getActiveDisplayPlugin()->getHeadPose(); - } - return mat4(); -} - -void Application::deadlockApplication() { - qCDebug(interfaceapp) << "Intentionally deadlocked Interface"; - // Using a loop that will *technically* eventually exit (in ~600 billion years) - // to avoid compiler warnings about a loop that will never exit - for (uint64_t i = 1; i != 0; ++i) { - QThread::sleep(1); - } -} - -// cause main thread to be unresponsive for 35 seconds -void Application::unresponsiveApplication() { - // to avoid compiler warnings about a loop that will never exit - uint64_t start = usecTimestampNow(); - uint64_t UNRESPONSIVE_FOR_SECONDS = 35; - uint64_t UNRESPONSIVE_FOR_USECS = UNRESPONSIVE_FOR_SECONDS * USECS_PER_SECOND; - qCDebug(interfaceapp) << "Intentionally cause Interface to be unresponsive for " << UNRESPONSIVE_FOR_SECONDS << " seconds"; - while (usecTimestampNow() - start < UNRESPONSIVE_FOR_USECS) { - QThread::sleep(1); - } -} - -void Application::setActiveDisplayPlugin(const QString& pluginName) { - DisplayPluginPointer newDisplayPlugin; - for (DisplayPluginPointer displayPlugin : PluginManager::getInstance()->getDisplayPlugins()) { - QString name = displayPlugin->getName(); - if (pluginName == name) { - newDisplayPlugin = displayPlugin; - break; - } - } - - if (newDisplayPlugin) { - setDisplayPlugin(newDisplayPlugin); - } -} - -void Application::handleLocalServerConnection() const { - auto server = qobject_cast(sender()); - - qCDebug(interfaceapp) << "Got connection on local server from additional instance - waiting for parameters"; - - auto socket = server->nextPendingConnection(); - - connect(socket, &QLocalSocket::readyRead, this, &Application::readArgumentsFromLocalSocket); - - qApp->getWindow()->raise(); - qApp->getWindow()->activateWindow(); -} - -void Application::readArgumentsFromLocalSocket() const { - auto socket = qobject_cast(sender()); - - auto message = socket->readAll(); - socket->deleteLater(); - - qCDebug(interfaceapp) << "Read from connection: " << message; - - // If we received a message, try to open it as a URL - if (message.length() > 0) { - qApp->openUrl(QString::fromUtf8(message)); - } -} - -void Application::showDesktop() { -} - -CompositorHelper& Application::getApplicationCompositor() const { - return *DependencyManager::get(); -} - - -// virtual functions required for PluginContainer -ui::Menu* Application::getPrimaryMenu() { - auto appMenu = _window->menuBar(); - auto uiMenu = dynamic_cast(appMenu); - return uiMenu; -} - -void Application::showDisplayPluginsTools(bool show) { - DependencyManager::get()->hmdTools(show); -} - -GLWidget* Application::getPrimaryWidget() { - return _glWidget; -} - -MainWindow* Application::getPrimaryWindow() { - return getWindow(); -} - -QOpenGLContext* Application::getPrimaryContext() { - return _glWidget->qglContext(); -} - -bool Application::makeRenderingContextCurrent() { - return true; -} - -bool Application::isForeground() const { - return _isForeground && !_window->isMinimized(); -} - -// FIXME? perhaps two, one for the main thread and one for the offscreen UI rendering thread? -static const int UI_RESERVED_THREADS = 1; -// Windows won't let you have all the cores -static const int OS_RESERVED_THREADS = 1; - -void Application::updateThreadPoolCount() const { - auto reservedThreads = UI_RESERVED_THREADS + OS_RESERVED_THREADS + _displayPlugin->getRequiredThreadCount(); - auto availableThreads = QThread::idealThreadCount() - reservedThreads; - auto threadPoolSize = std::max(MIN_PROCESSING_THREAD_POOL_SIZE, availableThreads); - qCDebug(interfaceapp) << "Ideal Thread Count " << QThread::idealThreadCount(); - qCDebug(interfaceapp) << "Reserved threads " << reservedThreads; - qCDebug(interfaceapp) << "Setting thread pool size to " << threadPoolSize; - QThreadPool::globalInstance()->setMaxThreadCount(threadPoolSize); -} - -void Application::updateSystemTabletMode() { - if (_settingsLoaded && !getLoginDialogPoppedUp()) { - qApp->setProperty(hifi::properties::HMD, isHMDMode()); - if (isHMDMode()) { - DependencyManager::get()->setToolbarMode(getHmdTabletBecomesToolbarSetting()); - } else { - DependencyManager::get()->setToolbarMode(getDesktopTabletBecomesToolbarSetting()); - } - } -} - -QUuid Application::getTabletScreenID() const { - auto HMD = DependencyManager::get(); - return HMD->getCurrentTabletScreenID(); -} - -QUuid Application::getTabletHomeButtonID() const { - auto HMD = DependencyManager::get(); - return HMD->getCurrentHomeButtonID(); -} - -QUuid Application::getTabletFrameID() const { - auto HMD = DependencyManager::get(); - return HMD->getCurrentTabletFrameID(); -} - -QVector Application::getTabletIDs() const { - // Most important first. - QVector result; - auto HMD = DependencyManager::get(); - result << HMD->getCurrentTabletScreenID(); - result << HMD->getCurrentHomeButtonID(); - result << HMD->getCurrentTabletFrameID(); - return result; -} - -void Application::setAvatarOverrideUrl(const QUrl& url, bool save) { - _avatarOverrideUrl = url; - _saveAvatarOverrideUrl = save; -} - -void Application::saveNextPhysicsStats(QString filename) { - _physicsEngine->saveNextPhysicsStats(filename); -} - -void Application::copyToClipboard(const QString& text) { - if (QThread::currentThread() != qApp->thread()) { - QMetaObject::invokeMethod(this, "copyToClipboard"); - return; - } - - // assume that the address is being copied because the user wants a shareable address - QApplication::clipboard()->setText(text); -} - -QString Application::getGraphicsCardType() { - return GPUIdent::getInstance()->getName(); -} - -#if defined(Q_OS_ANDROID) -void Application::beforeEnterBackground() { - auto nodeList = DependencyManager::get(); - nodeList->setSendDomainServerCheckInEnabled(false); - nodeList->reset(true); - clearDomainOctreeDetails(); -} - - - -void Application::enterBackground() { - QMetaObject::invokeMethod(DependencyManager::get().data(), - "stop", Qt::BlockingQueuedConnection); -// Quest only supports one plugin which can't be deactivated currently -#if !defined(ANDROID_APP_QUEST_INTERFACE) - if (getActiveDisplayPlugin()->isActive()) { - getActiveDisplayPlugin()->deactivate(); - } -#endif -} - -void Application::enterForeground() { - QMetaObject::invokeMethod(DependencyManager::get().data(), - "start", Qt::BlockingQueuedConnection); -// Quest only supports one plugin which can't be deactivated currently -#if !defined(ANDROID_APP_QUEST_INTERFACE) - if (!getActiveDisplayPlugin() || getActiveDisplayPlugin()->isActive() || !getActiveDisplayPlugin()->activate()) { - qWarning() << "Could not re-activate display plugin"; - } -#endif - auto nodeList = DependencyManager::get(); - nodeList->setSendDomainServerCheckInEnabled(true); -} - - -void Application::toggleAwayMode(){ - QKeyEvent event = QKeyEvent (QEvent::KeyPress, Qt::Key_Escape, Qt::NoModifier); - QCoreApplication::sendEvent (this, &event); -} - - -#endif - - -#include "Application.moc" From 7567e0d355b8ac58ae6b40f1c9c7953465f128d0 Mon Sep 17 00:00:00 2001 From: amantley Date: Tue, 12 Mar 2019 15:48:42 -0700 Subject: [PATCH 11/63] debugging the root of the fbx, it is not 0 in some cases --- libraries/fbx/src/FBXSerializer.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index e6e3d73815..f91eeb6519 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -508,6 +508,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } else if (child.name == "Objects") { + //qCDebug(modelformat) << " the root model id is " << getID(child.properties); foreach (const FBXNode& object, child.children) { nodeParentId++; if (object.name == "Geometry") { @@ -1169,8 +1170,19 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr counter++; } } - _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); - _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + + if ("2830302416448" == getID(connection.properties, 1) || "2830302416448" == getID(connection.properties, 2)) { + if ("2829544143536" == getID(connection.properties, 1)) { + _connectionParentMap.insert(getID(connection.properties, 1), "0"); + _connectionChildMap.insert("0", getID(connection.properties, 1)); + } + qCDebug(modelformat) << " parent map inserted with id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)) << " id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)); + } else { + _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); + _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); + } + + // qCDebug(modelformat) << " child map inserted with id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)) << " id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)); } } } @@ -1220,13 +1232,16 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) * glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale)); + for (QHash::const_iterator modelIDPair = modelIDsToNames.constBegin(); modelIDPair != modelIDsToNames.constEnd(); modelIDPair++) { + qCDebug(modelformat) << " model ID " << modelIDPair.key() << " name " << modelIDPair.value(); + } + // get the list of models in depth-first traversal order QVector modelIDs; QSet remainingFBXModels; for (QHash::const_iterator fbxModel = fbxModels.constBegin(); fbxModel != fbxModels.constEnd(); fbxModel++) { // models with clusters must be parented to the cluster top // Unless the model is a root node. - qCDebug(modelformat) << "fbx model name " << fbxModel.key(); bool isARootNode = !modelIDs.contains(_connectionParentMap.value(fbxModel.key())); if (!isARootNode) { foreach(const QString& deformerID, _connectionChildMap.values(fbxModel.key())) { @@ -1235,6 +1250,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr continue; } QString topID = getTopModelID(_connectionParentMap, fbxModels, _connectionChildMap.value(clusterID), url); + qCDebug(modelformat) << "fbx model name " << fbxModel.value().name << " top id " << topID << " modelID " << fbxModel.key(); _connectionChildMap.remove(_connectionParentMap.take(fbxModel.key()), fbxModel.key()); _connectionParentMap.insert(fbxModel.key(), topID); goto outerBreak; @@ -1258,6 +1274,8 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } QString topID = getTopModelID(_connectionParentMap, fbxModels, first, url); + qCDebug(modelformat) << "topper name fbx name " << modelIDsToNames.value(first) << " top id " << topID << " top name " << modelIDsToNames.value(topID); + qCDebug(modelformat) << "parent id " << _connectionParentMap.value(topID) << " parent name " << modelIDsToNames.value(_connectionParentMap.value(topID)) << " remaining models parent value " << remainingFBXModels.contains(_connectionParentMap.value(topID)); appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, fbxModels, remainingFBXModels, modelIDs, true); } From fff0d1a80e6801ad44e90974474289fb71b311cf Mon Sep 17 00:00:00 2001 From: Anthony Thibault Date: Tue, 12 Mar 2019 17:43:23 -0700 Subject: [PATCH 12/63] Rig.cpp: Fix for index out of range assert in debug builds --- libraries/animation/src/Rig.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 07bdfde189..fb55bd2c98 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -1210,7 +1210,8 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons _networkAnimState.blendTime += deltaTime; alpha = _computeNetworkAnimation ? (_networkAnimState.blendTime / TOTAL_BLEND_TIME) : (1.0f - (_networkAnimState.blendTime / TOTAL_BLEND_TIME)); alpha = glm::clamp(alpha, 0.0f, 1.0f); - for (size_t i = 0; i < _networkPoseSet._relativePoses.size(); i++) { + size_t numJoints = std::min(_networkPoseSet._relativePoses.size(), _internalPoseSet._relativePoses.size()); + for (size_t i = 0; i < numJoints; i++) { _networkPoseSet._relativePoses[i].blend(_internalPoseSet._relativePoses[i], alpha); } } From ddd411e1137935ca03ea4da0f97cb21a7d86a844 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Tue, 12 Mar 2019 17:46:52 -0700 Subject: [PATCH 13/63] Removed test code. --- interface/src/Application.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7dddaecadb..de4a6bb167 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1804,6 +1804,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo }); connect(offscreenUi.data(), &OffscreenUi::keyboardFocusActive, [this]() { +#if !defined(Q_OS_ANDROID) && !defined(DISABLE_QML) // Do not show login dialog if requested not to on the command line QString hifiNoLoginCommandLineKey = QString("--").append(HIFI_NO_LOGIN_COMMAND_LINE_KEY); int index = arguments().indexOf(hifiNoLoginCommandLineKey); @@ -1813,6 +1814,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo } showLoginScreen(); +#else + resumeAfterLoginDialogActionTaken(); +#endif }); // Make sure we don't time out during slow operations at startup From 93d7a4ae3b4ade0ac97017ee4737622cad825f5c Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 13 Mar 2019 11:14:15 -0700 Subject: [PATCH 14/63] will no longer allow a non-zero parent of the root of an fbx model --- libraries/animation/src/AnimClip.cpp | 2 +- libraries/fbx/src/FBXSerializer.cpp | 39 +++------------------------- 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 5d846a8f84..4fe02e9307 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -154,7 +154,7 @@ void AnimClip::copyFromNetworkAnim() { const float avatarToAnimationHeightRatio = avatarHeightInMeters / animHeightInMeters; const float unitsRatio = 1.0f / (avatarUnitScale / animationUnitScale); const float parentScaleRatio = 1.0f / (avatarHipsParentScale / animHipsParentScale); - qCDebug(animation) << " avatar unit scale " << avatarUnitScale << " animation unit scale " << animationUnitScale << " avatar height " << avatarHeightInMeters << " animation height " << animHeightInMeters << " avatar scale " << avatarHipsParentScale << " animation scale " << animHipsParentScale; + boneLengthScale = avatarToAnimationHeightRatio * unitsRatio * parentScaleRatio; } diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index c542eef6a6..4bb499bd84 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -142,7 +142,6 @@ glm::mat4 getGlobalTransform(const QMultiMap& _connectionParen visitedNodes.append(nodeID); // Append each node we visit const FBXModel& fbxModel = fbxModels.value(nodeID); - qCDebug(modelformat) << "this fbx model name is " << fbxModel.name; globalTransform = glm::translate(fbxModel.translation) * fbxModel.preTransform * glm::mat4_cast(fbxModel.preRotation * fbxModel.rotation * fbxModel.postRotation) * fbxModel.postTransform * globalTransform; if (fbxModel.hasGeometricOffset) { @@ -202,23 +201,13 @@ public: void appendModelIDs(const QString& parentID, const QMultiMap& connectionChildMap, QHash& fbxModels, QSet& remainingModels, QVector& modelIDs, bool isRootNode = false) { if (remainingModels.contains(parentID)) { - qCDebug(modelformat) << " remaining models contains parent " << parentID; modelIDs.append(parentID); remainingModels.remove(parentID); } - int parentIndex = 1000; - if (isRootNode) { - qCDebug(modelformat) << " found a root node " << parentID; - parentIndex = -1; - } else { - parentIndex = modelIDs.size() - 1; - } - //int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; + int parentIndex = isRootNode ? -1 : modelIDs.size() - 1; foreach (const QString& childID, connectionChildMap.values(parentID)) { - qCDebug(modelformat) << " searching children, parent id " << parentID; if (remainingModels.contains(childID)) { FBXModel& fbxModel = fbxModels[childID]; - qCDebug(modelformat) << " child id " << fbxModel.name; if (fbxModel.parentIndex == -1) { fbxModel.parentIndex = parentIndex; appendModelIDs(childID, connectionChildMap, fbxModels, remainingModels, modelIDs); @@ -452,7 +441,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; - int nodeParentId = -1; foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { @@ -508,9 +496,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } else if (child.name == "Objects") { - //qCDebug(modelformat) << " the root model id is " << getID(child.properties); foreach (const FBXNode& object, child.children) { - nodeParentId++; if (object.name == "Geometry") { if (object.properties.at(2) == "Mesh") { meshes.insert(getID(object.properties), extractMesh(object, meshIndex)); @@ -519,7 +505,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr blendshapes.append(extracted); } } else if (object.name == "Model") { - qCDebug(modelformat) << "model name from object properties " << getName(object.properties) << " node parentID " << nodeParentId; QString name = getName(object.properties); QString id = getID(object.properties); modelIDsToNames.insert(id, name); @@ -1174,19 +1159,13 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr counter++; } } - - if ("2830302416448" == getID(connection.properties, 1) || "2830302416448" == getID(connection.properties, 2)) { - if ("2829544143536" == getID(connection.properties, 1)) { - _connectionParentMap.insert(getID(connection.properties, 1), "0"); - _connectionChildMap.insert("0", getID(connection.properties, 1)); - } - qCDebug(modelformat) << " parent map inserted with id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)) << " id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)); + if (_connectionParentMap.value(getID(connection.properties, 1)) == "0") { + // don't assign the new parent + qCDebug(modelformat) << "root node " << getID(connection.properties, 1) << " has discarded parent " << getID(connection.properties, 2); } else { _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } - - // qCDebug(modelformat) << " child map inserted with id " << getID(connection.properties, 2) << " name " << modelIDsToNames.value(getID(connection.properties, 2)) << " id " << getID(connection.properties, 1) << " name " << modelIDsToNames.value(getID(connection.properties, 1)); } } } @@ -1211,7 +1190,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr #endif } - // TODO: check if is code is needed if (!lights.empty()) { if (hifiGlobalNodeID.isEmpty()) { @@ -1236,10 +1214,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr mapping.value("tz").toFloat())) * glm::mat4_cast(offsetRotation) * glm::scale(glm::vec3(offsetScale, offsetScale, offsetScale)); - for (QHash::const_iterator modelIDPair = modelIDsToNames.constBegin(); modelIDPair != modelIDsToNames.constEnd(); modelIDPair++) { - qCDebug(modelformat) << " model ID " << modelIDPair.key() << " name " << modelIDPair.value(); - } - // get the list of models in depth-first traversal order QVector modelIDs; QSet remainingFBXModels; @@ -1254,7 +1228,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr continue; } QString topID = getTopModelID(_connectionParentMap, fbxModels, _connectionChildMap.value(clusterID), url); - qCDebug(modelformat) << "fbx model name " << fbxModel.value().name << " top id " << topID << " modelID " << fbxModel.key(); _connectionChildMap.remove(_connectionParentMap.take(fbxModel.key()), fbxModel.key()); _connectionParentMap.insert(fbxModel.key(), topID); goto outerBreak; @@ -1278,8 +1251,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } QString topID = getTopModelID(_connectionParentMap, fbxModels, first, url); - qCDebug(modelformat) << "topper name fbx name " << modelIDsToNames.value(first) << " top id " << topID << " top name " << modelIDsToNames.value(topID); - qCDebug(modelformat) << "parent id " << _connectionParentMap.value(topID) << " parent name " << modelIDsToNames.value(_connectionParentMap.value(topID)) << " remaining models parent value " << remainingFBXModels.contains(_connectionParentMap.value(topID)); appendModelIDs(_connectionParentMap.value(topID), _connectionChildMap, fbxModels, remainingFBXModels, modelIDs, true); } @@ -1304,8 +1275,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.parentIndex = fbxModel.parentIndex; int jointIndex = hfmModel.joints.size(); - qCDebug(modelformat) << "fbx joint name " << fbxModel.name << " joint index " << jointIndex << " parent index " << joint.parentIndex; - joint.translation = fbxModel.translation; // these are usually in centimeters joint.preTransform = fbxModel.preTransform; joint.preRotation = fbxModel.preRotation; From 9d739277c8a358376532ded6156d587ca638871b Mon Sep 17 00:00:00 2001 From: amantley Date: Wed, 13 Mar 2019 14:26:27 -0700 Subject: [PATCH 15/63] changed the fix so that we allow the root to be child --- libraries/fbx/src/FBXSerializer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 4bb499bd84..f4eb1d57d9 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -1162,6 +1162,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr if (_connectionParentMap.value(getID(connection.properties, 1)) == "0") { // don't assign the new parent qCDebug(modelformat) << "root node " << getID(connection.properties, 1) << " has discarded parent " << getID(connection.properties, 2); + _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); } else { _connectionParentMap.insert(getID(connection.properties, 1), getID(connection.properties, 2)); _connectionChildMap.insert(getID(connection.properties, 2), getID(connection.properties, 1)); From c14b135f2b960acad5d3a089a1edddd82b826777 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Wed, 13 Mar 2019 15:42:04 -0700 Subject: [PATCH 16/63] Fix flow touch and scale issues --- interface/src/avatar/MyAvatar.cpp | 3 +- libraries/animation/src/Flow.cpp | 66 ++++++++++++++++++++----------- libraries/animation/src/Flow.h | 8 +++- 3 files changed, 52 insertions(+), 25 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 9211be3b4f..ecf904c7f4 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5381,7 +5381,7 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys } auto collisionJoints = collisionsConfig.keys(); if (collisionJoints.size() > 0) { - collisionSystem.resetCollisions(); + collisionSystem.clearSelfCollisions(); for (auto &jointName : collisionJoints) { int jointIndex = getJointIndex(jointName); FlowCollisionSettings collisionsSettings; @@ -5396,6 +5396,7 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys collisionSystem.addCollisionSphere(jointIndex, collisionsSettings); } } + flow.updateScale(); } } diff --git a/libraries/animation/src/Flow.cpp b/libraries/animation/src/Flow.cpp index 5bc2021e5e..1f9e72bf28 100644 --- a/libraries/animation/src/Flow.cpp +++ b/libraries/animation/src/Flow.cpp @@ -67,17 +67,23 @@ void FlowCollisionSystem::addCollisionSphere(int jointIndex, const FlowCollision auto collision = FlowCollisionSphere(jointIndex, settings, isTouch); collision.setPosition(position); if (isSelfCollision) { - _selfCollisions.push_back(collision); + if (!isTouch) { + _selfCollisions.push_back(collision); + } else { + _selfTouchCollisions.push_back(collision); + } } else { _othersCollisions.push_back(collision); } - }; + void FlowCollisionSystem::resetCollisions() { _allCollisions.clear(); _othersCollisions.clear(); + _selfTouchCollisions.clear(); _selfCollisions.clear(); } + FlowCollisionResult FlowCollisionSystem::computeCollision(const std::vector collisions) { FlowCollisionResult result; if (collisions.size() > 1) { @@ -106,6 +112,10 @@ void FlowCollisionSystem::setScale(float scale) { _selfCollisions[j]._radius = _selfCollisions[j]._initialRadius * scale; _selfCollisions[j]._offset = _selfCollisions[j]._initialOffset * scale; } + for (size_t j = 0; j < _selfTouchCollisions.size(); j++) { + _selfTouchCollisions[j]._radius = _selfTouchCollisions[j]._initialRadius * scale; + _selfTouchCollisions[j]._offset = _selfTouchCollisions[j]._initialOffset * scale; + } }; std::vector FlowCollisionSystem::checkFlowThreadCollisions(FlowThread* flowThread) { @@ -178,9 +188,9 @@ void FlowCollisionSystem::setCollisionSettingsByJoint(int jointIndex, const Flow } void FlowCollisionSystem::prepareCollisions() { _allCollisions.clear(); - _allCollisions.resize(_selfCollisions.size() + _othersCollisions.size()); - std::copy(_selfCollisions.begin(), _selfCollisions.begin() + _selfCollisions.size(), _allCollisions.begin()); - std::copy(_othersCollisions.begin(), _othersCollisions.begin() + _othersCollisions.size(), _allCollisions.begin() + _selfCollisions.size()); + _allCollisions.insert(_allCollisions.end(), _selfCollisions.begin(), _selfCollisions.end()); + _allCollisions.insert(_allCollisions.end(), _othersCollisions.begin(), _othersCollisions.end()); + _allCollisions.insert(_allCollisions.end(), _selfTouchCollisions.begin(), _selfTouchCollisions.end()); _othersCollisions.clear(); } @@ -273,18 +283,20 @@ void FlowJoint::setRecoveryPosition(const glm::vec3& recoveryPosition) { } void FlowJoint::update(float deltaTime) { - glm::vec3 accelerationOffset = glm::vec3(0.0f); - if (_settings._stiffness > 0.0f) { - glm::vec3 recoveryVector = _recoveryPosition - _currentPosition; - float recoveryFactor = powf(_settings._stiffness, 3.0f); - accelerationOffset = recoveryVector * recoveryFactor; - } - FlowNode::update(deltaTime, accelerationOffset); - if (_anchored) { - if (!_isHelper) { - _currentPosition = _updatedPosition; - } else { - _currentPosition = _parentPosition; + if (_settings._active) { + glm::vec3 accelerationOffset = glm::vec3(0.0f); + if (_settings._stiffness > 0.0f) { + glm::vec3 recoveryVector = _recoveryPosition - _currentPosition; + float recoveryFactor = powf(_settings._stiffness, 3.0f); + accelerationOffset = recoveryVector * recoveryFactor; + } + FlowNode::update(deltaTime, accelerationOffset); + if (_anchored) { + if (!_isHelper) { + _currentPosition = _updatedPosition; + } else { + _currentPosition = _parentPosition; + } } } }; @@ -674,6 +686,14 @@ bool Flow::updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t thr return true; } +void Flow::updateCollisionJoint(FlowCollisionSphere& collision, AnimPoseVec& absolutePoses) { + glm::quat jointRotation; + getJointPositionInWorldFrame(absolutePoses, collision._jointIndex, collision._position, _entityPosition, _entityRotation); + getJointRotationInWorldFrame(absolutePoses, collision._jointIndex, jointRotation, _entityRotation); + glm::vec3 worldOffset = jointRotation * collision._offset; + collision._position = collision._position + worldOffset; +} + void Flow::updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) { updateAbsolutePoses(relativePoses, absolutePoses); for (auto &jointData : _flowJointData) { @@ -695,11 +715,11 @@ void Flow::updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses) } auto &selfCollisions = _collisionSystem.getSelfCollisions(); for (auto &collision : selfCollisions) { - glm::quat jointRotation; - getJointPositionInWorldFrame(absolutePoses, collision._jointIndex, collision._position, _entityPosition, _entityRotation); - getJointRotationInWorldFrame(absolutePoses, collision._jointIndex, jointRotation, _entityRotation); - glm::vec3 worldOffset = jointRotation * collision._offset; - collision._position = collision._position + worldOffset; + updateCollisionJoint(collision, absolutePoses); + } + auto &selfTouchCollisions = _collisionSystem.getSelfTouchCollisions(); + for (auto &collision : selfTouchCollisions) { + updateCollisionJoint(collision, absolutePoses); } _collisionSystem.prepareCollisions(); } @@ -710,7 +730,7 @@ void Flow::setJoints(AnimPoseVec& relativePoses, const std::vector& overri for (int jointIndex : joints) { auto &joint = _flowJointData[jointIndex]; if (jointIndex >= 0 && jointIndex < (int)relativePoses.size() && !overrideFlags[jointIndex]) { - relativePoses[jointIndex].rot() = joint.getCurrentRotation(); + relativePoses[jointIndex].rot() = joint.getSettings()._active ? joint.getCurrentRotation() : joint.getInitialRotation(); } } } diff --git a/libraries/animation/src/Flow.h b/libraries/animation/src/Flow.h index ad81c2be77..5dc1a3ba3e 100644 --- a/libraries/animation/src/Flow.h +++ b/libraries/animation/src/Flow.h @@ -140,6 +140,7 @@ public: std::vector checkFlowThreadCollisions(FlowThread* flowThread); std::vector& getSelfCollisions() { return _selfCollisions; }; + std::vector& getSelfTouchCollisions() { return _selfTouchCollisions; }; void setOthersCollisions(const std::vector& othersCollisions) { _othersCollisions = othersCollisions; } void prepareCollisions(); void resetCollisions(); @@ -150,9 +151,11 @@ public: void setActive(bool active) { _active = active; } bool getActive() const { return _active; } const std::vector& getCollisions() const { return _selfCollisions; } + void clearSelfCollisions() { _selfCollisions.clear(); } protected: std::vector _selfCollisions; std::vector _othersCollisions; + std::vector _selfTouchCollisions; std::vector _allCollisions; float _scale { 1.0f }; bool _active { false }; @@ -210,7 +213,7 @@ public: bool isHelper() const { return _isHelper; } const FlowPhysicsSettings& getSettings() { return _settings; } - void setSettings(const FlowPhysicsSettings& settings) { _settings = settings; } + void setSettings(const FlowPhysicsSettings& settings) { _settings = settings; _initialRadius = _settings._radius; } const glm::vec3& getCurrentPosition() const { return _currentPosition; } int getIndex() const { return _index; } @@ -222,6 +225,7 @@ public: const glm::quat& getCurrentRotation() const { return _currentRotation; } const glm::vec3& getCurrentTranslation() const { return _initialTranslation; } const glm::vec3& getInitialPosition() const { return _initialPosition; } + const glm::quat& getInitialRotation() const { return _initialRotation; } bool isColliding() const { return _colliding; } protected: @@ -297,6 +301,7 @@ public: void setPhysicsSettingsForGroup(const QString& group, const FlowPhysicsSettings& settings); const std::map& getGroupSettings() const { return _groupSettings; } void cleanUp(); + void updateScale() { setScale(_scale); } signals: void onCleanup(); @@ -311,6 +316,7 @@ private: void setJoints(AnimPoseVec& relativePoses, const std::vector& overrideFlags); void updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses); + void updateCollisionJoint(FlowCollisionSphere& collision, AnimPoseVec& absolutePoses); bool updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t threadIndex); void updateGroupSettings(const QString& group, const FlowPhysicsSettings& settings); void setScale(float scale); From 748a80fed718bfe24a01ba1998ef3b9b772d9c26 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 14 Mar 2019 20:02:19 -0700 Subject: [PATCH 17/63] Store device ifconfig results in ifconfig.txt. --- tools/nitpick/src/TestRunnerMobile.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index ed48c37290..73f1da5d26 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -67,14 +67,26 @@ void TestRunnerMobile::connectDevice() { if (!_adbInterface) { _adbInterface = new AdbInterface(); } - + + // Get list of devices QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString command = _adbInterface->getAdbCommand() + " devices -l > " + devicesFullFilename; appendLog(command); system(command.toStdString().c_str()); if (!QFile::exists(devicesFullFilename)) { - QMessageBox::critical(0, "Internal error", "devicesFullFilename not found"); + QMessageBox::critical(0, "Internal error", "devices.txt not found"); + exit (-1); + } + + // Get device IP address + QString ifconfigFullFilename{ _workingFolder + "/ifconfig.txt" }; + command = _adbInterface->getAdbCommand() + " shell ifconfig > " + ifconfigFullFilename; + appendLog(command); + system(command.toStdString().c_str()); + + if (!QFile::exists(ifconfigFullFilename)) { + QMessageBox::critical(0, "Internal error", "ifconfig.txt not found"); exit (-1); } From cc2868a6f4ba5097d88045863584e3e83c7d3862 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 15 Mar 2019 12:46:47 -0700 Subject: [PATCH 18/63] Can get server IP. --- tools/nitpick/src/TestRunnerMobile.cpp | 84 +++++++++++++++++++++++++- tools/nitpick/src/TestRunnerMobile.h | 3 + 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 73f1da5d26..ad5151bcc0 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -9,6 +9,7 @@ // #include "TestRunnerMobile.h" +#include #include #include #include @@ -67,7 +68,7 @@ void TestRunnerMobile::connectDevice() { if (!_adbInterface) { _adbInterface = new AdbInterface(); } - + // Get list of devices QString devicesFullFilename{ _workingFolder + "/devices.txt" }; QString command = _adbInterface->getAdbCommand() + " devices -l > " + devicesFullFilename; @@ -202,6 +203,8 @@ void TestRunnerMobile::runInterface() { _adbInterface = new AdbInterface(); } + sendServerIPToDevice(); + _statusLabel->setText("Starting Interface"); QString testScript = (_runFullSuite->isChecked()) @@ -245,3 +248,82 @@ void TestRunnerMobile::pullFolder() { _statusLabel->setText("Pull complete"); #endif } + +void TestRunnerMobile::sendServerIPToDevice() { + // Get device IP + QFile ifconfigFile{ _workingFolder + "/ifconfig.txt" }; + if (!ifconfigFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Could not open 'ifconfig.txt'"); + exit(-1); + } + + QTextStream stream(&ifconfigFile); + QString line = ifconfigFile.readLine(); + while (!line.isNull()) { + // The device IP is in the line following the "wlan0" line + line = ifconfigFile.readLine(); + if (line.left(6) == "wlan0 ") { + break; + } + } + + // The following line looks like this "inet addr:192.168.0.15 Bcast:192.168.0.255 Mask:255.255.255.0" + // Extract the address and mask + line = ifconfigFile.readLine(); + QStringList lineParts = line.split(':'); + if (lineParts.size() < 4) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "IP address line not in expected format: " + line); + exit(-1); + } + + qint64 deviceIP = convertToBinary(lineParts[1].split(' ')[0]); + qint64 deviceMask = convertToBinary(lineParts[3].split(' ')[0]); + qint64 deviceSubnet = deviceMask & deviceIP; + + // The device needs to be on the same subnet as the server + // To find which of our IPs is the server - choose the 1st that is on the same subnet + // If more than one found then report an error + + QString serverIP; + + QList interfaces = QNetworkInterface::allInterfaces(); + for (int i = 0; i < interfaces.count(); i++) { + QList entries = interfaces.at(i).addressEntries(); + for (int j = 0; j < entries.count(); j++) { + if (entries.at(j).ip().protocol() == QAbstractSocket::IPv4Protocol) { + qint64 hostIP = convertToBinary(entries.at(j).ip().toString()); + qint64 hostMask = convertToBinary(entries.at(j).netmask().toString()); + qint64 hostSubnet = hostMask & hostIP; + + if (hostSubnet == deviceSubnet) { + if (!serverIP.isNull()) { + QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Cannot identify server IP (multiple interfaces on device submask)"); + return; + } else { + union { + uint32_t ip; + uint8_t bytes[4]; + } u; + u.ip = hostIP; + + serverIP = QString::number(u.bytes[3]) + '.' + QString::number(u.bytes[2]) + '.' + QString::number(u.bytes[1]) + '.' + QString::number(u.bytes[0]); + } + } + } + } + } + + ifconfigFile.close(); +} + +qint64 TestRunnerMobile::convertToBinary(const QString& str) { + QString binary; + foreach (const QString& s, str.split(".")) { + binary += QString::number(s.toInt(), 2).rightJustified(8, '0'); + } + + return binary.toLongLong(NULL, 2); +} \ No newline at end of file diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index 7dbf5456b3..7554a075c8 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -52,6 +52,9 @@ public: void pullFolder(); + void sendServerIPToDevice(); + qint64 convertToBinary (const QString& str); + private: QPushButton* _connectDeviceButton; QPushButton* _pullFolderButton; From 9e8e389e99c406c22dadea2987e07cd8e9075aaa Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 15 Mar 2019 13:38:43 -0700 Subject: [PATCH 19/63] Improved detection of device model. --- tools/nitpick/src/TestRunnerMobile.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index ad5151bcc0..72b3ea5cc9 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -98,6 +98,8 @@ void TestRunnerMobile::connectDevice() { QString line2 = devicesFile.readLine(); const QString DEVICE{ "device" }; + const QString MODEL{ "model" }; + if (line2.contains("unauthorized")) { QMessageBox::critical(0, "Unauthorized device detected", "Please allow USB debugging on device"); } else if (line2.contains(DEVICE)) { @@ -110,10 +112,21 @@ void TestRunnerMobile::connectDevice() { QStringList tokens = line2.split(QRegExp("[\r\n\t ]+")); QString deviceID = tokens[0]; - QString modelID = tokens[3].split(':')[1]; + // Find the model entry + int i; + for (i = 0; i < tokens.size(); ++i) { + if (tokens[i].contains(MODEL)) { + break; + } + } + _modelName = "UNKNOWN"; - if (modelNames.count(modelID) == 1) { - _modelName = modelNames[modelID]; + if (i < tokens.size()) { + QString modelID = tokens[i].split(':')[1]; + + if (modelNames.count(modelID) == 1) { + _modelName = modelNames[modelID]; + } } _detectedDeviceLabel->setText(_modelName + " [" + deviceID + "]"); From b91a1d930a1303b3d0a9c38daa3115f79176862e Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 15 Mar 2019 13:44:01 -0700 Subject: [PATCH 20/63] Missing newline. --- tools/nitpick/src/TestRunnerMobile.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 72b3ea5cc9..0710e48008 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -339,4 +339,4 @@ qint64 TestRunnerMobile::convertToBinary(const QString& str) { } return binary.toLongLong(NULL, 2); -} \ No newline at end of file +} From 2300fe471d8909aaeb63b664cd095220075fdfb3 Mon Sep 17 00:00:00 2001 From: amantley Date: Thu, 14 Mar 2019 13:18:12 -0700 Subject: [PATCH 21/63] we now read frame zero of the animation to override the bind pose in fbxs that are before version 7500 --- libraries/fbx/src/FBXSerializer.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 52f4189bdb..8ff3005ddc 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -441,6 +441,7 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr QString hifiGlobalNodeID; unsigned int meshIndex = 0; haveReportedUnhandledRotationOrder = false; + int fbxVersionNumber = -1; foreach (const FBXNode& child, node.children) { if (child.name == "FBXHeaderExtension") { @@ -463,6 +464,9 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } } + } else if (object.name == "FBXVersion") { + fbxVersionNumber = object.properties.at(0).toInt(); + qCDebug(modelformat) << "the fbx version number " << fbxVersionNumber; } } } else if (child.name == "GlobalSettings") { @@ -1309,8 +1313,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr joint.bindTransformFoundInCluster = false; - hfmModel.joints.append(joint); - QString rotationID = localRotations.value(modelID); AnimationCurve xRotCurve = animationCurves.value(xComponents.value(rotationID)); AnimationCurve yRotCurve = animationCurves.value(yComponents.value(rotationID)); @@ -1333,7 +1335,13 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr xPosCurve.values.isEmpty() ? defaultPosValues.x : xPosCurve.values.at(i % xPosCurve.values.size()), yPosCurve.values.isEmpty() ? defaultPosValues.y : yPosCurve.values.at(i % yPosCurve.values.size()), zPosCurve.values.isEmpty() ? defaultPosValues.z : zPosCurve.values.at(i % zPosCurve.values.size())); + if ((fbxVersionNumber < 7500) && (i == 0)) { + joint.translation = hfmModel.animationFrames[i].translations[jointIndex]; + joint.rotation = hfmModel.animationFrames[i].rotations[jointIndex]; + } + } + hfmModel.joints.append(joint); } // NOTE: shapeVertices are in joint-frame From 8947d4d133320b44001433d9330b274cc85ec96e Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 20 Mar 2019 15:45:06 -0700 Subject: [PATCH 22/63] When adding new Node clear any dangling Connection objects to its address --- libraries/networking/src/LimitedNodeList.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index a9dbc12b09..18a180ad79 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -677,6 +677,9 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t // If there is a new node with the same socket, this is a reconnection, kill the old node removeOldNode(findNodeWithAddr(publicSocket)); removeOldNode(findNodeWithAddr(localSocket)); + // If there is an old Connection to the new node's address kill it + _nodeSocket.cleanupConnection(publicSocket); + _nodeSocket.cleanupConnection(localSocket); auto it = _connectionIDs.find(uuid); if (it == _connectionIDs.end()) { From 4fd2c4449d7b322dcb045172ec01d69375079693 Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Thu, 21 Mar 2019 12:15:03 -0700 Subject: [PATCH 23/63] new procedural uniforms for time --- .../src/RenderableShapeEntityItem.cpp | 2 +- .../src/RenderableZoneEntityItem.cpp | 2 +- .../procedural/src/procedural/Procedural.cpp | 13 +++++++++++-- .../procedural/src/procedural/Procedural.h | 19 ++++++++++++------- .../src/procedural/ProceduralCommon.slh | 8 ++++++-- .../src/procedural/ProceduralSkybox.cpp | 4 ++-- .../src/procedural/ProceduralSkybox.h | 5 ++++- 7 files changed, 37 insertions(+), 16 deletions(-) diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index b33eb619c8..0375e236a4 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -261,7 +261,7 @@ void ShapeEntityRenderer::doRender(RenderArgs* args) { if (_procedural.isReady()) { outColor = _procedural.getColor(outColor); outColor.a *= _procedural.isFading() ? Interpolate::calculateFadeRatio(_procedural.getFadeStartTime()) : 1.0f; - _procedural.prepare(batch, _position, _dimensions, _orientation, ProceduralProgramKey(outColor.a < 1.0f)); + _procedural.prepare(batch, _position, _dimensions, _orientation, _created, ProceduralProgramKey(outColor.a < 1.0f)); proceduralRender = true; } }); diff --git a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp index 631148c27a..8a7fa3f8e7 100644 --- a/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableZoneEntityItem.cpp @@ -33,7 +33,7 @@ using namespace render::entities; ZoneEntityRenderer::ZoneEntityRenderer(const EntityItemPointer& entity) : Parent(entity) { - _background->setSkybox(std::make_shared()); + _background->setSkybox(std::make_shared(entity->getCreated())); } void ZoneEntityRenderer::onRemoveFromSceneTyped(const TypedEntityPointer& entity) { diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index ff8c270371..c5bfa43e75 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -225,11 +225,13 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, + const quint64& created, const ProceduralProgramKey key) { std::lock_guard lock(_mutex); _entityDimensions = size; _entityPosition = position; _entityOrientation = glm::mat3_cast(orientation); + _entityCreated = created; if (!_shaderPath.isEmpty()) { auto lastModified = (quint64)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); if (lastModified > _shaderModified) { @@ -278,7 +280,10 @@ void Procedural::prepare(gpu::Batch& batch, _proceduralPipelines[key] = gpu::Pipeline::create(program, key.isTransparent() ? _transparentState : _opaqueState); - _start = usecTimestampNow(); + _lastCompile = usecTimestampNow(); + if (_firstCompile == 0) { + _firstCompile = _lastCompile; + } _frameCount = 0; recompiledShader = true; } @@ -371,7 +376,11 @@ void Procedural::setupUniforms() { _uniforms.push_back([=](gpu::Batch& batch) { _standardInputs.position = vec4(_entityPosition, 1.0f); // Minimize floating point error by doing an integer division to milliseconds, before the floating point division to seconds - _standardInputs.time = (float)((usecTimestampNow() - _start) / USECS_PER_MSEC) / MSECS_PER_SECOND; + auto now = usecTimestampNow(); + _standardInputs.timeSinceLastCompile = (float)((now - _lastCompile) / USECS_PER_MSEC) / MSECS_PER_SECOND; + _standardInputs.timeSinceFirstCompile = (float)((now - _firstCompile) / USECS_PER_MSEC) / MSECS_PER_SECOND; + _standardInputs.timeSinceEntityCreation = (float)((now - _entityCreated) / USECS_PER_MSEC) / MSECS_PER_SECOND; + // Date { diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index 8477e69afc..b8fd77b052 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -82,7 +82,8 @@ public: bool isReady() const; bool isEnabled() const { return _enabled; } - void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, const ProceduralProgramKey key = ProceduralProgramKey()); + void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, + const quint64& created, const ProceduralProgramKey key = ProceduralProgramKey()); glm::vec4 getColor(const glm::vec4& entityColor) const; quint64 getFadeStartTime() const { return _fadeStartTime; } @@ -106,9 +107,10 @@ protected: vec4 date; vec4 position; vec4 scale; - float time; + float timeSinceLastCompile; + float timeSinceFirstCompile; + float timeSinceEntityCreation; int frameCount; - vec2 _spare1; vec4 resolution[4]; mat4 orientation; }; @@ -116,9 +118,10 @@ protected: static_assert(0 == offsetof(StandardInputs, date), "ProceduralOffsets"); static_assert(16 == offsetof(StandardInputs, position), "ProceduralOffsets"); static_assert(32 == offsetof(StandardInputs, scale), "ProceduralOffsets"); - static_assert(48 == offsetof(StandardInputs, time), "ProceduralOffsets"); - static_assert(52 == offsetof(StandardInputs, frameCount), "ProceduralOffsets"); - static_assert(56 == offsetof(StandardInputs, _spare1), "ProceduralOffsets"); + static_assert(48 == offsetof(StandardInputs, timeSinceLastCompile), "ProceduralOffsets"); + static_assert(52 == offsetof(StandardInputs, timeSinceFirstCompile), "ProceduralOffsets"); + static_assert(56 == offsetof(StandardInputs, timeSinceEntityCreation), "ProceduralOffsets"); + static_assert(60 == offsetof(StandardInputs, frameCount), "ProceduralOffsets"); static_assert(64 == offsetof(StandardInputs, resolution), "ProceduralOffsets"); static_assert(128 == offsetof(StandardInputs, orientation), "ProceduralOffsets"); @@ -126,7 +129,8 @@ protected: ProceduralData _data; bool _enabled { false }; - uint64_t _start { 0 }; + uint64_t _lastCompile { 0 }; + uint64_t _firstCompile { 0 }; int32_t _frameCount { 0 }; // Rendering object descriptions, from userData @@ -152,6 +156,7 @@ protected: glm::vec3 _entityDimensions; glm::vec3 _entityPosition; glm::mat3 _entityOrientation; + quint64 _entityCreated; private: void setupUniforms(); diff --git a/libraries/procedural/src/procedural/ProceduralCommon.slh b/libraries/procedural/src/procedural/ProceduralCommon.slh index bd894a9e92..6e73534440 100644 --- a/libraries/procedural/src/procedural/ProceduralCommon.slh +++ b/libraries/procedural/src/procedural/ProceduralCommon.slh @@ -36,9 +36,11 @@ LAYOUT_STD140(binding=0) uniform standardInputsBuffer { // Offset 48 float globalTime; // Offset 52 - int frameCount; + float localCreatedTime; // Offset 56 - vec2 _spare1; + float entityTime; + // Offset 60 + int frameCount; // Offset 64, acts as vec4[4] for alignment purposes vec3 channelResolution[4]; // Offset 128, acts as vec4[3] for alignment purposes @@ -52,6 +54,8 @@ LAYOUT_STD140(binding=0) uniform standardInputsBuffer { #define iWorldPosition standardInputs.worldPosition #define iWorldScale standardInputs.worldScale #define iGlobalTime standardInputs.globalTime +#define iLocalCreatedTime standardInputs.localCreatedTime +#define iEntityTime standardInputs.entityTime #define iFrameCount standardInputs.frameCount #define iChannelResolution standardInputs.channelResolution #define iWorldOrientation standardInputs.worldOrientation diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index 6eb6d531e1..bf8e408e70 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -17,7 +17,7 @@ #include #include -ProceduralSkybox::ProceduralSkybox() : graphics::Skybox() { +ProceduralSkybox::ProceduralSkybox(quint64 created) : graphics::Skybox(), _created(created) { _procedural._vertexSource = gpu::Shader::createVertex(shader::graphics::vertex::skybox)->getSource(); _procedural._opaqueFragmentSource = shader::Source::get(shader::procedural::fragment::proceduralSkybox); // Adjust the pipeline state for background using the stencil test @@ -59,7 +59,7 @@ void ProceduralSkybox::render(gpu::Batch& batch, const ViewFrustum& viewFrustum, batch.setModelTransform(Transform()); // only for Mac auto& procedural = skybox._procedural; - procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat()); + procedural.prepare(batch, glm::vec3(0), glm::vec3(1), glm::quat(), skybox.getCreated()); skybox.prepare(batch); batch.draw(gpu::TRIANGLE_STRIP, 4); } diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.h b/libraries/procedural/src/procedural/ProceduralSkybox.h index 5db1078a5f..1b01b891d3 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.h +++ b/libraries/procedural/src/procedural/ProceduralSkybox.h @@ -19,7 +19,7 @@ class ProceduralSkybox: public graphics::Skybox { public: - ProceduralSkybox(); + ProceduralSkybox(quint64 created = 0); void parse(const QString& userData) { _procedural.setProceduralData(ProceduralData::parse(userData)); } @@ -29,8 +29,11 @@ public: void render(gpu::Batch& batch, const ViewFrustum& frustum) const override; static void render(gpu::Batch& batch, const ViewFrustum& frustum, const ProceduralSkybox& skybox); + quint64 getCreated() const { return _created; } + protected: mutable Procedural _procedural; + quint64 _created; }; typedef std::shared_ptr< ProceduralSkybox > ProceduralSkyboxPointer; From b0e2b5fde1aebf9c13689bb885c6791808a060b4 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 21 Mar 2019 14:47:48 -0700 Subject: [PATCH 24/63] Remove unneeded variable. --- tools/nitpick/src/TestRunnerMobile.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 1ab3ed7737..105d30fca4 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -197,10 +197,6 @@ void TestRunnerMobile::installAPK() { return; } - // Remove the path - QStringList parts = installerPathname.split('/'); - _installerFilename = parts[parts.length() - 1]; - _statusLabel->setText("Installing"); QString command = _adbInterface->getAdbCommand() + " install -r -d " + installerPathname + " >" + _workingFolder + "/installOutput.txt"; appendLog(command); From faf11480b639e1b56e3a740b9223651376c51459 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 22 Mar 2019 11:22:14 -0700 Subject: [PATCH 25/63] Working changes before merge. --- tools/nitpick/src/Nitpick.cpp | 2 +- tools/nitpick/src/TestRunnerMobile.cpp | 12 ++++++------ tools/nitpick/src/TestRunnerMobile.h | 2 +- tools/nitpick/src/common.h | 1 + 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/tools/nitpick/src/Nitpick.cpp b/tools/nitpick/src/Nitpick.cpp index e72de9d1ad..e6894ee883 100644 --- a/tools/nitpick/src/Nitpick.cpp +++ b/tools/nitpick/src/Nitpick.cpp @@ -38,7 +38,7 @@ Nitpick::Nitpick(QWidget* parent) : QMainWindow(parent) { _ui.plainTextEdit->setReadOnly(true); - setWindowTitle("Nitpick - v3.1.3"); + setWindowTitle("Nitpick - " + nitpickVersion); clientProfiles << "VR-High" << "Desktop-High" << "Desktop-Low" << "Mobile-Touch" << "VR-Standalone"; _ui.clientProfileComboBox->insertItems(0, clientProfiles); diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 105d30fca4..a848c94755 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -211,8 +211,6 @@ void TestRunnerMobile::runInterface() { _adbInterface = new AdbInterface(); } - sendServerIPToDevice(); - _statusLabel->setText("Starting Interface"); QString testScript = (_runFullSuite->isChecked()) @@ -230,7 +228,7 @@ void TestRunnerMobile::runInterface() { QString command = _adbInterface->getAdbCommand() + " shell am start -n " + startCommand + " --es args \\\"" + - " --url file:///~/serverless/tutorial.json" + + " --url hifi://" + getServerIP() + "/0,0,0" " --no-updater" + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + @@ -257,8 +255,8 @@ void TestRunnerMobile::pullFolder() { #endif } -void TestRunnerMobile::sendServerIPToDevice() { - // Get device IP +QString TestRunnerMobile::getServerIP() { + // Get device IP (ifconfig.txt was created when connecting) QFile ifconfigFile{ _workingFolder + "/ifconfig.txt" }; if (!ifconfigFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), @@ -309,7 +307,7 @@ void TestRunnerMobile::sendServerIPToDevice() { if (!serverIP.isNull()) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Cannot identify server IP (multiple interfaces on device submask)"); - return; + return QString("CANNOT IDENTIFY SERVER IP"); } else { union { uint32_t ip; @@ -325,6 +323,8 @@ void TestRunnerMobile::sendServerIPToDevice() { } ifconfigFile.close(); + + return serverIP; } qint64 TestRunnerMobile::convertToBinary(const QString& str) { diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index 7554a075c8..b94cd47647 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -52,7 +52,7 @@ public: void pullFolder(); - void sendServerIPToDevice(); + QString getServerIP(); qint64 convertToBinary (const QString& str); private: diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index 17090c46db..09bf23fdfc 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -60,4 +60,5 @@ const double R_Y = 0.212655f; const double G_Y = 0.715158f; const double B_Y = 0.072187f; +const QString nitpickVersion{ "21660" }; #endif // hifi_common_h \ No newline at end of file From a1660dad9558758a7c0ad1ef42a5f62739262f7b Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 22 Mar 2019 12:30:49 -0700 Subject: [PATCH 26/63] Follow dynamic updates to hero zones; make reserved fraction a domain setting --- assignment-client/src/avatars/AvatarMixer.cpp | 69 ++++++++++++++++--- assignment-client/src/avatars/AvatarMixer.h | 11 ++- .../src/avatars/AvatarMixerClientData.cpp | 17 ++--- .../src/avatars/AvatarMixerSlave.cpp | 7 +- .../src/avatars/AvatarMixerSlave.h | 4 +- .../src/avatars/AvatarMixerSlavePool.cpp | 3 +- .../src/avatars/AvatarMixerSlavePool.h | 9 ++- assignment-client/src/avatars/MixerAvatar.h | 4 ++ .../resources/describe-settings.json | 9 +++ 9 files changed, 104 insertions(+), 29 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 33e1034128..ffe084bc33 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -253,10 +253,26 @@ void AvatarMixer::start() { int lockWait, nodeTransform, functor; + // Set our query each frame { _entityViewer.queryOctree(); } + // Dirty the hero status if there's been an entity change. + { + if (_dirtyHeroStatus) { + _dirtyHeroStatus = false; + nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + std::for_each(cbegin, cend, [](const SharedNodePointer& node) { + if (node->getType() == NodeType::Agent) { + auto& avatar = static_cast(node->getLinkedData())->getAvatar(); + avatar.setNeedsHeroCheck(); + } + }); + }); + } + } + // Allow nodes to process any pending/queued packets across our worker threads { auto start = usecTimestampNow(); @@ -973,19 +989,31 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { { const QString CONNECTION_RATE = "connection_rate"; auto nodeList = DependencyManager::get(); - auto defaultConnectionRate = nodeList->getMaxConnectionRate(); - int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toInt((int)defaultConnectionRate); - nodeList->setMaxConnectionRate(connectionRate); + bool success; + int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toString().toInt(&success); + if (success) { + nodeList->setMaxConnectionRate(connectionRate); + } + } + + { // Fraction of downstream bandwidth reserved for 'hero' avatars: + static const QString PRIORITY_FRACTION_KEY = "priority_fraction"; + if (avatarMixerGroupObject.contains(PRIORITY_FRACTION_KEY)) { + bool isDouble = avatarMixerGroupObject[PRIORITY_FRACTION_KEY].isDouble(); + float priorityFraction = float(avatarMixerGroupObject[PRIORITY_FRACTION_KEY].toDouble()); + _slavePool.setPriorityReservedFraction(std::min(std::max(0.0f, priorityFraction), 1.0f)); + qCDebug(avatars) << "Avatar mixer reserving" << priorityFraction << "of bandwidth for priority avatars"; + } } const QString AVATARS_SETTINGS_KEY = "avatars"; static const QString MIN_HEIGHT_OPTION = "min_avatar_height"; - float settingMinHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); + float settingMinHeight = avatarMixerGroupObject[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); _domainMinimumHeight = glm::clamp(settingMinHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); static const QString MAX_HEIGHT_OPTION = "max_avatar_height"; - float settingMaxHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); + float settingMaxHeight = avatarMixerGroupObject[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); _domainMaximumHeight = glm::clamp(settingMaxHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); // make sure that the domain owner didn't flip min and max @@ -997,11 +1025,11 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { << "and a maximum avatar height of" << _domainMaximumHeight; static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist"; - _slaveSharedData.skeletonURLWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION] + _slaveSharedData.skeletonURLWhitelist = avatarMixerGroupObject[AVATAR_WHITELIST_OPTION] .toString().split(',', QString::KeepEmptyParts); static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar"; - _slaveSharedData.skeletonReplacementURL = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION] + _slaveSharedData.skeletonReplacementURL = avatarMixerGroupObject[REPLACEMENT_AVATAR_OPTION] .toString(); if (_slaveSharedData.skeletonURLWhitelist.count() == 1 && _slaveSharedData.skeletonURLWhitelist[0].isEmpty()) { @@ -1018,9 +1046,12 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { void AvatarMixer::setupEntityQuery() { _entityViewer.init(); + EntityTreePointer entityTree = _entityViewer.getTree(); DependencyManager::registerInheritance(); - DependencyManager::set(_entityViewer.getTree()); - _slaveSharedData.entityTree = _entityViewer.getTree(); + DependencyManager::set(entityTree); + + connect(entityTree.get(), &EntityTree::addingEntityPointer, this, &AvatarMixer::entityAdded); + connect(entityTree.get(), &EntityTree::deletingEntityPointer, this, &AvatarMixer::entityChange); // ES query: {"avatarPriority": true, "type": "Zone"} QJsonObject priorityZoneQuery; @@ -1028,6 +1059,7 @@ void AvatarMixer::setupEntityQuery() { priorityZoneQuery["type"] = "Zone"; _entityViewer.getOctreeQuery().setJSONParameters(priorityZoneQuery); + _slaveSharedData.entityTree = entityTree; } void AvatarMixer::handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode) { @@ -1064,6 +1096,25 @@ void AvatarMixer::handleOctreePacket(QSharedPointer message, Sh } } +void AvatarMixer::entityAdded(EntityItem* entity) { + if (entity->getType() == EntityTypes::Zone) { + _dirtyHeroStatus = true; + entity->registerChangeHandler([this](const EntityItemID& entityItemID) { + this->entityChange(); + }); + } +} + +void AvatarMixer::entityRemoved(EntityItem * entity) { + if (entity->getType() == EntityTypes::Zone) { + _dirtyHeroStatus = true; + } +} + +void AvatarMixer::entityChange() { + _dirtyHeroStatus = true; +} + void AvatarMixer::aboutToFinish() { DependencyManager::destroy(); DependencyManager::destroy(); diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 9393ea6c56..f65f04f279 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -34,8 +34,8 @@ public: static bool shouldReplicateTo(const Node& from, const Node& to) { return to.getType() == NodeType::DownstreamAvatarMixer && - to.getPublicSocket() != from.getPublicSocket() && - to.getLocalSocket() != from.getLocalSocket(); + to.getPublicSocket() != from.getPublicSocket() && + to.getLocalSocket() != from.getLocalSocket(); } public slots: @@ -80,6 +80,7 @@ private: // Attach to entity tree for avatar-priority zone info. EntityTreeHeadlessViewer _entityViewer; + bool _dirtyHeroStatus { true }; // Dirty the needs-hero-update // FIXME - new throttling - use these values somehow float _trailingMixRatio { 0.0f }; @@ -146,6 +147,12 @@ private: AvatarMixerSlavePool _slavePool; SlaveSharedData _slaveSharedData; + +public slots: + // Avatar zone possibly changed + void entityAdded(EntityItem* entity); + void entityRemoved(EntityItem* entity); + void entityChange(); }; #endif // hifi_AvatarMixer_h diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 557c5c9fe3..a6b675efa4 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -141,22 +141,15 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); auto newPosition = getPosition(); - if (newPosition != oldPosition) { -//#define AVATAR_HERO_TEST_HACK -#ifdef AVATAR_HERO_TEST_HACK - { - const static QString heroKey { "HERO" }; - _avatar->setPriorityAvatar(_avatar->getDisplayName().contains(heroKey)); - } -#else + if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; FindPriorityZone findPriorityZone { newPosition, false } ; entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); _avatar->setHasPriority(findPriorityZone.isInPriorityZone); - //if (findPriorityZone.isInPriorityZone) { - // qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; - //} -#endif + _avatar->setNeedsHeroCheck(false); + if (findPriorityZone.isInPriorityZone) { + qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; + } } return true; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index e59c81f4b7..e7de764708 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -43,12 +43,14 @@ void AvatarMixerSlave::configure(ConstIter begin, ConstIter end) { void AvatarMixerSlave::configureBroadcast(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, - float maxKbpsPerNode, float throttlingRatio) { + float maxKbpsPerNode, float throttlingRatio, + float priorityReservedFraction) { _begin = begin; _end = end; _lastFrameTimestamp = lastFrameTimestamp; _maxKbpsPerNode = maxKbpsPerNode; _throttlingRatio = throttlingRatio; + _avatarHeroFraction = priorityReservedFraction; } void AvatarMixerSlave::harvestStats(AvatarMixerSlaveStats& stats) { @@ -308,7 +310,6 @@ namespace { } // Close anonymous namespace. void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) { - const float AVATAR_HERO_FRACTION { 0.4f }; const Node* destinationNode = node.data(); auto nodeList = DependencyManager::get(); @@ -343,7 +344,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // max number of avatarBytes per frame (13 900, typical) const int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND); - const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * AVATAR_HERO_FRACTION); // 5555, typical + const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * _avatarHeroFraction); // 5555, typical // keep track of the number of other avatars held back in this frame int numAvatarsHeldBack = 0; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.h b/assignment-client/src/avatars/AvatarMixerSlave.h index 8c5ad6b181..f14e50e11f 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.h +++ b/assignment-client/src/avatars/AvatarMixerSlave.h @@ -110,7 +110,8 @@ public: void configure(ConstIter begin, ConstIter end); void configureBroadcast(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, - float maxKbpsPerNode, float throttlingRatio); + float maxKbpsPerNode, float throttlingRatio, + float priorityReservedFraction); void processIncomingPackets(const SharedNodePointer& node); void broadcastAvatarData(const SharedNodePointer& node); @@ -140,6 +141,7 @@ private: p_high_resolution_clock::time_point _lastFrameTimestamp; float _maxKbpsPerNode { 0.0f }; float _throttlingRatio { 0.0f }; + float _avatarHeroFraction { 0.4f }; AvatarMixerSlaveStats _stats; SlaveSharedData* _sharedData; diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp index 013d914cbe..357b347a94 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -76,7 +76,8 @@ void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end, float maxKbpsPerNode, float throttlingRatio) { _function = &AvatarMixerSlave::broadcastAvatarData; _configure = [=](AvatarMixerSlave& slave) { - slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio); + slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio, + _priorityReservedFraction); }; run(begin, end); } diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.h b/assignment-client/src/avatars/AvatarMixerSlavePool.h index 71a9ace0d3..b05abde2a3 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.h +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.h @@ -73,7 +73,10 @@ public: void each(std::function functor); void setNumThreads(int numThreads); - int numThreads() { return _numThreads; } + int numThreads() const { return _numThreads; } + + void setPriorityReservedFraction(float fraction) { _priorityReservedFraction = fraction; } + float getPriorityReservedFraction() const { return _priorityReservedFraction; } private: void run(ConstIter begin, ConstIter end); @@ -91,7 +94,11 @@ private: ConditionVariable _poolCondition; void (AvatarMixerSlave::*_function)(const SharedNodePointer& node); std::function _configure; + + // Set from Domain Settings: + float _priorityReservedFraction { 0.4f }; int _numThreads { 0 }; + int _numStarted { 0 }; // guarded by _mutex int _numFinished { 0 }; // guarded by _mutex int _numStopped { 0 }; // guarded by _mutex diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 3e80704495..01e5e91b44 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -19,8 +19,12 @@ class MixerAvatar : public AvatarData { public: + bool getNeedsHeroCheck() const { return _needsHeroCheck; } + void setNeedsHeroCheck(bool needsHeroCheck = true) + { _needsHeroCheck = needsHeroCheck; } private: + bool _needsHeroCheck { false }; }; using MixerAvatarSharedPointer = std::shared_ptr; diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 140c7d6c17..352106dcf7 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1310,6 +1310,15 @@ "placeholder": "50", "default": "50", "advanced": true + }, + { + "name": "priority_fraction", + "type": "double", + "label": "Hero Bandwidth", + "help": "Fraction of downstream bandwidth reserved for avatars in 'Hero' zones", + "placeholder": "0.40", + "default": "0.40", + "advanced": true } ] }, From 137c25f907711cc3f926d06b40c463d06518e004 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 22 Mar 2019 15:40:13 -0700 Subject: [PATCH 27/63] Use a 1 m offset for position test; call-out nonavatars in stats web page --- assignment-client/src/avatars/AvatarMixer.cpp | 8 +++++++- assignment-client/src/avatars/AvatarMixerClientData.cpp | 8 ++++---- assignment-client/src/avatars/AvatarMixerClientData.h | 1 + 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index ffe084bc33..c6cd12f30a 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -843,7 +843,7 @@ void AvatarMixer::sendStatsPacket() { QJsonObject avatarsObject; auto nodeList = DependencyManager::get(); - // add stats for each listerner + // add stats for each listener nodeList->eachNode([&](const SharedNodePointer& node) { QJsonObject avatarStats; @@ -867,6 +867,12 @@ void AvatarMixer::sendStatsPacket() { avatarStats["delta_full_vs_avatar_data_kbps"] = (double)outboundAvatarDataKbps - avatarStats[OUTBOUND_AVATAR_DATA_STATS_KEY].toDouble(); } + + if (node->getType() != NodeType::Agent) { // Nodes that aren't avatars + const QString displayName + { node->getType() == NodeType::EntityScriptServer ? "ENTITY SCRIPT SERVER" : "ENTITY SERVER" }; + avatarStats["display_name"] = displayName; + } } avatarsObject[uuidStringWithoutCurlyBraces(node->getUUID())] = avatarStats; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index a6b675efa4..4880f73226 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -23,6 +23,9 @@ #include "AvatarMixerSlave.h" +// Offset from reported position for priority-zone purposes: +const glm::vec3 AvatarMixerClientData::AVATAR_CENTER_OFFSET { 0.0f, 1.0f, 0.0 }; + AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) : NodeData(nodeID, nodeLocalID) { // in case somebody calls getSessionUUID on the AvatarData instance, make sure it has the right ID @@ -143,13 +146,10 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared auto newPosition = getPosition(); if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; - FindPriorityZone findPriorityZone { newPosition, false } ; + FindPriorityZone findPriorityZone { newPosition + AVATAR_CENTER_OFFSET } ; entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); _avatar->setHasPriority(findPriorityZone.isInPriorityZone); _avatar->setNeedsHeroCheck(false); - if (findPriorityZone.isInPriorityZone) { - qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; - } } return true; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 98c8d7e15b..492dfc4720 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -223,6 +223,7 @@ private: PerNodeTraitVersions _perNodeSentTraitVersions; std::atomic_bool _isIgnoreRadiusEnabled { false }; + static const glm::vec3 AVATAR_CENTER_OFFSET; }; #endif // hifi_AvatarMixerClientData_h From d7a1ecdbb3ee5b808acb4f2d95b104e72eb568ca Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Fri, 22 Mar 2019 17:19:39 -0700 Subject: [PATCH 28/63] Expose hero-status to scripts --- interface/src/avatar/MyAvatar.h | 1 + libraries/avatars/src/AvatarData.cpp | 7 ++++--- libraries/avatars/src/AvatarData.h | 2 ++ libraries/avatars/src/ScriptAvatarData.cpp | 8 ++++++++ libraries/avatars/src/ScriptAvatarData.h | 4 ++++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index aadc8ee268..ccfa629fea 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -184,6 +184,7 @@ class MyAvatar : public Avatar { * @property {Mat4} controllerLeftHandMatrix Read-only. * @property {Mat4} controllerRightHandMatrix Read-only. * @property {number} sensorToWorldScale Read-only. + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only */ // FIXME: `glm::vec3 position` is not accessible from QML, so this exposes position in a QML-native type Q_PROPERTY(QVector3D qmlPosition READ getQmlPosition) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 26407c3564..3355228eea 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1143,10 +1143,11 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // we store the hand state as well as other items in a shared bitset. The hand state is an octal, but is split // into two sections to maintain backward compatibility. The bits are ordered as such (0-7 left to right). // AA 6/1/18 added three more flags bits 8,9, and 10 for procedural audio, blink, and eye saccade enabled - // +---+-----+-----+--+--+--+--+-----+ - // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|xxxxx| - // +---+-----+-----+--+--+--+--+-----+ + // +---+-----+-----+--+--+--+--+--+----+ + // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|He|xxxx| + // +---+-----+-----+--+--+--+--+--+----+ // Hand state - H0,H1,H2 is found in the 3rd, 4th, and 8th bits + // Hero-avatar status (He) - 12th bit auto newHandState = getSemiNibbleAt(bitItems, HAND_STATE_START_BIT) + (oneAtBit16(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 95bbcbeb16..424384c15e 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -453,6 +453,8 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(float sensorToWorldScale READ getSensorToWorldScale) + Q_PROPERTY(bool hasPriority READ getHasPriority) + public: virtual QString getName() const override { return QString("Avatar:") + _displayName; } diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp index a716a40ad8..18717c8ca3 100644 --- a/libraries/avatars/src/ScriptAvatarData.cpp +++ b/libraries/avatars/src/ScriptAvatarData.cpp @@ -343,6 +343,14 @@ glm::mat4 ScriptAvatarData::getControllerRightHandMatrix() const { // END // +bool ScriptAvatarData::getHasPriority() const { + if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { + return sharedAvatarData->getHasPriority(); + } else { + return false; + } +} + glm::quat ScriptAvatarData::getAbsoluteJointRotationInObjectFrame(int index) const { if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { return sharedAvatarData->getAbsoluteJointRotationInObjectFrame(index); diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index 91bac61728..01f7ff360a 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -68,6 +68,8 @@ class ScriptAvatarData : public QObject { Q_PROPERTY(glm::mat4 controllerLeftHandMatrix READ getControllerLeftHandMatrix) Q_PROPERTY(glm::mat4 controllerRightHandMatrix READ getControllerRightHandMatrix) + Q_PROPERTY(bool hasPriority READ getHasPriority) + public: ScriptAvatarData(AvatarSharedPointer avatarData); @@ -133,6 +135,8 @@ public: glm::mat4 getControllerLeftHandMatrix() const; glm::mat4 getControllerRightHandMatrix() const; + bool getHasPriority() const; + signals: void displayNameChanged(); void sessionDisplayNameChanged(); From 8872e9e4e7b318fef033ca8b174fc12d80125fcc Mon Sep 17 00:00:00 2001 From: Simon Walton <36682372+SimonWalton-HiFi@users.noreply.github.com> Date: Sat, 23 Mar 2019 20:05:26 -0900 Subject: [PATCH 29/63] Remove unused debugging variable --- assignment-client/src/avatars/AvatarMixer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index c6cd12f30a..32fb5b9b1a 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -1005,7 +1005,6 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { { // Fraction of downstream bandwidth reserved for 'hero' avatars: static const QString PRIORITY_FRACTION_KEY = "priority_fraction"; if (avatarMixerGroupObject.contains(PRIORITY_FRACTION_KEY)) { - bool isDouble = avatarMixerGroupObject[PRIORITY_FRACTION_KEY].isDouble(); float priorityFraction = float(avatarMixerGroupObject[PRIORITY_FRACTION_KEY].toDouble()); _slavePool.setPriorityReservedFraction(std::min(std::max(0.0f, priorityFraction), 1.0f)); qCDebug(avatars) << "Avatar mixer reserving" << priorityFraction << "of bandwidth for priority avatars"; From de5643b5b99a257c9fbe3f2475fed384c25899fa Mon Sep 17 00:00:00 2001 From: SamGondelman Date: Mon, 25 Mar 2019 10:33:32 -0700 Subject: [PATCH 30/63] quint64 -> uint64_t --- libraries/procedural/src/procedural/Procedural.cpp | 4 ++-- libraries/procedural/src/procedural/Procedural.h | 10 +++++----- .../procedural/src/procedural/ProceduralSkybox.cpp | 2 +- libraries/procedural/src/procedural/ProceduralSkybox.h | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index c5bfa43e75..9940da0b9a 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -225,7 +225,7 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, - const quint64& created, + const uint64_t& created, const ProceduralProgramKey key) { std::lock_guard lock(_mutex); _entityDimensions = size; @@ -233,7 +233,7 @@ void Procedural::prepare(gpu::Batch& batch, _entityOrientation = glm::mat3_cast(orientation); _entityCreated = created; if (!_shaderPath.isEmpty()) { - auto lastModified = (quint64)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); + auto lastModified = (uint64_t)QFileInfo(_shaderPath).lastModified().toMSecsSinceEpoch(); if (lastModified > _shaderModified) { QFile file(_shaderPath); file.open(QIODevice::ReadOnly); diff --git a/libraries/procedural/src/procedural/Procedural.h b/libraries/procedural/src/procedural/Procedural.h index b8fd77b052..956cef368f 100644 --- a/libraries/procedural/src/procedural/Procedural.h +++ b/libraries/procedural/src/procedural/Procedural.h @@ -83,10 +83,10 @@ public: bool isReady() const; bool isEnabled() const { return _enabled; } void prepare(gpu::Batch& batch, const glm::vec3& position, const glm::vec3& size, const glm::quat& orientation, - const quint64& created, const ProceduralProgramKey key = ProceduralProgramKey()); + const uint64_t& created, const ProceduralProgramKey key = ProceduralProgramKey()); glm::vec4 getColor(const glm::vec4& entityColor) const; - quint64 getFadeStartTime() const { return _fadeStartTime; } + uint64_t getFadeStartTime() const { return _fadeStartTime; } bool isFading() const { return _doesFade && _isFading; } void setIsFading(bool isFading) { _isFading = isFading; } void setDoesFade(bool doesFade) { _doesFade = doesFade; } @@ -136,7 +136,7 @@ protected: // Rendering object descriptions, from userData QString _shaderSource; QString _shaderPath; - quint64 _shaderModified { 0 }; + uint64_t _shaderModified { 0 }; NetworkShaderPointer _networkShader; bool _shaderDirty { true }; bool _uniformsDirty { true }; @@ -156,12 +156,12 @@ protected: glm::vec3 _entityDimensions; glm::vec3 _entityPosition; glm::mat3 _entityOrientation; - quint64 _entityCreated; + uint64_t _entityCreated; private: void setupUniforms(); - mutable quint64 _fadeStartTime { 0 }; + mutable uint64_t _fadeStartTime { 0 }; mutable bool _hasStartedFade { false }; mutable bool _isFading { false }; bool _doesFade { true }; diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.cpp b/libraries/procedural/src/procedural/ProceduralSkybox.cpp index bf8e408e70..53df1532dc 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.cpp +++ b/libraries/procedural/src/procedural/ProceduralSkybox.cpp @@ -17,7 +17,7 @@ #include #include -ProceduralSkybox::ProceduralSkybox(quint64 created) : graphics::Skybox(), _created(created) { +ProceduralSkybox::ProceduralSkybox(uint64_t created) : graphics::Skybox(), _created(created) { _procedural._vertexSource = gpu::Shader::createVertex(shader::graphics::vertex::skybox)->getSource(); _procedural._opaqueFragmentSource = shader::Source::get(shader::procedural::fragment::proceduralSkybox); // Adjust the pipeline state for background using the stencil test diff --git a/libraries/procedural/src/procedural/ProceduralSkybox.h b/libraries/procedural/src/procedural/ProceduralSkybox.h index 1b01b891d3..a1d7ea8fa7 100644 --- a/libraries/procedural/src/procedural/ProceduralSkybox.h +++ b/libraries/procedural/src/procedural/ProceduralSkybox.h @@ -19,7 +19,7 @@ class ProceduralSkybox: public graphics::Skybox { public: - ProceduralSkybox(quint64 created = 0); + ProceduralSkybox(uint64_t created = 0); void parse(const QString& userData) { _procedural.setProceduralData(ProceduralData::parse(userData)); } @@ -29,11 +29,11 @@ public: void render(gpu::Batch& batch, const ViewFrustum& frustum) const override; static void render(gpu::Batch& batch, const ViewFrustum& frustum, const ProceduralSkybox& skybox); - quint64 getCreated() const { return _created; } + uint64_t getCreated() const { return _created; } protected: mutable Procedural _procedural; - quint64 _created; + uint64_t _created; }; typedef std::shared_ptr< ProceduralSkybox > ProceduralSkyboxPointer; From 7f5d9cbd40ae95518ef9ae5cfc6cb64e05698d71 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Fri, 22 Mar 2019 13:16:56 -0700 Subject: [PATCH 31/63] adding push to talk fix for loadData --- interface/resources/qml/hifi/audio/MicBar.qml | 19 +++++++++++-------- interface/src/scripting/Audio.cpp | 8 ++++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index f51da9c381..e1b3e05d43 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -18,7 +18,10 @@ import TabletScriptingInterface 1.0 Rectangle { HifiConstants { id: hifi; } + property var muted: AudioScriptingInterface.muted; readonly property var level: AudioScriptingInterface.inputLevel; + readonly property var pushToTalk: AudioScriptingInterface.pushToTalk; + readonly property var pushingToTalk: AudioScriptingInterface.pushingToTalk; property bool gated: false; Component.onCompleted: { @@ -67,10 +70,10 @@ Rectangle { hoverEnabled: true; scrollGestureEnabled: false; onClicked: { - if (AudioScriptingInterface.pushToTalk) { + if (pushToTalk) { return; } - AudioScriptingInterface.muted = !AudioScriptingInterface.muted; + muted = !muted; Tablet.playSound(TabletEnums.ButtonClick); } drag.target: dragTarget; @@ -115,7 +118,7 @@ Rectangle { readonly property string pushToTalkIcon: "../../../icons/tablet-icons/mic-ptt-i.svg"; id: image; - source: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? pushToTalkIcon : AudioScriptingInterface.muted ? mutedIcon : unmutedIcon; + source: (pushToTalk && !pushingToTalk) ? pushToTalkIcon : muted ? mutedIcon : unmutedIcon; width: 30; height: 30; @@ -138,9 +141,9 @@ Rectangle { Item { id: status; - readonly property string color: AudioScriptingInterface.muted ? colors.muted : colors.unmuted; + readonly property string color: muted ? colors.muted : colors.unmuted; - visible: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) || AudioScriptingInterface.muted; + visible: (pushToTalk && !pushingToTalk) || muted; anchors { left: parent.left; @@ -159,7 +162,7 @@ Rectangle { color: parent.color; - text: (AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (AudioScriptingInterface.muted ? "MUTED" : "MUTE"); + text: (pushToTalk && !pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (muted ? "MUTED" : "MUTE"); font.pointSize: 12; } @@ -169,7 +172,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? (HMD.active ? 27 : 25) : 50; + width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; color: parent.color; } @@ -180,7 +183,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - width: AudioScriptingInterface.pushToTalk && !AudioScriptingInterface.pushingToTalk ? (HMD.active ? 27 : 25) : 50; + width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; color: parent.color; } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index b1b5077e60..0e0d13ae45 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -224,10 +224,10 @@ void Audio::saveData() { } void Audio::loadData() { - _desktopMuted = _desktopMutedSetting.get(); - _hmdMuted = _hmdMutedSetting.get(); - _pttDesktop = _pttDesktopSetting.get(); - _pttHMD = _pttHMDSetting.get(); + setMutedDesktop(_desktopMutedSetting.get()); + setMutedHMD(_hmdMutedSetting.get()); + setPTTDesktop(_pttDesktopSetting.get()); + setPTTHMD(_pttHMDSetting.get()); auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted()), Q_ARG(bool, false)); From f551d49f4f0ab549fbb1a7c086012d12b4aed87e Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 11:31:05 -0700 Subject: [PATCH 32/63] adding signal for updating push to talk variables --- interface/resources/qml/hifi/audio/MicBar.qml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index e1b3e05d43..c154eabfaa 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -20,13 +20,20 @@ Rectangle { property var muted: AudioScriptingInterface.muted; readonly property var level: AudioScriptingInterface.inputLevel; - readonly property var pushToTalk: AudioScriptingInterface.pushToTalk; - readonly property var pushingToTalk: AudioScriptingInterface.pushingToTalk; + property var pushToTalk: AudioScriptingInterface.pushToTalk; + property var pushingToTalk: AudioScriptingInterface.pushingToTalk; property bool gated: false; Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + AudioScriptingInterface.pushToTalkChanged.connect(function() { + pushToTalk = AudioScriptingInterface.pushToTalk; + }); + AudioScriptingInterface.pushingToTalkChanged.connect(function() { + pushingToTalk = AudioScriptingInterface.pushingToTalk; + }); + } property bool standalone: false; From 59a420b1602d2bc8eb9a494601658993bc295e0c Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 12:24:13 -0700 Subject: [PATCH 33/63] adding signals + slots to when muted + display mode changes --- interface/resources/qml/hifi/audio/MicBar.qml | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/hifi/audio/MicBar.qml b/interface/resources/qml/hifi/audio/MicBar.qml index c154eabfaa..d161f12d70 100644 --- a/interface/resources/qml/hifi/audio/MicBar.qml +++ b/interface/resources/qml/hifi/audio/MicBar.qml @@ -27,6 +27,13 @@ Rectangle { Component.onCompleted: { AudioScriptingInterface.noiseGateOpened.connect(function() { gated = false; }); AudioScriptingInterface.noiseGateClosed.connect(function() { gated = true; }); + HMD.displayModeChanged.connect(function() { + muted = AudioScriptingInterface.muted; + pushToTalk = AudioScriptingInterface.pushToTalk; + }); + AudioScriptingInterface.mutedChanged.connect(function() { + muted = AudioScriptingInterface.muted; + }); AudioScriptingInterface.pushToTalkChanged.connect(function() { pushToTalk = AudioScriptingInterface.pushToTalk; }); @@ -94,16 +101,16 @@ Rectangle { QtObject { id: colors; - readonly property string unmuted: "#FFF"; - readonly property string muted: "#E2334D"; + readonly property string unmutedColor: "#FFF"; + readonly property string mutedColor: "#E2334D"; readonly property string gutter: "#575757"; readonly property string greenStart: "#39A38F"; readonly property string greenEnd: "#1FC6A6"; readonly property string yellow: "#C0C000"; - readonly property string red: colors.muted; + readonly property string red: colors.mutedColor; readonly property string fill: "#55000000"; readonly property string border: standalone ? "#80FFFFFF" : "#55FFFFFF"; - readonly property string icon: AudioScriptingInterface.muted ? muted : unmuted; + readonly property string icon: muted ? colors.mutedColor : unmutedColor; } Item { @@ -148,8 +155,6 @@ Rectangle { Item { id: status; - readonly property string color: muted ? colors.muted : colors.unmuted; - visible: (pushToTalk && !pushingToTalk) || muted; anchors { @@ -167,7 +172,7 @@ Rectangle { verticalCenter: parent.verticalCenter; } - color: parent.color; + color: colors.icon; text: (pushToTalk && !pushingToTalk) ? (HMD.active ? "MUTED PTT" : "MUTED PTT-(T)") : (muted ? "MUTED" : "MUTE"); font.pointSize: 12; @@ -181,7 +186,7 @@ Rectangle { width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; - color: parent.color; + color: colors.icon; } Rectangle { @@ -192,7 +197,7 @@ Rectangle { width: pushToTalk && !pushingToTalk ? (HMD.active ? 27 : 25) : 50; height: 4; - color: parent.color; + color: colors.icon; } } From dc996c267f5af7a0203a1d9a4de84a630b68f2b7 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 25 Mar 2019 12:24:13 -0700 Subject: [PATCH 34/63] adding signals + slots to when muted + display mode changes From 1608b24be15df3fffeac3b074cfc4d977c3f9ec9 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Mon, 25 Mar 2019 14:28:54 -0700 Subject: [PATCH 35/63] ase 20832 - Inventory app login and cancel buttons don't work on logout --- .../resources/qml/hifi/commerce/marketplace/Marketplace.qml | 2 +- interface/src/Application.cpp | 4 ---- scripts/system/commerce/wallet.js | 3 +++ scripts/system/marketplaces/marketplaces.js | 3 +-- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml index 7dcdf9b434..619547ef43 100644 --- a/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml +++ b/interface/resources/qml/hifi/commerce/marketplace/Marketplace.qml @@ -664,7 +664,7 @@ Rectangle { text: "LOG IN" onClicked: { - sendToScript({method: 'needsLogIn_loginClicked'}); + sendToScript({method: 'marketplace_loginClicked'}); } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0c46404b39..1852c0007e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1206,10 +1206,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(&domainHandler, SIGNAL(connectedToDomain(QUrl)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { - auto tabletScriptingInterface = DependencyManager::get(); - if (tabletScriptingInterface) { - tabletScriptingInterface->setQmlTabletRoot(SYSTEM_TABLET, nullptr); - } auto entityScriptingInterface = DependencyManager::get(); entityScriptingInterface->deleteEntity(getTabletScreenID()); entityScriptingInterface->deleteEntity(getTabletHomeButtonID()); diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 17ff918243..86806fd8b4 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -377,6 +377,9 @@ function deleteSendMoneyParticleEffect() { } function onUsernameChanged() { + if (ui.checkIsOpen()) { + ui.open(WALLET_QML_SOURCE); + } } var MARKETPLACE_QML_PATH = "hifi/commerce/marketplace/Marketplace.qml"; diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index e059081741..38287e3af3 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -615,11 +615,10 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { openMarketplace(message.itemId, message.itemEdition); break; case 'passphrasePopup_cancelClicked': - case 'needsLogIn_cancelClicked': // Should/must NOT check for wallet setup. openMarketplace(); break; - case 'needsLogIn_loginClicked': + case 'marketplace_loginClicked': openLoginWindow(); break; case 'disableHmdPreview': From 46f897b69330e92e1a41d9130b414afd2a0eceea Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Mon, 25 Mar 2019 15:42:30 -0700 Subject: [PATCH 36/63] Better estimate of avatar centre for zone membership --- assignment-client/src/avatars/AvatarMixerClientData.cpp | 9 +++------ assignment-client/src/avatars/AvatarMixerClientData.h | 1 - assignment-client/src/avatars/MixerAvatar.h | 3 +++ 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 4880f73226..2175018824 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -23,9 +23,6 @@ #include "AvatarMixerSlave.h" -// Offset from reported position for priority-zone purposes: -const glm::vec3 AvatarMixerClientData::AVATAR_CENTER_OFFSET { 0.0f, 1.0f, 0.0 }; - AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) : NodeData(nodeID, nodeLocalID) { // in case somebody calls getSessionUUID on the AvatarData instance, make sure it has the right ID @@ -132,7 +129,7 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared incrementNumOutOfOrderSends(); } _lastReceivedSequenceNumber = sequenceNumber; - glm::vec3 oldPosition = getPosition(); + glm::vec3 oldPosition = _avatar->getCentroidPosition(); bool oldHasPriority = _avatar->getHasPriority(); // compute the offset to the data payload @@ -143,10 +140,10 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared // Regardless of what the client says, restore the priority as we know it without triggering any update. _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); - auto newPosition = getPosition(); + auto newPosition = _avatar->getCentroidPosition(); if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; - FindPriorityZone findPriorityZone { newPosition + AVATAR_CENTER_OFFSET } ; + FindPriorityZone findPriorityZone { newPosition } ; entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); _avatar->setHasPriority(findPriorityZone.isInPriorityZone); _avatar->setNeedsHeroCheck(false); diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 492dfc4720..98c8d7e15b 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -223,7 +223,6 @@ private: PerNodeTraitVersions _perNodeSentTraitVersions; std::atomic_bool _isIgnoreRadiusEnabled { false }; - static const glm::vec3 AVATAR_CENTER_OFFSET; }; #endif // hifi_AvatarMixerClientData_h diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 01e5e91b44..f812917614 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -22,6 +22,9 @@ public: bool getNeedsHeroCheck() const { return _needsHeroCheck; } void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; } + // Bounding-box World centre: + glm::vec3 getCentroidPosition() const + { return getWorldPosition() + _globalBoundingBoxOffset; } private: bool _needsHeroCheck { false }; From da6ca38282706ba272b88e47cb6f7155e2839d0c Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 26 Mar 2019 10:48:59 -0700 Subject: [PATCH 37/63] Revert to using avatar's _globalPosition for zone membership According to Tony this should be the hip position, i.e. a joint pos in T-pose, not neccesarily OK root. Also SpatiallyNestable::WorldPos may depend on parent entity and so not known by mixer. --- assignment-client/src/avatars/AvatarMixerClientData.cpp | 4 ++-- assignment-client/src/avatars/MixerAvatar.h | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 2175018824..0dbefb0109 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -129,7 +129,7 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared incrementNumOutOfOrderSends(); } _lastReceivedSequenceNumber = sequenceNumber; - glm::vec3 oldPosition = _avatar->getCentroidPosition(); + glm::vec3 oldPosition = _avatar->getClientGlobalPosition(); bool oldHasPriority = _avatar->getHasPriority(); // compute the offset to the data payload @@ -140,7 +140,7 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared // Regardless of what the client says, restore the priority as we know it without triggering any update. _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); - auto newPosition = _avatar->getCentroidPosition(); + auto newPosition = _avatar->getClientGlobalPosition(); if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; FindPriorityZone findPriorityZone { newPosition } ; diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index f812917614..01e5e91b44 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -22,9 +22,6 @@ public: bool getNeedsHeroCheck() const { return _needsHeroCheck; } void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; } - // Bounding-box World centre: - glm::vec3 getCentroidPosition() const - { return getWorldPosition() + _globalBoundingBoxOffset; } private: bool _needsHeroCheck { false }; From b8f79d33649b0fa452dd915df48ba56f512fc166 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Tue, 26 Mar 2019 17:41:22 -0700 Subject: [PATCH 38/63] Guard against Node linked-data being null --- assignment-client/src/avatars/AvatarMixer.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 32fb5b9b1a..5f7e197c8f 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -265,8 +265,11 @@ void AvatarMixer::start() { nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { std::for_each(cbegin, cend, [](const SharedNodePointer& node) { if (node->getType() == NodeType::Agent) { - auto& avatar = static_cast(node->getLinkedData())->getAvatar(); - avatar.setNeedsHeroCheck(); + NodeData* nodeData = node->getLinkedData(); + if (nodeData) { + auto& avatar = static_cast(nodeData)->getAvatar(); + avatar.setNeedsHeroCheck(); + } } }); }); From 29af3b16126690d6b2b678dd98b682031fefc8ff Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 27 Feb 2019 10:47:42 -0800 Subject: [PATCH 39/63] add a button to Avatar panel to lock or unlock wearables. allow grabbing / adjusting others' wearables if they are unlocked. --- interface/resources/qml/hifi/AvatarApp.qml | 17 +++ .../+android_interface/HifiConstants.qml | 1 + .../resources/qml/stylesUit/HifiConstants.qml | 1 + interface/src/avatar/MyAvatar.h | 2 +- interface/src/avatar/OtherAvatar.cpp | 2 +- .../src/avatars-renderer/Avatar.cpp | 30 +++++- .../src/avatars-renderer/Avatar.h | 3 +- scripts/system/avatarapp.js | 102 ++++++++++++------ .../libraries/controllerDispatcherUtils.js | 2 - 9 files changed, 116 insertions(+), 44 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index 753b9c5a81..a57b5713ed 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -16,6 +16,8 @@ Rectangle { property bool keyboardRaised: false property bool punctuationMode: false + HifiConstants { id: hifi } + HifiControls.Keyboard { id: keyboard z: 1000 @@ -48,6 +50,7 @@ Rectangle { property var jointNames: [] property var currentAvatarSettings; + property bool wearablesLocked; function fetchAvatarModelName(marketId, avatar) { var xmlhttp = new XMLHttpRequest(); @@ -187,6 +190,8 @@ Rectangle { updateCurrentAvatarInBookmarks(currentAvatar); } else if (message.method === 'selectAvatarEntity') { adjustWearables.selectWearableByID(message.entityID); + } else if (message.method === 'wearablesLockedChanged') { + wearablesLocked = message.wearablesLocked; } } @@ -507,6 +512,7 @@ Rectangle { } SquareLabel { + id: adjustLabel anchors.right: parent.right anchors.verticalCenter: wearablesLabel.verticalCenter glyphText: "\ue02e" @@ -515,6 +521,17 @@ Rectangle { adjustWearables.open(currentAvatar); } } + + SquareLabel { + anchors.right: adjustLabel.left + anchors.verticalCenter: wearablesLabel.verticalCenter + anchors.rightMargin: 15 + glyphText: wearablesLocked ? hifi.glyphs.lock : hifi.glyphs.unlock; + + onClicked: { + emitSendToScript({'method' : 'toggleWearablesLock'}); + } + } } Rectangle { diff --git a/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml b/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml index d5fab57501..995af90f0b 100644 --- a/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml +++ b/interface/resources/qml/stylesUit/+android_interface/HifiConstants.qml @@ -344,6 +344,7 @@ Item { readonly property string stop_square: "\ue01e" readonly property string avatarTPose: "\ue01f" readonly property string lock: "\ue006" + readonly property string unlock: "\ue039" readonly property string checkmark: "\ue020" readonly property string leftRightArrows: "\ue021" readonly property string hfc: "\ue022" diff --git a/interface/resources/qml/stylesUit/HifiConstants.qml b/interface/resources/qml/stylesUit/HifiConstants.qml index 75f028cd4f..2394b36106 100644 --- a/interface/resources/qml/stylesUit/HifiConstants.qml +++ b/interface/resources/qml/stylesUit/HifiConstants.qml @@ -330,6 +330,7 @@ QtObject { readonly property string stop_square: "\ue01e" readonly property string avatarTPose: "\ue01f" readonly property string lock: "\ue006" + readonly property string unlock: "\ue039" readonly property string checkmark: "\ue020" readonly property string leftRightArrows: "\ue021" readonly property string hfc: "\ue022" diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index edb686a6a6..3f60b9cada 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -2170,7 +2170,7 @@ private: bool getEnableStepResetRotation() const { return _stepResetRotationEnabled; } void setEnableDrawAverageFacing(bool drawAverage) { _drawAverageFacingEnabled = drawAverage; } bool getEnableDrawAverageFacing() const { return _drawAverageFacingEnabled; } - bool isMyAvatar() const override { return true; } + virtual bool isMyAvatar() const override { return true; } virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; int _skeletonModelChangeCount { 0 }; diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 11eb6542c4..2058408596 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -365,7 +365,7 @@ void OtherAvatar::handleChangedAvatarEntityData() { // AVATAR ENTITY UPDATE FLOW // - if queueEditEntityMessage() sees "AvatarEntity" HostType it calls _myAvatar->storeAvatarEntityDataPayload() // - storeAvatarEntityDataPayload() saves the payload and flags the trait instance for the entity as updated, - // - ClientTraitsHandler::sendChangedTraitsToMixea() sends the entity bytes to the mixer which relays them to other interfaces + // - ClientTraitsHandler::sendChangedTraitsToMixer() sends the entity bytes to the mixer which relays them to other interfaces // - AvatarHashMap::processBulkAvatarTraits() on other interfaces calls avatar->processTraitInstance() // - AvatarData::processTraitInstance() calls storeAvatarEntityDataPayload(), which sets _avatarEntityDataChanged = true // - (My)Avatar::simulate() calls handleChangedAvatarEntityData() every frame which checks _avatarEntityDataChanged diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 992ee5db96..8cff1cc52a 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -372,11 +372,22 @@ bool Avatar::applyGrabChanges() { target->removeGrab(grab); _avatarGrabs.erase(itr); grabAddedOrRemoved = true; - if (isMyAvatar()) { - const EntityItemPointer& entity = std::dynamic_pointer_cast(target); - if (entity && entity->getEntityHostType() == entity::HostType::AVATAR && entity->getSimulationOwner().getID() == getID()) { - EntityItemProperties properties = entity->getProperties(); - sendPacket(entity->getID()); + const EntityItemPointer& entity = std::dynamic_pointer_cast(target); + if (entity && entity->getEntityHostType() == entity::HostType::AVATAR) { + // grabs are able to move avatar-entities which belong ot other avatars (assuming + // the entities are grabbable, unlocked, etc). Regardless of who released the grab + // on this entity, the entity's owner needs to send off an update. + QUuid entityOwnerID = entity->getOwningAvatarID(); + if (entityOwnerID == getMyAvatarID() || entityOwnerID == AVATAR_SELF_ID) { + bool success; + SpatiallyNestablePointer myAvatarSN = SpatiallyNestable::findByID(entityOwnerID, success); + if (success) { + std::shared_ptr myAvatar = std::dynamic_pointer_cast(myAvatarSN); + if (myAvatar) { + EntityItemProperties properties = entity->getProperties(); + myAvatar->sendPacket(entity->getID(), properties); + } + } } } } else { @@ -2103,3 +2114,12 @@ void Avatar::updateDescendantRenderIDs() { } }); } + +QUuid Avatar::getMyAvatarID() const { + auto nodeList = DependencyManager::get(); + if (nodeList) { + return nodeList->getSessionUUID(); + } else { + return QUuid(); + } +} diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index b4683d6032..2bed13cb12 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -180,7 +180,8 @@ public: /// Returns the distance to use as a LOD parameter. float getLODDistance() const; - virtual bool isMyAvatar() const override { return false; } + QUuid getMyAvatarID() const; + virtual void createOrb() { } enum class LoadingStatus { diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index fb61b914a3..429053bb8b 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -1,6 +1,8 @@ "use strict"; /*jslint vars:true, plusplus:true, forin:true*/ -/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ +/*global Tablet, Script, Entities, MyAvatar, Camera, Quat, HMD, Account, UserActivityLogger, Messages, print, + AvatarBookmarks, ContextOverlay, AddressManager +*/ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // avatarapp.js @@ -14,7 +16,7 @@ (function() { // BEGIN LOCAL_SCOPE -var request = Script.require('request').request; +// var request = Script.require('request').request; var AVATARAPP_QML_SOURCE = "hifi/AvatarApp.qml"; Script.include("/~/system/libraries/controllers.js"); @@ -22,7 +24,7 @@ Script.include("/~/system/libraries/controllers.js"); var ENTRY_AVATAR_URL = "avatarUrl"; var ENTRY_AVATAR_ENTITIES = "avatarEntites"; var ENTRY_AVATAR_SCALE = "avatarScale"; -var ENTRY_VERSION = "version"; +// var ENTRY_VERSION = "version"; function executeLater(callback) { Script.setTimeout(callback, 300); @@ -44,7 +46,7 @@ function getMyAvatarWearables() { } var localRotation = entity.properties.localRotation; - entity.properties.localRotationAngles = Quat.safeEulerAngles(localRotation) + entity.properties.localRotationAngles = Quat.safeEulerAngles(localRotation); wearablesArray.push(entity); } @@ -52,7 +54,7 @@ function getMyAvatarWearables() { } function getMyAvatar() { - var avatar = {} + var avatar = {}; avatar[ENTRY_AVATAR_URL] = MyAvatar.skeletonModelURL; avatar[ENTRY_AVATAR_SCALE] = MyAvatar.getAvatarScale(); avatar[ENTRY_AVATAR_ENTITIES] = getMyAvatarWearables(); @@ -68,7 +70,7 @@ function getMyAvatarSettings() { collisionSoundUrl : MyAvatar.collisionSoundURL, animGraphUrl: MyAvatar.getAnimGraphUrl(), animGraphOverrideUrl : MyAvatar.getAnimGraphOverrideUrl(), - } + }; } function updateAvatarWearables(avatar, callback, wearablesOverride) { @@ -76,7 +78,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { var wearables = wearablesOverride ? wearablesOverride : getMyAvatarWearables(); avatar[ENTRY_AVATAR_ENTITIES] = wearables; - sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}) + sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); if(callback) callback(); @@ -101,7 +103,7 @@ var adjustWearables = { this.opened = value; } } -} +}; var currentAvatarWearablesBackup = null; var currentAvatar = null; @@ -112,7 +114,7 @@ function onTargetScaleChanged() { if(currentAvatar.scale !== MyAvatar.getAvatarScale()) { currentAvatar.scale = MyAvatar.getAvatarScale(); if(notifyScaleChanged) { - sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale}) + sendToQml({'method' : 'scaleChanged', 'value' : currentAvatar.scale}); } } } @@ -126,7 +128,7 @@ function onSkeletonModelURLChanged() { function onDominantHandChanged(dominantHand) { if(currentAvatarSettings.dominantHand !== dominantHand) { currentAvatarSettings.dominantHand = dominantHand; - sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand}) + sendToQml({'method' : 'settingChanged', 'name' : 'dominantHand', 'value' : dominantHand}); } } @@ -140,32 +142,33 @@ function onHmdAvatarAlignmentTypeChanged(type) { function onCollisionsEnabledChanged(enabled) { if(currentAvatarSettings.collisionsEnabled !== enabled) { currentAvatarSettings.collisionsEnabled = enabled; - sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled}) + sendToQml({'method' : 'settingChanged', 'name' : 'collisionsEnabled', 'value' : enabled}); } } function onOtherAvatarsCollisionsEnabledChanged(enabled) { if (currentAvatarSettings.otherAvatarsCollisionsEnabled !== enabled) { currentAvatarSettings.otherAvatarsCollisionsEnabled = enabled; - sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled }) + sendToQml({ 'method': 'settingChanged', 'name': 'otherAvatarsCollisionsEnabled', 'value': enabled }); } } function onNewCollisionSoundUrl(url) { if(currentAvatarSettings.collisionSoundUrl !== url) { currentAvatarSettings.collisionSoundUrl = url; - sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url}) + sendToQml({'method' : 'settingChanged', 'name' : 'collisionSoundUrl', 'value' : url}); } } function onAnimGraphUrlChanged(url) { if (currentAvatarSettings.animGraphUrl !== url) { currentAvatarSettings.animGraphUrl = url; - sendToQml({ 'method': 'settingChanged', 'name': 'animGraphUrl', 'value': currentAvatarSettings.animGraphUrl }) + sendToQml({ 'method': 'settingChanged', 'name': 'animGraphUrl', 'value': currentAvatarSettings.animGraphUrl }); if (currentAvatarSettings.animGraphOverrideUrl !== MyAvatar.getAnimGraphOverrideUrl()) { currentAvatarSettings.animGraphOverrideUrl = MyAvatar.getAnimGraphOverrideUrl(); - sendToQml({ 'method': 'settingChanged', 'name': 'animGraphOverrideUrl', 'value': currentAvatarSettings.animGraphOverrideUrl }) + sendToQml({ 'method': 'settingChanged', 'name': 'animGraphOverrideUrl', + 'value': currentAvatarSettings.animGraphOverrideUrl }); } } } @@ -178,6 +181,33 @@ var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js"); +function getWearablesLocked() { + var wearablesLocked = true; + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + if (isGrabbable(wearable.id)) { + wearablesLocked = false; + } + }); + + return wearablesLocked; +} + +function lockWearables() { + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + setGrabbable(wearable.id, false); + }); +} + +function unlockWearables() { + var wearablesArray = getMyAvatarWearables(); + wearablesArray.forEach(function(wearable) { + setGrabbable(wearable.id, true); + }); +} + + function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. switch (message.method) { case 'getAvatars': @@ -201,7 +231,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See } } - sendToQml(message) + sendToQml(message); break; case 'selectAvatar': Entities.addingWearable.disconnect(onAddingWearable); @@ -228,7 +258,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See message.properties.localRotationAngles = Quat.safeEulerAngles(message.properties.localRotation); } - sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false}) + sendToQml({'method' : 'wearableUpdated', 'entityID' : message.entityID, wearableIndex : message.wearableIndex, properties : message.properties, updateUI : false}); break; case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); @@ -305,11 +335,11 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See var currentAvatarURL = MyAvatar.getFullAvatarURLFromPreferences(); if(currentAvatarURL !== message.avatarURL) { MyAvatar.useFullAvatarURL(message.avatarURL); - sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL}) + sendToQml({'method' : 'externalAvatarApplied', 'avatarURL' : message.avatarURL}); } break; case 'navigate': - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system") + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); if(message.url.indexOf('app://') === 0) { if (message.url === 'app://marketplace') { tablet.gotoWebScreen(MARKETPLACE_URL, MARKETPLACES_INJECT_SCRIPT_URL); @@ -345,7 +375,17 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See MyAvatar.collisionSoundURL = message.settings.collisionSoundUrl; MyAvatar.setAnimGraphOverrideUrl(message.settings.animGraphOverrideUrl); - settings = getMyAvatarSettings(); + currentAvatarSettings = getMyAvatarSettings(); + break; + case 'toggleWearablesLock': + var wearablesLocked = getWearablesLocked(); + wearablesLocked = !wearablesLocked; + if (wearablesLocked) { + lockWearables(); + } else { + unlockWearables(); + } + sendToQml({'method' : 'wearablesLockedChanged', 'wearablesLocked' : wearablesLocked}); break; default: print('Unrecognized message from AvatarApp.qml'); @@ -398,7 +438,7 @@ function ensureWearableSelected(entityID) { function isEntityBeingWorn(entityID) { return Entities.getEntityProperties(entityID, 'parentID').parentID === MyAvatar.sessionUUID; -}; +} function onSelectedEntity(entityID, pointerEvent) { if(selectedAvatarEntityID !== entityID && isEntityBeingWorn(entityID)) @@ -445,14 +485,14 @@ function handleWearableMessages(channel, message, sender) { // for some reasons Entities.getEntityProperties returns more than was asked.. var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); - var properties = {} + var properties = {}; propertyNames.forEach(function(propertyName) { properties[propertyName] = entityProperties[propertyName]; - }) + }); properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); - sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}) + sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}); }, 1000); } else if(parsedMessage.action === 'release') { @@ -481,8 +521,8 @@ function onBookmarkDeleted(bookmarkName) { function onBookmarkAdded(bookmarkName) { var bookmark = AvatarBookmarks.getBookmark(bookmarkName); bookmark.avatarEntites.forEach(function(avatarEntity) { - avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation) - }) + avatarEntity.properties.localRotationAngles = Quat.safeEulerAngles(avatarEntity.properties.localRotation); + }); sendToQml({ 'method': 'bookmarkAdded', 'bookmarkName': bookmarkName, 'bookmark': bookmark }); } @@ -601,14 +641,8 @@ function onTabletScreenChanged(type, url) { onAvatarAppScreen = onAvatarAppScreenNow; if(onAvatarAppScreenNow) { - var message = { - 'method' : 'initialize', - 'data' : { - 'jointNames' : MyAvatar.getJointNames() - } - }; - - sendToQml(message) + sendToQml({ method : 'initialize', data : { jointNames : MyAvatar.getJointNames() }}); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } } diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 5cb95f625d..51645e5502 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -341,8 +341,6 @@ entityIsGrabbable = function (eigProps) { var grabbable = getGrabbableData(eigProps).grabbable; if (!grabbable || eigProps.locked || - isAnothersAvatarEntity(eigProps) || - isAnothersChildEntity(eigProps) || FORBIDDEN_GRAB_TYPES.indexOf(eigProps.type) >= 0) { return false; } From 4fe94a4b32404ab0e096d2274fa53046fcbcb7a4 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Mar 2019 14:16:18 -0700 Subject: [PATCH 40/63] when new wearables are added or removed, rerun getWearablesLocked() and update UI --- scripts/system/avatarapp.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 429053bb8b..eb44bc7fbd 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -79,6 +79,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { avatar[ENTRY_AVATAR_ENTITIES] = wearables; sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); if(callback) callback(); @@ -239,6 +240,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See AvatarBookmarks.loadBookmark(message.name); Entities.addingWearable.connect(onAddingWearable); Entities.deletingWearable.connect(onDeletingWearable); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); break; case 'deleteAvatar': AvatarBookmarks.removeBookmark(message.name); @@ -406,9 +408,11 @@ function isGrabbable(entityID) { } function setGrabbable(entityID, grabbable) { - var properties = Entities.getEntityProperties(entityID, ['avatarEntity']); - if (properties.avatarEntity) { - Entities.editEntity(entityID, { grab: { grabbable: grabbable }}); + var properties = Entities.getEntityProperties(entityID, ['avatarEntity', 'grab.grabbable']); + if (properties.avatarEntity && properties.grab.grabable != grabbable) { + var editProps = { grab: { grabbable: grabbable }}; + Entities.editEntity(entityID, editProps); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } } @@ -453,12 +457,14 @@ function onAddingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } function onDeletingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); + sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); } function handleWearableMessages(channel, message, sender) { From fa36f1214530252ad86af39da0bf2473b27822a1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Mar 2019 16:12:38 -0700 Subject: [PATCH 41/63] lock wearables when adjust-wearables page is opened --- .../src/avatars-renderer/Avatar.cpp | 3 +- scripts/system/avatarapp.js | 31 ++++++------------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 8cff1cc52a..b0a8875cbc 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -384,8 +384,7 @@ bool Avatar::applyGrabChanges() { if (success) { std::shared_ptr myAvatar = std::dynamic_pointer_cast(myAvatarSN); if (myAvatar) { - EntityItemProperties properties = entity->getProperties(); - myAvatar->sendPacket(entity->getID(), properties); + myAvatar->sendPacket(entity->getID()); } } } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index eb44bc7fbd..ee337694a2 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -16,7 +16,6 @@ (function() { // BEGIN LOCAL_SCOPE -// var request = Script.require('request').request; var AVATARAPP_QML_SOURCE = "hifi/AvatarApp.qml"; Script.include("/~/system/libraries/controllers.js"); @@ -24,7 +23,6 @@ Script.include("/~/system/libraries/controllers.js"); var ENTRY_AVATAR_URL = "avatarUrl"; var ENTRY_AVATAR_ENTITIES = "avatarEntites"; var ENTRY_AVATAR_SCALE = "avatarScale"; -// var ENTRY_VERSION = "version"; function executeLater(callback) { Script.setTimeout(callback, 300); @@ -79,7 +77,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { avatar[ENTRY_AVATAR_ENTITIES] = wearables; sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); if(callback) callback(); @@ -174,7 +172,6 @@ function onAnimGraphUrlChanged(url) { } } -var selectedAvatarEntityGrabbable = false; var selectedAvatarEntityID = null; var grabbedAvatarEntityChangeNotifier = null; @@ -240,7 +237,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See AvatarBookmarks.loadBookmark(message.name); Entities.addingWearable.connect(onAddingWearable); Entities.deletingWearable.connect(onDeletingWearable); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); break; case 'deleteAvatar': AvatarBookmarks.removeBookmark(message.name); @@ -265,6 +262,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); adjustWearables.setOpened(true); + lockWearables(); Entities.mousePressOnEntity.connect(onSelectedEntity); Messages.subscribe('Hifi-Object-Manipulation'); @@ -409,10 +407,10 @@ function isGrabbable(entityID) { function setGrabbable(entityID, grabbable) { var properties = Entities.getEntityProperties(entityID, ['avatarEntity', 'grab.grabbable']); - if (properties.avatarEntity && properties.grab.grabable != grabbable) { + if (properties.avatarEntity && properties.grab.grabbable != grabbable) { var editProps = { grab: { grabbable: grabbable }}; Entities.editEntity(entityID, editProps); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } } @@ -422,18 +420,7 @@ function ensureWearableSelected(entityID) { Script.clearInterval(grabbedAvatarEntityChangeNotifier); grabbedAvatarEntityChangeNotifier = null; } - - if(selectedAvatarEntityID !== null) { - setGrabbable(selectedAvatarEntityID, selectedAvatarEntityGrabbable); - } - selectedAvatarEntityID = entityID; - selectedAvatarEntityGrabbable = isGrabbable(entityID); - - if(selectedAvatarEntityID !== null) { - setGrabbable(selectedAvatarEntityID, true); - } - return true; } @@ -457,14 +444,14 @@ function onAddingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } function onDeletingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } function handleWearableMessages(channel, message, sender) { @@ -647,8 +634,8 @@ function onTabletScreenChanged(type, url) { onAvatarAppScreen = onAvatarAppScreenNow; if(onAvatarAppScreenNow) { - sendToQml({ method : 'initialize', data : { jointNames : MyAvatar.getJointNames() }}); - sendToQml({ method : 'wearablesLockedChanged', wearablesLocked : getWearablesLocked()}); + sendToQml({ 'method' : 'initialize', 'data' : { jointNames : MyAvatar.getJointNames() }}); + sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); } } From 5695c1580941e94eae37cb28b9afbed3e1bd8e70 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 22 Mar 2019 16:55:21 -0700 Subject: [PATCH 42/63] avoid a deadlock when code invoked by onAddingEntity or onDeletingEntity runs more code that locks the entity tree --- libraries/entities/src/EntityScriptingInterface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index aa4b3902c2..ca914731b5 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1101,13 +1101,13 @@ void EntityScriptingInterface::handleEntityScriptCallMethodPacket(QSharedPointer void EntityScriptingInterface::onAddingEntity(EntityItem* entity) { if (entity->isWearable()) { - emit addingWearable(entity->getEntityItemID()); + QMetaObject::invokeMethod(this, "addingWearable", Q_ARG(QUuid, entity->getEntityItemID())); } } void EntityScriptingInterface::onDeletingEntity(EntityItem* entity) { if (entity->isWearable()) { - emit deletingWearable(entity->getEntityItemID()); + QMetaObject::invokeMethod(this, "deletingWearable", Q_ARG(QUuid, entity->getEntityItemID())); } } From e6c279ee5bd2efe83c931a5621fb8aa9904da3c6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 25 Mar 2019 16:58:29 -0700 Subject: [PATCH 43/63] unlock, rather than lock entities when adjust-attachments window is open, because the windows says you can adjust with hand-controllers --- scripts/system/avatarapp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index ee337694a2..65b422f1ab 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -262,7 +262,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); adjustWearables.setOpened(true); - lockWearables(); + unlockWearables(); Entities.mousePressOnEntity.connect(onSelectedEntity); Messages.subscribe('Hifi-Object-Manipulation'); From d25d290394c34a4262a8e58d15e88e2dea462a2c Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Mon, 25 Mar 2019 17:06:24 -0700 Subject: [PATCH 44/63] refer to ungrabbable wearables as 'frozen' rather than locked, because locked is such an overloaded term --- interface/resources/qml/hifi/AvatarApp.qml | 10 +++--- scripts/system/avatarapp.js | 40 +++++++++++----------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index a57b5713ed..997407885b 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -50,7 +50,7 @@ Rectangle { property var jointNames: [] property var currentAvatarSettings; - property bool wearablesLocked; + property bool wearablesFrozen; function fetchAvatarModelName(marketId, avatar) { var xmlhttp = new XMLHttpRequest(); @@ -190,8 +190,8 @@ Rectangle { updateCurrentAvatarInBookmarks(currentAvatar); } else if (message.method === 'selectAvatarEntity') { adjustWearables.selectWearableByID(message.entityID); - } else if (message.method === 'wearablesLockedChanged') { - wearablesLocked = message.wearablesLocked; + } else if (message.method === 'wearablesFrozenChanged') { + wearablesFrozen = message.wearablesFrozen; } } @@ -526,10 +526,10 @@ Rectangle { anchors.right: adjustLabel.left anchors.verticalCenter: wearablesLabel.verticalCenter anchors.rightMargin: 15 - glyphText: wearablesLocked ? hifi.glyphs.lock : hifi.glyphs.unlock; + glyphText: wearablesFrozen ? hifi.glyphs.lock : hifi.glyphs.unlock; onClicked: { - emitSendToScript({'method' : 'toggleWearablesLock'}); + emitSendToScript({'method' : 'toggleWearablesFrozen'}); } } } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 65b422f1ab..509497669b 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -77,7 +77,7 @@ function updateAvatarWearables(avatar, callback, wearablesOverride) { avatar[ENTRY_AVATAR_ENTITIES] = wearables; sendToQml({'method' : 'wearablesUpdated', 'wearables' : wearables}); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); if(callback) callback(); @@ -179,26 +179,26 @@ var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("html/js/marketplacesInject.js"); -function getWearablesLocked() { - var wearablesLocked = true; +function getWearablesFrozen() { + var wearablesFrozen = true; var wearablesArray = getMyAvatarWearables(); wearablesArray.forEach(function(wearable) { if (isGrabbable(wearable.id)) { - wearablesLocked = false; + wearablesFrozen = false; } }); - return wearablesLocked; + return wearablesFrozen; } -function lockWearables() { +function freezeWearables() { var wearablesArray = getMyAvatarWearables(); wearablesArray.forEach(function(wearable) { setGrabbable(wearable.id, false); }); } -function unlockWearables() { +function unfreezeWearables() { var wearablesArray = getMyAvatarWearables(); wearablesArray.forEach(function(wearable) { setGrabbable(wearable.id, true); @@ -237,7 +237,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See AvatarBookmarks.loadBookmark(message.name); Entities.addingWearable.connect(onAddingWearable); Entities.deletingWearable.connect(onDeletingWearable); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); break; case 'deleteAvatar': AvatarBookmarks.removeBookmark(message.name); @@ -262,7 +262,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See case 'adjustWearablesOpened': currentAvatarWearablesBackup = getMyAvatarWearables(); adjustWearables.setOpened(true); - unlockWearables(); + unfreezeWearables(); Entities.mousePressOnEntity.connect(onSelectedEntity); Messages.subscribe('Hifi-Object-Manipulation'); @@ -377,15 +377,15 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See currentAvatarSettings = getMyAvatarSettings(); break; - case 'toggleWearablesLock': - var wearablesLocked = getWearablesLocked(); - wearablesLocked = !wearablesLocked; - if (wearablesLocked) { - lockWearables(); + case 'toggleWearablesFrozen': + var wearablesFrozen = getWearablesFrozen(); + wearablesFrozen = !wearablesFrozen; + if (wearablesFrozen) { + freezeWearables(); } else { - unlockWearables(); + unfreezeWearables(); } - sendToQml({'method' : 'wearablesLockedChanged', 'wearablesLocked' : wearablesLocked}); + sendToQml({'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : wearablesFrozen}); break; default: print('Unrecognized message from AvatarApp.qml'); @@ -410,7 +410,7 @@ function setGrabbable(entityID, grabbable) { if (properties.avatarEntity && properties.grab.grabbable != grabbable) { var editProps = { grab: { grabbable: grabbable }}; Entities.editEntity(entityID, editProps); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } } @@ -444,14 +444,14 @@ function onAddingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } function onDeletingWearable(entityID) { updateAvatarWearables(currentAvatar, function() { sendToQml({'method' : 'updateAvatarInBookmarks'}); }); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } function handleWearableMessages(channel, message, sender) { @@ -635,7 +635,7 @@ function onTabletScreenChanged(type, url) { if(onAvatarAppScreenNow) { sendToQml({ 'method' : 'initialize', 'data' : { jointNames : MyAvatar.getJointNames() }}); - sendToQml({ 'method' : 'wearablesLockedChanged', 'wearablesLocked' : getWearablesLocked()}); + sendToQml({ 'method' : 'wearablesFrozenChanged', 'wearablesFrozen' : getWearablesFrozen()}); } } From e085a00256d8bb1c83e13d481aa3c247d2bd5343 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 26 Mar 2019 09:32:12 -0700 Subject: [PATCH 45/63] cause 'save' button to unghost if an attachment is adjusted via grab while the adjust-wearables page is open --- .../qml/hifi/avatarapp/AdjustWearables.qml | 1 + scripts/system/avatarapp.js | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml index 136d535b3f..391e4fab37 100644 --- a/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml +++ b/interface/resources/qml/hifi/avatarapp/AdjustWearables.qml @@ -113,6 +113,7 @@ Rectangle { } else if (prop === 'dimensions') { scalespinner.set(wearable[prop].x / wearable.naturalDimensions.x); } + modified = true; } } diff --git a/scripts/system/avatarapp.js b/scripts/system/avatarapp.js index 509497669b..6439d30023 100644 --- a/scripts/system/avatarapp.js +++ b/scripts/system/avatarapp.js @@ -468,30 +468,35 @@ function handleWearableMessages(channel, message, sender) { } var entityID = parsedMessage.grabbedEntity; + + var updateWearable = function() { + // for some reasons Entities.getEntityProperties returns more than was asked.. + var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; + var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); + var properties = {}; + + propertyNames.forEach(function(propertyName) { + properties[propertyName] = entityProperties[propertyName]; + }); + + properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); + sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, + 'wearableIndex' : -1, 'properties' : properties, updateUI : true}); + + }; + if(parsedMessage.action === 'grab') { if(selectedAvatarEntityID !== entityID) { ensureWearableSelected(entityID); sendToQml({'method' : 'selectAvatarEntity', 'entityID' : selectedAvatarEntityID}); } - grabbedAvatarEntityChangeNotifier = Script.setInterval(function() { - // for some reasons Entities.getEntityProperties returns more than was asked.. - var propertyNames = ['localPosition', 'localRotation', 'dimensions', 'naturalDimensions']; - var entityProperties = Entities.getEntityProperties(selectedAvatarEntityID, propertyNames); - var properties = {}; - - propertyNames.forEach(function(propertyName) { - properties[propertyName] = entityProperties[propertyName]; - }); - - properties.localRotationAngles = Quat.safeEulerAngles(properties.localRotation); - sendToQml({'method' : 'wearableUpdated', 'entityID' : selectedAvatarEntityID, 'wearableIndex' : -1, 'properties' : properties, updateUI : true}); - - }, 1000); + grabbedAvatarEntityChangeNotifier = Script.setInterval(updateWearable, 1000); } else if(parsedMessage.action === 'release') { if(grabbedAvatarEntityChangeNotifier !== null) { Script.clearInterval(grabbedAvatarEntityChangeNotifier); grabbedAvatarEntityChangeNotifier = null; + updateWearable(); } } } From fb7daa185d63aa7445bf20959ff9ac1df63dee51 Mon Sep 17 00:00:00 2001 From: Andrew Meadows Date: Wed, 27 Mar 2019 09:53:55 -0700 Subject: [PATCH 46/63] improved physics for grabbed AvatarEntities --- interface/src/avatar/OtherAvatar.cpp | 12 +++++ .../src/avatars-renderer/Avatar.cpp | 26 ----------- .../src/avatars-renderer/Avatar.h | 2 - libraries/entities/src/EntityItem.cpp | 45 ++++++++++--------- libraries/entities/src/EntityItem.h | 8 ++-- 5 files changed, 41 insertions(+), 52 deletions(-) diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 2058408596..b100b33dc8 100755 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -495,6 +495,18 @@ void OtherAvatar::handleChangedAvatarEntityData() { const QUuid NULL_ID = QUuid("{00000000-0000-0000-0000-000000000005}"); entity->setParentID(NULL_ID); entity->setParentID(oldParentID); + + if (entity->stillHasMyGrabAction()) { + // For this case: we want to ignore transform+velocities coming from authoritative OtherAvatar + // because the MyAvatar is grabbing and we expect the local grab state + // to have enough information to prevent simulation drift. + // + // Clever readers might realize this could cause problems. For example, + // if an ignored OtherAvagtar were to simultanously grab the object then there would be + // a noticeable discrepancy between participants in the distributed physics simulation, + // however the difference would be stable and would not drift. + properties.clearTransformOrVelocityChanges(); + } if (entityTree->updateEntity(entityID, properties)) { entity->updateLastEditedFromRemote(); } else { diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index b0a8875cbc..839c4ed1d9 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -372,23 +372,6 @@ bool Avatar::applyGrabChanges() { target->removeGrab(grab); _avatarGrabs.erase(itr); grabAddedOrRemoved = true; - const EntityItemPointer& entity = std::dynamic_pointer_cast(target); - if (entity && entity->getEntityHostType() == entity::HostType::AVATAR) { - // grabs are able to move avatar-entities which belong ot other avatars (assuming - // the entities are grabbable, unlocked, etc). Regardless of who released the grab - // on this entity, the entity's owner needs to send off an update. - QUuid entityOwnerID = entity->getOwningAvatarID(); - if (entityOwnerID == getMyAvatarID() || entityOwnerID == AVATAR_SELF_ID) { - bool success; - SpatiallyNestablePointer myAvatarSN = SpatiallyNestable::findByID(entityOwnerID, success); - if (success) { - std::shared_ptr myAvatar = std::dynamic_pointer_cast(myAvatarSN); - if (myAvatar) { - myAvatar->sendPacket(entity->getID()); - } - } - } - } } else { undeleted.push_back(id); } @@ -2113,12 +2096,3 @@ void Avatar::updateDescendantRenderIDs() { } }); } - -QUuid Avatar::getMyAvatarID() const { - auto nodeList = DependencyManager::get(); - if (nodeList) { - return nodeList->getSessionUUID(); - } else { - return QUuid(); - } -} diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 2bed13cb12..974fae2034 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -180,8 +180,6 @@ public: /// Returns the distance to use as a LOD parameter. float getLODDistance() const; - QUuid getMyAvatarID() const; - virtual void createOrb() { } enum class LoadingStatus { diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index f0bf13891b..bd4c6e5c71 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -789,8 +789,10 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef auto lastEdited = lastEditedFromBufferAdjusted; bool otherOverwrites = overwriteLocalData && !weOwnSimulation; - auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection](quint64 updatedTimestamp, bool valueChanged) { - if (stillHasGrabActions()) { + // calculate hasGrab once outside the lambda rather than calling it every time inside + bool hasGrab = stillHasGrabAction(); + auto shouldUpdate = [this, lastEdited, otherOverwrites, filterRejection, hasGrab](quint64 updatedTimestamp, bool valueChanged) { + if (hasGrab) { return false; } bool simulationChanged = lastEdited > updatedTimestamp; @@ -957,12 +959,18 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef // by doing this parsing here... but it's not likely going to fully recover the content. // - if (overwriteLocalData && (getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES))) { + if (overwriteLocalData && + !hasGrab && + (getDirtyFlags() & (Simulation::DIRTY_TRANSFORM | Simulation::DIRTY_VELOCITIES))) { // NOTE: This code is attempting to "repair" the old data we just got from the server to make it more // closely match where the entities should be if they'd stepped forward in time to "now". The server // is sending us data with a known "last simulated" time. That time is likely in the past, and therefore // this "new" data is actually slightly out of date. We calculate the time we need to skip forward and // use our simulation helper routine to get a best estimate of where the entity should be. + // + // NOTE: We don't want to do this in the hasGrab case because grabs "know best" + // (e.g. grabs will prevent drift between distributed physics simulations). + // float skipTimeForward = (float)(now - lastSimulatedFromBufferAdjusted) / (float)(USECS_PER_SECOND); // we want to extrapolate the motion forward to compensate for packet travel time, but @@ -1426,7 +1434,7 @@ void EntityItem::getTransformAndVelocityProperties(EntityItemProperties& propert void EntityItem::upgradeScriptSimulationPriority(uint8_t priority) { uint8_t newPriority = glm::max(priority, _scriptSimulationPriority); - if (newPriority < SCRIPT_GRAB_SIMULATION_PRIORITY && stillHasGrabActions()) { + if (newPriority < SCRIPT_GRAB_SIMULATION_PRIORITY && stillHasMyGrabAction()) { newPriority = SCRIPT_GRAB_SIMULATION_PRIORITY; } if (newPriority != _scriptSimulationPriority) { @@ -1439,7 +1447,7 @@ void EntityItem::upgradeScriptSimulationPriority(uint8_t priority) { void EntityItem::clearScriptSimulationPriority() { // DO NOT markDirtyFlags(Simulation::DIRTY_SIMULATION_OWNERSHIP_PRIORITY) here, because this // is only ever called from the code that actually handles the dirty flags, and it knows best. - _scriptSimulationPriority = stillHasGrabActions() ? SCRIPT_GRAB_SIMULATION_PRIORITY : 0; + _scriptSimulationPriority = stillHasMyGrabAction() ? SCRIPT_GRAB_SIMULATION_PRIORITY : 0; } void EntityItem::setPendingOwnershipPriority(uint8_t priority) { @@ -2186,7 +2194,7 @@ void EntityItem::enableNoBootstrap() { } void EntityItem::disableNoBootstrap() { - if (!stillHasGrabActions()) { + if (!stillHasMyGrabAction()) { _flags &= ~Simulation::SPECIAL_FLAGS_NO_BOOTSTRAPPING; _flags |= Simulation::DIRTY_COLLISION_GROUP; // may need to not collide with own avatar @@ -2272,7 +2280,13 @@ bool EntityItem::removeAction(EntitySimulationPointer simulation, const QUuid& a return success; } -bool EntityItem::stillHasGrabActions() const { +bool EntityItem::stillHasGrabAction() const { + return !_grabActions.empty(); +} + +// retutrns 'true' if there exists an action that returns 'true' for EntityActionInterface::isMine() +// (e.g. the action belongs to the MyAvatar instance) +bool EntityItem::stillHasMyGrabAction() const { QList holdActions = getActionsOfType(DYNAMIC_TYPE_HOLD); QList::const_iterator i = holdActions.begin(); while (i != holdActions.end()) { @@ -2700,20 +2714,6 @@ void EntityItem::setLastEdited(quint64 lastEdited) { }); } -quint64 EntityItem::getLastBroadcast() const { - quint64 result; - withReadLock([&] { - result = _lastBroadcast; - }); - return result; -} - -void EntityItem::setLastBroadcast(quint64 lastBroadcast) { - withWriteLock([&] { - _lastBroadcast = lastBroadcast; - }); -} - void EntityItem::markAsChangedOnServer() { withWriteLock([&] { _changedOnServer = usecTimestampNow(); @@ -3479,6 +3479,9 @@ void EntityItem::addGrab(GrabPointer grab) { simulation->addDynamic(action); markDirtyFlags(Simulation::DIRTY_MOTION_TYPE); simulation->changeEntity(getThisPointer()); + + // don't forget to set isMine() for locally-created grabs + action->setIsMine(grab->getOwnerID() == Physics::getSessionUUID()); } } diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index fae871a124..01ed949a0c 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -124,8 +124,8 @@ public: { return (float)(usecTimestampNow() - getLastEdited()) / (float)USECS_PER_SECOND; } /// Last time we sent out an edit packet for this entity - quint64 getLastBroadcast() const; - void setLastBroadcast(quint64 lastBroadcast); + quint64 getLastBroadcast() const { return _lastBroadcast; } + void setLastBroadcast(quint64 lastBroadcast) { _lastBroadcast = lastBroadcast; } void markAsChangedOnServer(); quint64 getLastChangedOnServer() const; @@ -562,6 +562,8 @@ public: static void setPrimaryViewFrustumPositionOperator(std::function getPrimaryViewFrustumPositionOperator) { _getPrimaryViewFrustumPositionOperator = getPrimaryViewFrustumPositionOperator; } static glm::vec3 getPrimaryViewFrustumPosition() { return _getPrimaryViewFrustumPositionOperator(); } + bool stillHasMyGrabAction() const; + signals: void requestRenderUpdate(); void spaceUpdate(std::pair data); @@ -574,7 +576,7 @@ protected: void setSimulated(bool simulated) { _simulated = simulated; } const QByteArray getDynamicDataInternal() const; - bool stillHasGrabActions() const; + bool stillHasGrabAction() const; void setDynamicDataInternal(QByteArray dynamicData); virtual void dimensionsChanged() override; From dae69ea4cd75d249562fc3773eaff47cc5dca216 Mon Sep 17 00:00:00 2001 From: Ken Cooke Date: Wed, 27 Mar 2019 10:58:52 -0700 Subject: [PATCH 47/63] Ensure server echo always has unity gain --- .../src/audio/AudioMixerSlave.cpp | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp index f7f8e8a9c1..cb90df58e5 100644 --- a/assignment-client/src/audio/AudioMixerSlave.cpp +++ b/assignment-client/src/audio/AudioMixerSlave.cpp @@ -51,7 +51,7 @@ void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& // mix helpers inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd); inline float computeGain(float masterAvatarGain, float masterInjectorGain, const AvatarAudioStream& listeningNodeStream, - const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho); + const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance); inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition); @@ -504,14 +504,12 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); + float gain = isEcho ? 1.0f + : (isSoloing ? masterAvatarGain + : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, + relativePosition, distance)); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); - float gain = masterAvatarGain; - if (!isSoloing) { - gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, - distance, isEcho); - } - const int HRTF_DATASET_INDEX = 1; if (!streamToAdd->lastPopSucceeded()) { @@ -599,8 +597,8 @@ void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition(); float distance = glm::max(glm::length(relativePosition), EPSILON); - float gain = computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, relativePosition, - distance, isEcho); + float gain = isEcho ? 1.0f : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd, + relativePosition, distance); float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition); mixableStream.hrtf->setParameterHistory(azimuth, distance, gain); @@ -743,8 +741,7 @@ float computeGain(float masterAvatarGain, const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, - float distance, - bool isEcho) { + float distance) { float gain = 1.0f; // injector: apply attenuation @@ -754,7 +751,7 @@ float computeGain(float masterAvatarGain, gain *= masterInjectorGain; // avatar: apply fixed off-axis attenuation to make them quieter as they turn away - } else if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) { + } else if (streamToAdd.getType() == PositionalAudioStream::Microphone) { glm::vec3 rotatedListenerPosition = glm::inverse(streamToAdd.getOrientation()) * relativePosition; // source directivity is based on angle of emission, in local coordinates From fcb45802bde88aefe26154acbeead8ae63a38465 Mon Sep 17 00:00:00 2001 From: Angus Antley Date: Wed, 27 Mar 2019 11:19:28 -0700 Subject: [PATCH 48/63] removed debug print --- libraries/fbx/src/FBXSerializer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/fbx/src/FBXSerializer.cpp b/libraries/fbx/src/FBXSerializer.cpp index 8ff3005ddc..0090eea2e7 100644 --- a/libraries/fbx/src/FBXSerializer.cpp +++ b/libraries/fbx/src/FBXSerializer.cpp @@ -466,7 +466,6 @@ HFMModel* FBXSerializer::extractHFMModel(const QVariantHash& mapping, const QStr } } else if (object.name == "FBXVersion") { fbxVersionNumber = object.properties.at(0).toInt(); - qCDebug(modelformat) << "the fbx version number " << fbxVersionNumber; } } } else if (child.name == "GlobalSettings") { From e62270fccf4156a238e4e58cf1b8cd814eb7e1aa Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Wed, 27 Mar 2019 12:00:30 -0700 Subject: [PATCH 49/63] Fixes for inline jsdoc --- assignment-client/src/avatars/ScriptableAvatar.h | 1 + libraries/avatars/src/AvatarData.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index fbe5675bd8..e5df411099 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -74,6 +74,7 @@ * avatar. Read-only. * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only. * * @example Create a scriptable avatar. * (function () { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index c55d5270e1..43ddbda996 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -479,7 +479,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * avatar. Read-only. * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. - * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only. */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale) From 483b7a67b9322974075b64590907db32f749f226 Mon Sep 17 00:00:00 2001 From: Clement Date: Tue, 12 Feb 2019 15:10:32 -0800 Subject: [PATCH 50/63] Fix simple traits vector bad init --- libraries/avatars/src/AssociatedTraitValues.h | 13 +++++++------ libraries/avatars/src/AvatarTraits.h | 14 +++++++++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/libraries/avatars/src/AssociatedTraitValues.h b/libraries/avatars/src/AssociatedTraitValues.h index e3060a8097..0df8cd9bb5 100644 --- a/libraries/avatars/src/AssociatedTraitValues.h +++ b/libraries/avatars/src/AssociatedTraitValues.h @@ -28,9 +28,10 @@ namespace AvatarTraits { template class AssociatedTraitValues { + using SimpleTypesArray = std::array; public: // constructor that pre-fills _simpleTypes with the default value specified by the template - AssociatedTraitValues() : _simpleTypes(FirstInstancedTrait, defaultValue) {} + AssociatedTraitValues() { std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue); } /// inserts the given value for the given simple trait type void insert(TraitType type, T value) { _simpleTypes[type] = value; } @@ -71,12 +72,12 @@ namespace AvatarTraits { } /// const iterators for the vector of simple type values - typename std::vector::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); } - typename std::vector::const_iterator simpleCEnd() const { return _simpleTypes.cend(); } + typename SimpleTypesArray::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); } + typename SimpleTypesArray::const_iterator simpleCEnd() const { return _simpleTypes.cend(); } /// non-const iterators for the vector of simple type values - typename std::vector::iterator simpleBegin() { return _simpleTypes.begin(); } - typename std::vector::iterator simpleEnd() { return _simpleTypes.end(); } + typename SimpleTypesArray::iterator simpleBegin() { return _simpleTypes.begin(); } + typename SimpleTypesArray::iterator simpleEnd() { return _simpleTypes.end(); } struct TraitWithInstances { TraitType traitType; @@ -96,7 +97,7 @@ namespace AvatarTraits { typename std::vector::iterator instancedEnd() { return _instancedTypes.end(); } private: - std::vector _simpleTypes; + SimpleTypesArray _simpleTypes; /// return the iterator to the matching TraitWithInstances object for a given instanced trait type typename std::vector::iterator instancesForTrait(TraitType traitType) { diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h index 4516572e42..5542acd37f 100644 --- a/libraries/avatars/src/AvatarTraits.h +++ b/libraries/avatars/src/AvatarTraits.h @@ -14,19 +14,31 @@ #include #include +#include #include #include namespace AvatarTraits { enum TraitType : int8_t { + // Null trait NullTrait = -1, - SkeletonModelURL, + + // Simple traits + SkeletonModelURL = 0, + + + // Instanced traits FirstInstancedTrait, AvatarEntity = FirstInstancedTrait, Grab, + + // Traits count TotalTraitTypes }; + const int NUM_SIMPLE_TRAITS = (int)FirstInstancedTrait; + const int NUM_INSTANCED_TRAITS = (int)TotalTraitTypes - (int)FirstInstancedTrait; + const int NUM_TRAITS = (int)TotalTraitTypes; using TraitInstanceID = QUuid; From d7d5938c20ae6718428e1085fe22b13d6cc93ca5 Mon Sep 17 00:00:00 2001 From: Clement Date: Mon, 18 Mar 2019 17:47:52 -0700 Subject: [PATCH 51/63] Pack all simple traits --- libraries/avatars/src/ClientTraitsHandler.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index bcbe5308c7..c9e3b67f16 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -106,9 +106,10 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { auto traitType = static_cast(std::distance(traitStatusesCopy.simpleCBegin(), simpleIt)); if (initialSend || *simpleIt == Updated) { - if (traitType == AvatarTraits::SkeletonModelURL) { - bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); + bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); + + if (traitType == AvatarTraits::SkeletonModelURL) { // keep track of our skeleton version in case we get an override back _currentSkeletonVersion = _currentTraitVersion; } From 3221e1dbd59c4b333e4285427a3b0a595acf732e Mon Sep 17 00:00:00 2001 From: Clement Date: Tue, 19 Mar 2019 19:39:17 -0700 Subject: [PATCH 52/63] Simplify packing/unpacking for easier extension --- .../src/avatars/AvatarMixerClientData.cpp | 2 +- .../src/avatars/AvatarMixerSlave.cpp | 6 +- libraries/avatars/src/AvatarData.cpp | 151 +++++------------- libraries/avatars/src/AvatarData.h | 22 ++- libraries/avatars/src/AvatarTraits.cpp | 135 ++++++++++++++++ libraries/avatars/src/AvatarTraits.h | 27 ++-- libraries/avatars/src/ClientTraitsHandler.cpp | 6 +- 7 files changed, 209 insertions(+), 140 deletions(-) create mode 100644 libraries/avatars/src/AvatarTraits.cpp diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 557c5c9fe3..dfbeca96ce 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -341,7 +341,7 @@ void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedDa // the returned set traits packet uses the trait version from the incoming packet // so the client knows they should not overwrite if they have since changed the trait - _avatar->packTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion); + AvatarTraits::packVersionedTrait(AvatarTraits::SkeletonModelURL, *packet, traitVersion, *_avatar); auto nodeList = DependencyManager::get(); nodeList->sendPacket(std::move(packet), sendingNode); diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index e59c81f4b7..fdbdf21607 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -139,7 +139,8 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis if (lastReceivedVersion > lastSentVersionRef) { bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten); // there is an update to this trait, add it to the traits packet - bytesWritten += sendingAvatar->packTrait(traitType, traitsPacketList, lastReceivedVersion); + bytesWritten += AvatarTraits::packVersionedTrait(traitType, traitsPacketList, + lastReceivedVersion, *sendingAvatar); // update the last sent version lastSentVersionRef = lastReceivedVersion; // Remember which versions we sent in this particular packet @@ -194,7 +195,8 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten); // this instance version exists and has never been sent or is newer so we need to send it - bytesWritten += sendingAvatar->packTraitInstance(traitType, instanceID, traitsPacketList, receivedVersion); + bytesWritten += AvatarTraits::packVersionedTraitInstance(traitType, instanceID, traitsPacketList, + receivedVersion, *sendingAvatar); if (sentInstanceIt != sentIDValuePairs.end()) { sentInstanceIt->value = receivedVersion; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 39dfaa8a1a..9c923580f9 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1990,42 +1990,16 @@ QUrl AvatarData::getWireSafeSkeletonModelURL() const { } } -qint64 AvatarData::packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination, - AvatarTraits::TraitVersion traitVersion) { - - qint64 bytesWritten = 0; - - if (traitType == AvatarTraits::SkeletonModelURL) { - - QByteArray encodedSkeletonURL = getWireSafeSkeletonModelURL().toEncoded(); - - if (encodedSkeletonURL.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack simple trait" << traitType << "of size" << encodedSkeletonURL.size() - << "bytes since it exceeds the maximum size" << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - AvatarTraits::TraitWireSize encodedURLSize = encodedSkeletonURL.size(); - bytesWritten += destination.writePrimitive(encodedURLSize); - - bytesWritten += destination.write(encodedSkeletonURL); - } - - return bytesWritten; +QByteArray AvatarData::packSkeletonModelURL() const { + return getWireSafeSkeletonModelURL().toEncoded(); } +void AvatarData::unpackSkeletonModelURL(const QByteArray& data) { + auto skeletonModelURL = QUrl::fromEncoded(data); + setSkeletonModelURL(skeletonModelURL); +} -qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; - +QByteArray AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) { // grab a read lock on the avatar entities and check for entity data for the given ID QByteArray entityBinaryData; _avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] { @@ -2034,104 +2008,48 @@ qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitTy } }); - if (entityBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << entityBinaryData.size() - << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - bytesWritten += destination.write(traitInstanceID.toRfc4122()); - - if (!entityBinaryData.isNull()) { - AvatarTraits::TraitWireSize entityBinarySize = entityBinaryData.size(); - - bytesWritten += destination.writePrimitive(entityBinarySize); - bytesWritten += destination.write(entityBinaryData); - } else { - bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); - } - - return bytesWritten; + return entityBinaryData; } - -qint64 AvatarData::packGrabTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; - +QByteArray AvatarData::packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) { // grab a read lock on the avatar grabs and check for grab data for the given ID QByteArray grabBinaryData; - _avatarGrabsLock.withReadLock([this, &grabBinaryData, &traitInstanceID] { if (_avatarGrabData.contains(traitInstanceID)) { grabBinaryData = _avatarGrabData[traitInstanceID]; } }); - if (grabBinaryData.size() > AvatarTraits::MAXIMUM_TRAIT_SIZE) { - qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << grabBinaryData.size() - << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; - return 0; - } - - bytesWritten += destination.writePrimitive(traitType); - - if (traitVersion > AvatarTraits::DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } - - bytesWritten += destination.write(traitInstanceID.toRfc4122()); - - if (!grabBinaryData.isNull()) { - AvatarTraits::TraitWireSize grabBinarySize = grabBinaryData.size(); - - bytesWritten += destination.writePrimitive(grabBinarySize); - bytesWritten += destination.write(grabBinaryData); - } else { - bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); - } - - return bytesWritten; + return grabBinaryData; } -qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion) { - qint64 bytesWritten = 0; +QByteArray AvatarData::packTrait(AvatarTraits::TraitType traitType) const { + QByteArray traitBinaryData; + // Call packer function + if (traitType == AvatarTraits::SkeletonModelURL) { + traitBinaryData = packSkeletonModelURL(); + } + + return traitBinaryData; +} + +QByteArray AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID) { + QByteArray traitBinaryData; + + // Call packer function if (traitType == AvatarTraits::AvatarEntity) { - bytesWritten += packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion); + traitBinaryData = packAvatarEntityTraitInstance(traitInstanceID); } else if (traitType == AvatarTraits::Grab) { - bytesWritten += packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion); + traitBinaryData = packGrabTraitInstance(traitInstanceID); } - return bytesWritten; -} - -void AvatarData::prepareResetTraitInstances() { - if (_clientTraitsHandler) { - _avatarEntitiesLock.withReadLock([this]{ - foreach (auto entityID, _packedAvatarEntityData.keys()) { - _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); - } - foreach (auto grabID, _avatarGrabData.keys()) { - _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID); - } - }); - } + return traitBinaryData; } void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData) { if (traitType == AvatarTraits::SkeletonModelURL) { - // get the URL from the binary data - auto skeletonModelURL = QUrl::fromEncoded(traitBinaryData); - setSkeletonModelURL(skeletonModelURL); + unpackSkeletonModelURL(traitBinaryData); } } @@ -2152,6 +2070,19 @@ void AvatarData::processDeletedTraitInstance(AvatarTraits::TraitType traitType, } } +void AvatarData::prepareResetTraitInstances() { + if (_clientTraitsHandler) { + _avatarEntitiesLock.withReadLock([this]{ + foreach (auto entityID, _packedAvatarEntityData.keys()) { + _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); + } + foreach (auto grabID, _avatarGrabData.keys()) { + _clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::Grab, grabID); + } + }); + } +} + QByteArray AvatarData::identityByteArray(bool setIsReplicated) const { QByteArray identityData; QDataStream identityStream(&identityData, QIODevice::Append); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 00e7e67923..dd6f0a9efd 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1134,18 +1134,16 @@ public: // identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange. void processAvatarIdentity(QDataStream& packetStream, bool& identityChanged, bool& displayNameChanged); - qint64 packTrait(AvatarTraits::TraitType traitType, ExtendedIODevice& destination, - AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION); - qint64 packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion = AvatarTraits::NULL_TRAIT_VERSION); - - void prepareResetTraitInstances(); + QByteArray packTrait(AvatarTraits::TraitType traitType) const; + QByteArray packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); void processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData); void processTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData); void processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); + void prepareResetTraitInstances(); + QByteArray identityByteArray(bool setIsReplicated = false) const; QUrl getWireSafeSkeletonModelURL() const; @@ -1596,13 +1594,13 @@ protected: bool hasParent() const { return !getParentID().isNull(); } bool hasFaceTracker() const { return _headData ? _headData->_isFaceTrackerConnected : false; } - qint64 packAvatarEntityTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion); - qint64 packGrabTraitInstance(AvatarTraits::TraitType traitType, - AvatarTraits::TraitInstanceID traitInstanceID, - ExtendedIODevice& destination, AvatarTraits::TraitVersion traitVersion); + QByteArray packSkeletonModelURL() const; + QByteArray packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); + QByteArray packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); + void unpackSkeletonModelURL(const QByteArray& data); + + // isReplicated will be true on downstream Avatar Mixers and their clients, but false on the upstream "master" // Audio Mixer that the replicated avatar is connected to. bool _isReplicated{ false }; diff --git a/libraries/avatars/src/AvatarTraits.cpp b/libraries/avatars/src/AvatarTraits.cpp new file mode 100644 index 0000000000..724f30e2f3 --- /dev/null +++ b/libraries/avatars/src/AvatarTraits.cpp @@ -0,0 +1,135 @@ +// +// AvatarTraits.cpp +// libraries/avatars/src +// +// Created by Clement Brisset on 3/19/19. +// Copyright 2019 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 "AvatarTraits.h" + +#include + +#include "AvatarData.h" + +namespace AvatarTraits { + + qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTrait(traitType); + auto traitBinaryDataSize = traitBinaryData.size(); + + // Verify packed data + if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + return bytesWritten; + } + + qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination, + TraitVersion traitVersion, const AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTrait(traitType); + auto traitBinaryDataSize = traitBinaryData.size(); + + // Verify packed data + if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitVersion)traitVersion); + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + return bytesWritten; + } + + + qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID); + auto traitBinaryDataSize = traitBinaryData.size(); + + + // Verify packed data + if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.write(traitInstanceID.toRfc4122()); + + if (!traitBinaryData.isNull()) { + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + } else { + bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); + } + + return bytesWritten; + } + + qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, TraitVersion traitVersion, + AvatarData& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID); + auto traitBinaryDataSize = traitBinaryData.size(); + + + // Verify packed data + if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitVersion)traitVersion); + bytesWritten += destination.write(traitInstanceID.toRfc4122()); + + if (!traitBinaryData.isNull()) { + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + } else { + bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); + } + + return bytesWritten; + } + + + qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, + TraitVersion traitVersion) { + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive(traitType); + if (traitVersion > DEFAULT_TRAIT_VERSION) { + bytesWritten += destination.writePrimitive(traitVersion); + } + bytesWritten += destination.write(instanceID.toRfc4122()); + bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE); + return bytesWritten; + } +}; diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h index 5542acd37f..adff34f351 100644 --- a/libraries/avatars/src/AvatarTraits.h +++ b/libraries/avatars/src/AvatarTraits.h @@ -19,6 +19,9 @@ #include +class ExtendedIODevice; +class AvatarData; + namespace AvatarTraits { enum TraitType : int8_t { // Null trait @@ -36,6 +39,7 @@ namespace AvatarTraits { // Traits count TotalTraitTypes }; + const int NUM_SIMPLE_TRAITS = (int)FirstInstancedTrait; const int NUM_INSTANCED_TRAITS = (int)TotalTraitTypes - (int)FirstInstancedTrait; const int NUM_TRAITS = (int)TotalTraitTypes; @@ -58,22 +62,19 @@ namespace AvatarTraits { const TraitMessageSequence FIRST_TRAIT_SEQUENCE = 0; const TraitMessageSequence MAX_TRAIT_SEQUENCE = INT64_MAX; - inline qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, - TraitVersion traitVersion = NULL_TRAIT_VERSION) { - qint64 bytesWritten = 0; + qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarData& avatar); + qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination, + TraitVersion traitVersion, const AvatarData& avatar); - bytesWritten += destination.writePrimitive(traitType); + qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, AvatarData& avatar); + qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, TraitVersion traitVersion, + AvatarData& avatar); - if (traitVersion > DEFAULT_TRAIT_VERSION) { - bytesWritten += destination.writePrimitive(traitVersion); - } + qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, + TraitVersion traitVersion = NULL_TRAIT_VERSION); - bytesWritten += destination.write(instanceID.toRfc4122()); - - bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE); - - return bytesWritten; - } }; #endif // hifi_AvatarTraits_h diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index c9e3b67f16..a2d21fed54 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -106,7 +106,7 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { auto traitType = static_cast(std::distance(traitStatusesCopy.simpleCBegin(), simpleIt)); if (initialSend || *simpleIt == Updated) { - bytesWritten += _owningAvatar->packTrait(traitType, *traitsPacketList); + bytesWritten += AvatarTraits::packTrait(traitType, *traitsPacketList, *_owningAvatar); if (traitType == AvatarTraits::SkeletonModelURL) { @@ -125,7 +125,9 @@ int ClientTraitsHandler::sendChangedTraitsToMixer() { || instanceIDValuePair.value == Updated) { // this is a changed trait we need to send or we haven't send out trait information yet // ask the owning avatar to pack it - bytesWritten += _owningAvatar->packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, *traitsPacketList); + bytesWritten += AvatarTraits::packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, + *traitsPacketList, *_owningAvatar); + } else if (!initialSend && instanceIDValuePair.value == Deleted) { // pack delete for this trait instance bytesWritten += AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id, From 88a19f26e20e7d5e88e1d2b2c00cadf96e5c5540 Mon Sep 17 00:00:00 2001 From: Clement Date: Wed, 20 Mar 2019 17:04:22 -0700 Subject: [PATCH 53/63] Use process function for overrides --- libraries/avatars/src/AvatarTraits.h | 1 - libraries/avatars/src/ClientTraitsHandler.cpp | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/avatars/src/AvatarTraits.h b/libraries/avatars/src/AvatarTraits.h index adff34f351..13d64ec225 100644 --- a/libraries/avatars/src/AvatarTraits.h +++ b/libraries/avatars/src/AvatarTraits.h @@ -30,7 +30,6 @@ namespace AvatarTraits { // Simple traits SkeletonModelURL = 0, - // Instanced traits FirstInstancedTrait, AvatarEntity = FirstInstancedTrait, diff --git a/libraries/avatars/src/ClientTraitsHandler.cpp b/libraries/avatars/src/ClientTraitsHandler.cpp index a2d21fed54..f6bd66e89a 100644 --- a/libraries/avatars/src/ClientTraitsHandler.cpp +++ b/libraries/avatars/src/ClientTraitsHandler.cpp @@ -165,11 +165,11 @@ void ClientTraitsHandler::processTraitOverride(QSharedPointer m // override the skeleton URL but do not mark the trait as having changed // so that we don't unecessarily send a new trait packet to the mixer with the overriden URL - auto encodedSkeletonURL = QUrl::fromEncoded(message->readWithoutCopy(traitBinarySize)); auto hasChangesBefore = _hasChangedTraits; - _owningAvatar->setSkeletonModelURL(encodedSkeletonURL); + auto traitBinaryData = message->readWithoutCopy(traitBinarySize); + _owningAvatar->processTrait(traitType, traitBinaryData); // setSkeletonModelURL will flag us for changes to the SkeletonModelURL so we reset some state here to // avoid unnecessarily sending the overriden skeleton model URL back to the mixer From e95efc29e4c955e8f3573e6a6f794a2bdefbdeae Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Wed, 27 Mar 2019 19:00:28 -0700 Subject: [PATCH 54/63] Bug fix after code review. --- tools/nitpick/src/TestRunnerMobile.cpp | 35 +++++++++++++++----------- tools/nitpick/src/TestRunnerMobile.h | 2 ++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index a848c94755..969da02c9e 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -114,19 +114,17 @@ void TestRunnerMobile::connectDevice() { QString deviceID = tokens[0]; // Find the model entry - int i; - for (i = 0; i < tokens.size(); ++i) { - if (tokens[i].contains(MODEL)) { - break; - } - } - _modelName = "UNKNOWN"; - if (i < tokens.size()) { - QString modelID = tokens[i].split(':')[1]; + for (int i = 0; i < tokens.size(); ++i) { + if (tokens[i].contains(MODEL)) { + if (i < tokens.size()) { + QString modelID = tokens[i].split(':')[1]; - if (modelNames.count(modelID) == 1) { - _modelName = modelNames[modelID]; + if (modelNames.count(modelID) == 1) { + _modelName = modelNames[modelID]; + } + } + break; } } @@ -225,10 +223,16 @@ void TestRunnerMobile::runInterface() { startCommand = "io.highfidelity.hifiinterface/.PermissionChecker"; } + QString serverIP { getServerIP() }; + if (serverIP == NETWORK_NOT_FOUND) { + _runInterfacePushbutton->setEnabled(false); + return; + } + QString command = _adbInterface->getAdbCommand() + " shell am start -n " + startCommand + " --es args \\\"" + - " --url hifi://" + getServerIP() + "/0,0,0" + " --url hifi://" + serverIP + "/0,0,0" " --no-updater" + " --no-login-suggestion" + " --testScript " + testScript + " quitWhenFinished" + @@ -268,10 +272,10 @@ QString TestRunnerMobile::getServerIP() { QString line = ifconfigFile.readLine(); while (!line.isNull()) { // The device IP is in the line following the "wlan0" line - line = ifconfigFile.readLine(); if (line.left(6) == "wlan0 ") { break; } + line = ifconfigFile.readLine(); } // The following line looks like this "inet addr:192.168.0.15 Bcast:192.168.0.255 Mask:255.255.255.0" @@ -280,8 +284,9 @@ QString TestRunnerMobile::getServerIP() { QStringList lineParts = line.split(':'); if (lineParts.size() < 4) { QMessageBox::critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), - "IP address line not in expected format: " + line); - exit(-1); + "IP address line not in expected format: " + line + "(check that device WIFI is on)"); + + return NETWORK_NOT_FOUND; } qint64 deviceIP = convertToBinary(lineParts[1].split(' ')[0]); diff --git a/tools/nitpick/src/TestRunnerMobile.h b/tools/nitpick/src/TestRunnerMobile.h index b94cd47647..09f847785b 100644 --- a/tools/nitpick/src/TestRunnerMobile.h +++ b/tools/nitpick/src/TestRunnerMobile.h @@ -80,5 +80,7 @@ private: AdbInterface* _adbInterface; QString _modelName; + + QString NETWORK_NOT_FOUND{ "NETWORK NOT FOUND"}; }; #endif From 3d5035886c8533ccce974e59e3db6ff054f65c10 Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Thu, 27 Dec 2018 12:55:52 +0200 Subject: [PATCH 55/63] Allow logging-in with an email that contains a '+' sign. Previously, attempts to login using an email such as "my+name@example.com" didn't work because the username wasn't URL-encoded when it was sent to the server, so on the server the '+' was changed to a space. --- libraries/networking/src/AccountManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 4647c50496..226433e388 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -536,7 +536,7 @@ void AccountManager::requestAccessToken(const QString& login, const QString& pas QByteArray postData; postData.append("grant_type=password&"); - postData.append("username=" + login + "&"); + postData.append("username=" + QUrl::toPercentEncoding(login) + "&"); postData.append("password=" + QUrl::toPercentEncoding(password) + "&"); postData.append("scope=" + ACCOUNT_MANAGER_REQUESTED_SCOPE); From de1c40e994092e6c56f16434ba5df051c63743eb Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 28 Mar 2019 11:27:35 -0700 Subject: [PATCH 56/63] Changed version. --- tools/nitpick/src/common.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/nitpick/src/common.h b/tools/nitpick/src/common.h index 09bf23fdfc..51f1f85113 100644 --- a/tools/nitpick/src/common.h +++ b/tools/nitpick/src/common.h @@ -60,5 +60,5 @@ const double R_Y = 0.212655f; const double G_Y = 0.715158f; const double B_Y = 0.072187f; -const QString nitpickVersion{ "21660" }; +const QString nitpickVersion{ "v3.1.5" }; #endif // hifi_common_h \ No newline at end of file From d230fc86db509ef1d55243a983983106b5822272 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Thu, 28 Mar 2019 12:23:25 -0700 Subject: [PATCH 57/63] CR comments. --- tools/nitpick/src/TestRunnerMobile.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tools/nitpick/src/TestRunnerMobile.cpp b/tools/nitpick/src/TestRunnerMobile.cpp index 969da02c9e..53a74da82f 100644 --- a/tools/nitpick/src/TestRunnerMobile.cpp +++ b/tools/nitpick/src/TestRunnerMobile.cpp @@ -117,12 +117,10 @@ void TestRunnerMobile::connectDevice() { _modelName = "UNKNOWN"; for (int i = 0; i < tokens.size(); ++i) { if (tokens[i].contains(MODEL)) { - if (i < tokens.size()) { - QString modelID = tokens[i].split(':')[1]; + QString modelID = tokens[i].split(':')[1]; - if (modelNames.count(modelID) == 1) { - _modelName = modelNames[modelID]; - } + if (modelNames.count(modelID) == 1) { + _modelName = modelNames[modelID]; } break; } @@ -223,7 +221,7 @@ void TestRunnerMobile::runInterface() { startCommand = "io.highfidelity.hifiinterface/.PermissionChecker"; } - QString serverIP { getServerIP() }; + QString serverIP { getServerIP() }; if (serverIP == NETWORK_NOT_FOUND) { _runInterfacePushbutton->setEnabled(false); return; From 88b7687183b1533716bb0cf44d8f53498c0f1608 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 28 Mar 2019 13:19:16 -0700 Subject: [PATCH 58/63] Case 21726 - Domain lost recent entity edits upon restart When an entity server starts up, grabs the version of the models.json file locally, and then queries the domain server for its copy of the models.json file...iff the domain server version is newer. There was a bug in this process in that the comparison was made between the wrong version, specifically the 'file format version' which doesn't change unless there was a protocol change...and not the data version, which increments every time a change is made to a domain. Therefore, the version of the models.json on the domain server was never downloaded to the entity server, even when it was newer. It would be newer if the entity server assignment was moved to a machine with an old version of the models.json file, which was in fact the case on distributed3 during this period of time. --- domain-server/src/DomainServer.cpp | 10 +++++----- libraries/octree/src/OctreePersistThread.cpp | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 8d5cb165cb..5f82700e9c 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1766,14 +1766,14 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointerreadPrimitive(&remoteHasExistingData); if (remoteHasExistingData) { constexpr size_t UUID_SIZE_BYTES = 16; auto idData = message->read(UUID_SIZE_BYTES); id = QUuid::fromRfc4122(idData); - message->readPrimitive(&version); - qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << version << ")"; + message->readPrimitive(&dataVersion); + qCDebug(domain_server) << "Entity server does have existing data: ID(" << id << ") DataVersion(" << dataVersion << ")"; } else { qCDebug(domain_server) << "Entity server does not have existing data"; } @@ -1782,11 +1782,11 @@ void DomainServer::processOctreeDataRequestMessage(QSharedPointerwritePrimitive(false); } else { - qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.version << ")"; + qCDebug(domain_server) << "Sending newer octree data to ES: ID(" << data.id << ") DataVersion(" << data.dataVersion << ")"; QFile file(entityFilePath); if (file.open(QIODevice::ReadOnly)) { reply->writePrimitive(true); diff --git a/libraries/octree/src/OctreePersistThread.cpp b/libraries/octree/src/OctreePersistThread.cpp index 32ee72ea1c..20ba3cde60 100644 --- a/libraries/octree/src/OctreePersistThread.cpp +++ b/libraries/octree/src/OctreePersistThread.cpp @@ -82,11 +82,11 @@ void OctreePersistThread::start() { } if (data.readOctreeDataInfoFromData(_cachedJSONData)) { - qCDebug(octree) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.version << ")"; + qCDebug(octree) << "Current octree data: ID(" << data.id << ") DataVersion(" << data.dataVersion << ")"; packet->writePrimitive(true); auto id = data.id.toRfc4122(); packet->write(id); - packet->writePrimitive(data.version); + packet->writePrimitive(data.dataVersion); } else { _cachedJSONData.clear(); qCWarning(octree) << "No octree data found"; @@ -144,8 +144,8 @@ void OctreePersistThread::handleOctreeDataFileReply(QSharedPointersetOctreeVersionInfo(data.id, data.version); + qDebug() << "Setting entity version info to: " << data.id << data.dataVersion; + _tree->setOctreeVersionInfo(data.id, data.dataVersion); } bool persistentFileRead; From 44b92c542b2706bd30f77ccd98529fe14b69c099 Mon Sep 17 00:00:00 2001 From: Roxanne Skelly Date: Thu, 28 Mar 2019 16:05:24 -0700 Subject: [PATCH 59/63] Case 20499 - Scripts that use AppUI don't call `that.onClosed()` if the script is restarted while the app is open --- scripts/modules/appUi.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js index 3e8e0b1008..9771348377 100644 --- a/scripts/modules/appUi.js +++ b/scripts/modules/appUi.js @@ -353,10 +353,11 @@ function AppUi(properties) { // Close if necessary, clean up any remaining handlers, and remove the button. GlobalServices.myUsernameChanged.disconnect(restartNotificationPoll); GlobalServices.findableByChanged.disconnect(restartNotificationPoll); + that.tablet.screenChanged.disconnect(that.onScreenChanged); if (that.isOpen) { that.close(); + that.onScreenChanged("", ""); } - that.tablet.screenChanged.disconnect(that.onScreenChanged); if (that.button) { if (that.onClicked) { that.button.clicked.disconnect(that.onClicked); From 687409b756081de9997fc4173ee945c7a6827f83 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 29 Mar 2019 01:46:57 +0100 Subject: [PATCH 60/63] ignore case for .fbx file extension in AvatarDoctor --- interface/src/avatar/AvatarDoctor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/AvatarDoctor.cpp b/interface/src/avatar/AvatarDoctor.cpp index 43e50ea049..01a40e89fd 100644 --- a/interface/src/avatar/AvatarDoctor.cpp +++ b/interface/src/avatar/AvatarDoctor.cpp @@ -92,7 +92,7 @@ void AvatarDoctor::startDiagnosing() { _model = resource; const auto model = resource.data(); const auto avatarModel = resource.data()->getHFMModel(); - if (!avatarModel.originalURL.endsWith(".fbx")) { + if (!avatarModel.originalURL.toLower().endsWith(".fbx")) { addError("Unsupported avatar model format.", "unsupported-format"); emit complete(getErrors()); return; From 7e21a3f372f6c6ce04dffa621bd36c48f0727ef6 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 29 Mar 2019 01:48:57 +0100 Subject: [PATCH 61/63] disable case sensitivity in file browsers (allow .fbx .fBx .FBX instead of only .fbx) --- interface/resources/qml/dialogs/FileDialog.qml | 3 ++- interface/resources/qml/dialogs/TabletFileDialog.qml | 3 ++- .../qml/hifi/tablet/tabletWindows/TabletFileDialog.qml | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index ba5e162391..4eea3566b8 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -320,6 +320,7 @@ ModalWindow { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory diff --git a/interface/resources/qml/dialogs/TabletFileDialog.qml b/interface/resources/qml/dialogs/TabletFileDialog.qml index 6c4e32dc5a..5bcc42f101 100644 --- a/interface/resources/qml/dialogs/TabletFileDialog.qml +++ b/interface/resources/qml/dialogs/TabletFileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -285,6 +285,7 @@ TabletModalWindow { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory diff --git a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml index a27c7b59dc..36a37134bf 100644 --- a/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml +++ b/interface/resources/qml/hifi/tablet/tabletWindows/TabletFileDialog.qml @@ -9,7 +9,7 @@ // import QtQuick 2.7 -import Qt.labs.folderlistmodel 2.1 +import Qt.labs.folderlistmodel 2.2 import Qt.labs.settings 1.0 import QtQuick.Dialogs 1.2 as OriginalDialogs import QtQuick.Controls 1.4 as QQC1 @@ -279,6 +279,7 @@ Rectangle { FolderListModel { id: folderListModel nameFilters: selectionType.currentFilter + caseSensitive: false showDirsFirst: true showDotAndDotDot: false showFiles: !root.selectDirectory From bc7fb10ab95c3f648a5bb1bd056568410fd4a593 Mon Sep 17 00:00:00 2001 From: Simon Walton Date: Thu, 28 Mar 2019 17:54:35 -0700 Subject: [PATCH 62/63] Fixes from review --- assignment-client/src/avatars/AvatarMixer.cpp | 4 ++-- assignment-client/src/avatars/AvatarMixer.h | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 5f7e197c8f..9816cebf43 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -262,7 +262,7 @@ void AvatarMixer::start() { { if (_dirtyHeroStatus) { _dirtyHeroStatus = false; - nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + nodeList->nestedEach([](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { std::for_each(cbegin, cend, [](const SharedNodePointer& node) { if (node->getType() == NodeType::Agent) { NodeData* nodeData = node->getLinkedData(); @@ -1108,7 +1108,7 @@ void AvatarMixer::entityAdded(EntityItem* entity) { if (entity->getType() == EntityTypes::Zone) { _dirtyHeroStatus = true; entity->registerChangeHandler([this](const EntityItemID& entityItemID) { - this->entityChange(); + entityChange(); }); } } diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index f65f04f279..10dff5e8a4 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -46,6 +46,11 @@ public slots: void sendStatsPacket() override; + // Avatar zone possibly changed + void entityAdded(EntityItem* entity); + void entityRemoved(EntityItem* entity); + void entityChange(); + private slots: void queueIncomingPacket(QSharedPointer message, SharedNodePointer node); void handleAdjustAvatarSorting(QSharedPointer message, SharedNodePointer senderNode); @@ -147,12 +152,6 @@ private: AvatarMixerSlavePool _slavePool; SlaveSharedData _slaveSharedData; - -public slots: - // Avatar zone possibly changed - void entityAdded(EntityItem* entity); - void entityRemoved(EntityItem* entity); - void entityChange(); }; #endif // hifi_AvatarMixer_h From 709515dd7408d3283dedc00098bdfb85e2b94dd2 Mon Sep 17 00:00:00 2001 From: NissimHadar Date: Fri, 29 Mar 2019 11:48:15 -0700 Subject: [PATCH 63/63] Use place holder instead of RCC file. --- tools/nitpick/compiledResources/.placeholder | 0 tools/nitpick/compiledResources/resources.rcc | Bin 42 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tools/nitpick/compiledResources/.placeholder delete mode 100644 tools/nitpick/compiledResources/resources.rcc diff --git a/tools/nitpick/compiledResources/.placeholder b/tools/nitpick/compiledResources/.placeholder new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tools/nitpick/compiledResources/resources.rcc b/tools/nitpick/compiledResources/resources.rcc deleted file mode 100644 index 15f51ed7f4d9aa2328eca21473fd352cf3605021..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42 dcmXRcN-bt!U|?ckU=TsV5D^ey1d|L53;<0C0sQ~~