diff --git a/interface/resources/icons/tablet-icons/goto-a-msg.svg b/interface/resources/icons/tablet-icons/goto-a-msg.svg new file mode 100644 index 0000000000..f1f611adb9 --- /dev/null +++ b/interface/resources/icons/tablet-icons/goto-a-msg.svg @@ -0,0 +1,57 @@ + + + +image/svg+xml + + \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/goto-msg.svg b/interface/resources/icons/tablet-icons/goto-i-msg.svg similarity index 100% rename from interface/resources/icons/tablet-icons/goto-msg.svg rename to interface/resources/icons/tablet-icons/goto-i-msg.svg diff --git a/interface/resources/icons/tablet-icons/wallet-a-msg.svg b/interface/resources/icons/tablet-icons/wallet-a-msg.svg new file mode 100644 index 0000000000..d51c3e99a2 --- /dev/null +++ b/interface/resources/icons/tablet-icons/wallet-a-msg.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/interface/resources/icons/tablet-icons/wallet-i-msg.svg b/interface/resources/icons/tablet-icons/wallet-i-msg.svg new file mode 100644 index 0000000000..676f97a966 --- /dev/null +++ b/interface/resources/icons/tablet-icons/wallet-i-msg.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml new file mode 100644 index 0000000000..8f391f24c0 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml @@ -0,0 +1,290 @@ +// +// marketplaceItemTester +// qml/hifi/commerce/marketplaceItemTester +// +// Load items not in the marketplace for testing purposes +// +// Created by Zach Fox on 2018-09-05 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.0 +import QtQuick.Layouts 1.1 +import Hifi 1.0 as Hifi +import "../../../styles-uit" as HifiStylesUit +import "../../../controls-uit" as HifiControlsUit + + + +Rectangle { + id: root + + property string installedApps + property var nextResourceObjectId: 0 + signal sendToScript(var message) + + HifiStylesUit.HifiConstants { id: hifi } + ListModel { id: resourceListModel } + + color: hifi.colors.white + + AnimatedImage { + id: spinner; + source: "spinner.gif" + width: 74; + height: width; + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + } + + function fromScript(message) { + switch (message.method) { + case "newResourceObjectInTest": + var resourceObject = message.resourceObject; + resourceListModel.append(resourceObject); + spinner.visible = false; + break; + case "nextObjectIdInTest": + nextResourceObjectId = message.id; + spinner.visible = false; + break; + } + } + + function buildResourceObj(resource) { + resource = resource.trim(); + var assetType = (resource.match(/\.app\.json$/) ? "application" : + resource.match(/\.fst$/) ? "avatar" : + resource.match(/\.json\.gz$/) ? "content set" : + resource.match(/\.json$/) ? "entity or wearable" : + "unknown"); + return { "id": nextResourceObjectId++, + "resource": resource, + "assetType": assetType }; + } + + function installResourceObj(resourceObj) { + if ("application" === resourceObj.assetType) { + Commerce.installApp(resourceObj.resource); + } + } + + function addAllInstalledAppsToList() { + var i, apps = Commerce.getInstalledApps().split(","), len = apps.length; + for(i = 0; i < len - 1; ++i) { + if (i in apps) { + resourceListModel.append(buildResourceObj(apps[i])); + } + } + } + + function toUrl(resource) { + var httpPattern = /^http/i; + return httpPattern.test(resource) ? resource : "file:///" + resource; + } + + function rezEntity(resource, entityType) { + sendToScript({ + method: 'tester_rezClicked', + itemHref: toUrl(resource), + itemType: entityType}); + } + + ListView { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.bottomMargin: 40 + anchors.rightMargin: 12 + model: resourceListModel + spacing: 5 + interactive: false + + delegate: RowLayout { + anchors.left: parent.left + width: parent.width + spacing: 5 + + property var actions: { + "forward": function(resource, assetType){ + switch(assetType) { + case "application": + Commerce.openApp(resource); + break; + case "avatar": + MyAvatar.useFullAvatarURL(resource); + break; + case "content set": + urlHandler.handleUrl("hifi://localhost/0,0,0"); + Commerce.replaceContentSet(toUrl(resource), ""); + break; + case "entity": + case "wearable": + rezEntity(resource, assetType); + break; + default: + print("Marketplace item tester unsupported assetType " + assetType); + } + }, + "trash": function(){ + if ("application" === assetType) { + Commerce.uninstallApp(resource); + } + sendToScript({ + method: "tester_deleteResourceObject", + objectId: resourceListModel.get(index).id}); + resourceListModel.remove(index); + } + } + + Column { + Layout.preferredWidth: root.width * .6 + spacing: 5 + Text { + text: { + var match = resource.match(/\/([^/]*)$/); + return match ? match[1] : resource; + } + font.pointSize: 12 + horizontalAlignment: Text.AlignBottom + } + Text { + text: resource + font.pointSize: 8 + width: root.width * .6 + horizontalAlignment: Text.AlignBottom + wrapMode: Text.WrapAnywhere + } + } + + ComboBox { + id: comboBox + + Layout.preferredWidth: root.width * .2 + + model: [ + "application", + "avatar", + "content set", + "entity", + "wearable", + "unknown" + ] + + currentIndex: (("entity or wearable" === assetType) ? + model.indexOf("unknown") : model.indexOf(assetType)) + + Component.onCompleted: { + onCurrentIndexChanged.connect(function() { + assetType = model[currentIndex]; + sendToScript({ + method: "tester_updateResourceObjectAssetType", + objectId: resourceListModel.get(index)["id"], + assetType: assetType }); + }); + } + } + + Repeater { + model: [ "forward", "trash" ] + + HifiStylesUit.HiFiGlyphs { + property var glyphs: { + "application": hifi.glyphs.install, + "avatar": hifi.glyphs.avatar, + "content set": hifi.glyphs.globe, + "entity": hifi.glyphs.wand, + "trash": hifi.glyphs.trash, + "unknown": hifi.glyphs.circleSlash, + "wearable": hifi.glyphs.hat, + } + text: (("trash" === modelData) ? + glyphs.trash : + glyphs[comboBox.model[comboBox.currentIndex]]) + size: ("trash" === modelData) ? 22 : 30 + color: hifi.colors.black + horizontalAlignment: Text.AlignHCenter + MouseArea { + anchors.fill: parent + onClicked: { + actions[modelData](resource, comboBox.currentText); + } + } + } + } + } + + headerPositioning: ListView.OverlayHeader + header: HifiStylesUit.RalewayRegular { + id: rootHeader + text: "Marketplace Item Tester" + height: 80 + width: paintedWidth + size: 22 + color: hifi.colors.black + anchors.left: parent.left + anchors.leftMargin: 12 + } + + footerPositioning: ListView.OverlayFooter + footer: Row { + id: rootActions + spacing: 20 + anchors.horizontalCenter: parent.horizontalCenter + + property string currentAction + property var actions: { + "Load File": function(){ + rootActions.currentAction = "load file"; + Window.browseChanged.connect(onResourceSelected); + Window.browseAsync("Please select a file (*.app.json *.json *.fst *.json.gz)", "", "Assets (*.app.json *.json *.fst *.json.gz)"); + }, + "Load URL": function(){ + rootActions.currentAction = "load url"; + Window.promptTextChanged.connect(onResourceSelected); + Window.promptAsync("Please enter a URL", ""); + } + } + + function onResourceSelected(resource) { + // It is possible that we received the present signal + // from something other than our browserAsync window. + // Alas, there is nothing we can do about that so charge + // ahead as though we are sure the present signal is one + // we expect. + switch(currentAction) { + case "load file": + Window.browseChanged.disconnect(onResourceSelected); + break + case "load url": + Window.promptTextChanged.disconnect(onResourceSelected); + break; + } + if (resource) { + var resourceObj = buildResourceObj(resource); + installResourceObj(resourceObj); + sendToScript({ + method: 'tester_newResourceObject', + resourceObject: resourceObj }); + } + } + + Repeater { + model: [ "Load File", "Load URL" ] + HifiControlsUit.Button { + color: hifi.buttons.blue + fontSize: 20 + text: modelData + width: root.width / 3 + height: 40 + onClicked: actions[text]() + } + } + } + } +} diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif b/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif new file mode 100644 index 0000000000..00f75ae62f Binary files /dev/null and b/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif differ diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 032d9b0199..eeb9ac3c54 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -59,7 +59,7 @@ Item { Connections { target: Commerce; - + onContentSetChanged: { if (contentSetHref === root.itemHref) { showConfirmation = true; @@ -135,7 +135,7 @@ Item { anchors.topMargin: 8; width: 30; height: width; - + HiFiGlyphs { id: closeContextMenuGlyph; text: hifi.glyphs.close; @@ -376,7 +376,7 @@ Item { } } } - + transform: Rotation { id: rotation; origin.x: flipable.width/2; @@ -509,7 +509,7 @@ Item { } verticalAlignment: Text.AlignTop; } - + HiFiGlyphs { id: statusIcon; text: { @@ -588,7 +588,7 @@ Item { border.width: 1; border.color: "#E2334D"; } - + HiFiGlyphs { id: contextMenuGlyph; text: hifi.glyphs.verticalEllipsis; @@ -615,7 +615,7 @@ Item { } } } - + Rectangle { id: rezzedNotifContainer; z: 998; @@ -663,13 +663,13 @@ Item { Tablet.playSound(TabletEnums.ButtonHover); } } - + onFocusChanged: { if (focus) { Tablet.playSound(TabletEnums.ButtonHover); } } - + onClicked: { Tablet.playSound(TabletEnums.ButtonClick); if (root.itemType === "contentSet") { @@ -775,7 +775,7 @@ Item { // Style color: hifi.colors.redAccent; horizontalAlignment: Text.AlignRight; - + MouseArea { anchors.fill: parent; hoverEnabled: true; diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 3b8e2c0f4d..2435678e77 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -93,7 +93,7 @@ Rectangle { console.log("Failed to get Available Updates", result.data.message); } else { sendToScript({method: 'purchases_availableUpdatesReceived', numUpdates: result.data.updates.length }); - root.numUpdatesAvailable = result.data.updates.length; + root.numUpdatesAvailable = result.total_entries; } } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 50208793fe..627da1d43f 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -45,14 +45,6 @@ Item { onHistoryResult : { transactionHistoryModel.handlePage(null, result); } - - onAvailableUpdatesResult: { - if (result.status !== 'success') { - console.log("Failed to get Available Updates", result.data.message); - } else { - sendToScript({method: 'wallet_availableUpdatesReceived', numUpdates: result.data.updates.length }); - } - } } Connections { diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2ae5dff573..f23410bff9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1691,21 +1691,21 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return DependencyManager::get()->navigationFocused() ? 1 : 0; }); _applicationStateDevice->setInputVariant(STATE_PLATFORM_WINDOWS, []() -> float { -#if defined(Q_OS_WIN) +#if defined(Q_OS_WIN) return 1; #else return 0; #endif }); _applicationStateDevice->setInputVariant(STATE_PLATFORM_MAC, []() -> float { -#if defined(Q_OS_MAC) +#if defined(Q_OS_MAC) return 1; #else return 0; #endif }); _applicationStateDevice->setInputVariant(STATE_PLATFORM_ANDROID, []() -> float { -#if defined(Q_OS_ANDROID) +#if defined(Q_OS_ANDROID) return 1; #else return 0; @@ -1759,10 +1759,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo // Make sure we don't time out during slow operations at startup updateHeartbeat(); - QTimer* settingsTimer = new QTimer(); moveToNewNamedThread(settingsTimer, "Settings Thread", [this, settingsTimer]{ - connect(qApp, &Application::beforeAboutToQuit, [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 @@ -2881,9 +2883,10 @@ void Application::initializeUi() { QUrl{ "hifi/commerce/common/CommerceLightbox.qml" }, QUrl{ "hifi/commerce/common/EmulatedMarketplaceHeader.qml" }, QUrl{ "hifi/commerce/common/FirstUseTutorial.qml" }, - QUrl{ "hifi/commerce/common/SortableListModel.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" }, @@ -3503,13 +3506,14 @@ bool Application::isServerlessMode() const { } void Application::setIsInterstitialMode(bool interstitialMode) { - Settings settings; - bool enableInterstitial = settings.value("enableIntersitialMode", false).toBool(); - if (_interstitialMode != interstitialMode && enableInterstitial) { - _interstitialMode = interstitialMode; + bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + if (enableInterstitial) { + if (_interstitialMode != interstitialMode) { + _interstitialMode = interstitialMode; - DependencyManager::get()->setAudioPaused(_interstitialMode); - DependencyManager::get()->setMyAvatarDataPacketsPaused(_interstitialMode); + DependencyManager::get()->setAudioPaused(_interstitialMode); + DependencyManager::get()->setMyAvatarDataPacketsPaused(_interstitialMode); + } } } diff --git a/interface/src/Application.h b/interface/src/Application.h index 3bebc60480..eedbdb7622 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -432,7 +432,7 @@ public slots: void setIsServerlessMode(bool serverlessDomain); void loadServerlessDomain(QUrl domainURL, bool errorDomain = false); - void setIsInterstitialMode(bool interstialMode); + void setIsInterstitialMode(bool interstitialMode); void updateVerboseLogging(); diff --git a/interface/src/ConnectionMonitor.cpp b/interface/src/ConnectionMonitor.cpp index 3c85cfb339..e86061b090 100644 --- a/interface/src/ConnectionMonitor.cpp +++ b/interface/src/ConnectionMonitor.cpp @@ -41,9 +41,15 @@ void ConnectionMonitor::init() { } connect(&_timer, &QTimer::timeout, this, [this]() { - qDebug() << "ConnectionMonitor: Redirecting to 404 error domain"; // set in a timeout error - emit setRedirectErrorState(REDIRECT_HIFI_ADDRESS, 5); + bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + if (enableInterstitial) { + qDebug() << "ConnectionMonitor: Redirecting to 404 error domain"; + emit setRedirectErrorState(REDIRECT_HIFI_ADDRESS, "", 5); + } else { + qDebug() << "ConnectionMonitor: Showing connection failure window"; + DependencyManager::get()->setDomainConnectionFailureVisibility(true); + } }); } @@ -53,4 +59,8 @@ void ConnectionMonitor::startTimer() { void ConnectionMonitor::stopTimer() { _timer.stop(); + bool enableInterstitial = DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); + if (!enableInterstitial) { + DependencyManager::get()->setDomainConnectionFailureVisibility(false); + } } diff --git a/interface/src/ConnectionMonitor.h b/interface/src/ConnectionMonitor.h index 5e75e2618b..2fda6ef7cd 100644 --- a/interface/src/ConnectionMonitor.h +++ b/interface/src/ConnectionMonitor.h @@ -24,7 +24,7 @@ public: void init(); signals: - void setRedirectErrorState(QUrl errorURL, int reasonCode); + void setRedirectErrorState(QUrl errorURL, QString reasonMessage = "", int reasonCode = -1, const QString& extraInfo = ""); private slots: void startTimer(); @@ -34,4 +34,4 @@ private: QTimer _timer; }; -#endif // hifi_ConnectionMonitor_h \ No newline at end of file +#endif // hifi_ConnectionMonitor_h diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 1faf17ea9a..5705e7347c 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -234,11 +234,13 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { const SortableAvatar& sortData = *it; const auto avatar = std::static_pointer_cast(sortData.getAvatar()); - // TODO: to help us scale to more avatars it would be nice to not have to poll orb state here - // if the geometry is loaded then turn off the orb + // TODO: to help us scale to more avatars it would be nice to not have to poll this stuff every update if (avatar->getSkeletonModel()->isLoaded()) { // remove the orb if it is there avatar->removeOrb(); + if (avatar->needsPhysicsUpdate()) { + _avatarsToChangeInPhysics.insert(avatar); + } } else { avatar->updateOrbPosition(); } @@ -456,31 +458,37 @@ void AvatarManager::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar } void AvatarManager::clearOtherAvatars() { - // Remove other avatars from the world but don't actually remove them from _avatarHash - // each will either be removed on timeout or will re-added to the world on receipt of update. - const render::ScenePointer& scene = qApp->getMain3DScene(); - render::Transaction transaction; - - QReadLocker locker(&_hashLock); - AvatarHash::iterator avatarIterator = _avatarHash.begin(); - while (avatarIterator != _avatarHash.end()) { - auto avatar = std::static_pointer_cast(avatarIterator.value()); - if (avatar != _myAvatar) { - handleRemovedAvatar(avatar); - avatarIterator = _avatarHash.erase(avatarIterator); - } else { - ++avatarIterator; - } - } - assert(scene); - scene->enqueueTransaction(transaction); _myAvatar->clearLookAtTargetAvatar(); + + // setup a vector of removed avatars outside the scope of the hash lock + std::vector removedAvatars; + + { + QWriteLocker locker(&_hashLock); + + removedAvatars.reserve(_avatarHash.size()); + + auto avatarIterator = _avatarHash.begin(); + while (avatarIterator != _avatarHash.end()) { + auto avatar = std::static_pointer_cast(avatarIterator.value()); + if (avatar != _myAvatar) { + removedAvatars.push_back(avatar); + avatarIterator = _avatarHash.erase(avatarIterator); + } else { + ++avatarIterator; + } + } + } + + for (auto& av : removedAvatars) { + handleRemovedAvatar(av); + } } void AvatarManager::deleteAllAvatars() { assert(_avatarsToChangeInPhysics.empty()); - QReadLocker locker(&_hashLock); + QWriteLocker locker(&_hashLock); AvatarHash::iterator avatarIterator = _avatarHash.begin(); while (avatarIterator != _avatarHash.end()) { auto avatar = std::static_pointer_cast(avatarIterator.value()); diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 306ba6f39b..9c4287728d 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -204,7 +204,12 @@ private: void simulateAvatarFades(float deltaTime); AvatarSharedPointer newSharedAvatar() override; - void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason) override; + + // called only from the AvatarHashMap thread - cannot be called while this thread holds the + // hash lock, since handleRemovedAvatar needs a write lock on the entity tree and the entity tree + // frequently grabs a read lock on the hash to get a given avatar by ID + void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, + KillAvatarReason removalReason = KillAvatarReason::NoReason) override; QVector _avatarsToFade; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index df7ec93b6a..f2e6b68a0f 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -116,6 +116,7 @@ MyAvatar::MyAvatar(QThread* thread) : _bodySensorMatrix(), _goToPending(false), _goToSafe(true), + _goToFeetAjustment(false), _goToPosition(), _goToOrientation(), _prevShouldDrawHead(true), @@ -498,7 +499,7 @@ void MyAvatar::update(float deltaTime) { setCurrentStandingHeight(computeStandingHeightMode(getControllerPoseInAvatarFrame(controller::Action::HEAD))); setAverageHeadRotation(computeAverageHeadRotation(getControllerPoseInAvatarFrame(controller::Action::HEAD))); - if (_drawAverageFacingEnabled) { + if (_drawAverageFacingEnabled) { auto sensorHeadPose = getControllerPoseInSensorFrame(controller::Action::HEAD); glm::vec3 worldHeadPos = transformPoint(getSensorToWorldMatrix(), sensorHeadPose.getTranslation()); glm::vec3 worldFacingAverage = transformVectorFast(getSensorToWorldMatrix(), glm::vec3(_headControllerFacingMovingAverage.x, 0.0f, _headControllerFacingMovingAverage.y)); @@ -526,6 +527,11 @@ void MyAvatar::update(float deltaTime) { _physicsSafetyPending = getCollisionsEnabled(); _characterController.recomputeFlying(); // In case we've gone to into the sky. } + if (_goToFeetAjustment && _skeletonModelLoaded) { + auto feetAjustment = getWorldPosition() - getWorldFeetPosition(); + goToLocation(getWorldPosition() + feetAjustment); + _goToFeetAjustment = false; + } if (_physicsSafetyPending && qApp->isPhysicsEnabled() && _characterController.isEnabledAndReady()) { // When needed and ready, arrange to check and fix. _physicsSafetyPending = false; @@ -1728,6 +1734,7 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { _headBoneSet.clear(); _cauterizationNeedsUpdate = true; + _skeletonModelLoaded = false; std::shared_ptr skeletonConnection = std::make_shared(); *skeletonConnection = QObject::connect(_skeletonModel.get(), &SkeletonModel::skeletonLoaded, [this, skeletonModelChangeCount, skeletonConnection]() { @@ -1745,6 +1752,7 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) { _skeletonModel->setCauterizeBoneSet(_headBoneSet); _fstAnimGraphOverrideUrl = _skeletonModel->getGeometry()->getAnimGraphOverrideUrl(); initAnimGraph(); + _skeletonModelLoaded = true; } QObject::disconnect(*skeletonConnection); }); @@ -2945,46 +2953,10 @@ void MyAvatar::goToLocation(const QVariant& propertiesVar) { } void MyAvatar::goToFeetLocation(const glm::vec3& newPosition, - bool hasOrientation, const glm::quat& newOrientation, - bool shouldFaceLocation) { - - qCDebug(interfaceapp).nospace() << "MyAvatar goToFeetLocation - moving to " << newPosition.x << ", " - << newPosition.y << ", " << newPosition.z; - - ShapeInfo shapeInfo; - computeShapeInfo(shapeInfo); - glm::vec3 halfExtents = shapeInfo.getHalfExtents(); - glm::vec3 localFeetPos = shapeInfo.getOffset() - glm::vec3(0.0f, halfExtents.y + halfExtents.x, 0.0f); - glm::mat4 localFeet = createMatFromQuatAndPos(Quaternions::IDENTITY, localFeetPos); - - glm::mat4 worldFeet = createMatFromQuatAndPos(Quaternions::IDENTITY, newPosition); - - glm::mat4 avatarMat = worldFeet * glm::inverse(localFeet); - - glm::vec3 adjustedPosition = extractTranslation(avatarMat); - - _goToPending = true; - _goToPosition = adjustedPosition; - _goToOrientation = getWorldOrientation(); - if (hasOrientation) { - qCDebug(interfaceapp).nospace() << "MyAvatar goToFeetLocation - new orientation is " - << newOrientation.x << ", " << newOrientation.y << ", " << newOrientation.z << ", " << newOrientation.w; - - // orient the user to face the target - glm::quat quatOrientation = cancelOutRollAndPitch(newOrientation); - - if (shouldFaceLocation) { - quatOrientation = newOrientation * glm::angleAxis(PI, Vectors::UP); - - // move the user a couple units away - const float DISTANCE_TO_USER = 2.0f; - _goToPosition = adjustedPosition - quatOrientation * IDENTITY_FORWARD * DISTANCE_TO_USER; - } - - _goToOrientation = quatOrientation; - } - - emit transformChanged(); + bool hasOrientation, const glm::quat& newOrientation, + bool shouldFaceLocation) { + _goToFeetAjustment = true; + goToLocation(newPosition, hasOrientation, newOrientation, shouldFaceLocation); } void MyAvatar::goToLocation(const glm::vec3& newPosition, diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 1dc0b3cd40..d7379a18c4 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1732,6 +1732,7 @@ private: bool _goToPending { false }; bool _physicsSafetyPending { false }; bool _goToSafe { true }; + bool _goToFeetAjustment { false }; glm::vec3 _goToPosition; glm::quat _goToOrientation; @@ -1807,6 +1808,7 @@ private: bool _haveReceivedHeightLimitsFromDomain { false }; int _disableHandTouchCount { 0 }; + bool _skeletonModelLoaded { false }; Setting::Handle _dominantHandSetting; Setting::Handle _headPitchSetting; diff --git a/interface/src/avatar/OtherAvatar.cpp b/interface/src/avatar/OtherAvatar.cpp index 29fa98fd1d..625998eb95 100644 --- a/interface/src/avatar/OtherAvatar.cpp +++ b/interface/src/avatar/OtherAvatar.cpp @@ -119,6 +119,11 @@ bool OtherAvatar::shouldBeInPhysicsSimulation() const { return (_workloadRegion < workload::Region::R3 && !isDead()); } +bool OtherAvatar::needsPhysicsUpdate() const { + constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION; + return (_motionState && (bool)(_motionState->getIncomingDirtyFlags() & FLAGS_OF_INTEREST)); +} + void OtherAvatar::rebuildCollisionShape() { if (_motionState) { _motionState->addDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS); diff --git a/interface/src/avatar/OtherAvatar.h b/interface/src/avatar/OtherAvatar.h index 94b98f2747..5b72815757 100644 --- a/interface/src/avatar/OtherAvatar.h +++ b/interface/src/avatar/OtherAvatar.h @@ -43,6 +43,7 @@ public: void setWorkloadRegion(uint8_t region); bool shouldBeInPhysicsSimulation() const; + bool needsPhysicsUpdate() const; friend AvatarManager; diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 7d7f41ad0e..aa39fdc1b9 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -247,12 +247,18 @@ void QmlCommerce::transferAssetToUsername(const QString& username, } void QmlCommerce::replaceContentSet(const QString& itemHref, const QString& certificateID) { - auto ledger = DependencyManager::get(); - ledger->updateLocation(certificateID, DependencyManager::get()->getPlaceName(), true); + if (!certificateID.isEmpty()) { + auto ledger = DependencyManager::get(); + ledger->updateLocation( + certificateID, + DependencyManager::get()->getPlaceName(), + true); + } qApp->replaceDomainContent(itemHref); - QJsonObject messageProperties = { { "status", "SuccessfulRequestToReplaceContent" }, { "content_set_url", itemHref } }; + QJsonObject messageProperties = { + { "status", "SuccessfulRequestToReplaceContent" }, + { "content_set_url", itemHref } }; UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); - emit contentSetChanged(itemHref); } @@ -276,6 +282,7 @@ QString QmlCommerce::getInstalledApps(const QString& justInstalledAppID) { // Thus, we protect against deleting the .app.json from the user's disk (below) // by skipping that check for the app we just installed. if ((justInstalledAppID != "") && ((justInstalledAppID + ".app.json") == appFileName)) { + installedAppsFromMarketplace += appFileName + ","; continue; } diff --git a/interface/src/raypick/LaserPointer.cpp b/interface/src/raypick/LaserPointer.cpp index 5fbe3a90b5..3c66923b4e 100644 --- a/interface/src/raypick/LaserPointer.cpp +++ b/interface/src/raypick/LaserPointer.cpp @@ -35,6 +35,14 @@ void LaserPointer::editRenderStatePath(const std::string& state, const QVariant& } } +PickResultPointer LaserPointer::getPickResultCopy(const PickResultPointer& pickResult) const { + auto rayPickResult = std::dynamic_pointer_cast(pickResult); + if (!rayPickResult) { + return std::make_shared(); + } + return std::make_shared(*rayPickResult.get()); +} + QVariantMap LaserPointer::toVariantMap() const { QVariantMap qVariantMap; diff --git a/interface/src/raypick/LaserPointer.h b/interface/src/raypick/LaserPointer.h index c0ac3259d9..b391f60f85 100644 --- a/interface/src/raypick/LaserPointer.h +++ b/interface/src/raypick/LaserPointer.h @@ -47,6 +47,8 @@ public: static std::shared_ptr buildRenderState(const QVariantMap& propMap); protected: + PickResultPointer getPickResultCopy(const PickResultPointer& pickResult) const override; + void editRenderStatePath(const std::string& state, const QVariant& pathProps) override; glm::vec3 getPickOrigin(const PickResultPointer& pickResult) const override; diff --git a/interface/src/raypick/ParabolaPointer.cpp b/interface/src/raypick/ParabolaPointer.cpp index ad698c409b..33fa8738d9 100644 --- a/interface/src/raypick/ParabolaPointer.cpp +++ b/interface/src/raypick/ParabolaPointer.cpp @@ -30,6 +30,14 @@ ParabolaPointer::ParabolaPointer(const QVariant& rayProps, const RenderStateMap& { } +PickResultPointer ParabolaPointer::getPickResultCopy(const PickResultPointer& pickResult) const { + auto parabolaPickResult = std::dynamic_pointer_cast(pickResult); + if (!parabolaPickResult) { + return std::make_shared(); + } + return std::make_shared(*parabolaPickResult.get()); +} + void ParabolaPointer::editRenderStatePath(const std::string& state, const QVariant& pathProps) { auto renderState = std::static_pointer_cast(_renderStates[state]); if (renderState) { diff --git a/interface/src/raypick/ParabolaPointer.h b/interface/src/raypick/ParabolaPointer.h index 526abe3b0d..8fb864c07b 100644 --- a/interface/src/raypick/ParabolaPointer.h +++ b/interface/src/raypick/ParabolaPointer.h @@ -102,6 +102,8 @@ public: static std::shared_ptr buildRenderState(const QVariantMap& propMap); protected: + virtual PickResultPointer getPickResultCopy(const PickResultPointer& pickResult) const override; + void editRenderStatePath(const std::string& state, const QVariant& pathProps) override; glm::vec3 getPickOrigin(const PickResultPointer& pickResult) const override; diff --git a/interface/src/raypick/StylusPointer.cpp b/interface/src/raypick/StylusPointer.cpp index 06e3e52d21..b648e125bf 100644 --- a/interface/src/raypick/StylusPointer.cpp +++ b/interface/src/raypick/StylusPointer.cpp @@ -147,6 +147,14 @@ bool StylusPointer::shouldTrigger(const PickResultPointer& pickResult) { return false; } +PickResultPointer StylusPointer::getPickResultCopy(const PickResultPointer& pickResult) const { + auto stylusPickResult = std::dynamic_pointer_cast(pickResult); + if (!stylusPickResult) { + return std::make_shared(); + } + return std::make_shared(*stylusPickResult.get()); +} + Pointer::PickedObject StylusPointer::getHoveredObject(const PickResultPointer& pickResult) { auto stylusPickResult = std::static_pointer_cast(pickResult); if (!stylusPickResult) { diff --git a/interface/src/raypick/StylusPointer.h b/interface/src/raypick/StylusPointer.h index 4095acb529..ff60fd78e5 100644 --- a/interface/src/raypick/StylusPointer.h +++ b/interface/src/raypick/StylusPointer.h @@ -42,6 +42,7 @@ protected: Buttons getPressedButtons(const PickResultPointer& pickResult) override; bool shouldHover(const PickResultPointer& pickResult) override; bool shouldTrigger(const PickResultPointer& pickResult) override; + virtual PickResultPointer getPickResultCopy(const PickResultPointer& pickResult) const override; PointerEvent buildPointerEvent(const PickedObject& target, const PickResultPointer& pickResult, const std::string& button = "", bool hover = true) override; diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index e3ae65aee1..d4eb37e0aa 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -180,6 +180,14 @@ void WindowScriptingInterface::setPreviousBrowseAssetLocation(const QString& loc Setting::Handle(LAST_BROWSE_ASSETS_LOCATION_SETTING).set(location); } +bool WindowScriptingInterface::getInterstitialModeEnabled() const { + return DependencyManager::get()->getDomainHandler().getInterstitialModeEnabled(); +} + +void WindowScriptingInterface::setInterstitialModeEnabled(bool enableInterstitialMode) { + DependencyManager::get()->getDomainHandler().setInterstitialModeEnabled(enableInterstitialMode); +} + bool WindowScriptingInterface::isPointOnDesktopWindow(QVariant point) { auto offscreenUi = DependencyManager::get(); return offscreenUi->isPointOnDesktopWindow(point); diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 3827406729..ddd7159f23 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -49,6 +49,7 @@ class WindowScriptingInterface : public QObject, public Dependency { Q_PROPERTY(int innerHeight READ getInnerHeight) Q_PROPERTY(int x READ getX) Q_PROPERTY(int y READ getY) + Q_PROPERTY(bool interstitialModeEnabled READ getInterstitialModeEnabled WRITE setInterstitialModeEnabled) public: WindowScriptingInterface(); @@ -758,6 +759,9 @@ private: QString getPreviousBrowseAssetLocation() const; void setPreviousBrowseAssetLocation(const QString& location); + bool getInterstitialModeEnabled() const; + void setInterstitialModeEnabled(bool enableInterstitialMode); + void ensureReticleVisible() const; int createMessageBox(QString title, QString text, int buttons, int defaultButton); diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 3c84b7ce1f..d00bc29054 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -100,6 +100,13 @@ QList getAvailableDevices(QAudio::Mode mode) { // now called from a background thread, to keep blocking operations off the audio thread void AudioClient::checkDevices() { + // Make sure we're not shutting down + Lock timerMutex(_checkDevicesMutex); + // If we HAVE shut down after we were queued, but prior to execution, early exit + if (nullptr == _checkDevicesTimer) { + return; + } + auto inputDevices = getAvailableDevices(QAudio::AudioInput); auto outputDevices = getAvailableDevices(QAudio::AudioOutput); @@ -278,9 +285,6 @@ void AudioClient::customDeleter() { _shouldRestartInputSetup = false; #endif stop(); - _checkDevicesTimer->stop(); - _checkPeakValuesTimer->stop(); - deleteLater(); } @@ -653,12 +657,26 @@ void AudioClient::start() { } void AudioClient::stop() { - qCDebug(audioclient) << "AudioClient::stop(), requesting switchInputToAudioDevice() to shut down"; switchInputToAudioDevice(QAudioDeviceInfo(), true); qCDebug(audioclient) << "AudioClient::stop(), requesting switchOutputToAudioDevice() to shut down"; switchOutputToAudioDevice(QAudioDeviceInfo(), true); + + // Stop triggering the checks + QObject::disconnect(_checkPeakValuesTimer, &QTimer::timeout, nullptr, nullptr); + QObject::disconnect(_checkDevicesTimer, &QTimer::timeout, nullptr, nullptr); + + // Destruction of the pointers will occur when the parent object (this) is destroyed) + { + Lock lock(_checkDevicesMutex); + _checkDevicesTimer = nullptr; + } + { + Lock lock(_checkPeakValuesMutex); + _checkPeakValuesTimer = nullptr; + } + #if defined(Q_OS_ANDROID) _checkInputTimer.stop(); disconnect(&_checkInputTimer, &QTimer::timeout, 0, 0); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 499397b385..5e7f1fb8a0 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -447,7 +447,9 @@ private: bool _shouldRestartInputSetup { true }; // Should we restart the input device because of an unintended stop? #endif + Mutex _checkDevicesMutex; QTimer* _checkDevicesTimer { nullptr }; + Mutex _checkPeakValuesMutex; QTimer* _checkPeakValuesTimer { nullptr }; bool _isRecording { false }; diff --git a/libraries/audio-client/src/AudioPeakValues.cpp b/libraries/audio-client/src/AudioPeakValues.cpp index 0b8921a117..a50567da7f 100644 --- a/libraries/audio-client/src/AudioPeakValues.cpp +++ b/libraries/audio-client/src/AudioPeakValues.cpp @@ -40,6 +40,12 @@ void release(IAudioClient* audioClient) { } void AudioClient::checkPeakValues() { + // Guard against running during shutdown + Lock timerMutex(_checkPeakValuesMutex); + if (nullptr == _checkPeakValuesTimer) { + return; + } + // prepare the windows environment CoInitialize(NULL); diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index 67f9952771..da284f19a3 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -43,8 +43,11 @@ void soundSharedPointerFromScriptValue(const QScriptValue& object, SharedSoundPo } } -SoundScriptingInterface::SoundScriptingInterface(SharedSoundPointer sound) : _sound(sound) { - QObject::connect(sound.data(), &Sound::ready, this, &SoundScriptingInterface::ready); +SoundScriptingInterface::SoundScriptingInterface(const SharedSoundPointer& sound) : _sound(sound) { + // During shutdown we can sometimes get an empty sound pointer back + if (_sound) { + QObject::connect(_sound.data(), &Sound::ready, this, &SoundScriptingInterface::ready); + } } Sound::Sound(const QUrl& url, bool isStereo, bool isAmbisonic) : diff --git a/libraries/audio/src/Sound.h b/libraries/audio/src/Sound.h index 348600e4ae..a0544870d0 100644 --- a/libraries/audio/src/Sound.h +++ b/libraries/audio/src/Sound.h @@ -105,11 +105,11 @@ class SoundScriptingInterface : public QObject { Q_PROPERTY(float duration READ getDuration) public: - SoundScriptingInterface(SharedSoundPointer sound); - SharedSoundPointer getSound() { return _sound; } + SoundScriptingInterface(const SharedSoundPointer& sound); + const SharedSoundPointer& getSound() { return _sound; } - bool isReady() const { return _sound->isReady(); } - float getDuration() { return _sound->getDuration(); } + bool isReady() const { return _sound ? _sound->isReady() : false; } + float getDuration() { return _sound ? _sound->getDuration() : 0.0f; } /**jsdoc * Triggered when the sound has been downloaded and is ready to be played. diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index d205a915f8..01557e307e 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -66,6 +66,22 @@ void AvatarReplicas::removeReplicas(const QUuid& parentID) { } } +std::vector AvatarReplicas::takeReplicas(const QUuid& parentID) { + std::vector replicas; + + auto it = _replicasMap.find(parentID); + + if (it != _replicasMap.end()) { + // take a copy of the replica shared pointers for this parent + replicas.swap(it->second); + + // erase the replicas for this parent from our map + _replicasMap.erase(it); + } + + return replicas; +} + void AvatarReplicas::processAvatarIdentity(const QUuid& parentID, const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged) { if (_replicasMap.find(parentID) != _replicasMap.end()) { auto &replicas = _replicasMap[parentID]; @@ -386,24 +402,31 @@ void AvatarHashMap::processKillAvatar(QSharedPointer message, S } void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason) { - QWriteLocker locker(&_hashLock); + std::vector removedAvatars; - auto replicaIDs = _replicas.getReplicaIDs(sessionUUID); - _replicas.removeReplicas(sessionUUID); - for (auto id : replicaIDs) { - auto removedReplica = _avatarHash.take(id); - if (removedReplica) { - handleRemovedAvatar(removedReplica, removalReason); + { + QWriteLocker locker(&_hashLock); + + auto replicas = _replicas.takeReplicas(sessionUUID); + + for (auto& replica : replicas) { + auto removedReplica = _avatarHash.take(replica->getID()); + if (removedReplica) { + removedAvatars.push_back(removedReplica); + } + } + + _pendingAvatars.remove(sessionUUID); + auto removedAvatar = _avatarHash.take(sessionUUID); + + if (removedAvatar) { + removedAvatars.push_back(removedAvatar); } } - _pendingAvatars.remove(sessionUUID); - auto removedAvatar = _avatarHash.take(sessionUUID); - - if (removedAvatar) { + for (auto& removedAvatar: removedAvatars) { handleRemovedAvatar(removedAvatar, removalReason); } - } void AvatarHashMap::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) { @@ -421,11 +444,18 @@ void AvatarHashMap::sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& ol } void AvatarHashMap::clearOtherAvatars() { - QWriteLocker locker(&_hashLock); + QList removedAvatars; - for (auto& av : _avatarHash) { - handleRemovedAvatar(av); + { + QWriteLocker locker(&_hashLock); + + // grab a copy of the current avatars so we can call handleRemoveAvatar for them + removedAvatars = _avatarHash.values(); + + _avatarHash.clear(); } - _avatarHash.clear(); + for (auto& av : removedAvatars) { + handleRemovedAvatar(av); + } } diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 70d7f8c04d..c2cb448e52 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -49,6 +49,7 @@ public: void parseDataFromBuffer(const QUuid& parentID, const QByteArray& buffer); void processAvatarIdentity(const QUuid& parentID, const QByteArray& identityData, bool& identityChanged, bool& displayNameChanged); void removeReplicas(const QUuid& parentID); + std::vector takeReplicas(const QUuid& parentID); void processTrait(const QUuid& parentID, AvatarTraits::TraitType traitType, QByteArray traitBinaryData); void processDeletedTraitInstance(const QUuid& parentID, AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); void processTraitInstance(const QUuid& parentID, AvatarTraits::TraitType traitType, @@ -179,7 +180,7 @@ protected: bool& isNew); virtual AvatarSharedPointer findAvatar(const QUuid& sessionUUID) const; // uses a QReadLocker on the hashLock virtual void removeAvatar(const QUuid& sessionUUID, KillAvatarReason removalReason = KillAvatarReason::NoReason); - + virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason = KillAvatarReason::NoReason); AvatarHash _avatarHash; diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index c7cdf8f4ea..17041a5fd7 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -140,8 +140,7 @@ public: * * @typedef {number} location.LookupTrigger */ - enum LookupTrigger - { + enum LookupTrigger { UserInput, Back, Forward, @@ -207,9 +206,8 @@ public slots: // functions and signals that should be exposed are moved to a scripting interface class. // // we currently expect this to be called from NodeList once handleLookupString has been called with a path - bool goToViewpointForPath(const QString& viewpointString, const QString& pathString) { - return handleViewpoint(viewpointString, false, DomainPathResponse, false, pathString); - } + bool goToViewpointForPath(const QString& viewpointString, const QString& pathString) + { return handleViewpoint(viewpointString, false, DomainPathResponse, false, pathString); } /**jsdoc * Go back to the previous location in your navigation history, if there is one. @@ -231,8 +229,7 @@ public slots: * location history is correctly maintained. */ void goToLocalSandbox(QString path = "", LookupTrigger trigger = LookupTrigger::StartupFromSettings) { - handleUrl(SANDBOX_HIFI_ADDRESS + path, trigger); - } + handleUrl(SANDBOX_HIFI_ADDRESS + path, trigger); } /**jsdoc * Go to the default "welcome" metaverse address. @@ -364,8 +361,7 @@ signals: * location.locationChangeRequired.connect(onLocationChangeRequired); */ void locationChangeRequired(const glm::vec3& newPosition, - bool hasOrientationChange, - const glm::quat& newOrientation, + bool hasOrientationChange, const glm::quat& newOrientation, bool shouldFaceLocation); /**jsdoc @@ -448,11 +444,8 @@ private: bool handleNetworkAddress(const QString& lookupString, LookupTrigger trigger, bool& hostChanged); void handlePath(const QString& path, LookupTrigger trigger, bool wasPathOnly = false); - bool handleViewpoint(const QString& viewpointString, - bool shouldFace, - LookupTrigger trigger, - bool definitelyPathOnly = false, - const QString& pathString = QString()); + bool handleViewpoint(const QString& viewpointString, bool shouldFace, LookupTrigger trigger, + bool definitelyPathOnly = false, const QString& pathString = QString()); bool handleUsername(const QString& lookupString); bool handleDomainID(const QString& host); diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index f34a93de96..df34a1fb59 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -15,6 +15,10 @@ #include +#include + +#include + #include #include @@ -134,6 +138,18 @@ void DomainHandler::hardReset() { _pendingPath.clear(); } +bool DomainHandler::getInterstitialModeEnabled() const { + return _interstitialModeSettingLock.resultWithReadLock([&] { + return _enableInterstitialMode.get(); + }); +} + +void DomainHandler::setInterstitialModeEnabled(bool enableInterstitialMode) { + _interstitialModeSettingLock.withWriteLock([&] { + _enableInterstitialMode.set(enableInterstitialMode); + }); +} + void DomainHandler::setErrorDomainURL(const QUrl& url) { _errorDomainURL = url; return; @@ -340,11 +356,15 @@ void DomainHandler::loadedErrorDomain(std::map namedPaths) { DependencyManager::get()->goToViewpointForPath(viewpoint, QString()); } -void DomainHandler::setRedirectErrorState(QUrl errorUrl, int reasonCode) { - _errorDomainURL = errorUrl; +void DomainHandler::setRedirectErrorState(QUrl errorUrl, QString reasonMessage, int reasonCode, const QString& extraInfo) { _lastDomainConnectionError = reasonCode; - _isInErrorState = true; - emit redirectToErrorDomainURL(_errorDomainURL); + if (getInterstitialModeEnabled()) { + _errorDomainURL = errorUrl; + _isInErrorState = true; + emit redirectToErrorDomainURL(_errorDomainURL); + } else { + emit domainConnectionRefused(reasonMessage, reasonCode, extraInfo); + } } void DomainHandler::requestDomainSettings() { @@ -485,13 +505,9 @@ void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer #include +#include +#include + #include "HifiSockAddr.h" #include "NetworkPeer.h" #include "NLPacket.h" @@ -83,6 +86,8 @@ public: bool isConnected() const { return _isConnected; } void setIsConnected(bool isConnected); bool isServerless() const { return _domainURL.scheme() != URL_SCHEME_HIFI; } + bool getInterstitialModeEnabled() const; + void setInterstitialModeEnabled(bool enableInterstitialMode); void connectedToServerless(std::map namedPaths); @@ -171,7 +176,7 @@ public slots: void processDomainServerConnectionDeniedPacket(QSharedPointer message); // sets domain handler in error state. - void setRedirectErrorState(QUrl errorUrl, int reasonCode); + void setRedirectErrorState(QUrl errorUrl, QString reasonMessage = "", int reason = -1, const QString& extraInfo = ""); bool isInErrorState() { return _isInErrorState; } @@ -224,6 +229,8 @@ private: QJsonObject _settingsObject; QString _pendingPath; QTimer _settingsTimer; + mutable ReadWriteLockable _interstitialModeSettingLock; + Setting::Handle _enableInterstitialMode{ "enableInterstitialMode", false }; QSet _domainConnectionRefusals; bool _hasCheckedForAccessToken { false }; diff --git a/libraries/pointers/src/Pointer.cpp b/libraries/pointers/src/Pointer.cpp index 031baece5f..26460cbdd7 100644 --- a/libraries/pointers/src/Pointer.cpp +++ b/libraries/pointers/src/Pointer.cpp @@ -68,7 +68,8 @@ void Pointer::update(unsigned int pointerID) { // This only needs to be a read lock because update won't change any of the properties that can be modified from scripts withReadLock([&] { auto pickResult = getPrevPickResult(); - auto visualPickResult = getVisualPickResult(pickResult); + // Pointer needs its own PickResult object so it doesn't modify the cached pick result + auto visualPickResult = getVisualPickResult(getPickResultCopy(pickResult)); updateVisuals(visualPickResult); generatePointerEvents(pointerID, visualPickResult); }); diff --git a/libraries/pointers/src/Pointer.h b/libraries/pointers/src/Pointer.h index 4264a60079..173163374f 100644 --- a/libraries/pointers/src/Pointer.h +++ b/libraries/pointers/src/Pointer.h @@ -91,6 +91,7 @@ protected: virtual bool shouldHover(const PickResultPointer& pickResult) { return true; } virtual bool shouldTrigger(const PickResultPointer& pickResult) { return true; } + virtual PickResultPointer getPickResultCopy(const PickResultPointer& pickResult) const = 0; virtual PickResultPointer getVisualPickResult(const PickResultPointer& pickResult) { return pickResult; }; static const float POINTER_MOVE_DELAY; diff --git a/libraries/render-utils/src/HighlightEffect.cpp b/libraries/render-utils/src/HighlightEffect.cpp index 11326b1120..bcac31dd5a 100644 --- a/libraries/render-utils/src/HighlightEffect.cpp +++ b/libraries/render-utils/src/HighlightEffect.cpp @@ -37,6 +37,8 @@ namespace gr { #define OUTLINE_STENCIL_MASK 1 +extern void initZPassPipelines(ShapePlumber& plumber, gpu::StatePointer state); + HighlightRessources::HighlightRessources() { } @@ -180,6 +182,7 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c auto maskPipeline = _shapePlumber->pickPipeline(args, defaultKeyBuilder); auto maskSkinnedPipeline = _shapePlumber->pickPipeline(args, defaultKeyBuilder.withSkinned()); + auto maskSkinnedDQPipeline = _shapePlumber->pickPipeline(args, defaultKeyBuilder.withSkinned().withDualQuatSkinned()); // Setup camera, projection and viewport for all items batch.setViewportTransform(args->_viewport); @@ -187,14 +190,17 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c batch.setProjectionJitter(jitter.x, jitter.y); batch.setViewTransform(viewMat); - std::vector skinnedShapeKeys{}; + std::vector skinnedShapeKeys; + std::vector skinnedDQShapeKeys; // Iterate through all inShapes and render the unskinned args->_shapePipeline = maskPipeline; batch.setPipeline(maskPipeline->pipeline); for (const auto& items : inShapes) { itemBounds.insert(itemBounds.end(), items.second.begin(), items.second.end()); - if (items.first.isSkinned()) { + if (items.first.isSkinned() && items.first.isDualQuatSkinned()) { + skinnedDQShapeKeys.push_back(items.first); + } else if (items.first.isSkinned()) { skinnedShapeKeys.push_back(items.first); } else { renderItems(renderContext, items.second); @@ -202,10 +208,21 @@ void DrawHighlightMask::run(const render::RenderContextPointer& renderContext, c } // Reiterate to render the skinned - args->_shapePipeline = maskSkinnedPipeline; - batch.setPipeline(maskSkinnedPipeline->pipeline); - for (const auto& key : skinnedShapeKeys) { - renderItems(renderContext, inShapes.at(key)); + if (skinnedShapeKeys.size() > 0) { + args->_shapePipeline = maskSkinnedPipeline; + batch.setPipeline(maskSkinnedPipeline->pipeline); + for (const auto& key : skinnedShapeKeys) { + renderItems(renderContext, inShapes.at(key)); + } + } + + // Reiterate to render the DQ skinned + if (skinnedDQShapeKeys.size() > 0) { + args->_shapePipeline = maskSkinnedDQPipeline; + batch.setPipeline(maskSkinnedDQPipeline->pipeline); + for (const auto& key : skinnedDQShapeKeys) { + renderItems(renderContext, inShapes.at(key)); + } } args->_shapePipeline = nullptr; @@ -488,7 +505,7 @@ void DrawHighlightTask::build(JobModel& task, const render::Varying& inputs, ren state->setDepthTest(true, true, gpu::LESS_EQUAL); state->setColorWriteMask(false, false, false, false); - initMaskPipelines(*shapePlumber, state); + initZPassPipelines(*shapePlumber, state); } auto sharedParameters = std::make_shared(); @@ -548,16 +565,4 @@ const render::Varying DrawHighlightTask::addSelectItemJobs(JobModel& task, const const auto selectedMetasAndOpaques = task.addJob("OpaqueSelection", selectMetaAndOpaqueInput); const auto selectItemInput = SelectItems::Inputs(transparents, selectedMetasAndOpaques, selectionName).asVarying(); return task.addJob("TransparentSelection", selectItemInput); -} - -void DrawHighlightTask::initMaskPipelines(render::ShapePlumber& shapePlumber, gpu::StatePointer state) { - gpu::ShaderPointer modelProgram = gpu::Shader::createProgram(shader::render_utils::program::model_shadow); - shapePlumber.addPipeline( - ShapeKey::Filter::Builder().withoutSkinned(), - modelProgram, state); - - gpu::ShaderPointer skinProgram = gpu::Shader::createProgram(shader::render_utils::program::skin_model_shadow); - shapePlumber.addPipeline( - ShapeKey::Filter::Builder().withSkinned(), - skinProgram, state); -} +} \ No newline at end of file diff --git a/libraries/render-utils/src/HighlightEffect.h b/libraries/render-utils/src/HighlightEffect.h index 64a97a549e..32668c1ab6 100644 --- a/libraries/render-utils/src/HighlightEffect.h +++ b/libraries/render-utils/src/HighlightEffect.h @@ -208,8 +208,6 @@ public: void build(JobModel& task, const render::Varying& inputs, render::Varying& outputs); private: - - static void initMaskPipelines(render::ShapePlumber& plumber, gpu::StatePointer state); static const render::Varying addSelectItemJobs(JobModel& task, const render::Varying& selectionName, const RenderFetchCullSortTask::BucketList& items); }; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index ab6507b29c..be78a69b4c 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1281,92 +1281,6 @@ QStringList Model::getJointNames() const { return isActive() ? getFBXGeometry().getJointNames() : QStringList(); } -class Blender : public QRunnable { -public: - - Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& blendshapeCoefficients); - - virtual void run() override; - -private: - - ModelPointer _model; - int _blendNumber; - Geometry::WeakPointer _geometry; - QVector _blendshapeCoefficients; -}; - -Blender::Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& blendshapeCoefficients) : - _model(model), - _blendNumber(blendNumber), - _geometry(geometry), - _blendshapeCoefficients(blendshapeCoefficients) { -} - -void Blender::run() { - QVector vertices; - QVector normalsAndTangents; - if (_model && _model->isLoaded()) { - DETAILED_PROFILE_RANGE_EX(simulation_animation, __FUNCTION__, 0xFFFF0000, 0, { { "url", _model->getURL().toString() } }); - int offset = 0; - int normalsAndTangentsOffset = 0; - auto meshes = _model->getFBXGeometry().meshes; - int meshIndex = 0; - foreach (const FBXMesh& mesh, meshes) { - auto modelMeshNormalsAndTangents = _model->_normalsAndTangents.find(meshIndex++); - if (mesh.blendshapes.isEmpty() || modelMeshNormalsAndTangents == _model->_normalsAndTangents.end()) { - continue; - } - - vertices += mesh.vertices; - normalsAndTangents += modelMeshNormalsAndTangents->second; - glm::vec3* meshVertices = vertices.data() + offset; - NormalType* meshNormalsAndTangents = normalsAndTangents.data() + normalsAndTangentsOffset; - offset += mesh.vertices.size(); - normalsAndTangentsOffset += modelMeshNormalsAndTangents->second.size(); - const float NORMAL_COEFFICIENT_SCALE = 0.01f; - for (int i = 0, n = qMin(_blendshapeCoefficients.size(), mesh.blendshapes.size()); i < n; i++) { - float vertexCoefficient = _blendshapeCoefficients.at(i); - const float EPSILON = 0.0001f; - if (vertexCoefficient < EPSILON) { - continue; - } - float normalCoefficient = vertexCoefficient * NORMAL_COEFFICIENT_SCALE; - const FBXBlendshape& blendshape = mesh.blendshapes.at(i); - tbb::parallel_for(tbb::blocked_range(0, blendshape.indices.size()), [&](const tbb::blocked_range& range) { - for (auto j = range.begin(); j < range.end(); j++) { - int index = blendshape.indices.at(j); - meshVertices[index] += blendshape.vertices.at(j) * vertexCoefficient; - - glm::vec3 normal = mesh.normals.at(index) + blendshape.normals.at(j) * normalCoefficient; - glm::vec3 tangent; - if (index < mesh.tangents.size()) { - tangent = mesh.tangents.at(index); - if ((int)j < blendshape.tangents.size()) { - tangent += blendshape.tangents.at(j) * normalCoefficient; - } - } -#if FBX_PACK_NORMALS - glm::uint32 finalNormal; - glm::uint32 finalTangent; - buffer_helpers::packNormalAndTangent(normal, tangent, finalNormal, finalTangent); -#else - const auto& finalNormal = normal; - const auto& finalTangent = tangent; -#endif - meshNormalsAndTangents[2 * index] = finalNormal; - meshNormalsAndTangents[2 * index + 1] = finalTangent; - } - }); - } - } - } - // post the result to the ModelBlender, which will dispatch to the model if still alive - QMetaObject::invokeMethod(DependencyManager::get().data(), "setBlendedVertices", - Q_ARG(ModelPointer, _model), Q_ARG(int, _blendNumber), Q_ARG(QVector, vertices), - Q_ARG(QVector, normalsAndTangents)); -} - void Model::setScaleToFit(bool scaleToFit, const glm::vec3& dimensions, bool forceRescale) { if (forceRescale || _scaleToFit != scaleToFit || _scaleToFitDimensions != dimensions) { _scaleToFit = scaleToFit; @@ -1531,44 +1445,6 @@ void Model::updateClusterMatrices() { } } -bool Model::maybeStartBlender() { - if (isLoaded()) { - const FBXGeometry& fbxGeometry = getFBXGeometry(); - if (fbxGeometry.hasBlendedMeshes()) { - QThreadPool::globalInstance()->start(new Blender(getThisPointer(), ++_blendNumber, _renderGeometry, _blendshapeCoefficients)); - return true; - } - } - return false; -} - -void Model::setBlendedVertices(int blendNumber, const QVector& vertices, const QVector& normalsAndTangents) { - if (!isLoaded() || blendNumber < _appliedBlendNumber || !_blendedVertexBuffersInitialized) { - return; - } - _appliedBlendNumber = blendNumber; - const FBXGeometry& fbxGeometry = getFBXGeometry(); - int index = 0; - int normalAndTangentIndex = 0; - for (int i = 0; i < fbxGeometry.meshes.size(); i++) { - const FBXMesh& mesh = fbxGeometry.meshes.at(i); - auto meshNormalsAndTangents = _normalsAndTangents.find(i); - const auto& buffer = _blendedVertexBuffers.find(i); - if (mesh.blendshapes.isEmpty() || meshNormalsAndTangents == _normalsAndTangents.end() || buffer == _blendedVertexBuffers.end()) { - continue; - } - - const auto vertexCount = mesh.vertices.size(); - const auto verticesSize = vertexCount * sizeof(glm::vec3); - buffer->second->resize(mesh.vertices.size() * sizeof(glm::vec3) + meshNormalsAndTangents->second.size() * sizeof(NormalType)); - buffer->second->setSubData(0, verticesSize, (gpu::Byte*) vertices.constData() + index * sizeof(glm::vec3)); - buffer->second->setSubData(verticesSize, meshNormalsAndTangents->second.size() * sizeof(NormalType), (const gpu::Byte*) normalsAndTangents.data() + normalAndTangentIndex * sizeof(NormalType)); - - index += vertexCount; - normalAndTangentIndex += meshNormalsAndTangents->second.size(); - } -} - void Model::deleteGeometry() { _deleteGeometryCounter++; _blendedVertexBuffers.clear(); @@ -1605,42 +1481,6 @@ const render::ItemIDs& Model::fetchRenderItemIDs() const { return _modelMeshRenderItemIDs; } -void Model::initializeBlendshapes(const FBXMesh& mesh, int index) { - _blendedVertexBuffers[index] = std::make_shared(); - QVector normalsAndTangents; - normalsAndTangents.resize(2 * mesh.normals.size()); - - // Interleave normals and tangents - // Parallel version for performance - tbb::parallel_for(tbb::blocked_range(0, mesh.normals.size()), [&](const tbb::blocked_range& range) { - auto normalsRange = std::make_pair(mesh.normals.begin() + range.begin(), mesh.normals.begin() + range.end()); - auto tangentsRange = std::make_pair(mesh.tangents.begin() + range.begin(), mesh.tangents.begin() + range.end()); - auto normalsAndTangentsIt = normalsAndTangents.begin() + 2 * range.begin(); - - for (auto normalIt = normalsRange.first, tangentIt = tangentsRange.first; - normalIt != normalsRange.second; - ++normalIt, ++tangentIt) { -#if FBX_PACK_NORMALS - glm::uint32 finalNormal; - glm::uint32 finalTangent; - buffer_helpers::packNormalAndTangent(*normalIt, *tangentIt, finalNormal, finalTangent); -#else - const auto& finalNormal = *normalIt; - const auto& finalTangent = *tangentIt; -#endif - *normalsAndTangentsIt = finalNormal; - ++normalsAndTangentsIt; - *normalsAndTangentsIt = finalTangent; - ++normalsAndTangentsIt; - } - }); - const auto verticesSize = mesh.vertices.size() * sizeof(glm::vec3); - _blendedVertexBuffers[index]->resize(mesh.vertices.size() * sizeof(glm::vec3) + normalsAndTangents.size() * sizeof(NormalType)); - _blendedVertexBuffers[index]->setSubData(0, verticesSize, (const gpu::Byte*) mesh.vertices.constData()); - _blendedVertexBuffers[index]->setSubData(verticesSize, normalsAndTangents.size() * sizeof(NormalType), (const gpu::Byte*) normalsAndTangents.data()); - _normalsAndTangents[index] = normalsAndTangents; -} - void Model::createRenderItemSet() { assert(isLoaded()); const auto& meshes = _renderGeometry->getMeshes(); @@ -1774,6 +1614,164 @@ public: } }; + +class Blender : public QRunnable { +public: + + Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& blendshapeCoefficients); + + virtual void run() override; + +private: + + ModelPointer _model; + int _blendNumber; + Geometry::WeakPointer _geometry; + QVector _blendshapeCoefficients; +}; + +Blender::Blender(ModelPointer model, int blendNumber, const Geometry::WeakPointer& geometry, const QVector& blendshapeCoefficients) : + _model(model), + _blendNumber(blendNumber), + _geometry(geometry), + _blendshapeCoefficients(blendshapeCoefficients) { +} + +void Blender::run() { + QVector vertices; + QVector normalsAndTangents; + if (_model && _model->isLoaded()) { + DETAILED_PROFILE_RANGE_EX(simulation_animation, __FUNCTION__, 0xFFFF0000, 0, { { "url", _model->getURL().toString() } }); + int offset = 0; + int normalsAndTangentsOffset = 0; + auto meshes = _model->getFBXGeometry().meshes; + int meshIndex = 0; + foreach(const FBXMesh& mesh, meshes) { + auto modelMeshNormalsAndTangents = _model->_normalsAndTangents.find(meshIndex++); + if (mesh.blendshapes.isEmpty() || modelMeshNormalsAndTangents == _model->_normalsAndTangents.end()) { + continue; + } + + vertices += mesh.vertices; + normalsAndTangents += modelMeshNormalsAndTangents->second; + glm::vec3* meshVertices = vertices.data() + offset; + NormalType* meshNormalsAndTangents = normalsAndTangents.data() + normalsAndTangentsOffset; + offset += mesh.vertices.size(); + normalsAndTangentsOffset += modelMeshNormalsAndTangents->second.size(); + const float NORMAL_COEFFICIENT_SCALE = 0.01f; + for (int i = 0, n = qMin(_blendshapeCoefficients.size(), mesh.blendshapes.size()); i < n; i++) { + float vertexCoefficient = _blendshapeCoefficients.at(i); + const float EPSILON = 0.0001f; + if (vertexCoefficient < EPSILON) { + continue; + } + float normalCoefficient = vertexCoefficient * NORMAL_COEFFICIENT_SCALE; + const FBXBlendshape& blendshape = mesh.blendshapes.at(i); + tbb::parallel_for(tbb::blocked_range(0, blendshape.indices.size()), [&](const tbb::blocked_range& range) { + for (auto j = range.begin(); j < range.end(); j++) { + int index = blendshape.indices.at(j); + meshVertices[index] += blendshape.vertices.at(j) * vertexCoefficient; + + glm::vec3 normal = mesh.normals.at(index) + blendshape.normals.at(j) * normalCoefficient; + glm::vec3 tangent; + if (index < mesh.tangents.size()) { + tangent = mesh.tangents.at(index); + if ((int)j < blendshape.tangents.size()) { + tangent += blendshape.tangents.at(j) * normalCoefficient; + } + } +#if FBX_PACK_NORMALS + glm::uint32 finalNormal; + glm::uint32 finalTangent; + buffer_helpers::packNormalAndTangent(normal, tangent, finalNormal, finalTangent); +#else + const auto& finalNormal = normal; + const auto& finalTangent = tangent; +#endif + meshNormalsAndTangents[2 * index] = finalNormal; + meshNormalsAndTangents[2 * index + 1] = finalTangent; + } + }); + } + } + } + // post the result to the ModelBlender, which will dispatch to the model if still alive + QMetaObject::invokeMethod(DependencyManager::get().data(), "setBlendedVertices", + Q_ARG(ModelPointer, _model), Q_ARG(int, _blendNumber), Q_ARG(QVector, vertices), + Q_ARG(QVector, normalsAndTangents)); +} + +bool Model::maybeStartBlender() { + if (isLoaded()) { + QThreadPool::globalInstance()->start(new Blender(getThisPointer(), ++_blendNumber, _renderGeometry, _blendshapeCoefficients)); + return true; + } + return false; +} + +void Model::setBlendedVertices(int blendNumber, const QVector& vertices, const QVector& normalsAndTangents) { + if (!isLoaded() || blendNumber < _appliedBlendNumber || !_blendedVertexBuffersInitialized) { + return; + } + _appliedBlendNumber = blendNumber; + const FBXGeometry& fbxGeometry = getFBXGeometry(); + int index = 0; + int normalAndTangentIndex = 0; + for (int i = 0; i < fbxGeometry.meshes.size(); i++) { + const FBXMesh& mesh = fbxGeometry.meshes.at(i); + auto meshNormalsAndTangents = _normalsAndTangents.find(i); + const auto& buffer = _blendedVertexBuffers.find(i); + if (mesh.blendshapes.isEmpty() || meshNormalsAndTangents == _normalsAndTangents.end() || buffer == _blendedVertexBuffers.end()) { + continue; + } + + const auto vertexCount = mesh.vertices.size(); + const auto verticesSize = vertexCount * sizeof(glm::vec3); + buffer->second->resize(mesh.vertices.size() * sizeof(glm::vec3) + meshNormalsAndTangents->second.size() * sizeof(NormalType)); + buffer->second->setSubData(0, verticesSize, (gpu::Byte*) vertices.constData() + index * sizeof(glm::vec3)); + buffer->second->setSubData(verticesSize, meshNormalsAndTangents->second.size() * sizeof(NormalType), (const gpu::Byte*) normalsAndTangents.data() + normalAndTangentIndex * sizeof(NormalType)); + + index += vertexCount; + normalAndTangentIndex += meshNormalsAndTangents->second.size(); + } +} + +void Model::initializeBlendshapes(const FBXMesh& mesh, int index) { + _blendedVertexBuffers[index] = std::make_shared(); + QVector normalsAndTangents; + normalsAndTangents.resize(2 * mesh.normals.size()); + + // Interleave normals and tangents + // Parallel version for performance + tbb::parallel_for(tbb::blocked_range(0, mesh.normals.size()), [&](const tbb::blocked_range& range) { + auto normalsRange = std::make_pair(mesh.normals.begin() + range.begin(), mesh.normals.begin() + range.end()); + auto tangentsRange = std::make_pair(mesh.tangents.begin() + range.begin(), mesh.tangents.begin() + range.end()); + auto normalsAndTangentsIt = normalsAndTangents.begin() + 2 * range.begin(); + + for (auto normalIt = normalsRange.first, tangentIt = tangentsRange.first; + normalIt != normalsRange.second; + ++normalIt, ++tangentIt) { +#if FBX_PACK_NORMALS + glm::uint32 finalNormal; + glm::uint32 finalTangent; + buffer_helpers::packNormalAndTangent(*normalIt, *tangentIt, finalNormal, finalTangent); +#else + const auto& finalNormal = *normalIt; + const auto& finalTangent = *tangentIt; +#endif + *normalsAndTangentsIt = finalNormal; + ++normalsAndTangentsIt; + *normalsAndTangentsIt = finalTangent; + ++normalsAndTangentsIt; + } + }); + const auto verticesSize = mesh.vertices.size() * sizeof(glm::vec3); + _blendedVertexBuffers[index]->resize(mesh.vertices.size() * sizeof(glm::vec3) + normalsAndTangents.size() * sizeof(NormalType)); + _blendedVertexBuffers[index]->setSubData(0, verticesSize, (const gpu::Byte*) mesh.vertices.constData()); + _blendedVertexBuffers[index]->setSubData(verticesSize, normalsAndTangents.size() * sizeof(NormalType), (const gpu::Byte*) normalsAndTangents.data()); + _normalsAndTangents[index] = normalsAndTangents; +} + ModelBlender::ModelBlender() : _pendingBlenders(0) { } @@ -1783,14 +1781,23 @@ ModelBlender::~ModelBlender() { void ModelBlender::noteRequiresBlend(ModelPointer model) { Lock lock(_mutex); - if (_pendingBlenders < QThread::idealThreadCount()) { - if (model->maybeStartBlender()) { - _pendingBlenders++; - return; - } + if (_modelsRequiringBlendsSet.find(model) == _modelsRequiringBlendsSet.end()) { + _modelsRequiringBlendsQueue.push(model); + _modelsRequiringBlendsSet.insert(model); } - _modelsRequiringBlends.insert(model); + if (_pendingBlenders < QThread::idealThreadCount()) { + while (!_modelsRequiringBlendsQueue.empty()) { + auto weakPtr = _modelsRequiringBlendsQueue.front(); + _modelsRequiringBlendsQueue.pop(); + _modelsRequiringBlendsSet.erase(weakPtr); + ModelPointer nextModel = weakPtr.lock(); + if (nextModel && nextModel->maybeStartBlender()) { + _pendingBlenders++; + return; + } + } + } } void ModelBlender::setBlendedVertices(ModelPointer model, int blendNumber, QVector vertices, QVector normalsAndTangents) { @@ -1800,20 +1807,15 @@ void ModelBlender::setBlendedVertices(ModelPointer model, int blendNumber, QVect { Lock lock(_mutex); _pendingBlenders--; - _modelsRequiringBlends.erase(model); - std::set> modelsToErase; - for (auto i = _modelsRequiringBlends.begin(); i != _modelsRequiringBlends.end(); i++) { - auto weakPtr = *i; + while (!_modelsRequiringBlendsQueue.empty()) { + auto weakPtr = _modelsRequiringBlendsQueue.front(); + _modelsRequiringBlendsQueue.pop(); + _modelsRequiringBlendsSet.erase(weakPtr); ModelPointer nextModel = weakPtr.lock(); if (nextModel && nextModel->maybeStartBlender()) { _pendingBlenders++; break; - } else { - modelsToErase.insert(weakPtr); } } - for (auto& weakPtr : modelsToErase) { - _modelsRequiringBlends.erase(weakPtr); - } } } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 447f75dd9d..c763197bc6 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -530,7 +530,8 @@ private: ModelBlender(); virtual ~ModelBlender(); - std::set> _modelsRequiringBlends; + std::queue _modelsRequiringBlendsQueue; + std::set> _modelsRequiringBlendsSet; int _pendingBlenders; Mutex _mutex; diff --git a/libraries/shared/src/SharedUtil.cpp b/libraries/shared/src/SharedUtil.cpp index bb22a1e753..012e7aa1f5 100644 --- a/libraries/shared/src/SharedUtil.cpp +++ b/libraries/shared/src/SharedUtil.cpp @@ -19,8 +19,8 @@ #include #include #include -#include #include +#include #include @@ -127,82 +127,10 @@ void usecTimestampNowForceClockSkew(qint64 clockSkew) { ::usecTimestampNowAdjust = clockSkew; } -static std::atomic TIME_REFERENCE { 0 }; // in usec -static std::once_flag usecTimestampNowIsInitialized; -static QElapsedTimer timestampTimer; - quint64 usecTimestampNow(bool wantDebug) { - std::call_once(usecTimestampNowIsInitialized, [&] { - TIME_REFERENCE = QDateTime::currentMSecsSinceEpoch() * USECS_PER_MSEC; // ms to usec - timestampTimer.start(); - }); - - quint64 now; - quint64 nsecsElapsed = timestampTimer.nsecsElapsed(); - quint64 usecsElapsed = nsecsElapsed / NSECS_PER_USEC; // nsec to usec - - // QElapsedTimer may not advance if the CPU has gone to sleep. In which case it - // will begin to deviate from real time. We detect that here, and reset if necessary - quint64 msecsCurrentTime = QDateTime::currentMSecsSinceEpoch(); - quint64 msecsEstimate = (TIME_REFERENCE + usecsElapsed) / USECS_PER_MSEC; // usecs to msecs - int possibleSkew = msecsEstimate - msecsCurrentTime; - const int TOLERANCE = 10 * MSECS_PER_SECOND; // up to 10 seconds of skew is tolerated - if (abs(possibleSkew) > TOLERANCE) { - // reset our TIME_REFERENCE and timer - TIME_REFERENCE = QDateTime::currentMSecsSinceEpoch() * USECS_PER_MSEC; // ms to usec - timestampTimer.restart(); - now = TIME_REFERENCE + ::usecTimestampNowAdjust; - - if (wantDebug) { - qCDebug(shared) << "usecTimestampNow() - resetting QElapsedTimer. "; - qCDebug(shared) << " msecsCurrentTime:" << msecsCurrentTime; - qCDebug(shared) << " msecsEstimate:" << msecsEstimate; - qCDebug(shared) << " possibleSkew:" << possibleSkew; - qCDebug(shared) << " TOLERANCE:" << TOLERANCE; - - qCDebug(shared) << " nsecsElapsed:" << nsecsElapsed; - qCDebug(shared) << " usecsElapsed:" << usecsElapsed; - - QDateTime currentLocalTime = QDateTime::currentDateTime(); - - quint64 msecsNow = now / 1000; // usecs to msecs - QDateTime nowAsString; - nowAsString.setMSecsSinceEpoch(msecsNow); - - qCDebug(shared) << " now:" << now; - qCDebug(shared) << " msecsNow:" << msecsNow; - - qCDebug(shared) << " nowAsString:" << nowAsString.toString("yyyy-MM-dd hh:mm:ss.zzz"); - qCDebug(shared) << " currentLocalTime:" << currentLocalTime.toString("yyyy-MM-dd hh:mm:ss.zzz"); - } - } else { - now = TIME_REFERENCE + usecsElapsed + ::usecTimestampNowAdjust; - } - - if (wantDebug) { - QDateTime currentLocalTime = QDateTime::currentDateTime(); - - quint64 msecsNow = now / 1000; // usecs to msecs - QDateTime nowAsString; - nowAsString.setMSecsSinceEpoch(msecsNow); - - quint64 msecsTimeReference = TIME_REFERENCE / 1000; // usecs to msecs - QDateTime timeReferenceAsString; - timeReferenceAsString.setMSecsSinceEpoch(msecsTimeReference); - - qCDebug(shared) << "usecTimestampNow() - details... "; - qCDebug(shared) << " TIME_REFERENCE:" << TIME_REFERENCE; - qCDebug(shared) << " timeReferenceAsString:" << timeReferenceAsString.toString("yyyy-MM-dd hh:mm:ss.zzz"); - qCDebug(shared) << " usecTimestampNowAdjust:" << usecTimestampNowAdjust; - qCDebug(shared) << " nsecsElapsed:" << nsecsElapsed; - qCDebug(shared) << " usecsElapsed:" << usecsElapsed; - qCDebug(shared) << " now:" << now; - qCDebug(shared) << " msecsNow:" << msecsNow; - qCDebug(shared) << " nowAsString:" << nowAsString.toString("yyyy-MM-dd hh:mm:ss.zzz"); - qCDebug(shared) << " currentLocalTime:" << currentLocalTime.toString("yyyy-MM-dd hh:mm:ss.zzz"); - } - - return now; + using namespace std::chrono; + static const auto unixEpoch = system_clock::from_time_t(0); + return duration_cast(system_clock::now() - unixEpoch).count() + usecTimestampNowAdjust; } float secTimestampNow() { diff --git a/scripts/modules/appUi.js b/scripts/modules/appUi.js index 0e7461c5f1..83d99cd42b 100644 --- a/scripts/modules/appUi.js +++ b/scripts/modules/appUi.js @@ -107,7 +107,9 @@ function AppUi(properties) { that.notificationPollCaresAboutSince = false; that.notificationInitialCallbackMade = false; that.notificationDisplayBanner = function (message) { - Window.displayAnnouncement(message); + if (!that.isOpen) { + Window.displayAnnouncement(message); + } }; // // END Notification Handling Defaults @@ -118,6 +120,7 @@ function AppUi(properties) { // Set isOpen, wireEventBridge, set buttonActive as appropriate, // and finally call onOpened() or onClosed() IFF defined. that.setCurrentVisibleScreenMetadata(type, url); + if (that.checkIsOpen(type, url)) { that.wireEventBridge(true); if (!that.isOpen) { @@ -155,17 +158,21 @@ function AppUi(properties) { return; } - // User is "appearing offline" - if (GlobalServices.findableBy === "none") { + // User is "appearing offline" or is offline + if (GlobalServices.findableBy === "none" || Account.username === "") { that.notificationPollTimeout = Script.setTimeout(that.notificationPoll, that.notificationPollTimeoutMs); return; } var url = METAVERSE_BASE + that.notificationPollEndpoint; + var settingsKey = "notifications/" + that.buttonName + "/lastPoll"; + var currentTimestamp = new Date().getTime(); + var lastPollTimestamp = Settings.getValue(settingsKey, currentTimestamp); if (that.notificationPollCaresAboutSince) { - url = url + "&since=" + (new Date().getTime()); + url = url + "&since=" + lastPollTimestamp/1000; } + Settings.setValue(settingsKey, currentTimestamp); console.debug(that.buttonName, 'polling for notifications at endpoint', url); @@ -193,17 +200,18 @@ function AppUi(properties) { } else { concatenatedServerResponse = concatenatedServerResponse.concat(that.notificationDataProcessPage(response)); currentDataPageToRetrieve++; - request({ uri: (url + "&page=" + currentDataPageToRetrieve) }, requestCallback); + request({ json: true, uri: (url + "&page=" + currentDataPageToRetrieve) }, requestCallback); } } - request({ uri: url }, requestCallback); + request({ json: true, uri: url }, requestCallback); }; // This won't do anything if there isn't a notification endpoint set that.notificationPoll(); - function availabilityChanged() { + function restartNotificationPoll() { + that.notificationInitialCallbackMade = false; if (that.notificationPollTimeout) { Script.clearTimeout(that.notificationPollTimeout); that.notificationPollTimeout = false; @@ -303,7 +311,8 @@ function AppUi(properties) { } : that.ignore; that.onScriptEnding = function onScriptEnding() { // Close if necessary, clean up any remaining handlers, and remove the button. - GlobalServices.findableByChanged.disconnect(availabilityChanged); + GlobalServices.myUsernameChanged.disconnect(restartNotificationPoll); + GlobalServices.findableByChanged.disconnect(restartNotificationPoll); if (that.isOpen) { that.close(); } @@ -323,7 +332,8 @@ function AppUi(properties) { that.tablet.screenChanged.connect(that.onScreenChanged); that.button.clicked.connect(that.onClicked); Script.scriptEnding.connect(that.onScriptEnding); - GlobalServices.findableByChanged.connect(availabilityChanged); + GlobalServices.findableByChanged.connect(restartNotificationPoll); + GlobalServices.myUsernameChanged.connect(restartNotificationPoll); if (that.buttonName == Settings.getValue("startUpApp")) { Settings.setValue("startUpApp", ""); Script.setTimeout(function () { diff --git a/scripts/modules/request.js b/scripts/modules/request.js index 3516554567..d0037f9b43 100644 --- a/scripts/modules/request.js +++ b/scripts/modules/request.js @@ -19,7 +19,7 @@ module.exports = { // ------------------------------------------------------------------ request: function (options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request. - var httpRequest = new XMLHttpRequest(), key; + var httpRequest = new XMLHttpRequest(), key; // QT bug: apparently doesn't handle onload. Workaround using readyState. httpRequest.onreadystatechange = function () { var READY_STATE_DONE = 4; @@ -72,7 +72,7 @@ module.exports = { } httpRequest.open(options.method, options.uri, true); httpRequest.send(options.body || null); - } + } }; // =========================================================================================== diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 993ea30c2e..5b91afea33 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -474,9 +474,6 @@ function fromQml(message) { Window.location = "hifi://BankOfHighFidelity"; } break; - case 'wallet_availableUpdatesReceived': - // NOP - break; case 'http.request': // Handled elsewhere, don't log. break; @@ -491,15 +488,77 @@ function walletOpened() { Controller.mouseMoveEvent.connect(handleMouseMoveEvent); triggerMapping.enable(); triggerPressMapping.enable(); + shouldShowDot = false; + ui.messagesWaiting(shouldShowDot); } function walletClosed() { off(); } -// -// Manage the connection between the button and the window. -// +function notificationDataProcessPage(data) { + return data.data.history; +} + +var shouldShowDot = false; +function notificationPollCallback(historyArray) { + if (!ui.isOpen) { + var notificationCount = historyArray.length; + shouldShowDot = shouldShowDot || notificationCount > 0; + ui.messagesWaiting(shouldShowDot); + + if (notificationCount > 0) { + var message; + if (!ui.notificationInitialCallbackMade) { + message = "You have " + notificationCount + " unread wallet " + + "transaction" + (notificationCount === 1 ? "" : "s") + ". Open WALLET to see all activity."; + ui.notificationDisplayBanner(message); + } else { + for (var i = 0; i < notificationCount; i++) { + message = '"' + (historyArray[i].message) + '" ' + + "Open WALLET to see all activity."; + ui.notificationDisplayBanner(message); + } + } + } + } +} + +function isReturnedDataEmpty(data) { + var historyArray = data.data.history; + return historyArray.length === 0; +} + +var DEVELOPER_MENU = "Developer"; +var MARKETPLACE_ITEM_TESTER_LABEL = "Marketplace Item Tester"; +var MARKETPLACE_ITEM_TESTER_QML_SOURCE = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"; +function installMarketplaceItemTester() { + if (!Menu.menuExists(DEVELOPER_MENU)) { + Menu.addMenu(DEVELOPER_MENU); + } + if (!Menu.menuItemExists(DEVELOPER_MENU, MARKETPLACE_ITEM_TESTER_LABEL)) { + Menu.addMenuItem({ + menuName: DEVELOPER_MENU, + menuItemName: MARKETPLACE_ITEM_TESTER_LABEL, + isCheckable: false + }); + } + + Menu.menuItemEvent.connect(function (menuItem) { + if (menuItem === MARKETPLACE_ITEM_TESTER_LABEL) { + ui.open(MARKETPLACE_ITEM_TESTER_QML_SOURCE); + } + }); +} + +function uninstallMarketplaceItemTester() { + if (Menu.menuExists(DEVELOPER_MENU) && + Menu.menuItemExists(DEVELOPER_MENU, MARKETPLACE_ITEM_TESTER_LABEL) + ) { + Menu.removeMenuItem(DEVELOPER_MENU, MARKETPLACE_ITEM_TESTER_LABEL); + } +} + var BUTTON_NAME = "WALLET"; var WALLET_QML_SOURCE = "hifi/commerce/wallet/Wallet.qml"; var ui; @@ -510,10 +569,18 @@ function startup() { home: WALLET_QML_SOURCE, onOpened: walletOpened, onClosed: walletClosed, - onMessage: fromQml + onMessage: fromQml, + notificationPollEndpoint: "/api/v1/commerce/history?per_page=10", + notificationPollTimeoutMs: 300000, + notificationDataProcessPage: notificationDataProcessPage, + notificationPollCallback: notificationPollCallback, + notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, + notificationPollCaresAboutSince: true }); GlobalServices.myUsernameChanged.connect(onUsernameChanged); + installMarketplaceItemTester(); } + var isUpdateOverlaysWired = false; function off() { Users.usernameFromIDReply.disconnect(usernameFromIDReply); @@ -528,9 +595,11 @@ function off() { } removeOverlays(); } + function shutdown() { GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); deleteSendMoneyParticleEffect(); + uninstallMarketplaceItemTester(); off(); } diff --git a/scripts/system/controllers/controllerModules/highlightNearbyEntities.js b/scripts/system/controllers/controllerModules/highlightNearbyEntities.js index 3a33082f64..bc09ebee7a 100644 --- a/scripts/system/controllers/controllerModules/highlightNearbyEntities.js +++ b/scripts/system/controllers/controllerModules/highlightNearbyEntities.js @@ -37,7 +37,7 @@ this.highlightedEntities = []; this.parameters = dispatcherUtils.makeDispatcherModuleParameters( - 120, + 480, this.hand === dispatcherUtils.RIGHT_HAND ? ["rightHand"] : ["leftHand"], [], 100); diff --git a/scripts/system/controllers/controllerModules/inEditMode.js b/scripts/system/controllers/controllerModules/inEditMode.js index 2bdd89f141..d590545532 100644 --- a/scripts/system/controllers/controllerModules/inEditMode.js +++ b/scripts/system/controllers/controllerModules/inEditMode.js @@ -29,7 +29,7 @@ Script.include("/~/system/libraries/utils.js"); this.reticleMaxY; this.parameters = makeDispatcherModuleParameters( - 200, + 160, this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"], [], 100, diff --git a/scripts/system/controllers/controllerModules/inVREditMode.js b/scripts/system/controllers/controllerModules/inVREditMode.js index 02863cf935..7b78d5e1c4 100644 --- a/scripts/system/controllers/controllerModules/inVREditMode.js +++ b/scripts/system/controllers/controllerModules/inVREditMode.js @@ -21,7 +21,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.disableModules = false; var NO_HAND_LASER = -1; // Invalid hand parameter so that default laser is not displayed. this.parameters = makeDispatcherModuleParameters( - 240, // Not too high otherwise the tablet laser doesn't work. + 200, // Not too high otherwise the tablet laser doesn't work. this.hand === RIGHT_HAND ? ["rightHand", "rightHandEquip", "rightHandTrigger"] : ["leftHand", "leftHandEquip", "leftHandTrigger"], diff --git a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js index 27c1b458b8..a8de76aebd 100644 --- a/scripts/system/controllers/controllerModules/nearActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearActionGrabEntity.js @@ -26,7 +26,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js"); this.hapticTargetID = null; this.parameters = makeDispatcherModuleParameters( - 140, + 500, this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], [], 100); diff --git a/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js b/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js index 366fcd3032..962ae89bb9 100644 --- a/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js +++ b/scripts/system/controllers/controllerModules/nearGrabHyperLinkEntity.js @@ -21,7 +21,7 @@ this.hyperlink = ""; this.parameters = makeDispatcherModuleParameters( - 125, + 485, this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], [], 100); diff --git a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js index f805dbf60e..cc88371441 100644 --- a/scripts/system/controllers/controllerModules/nearParentGrabEntity.js +++ b/scripts/system/controllers/controllerModules/nearParentGrabEntity.js @@ -57,7 +57,7 @@ Script.include("/~/system/libraries/controllers.js"); this.cloneAllowed = true; this.parameters = makeDispatcherModuleParameters( - 140, + 500, this.hand === RIGHT_HAND ? ["rightHand"] : ["leftHand"], [], 100); diff --git a/scripts/system/controllers/controllerModules/nearTrigger.js b/scripts/system/controllers/controllerModules/nearTrigger.js index f1126dedc3..6a9cd9fbcd 100644 --- a/scripts/system/controllers/controllerModules/nearTrigger.js +++ b/scripts/system/controllers/controllerModules/nearTrigger.js @@ -29,7 +29,7 @@ Script.include("/~/system/libraries/controllerDispatcherUtils.js"); this.startSent = false; this.parameters = makeDispatcherModuleParameters( - 120, + 480, this.hand === RIGHT_HAND ? ["rightHandTrigger", "rightHand"] : ["leftHandTrigger", "leftHand"], [], 100); diff --git a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js index 4e36355621..2412e2fa1c 100644 --- a/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js +++ b/scripts/system/controllers/controllerModules/webSurfaceLaserInput.js @@ -121,7 +121,7 @@ Script.include("/~/system/libraries/controllers.js"); controllerData.triggerValues[this.otherHand] <= TRIGGER_OFF_VALUE; var allowThisModule = !otherModuleRunning || isTriggerPressed; - if (allowThisModule && this.isPointingAtTriggerable(controllerData, isTriggerPressed, false)) { + if ((allowThisModule && this.isPointingAtTriggerable(controllerData, isTriggerPressed, false)) && !this.grabModuleWantsNearbyOverlay(controllerData)) { this.updateAllwaysOn(); if (isTriggerPressed) { this.dominantHandOverride = true; // Override dominant hand. diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 13ad1f6b69..85cd499d20 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -20,13 +20,14 @@ var AppUi = Script.require('appUi'); Script.include("/~/system/libraries/gridTool.js"); Script.include("/~/system/libraries/connectionUtils.js"); -var METAVERSE_SERVER_URL = Account.metaverseServerURL; -var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); -var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml"; +var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; +var MARKETPLACE_ITEM_TESTER_QML_PATH = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"; var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; -var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; +var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); +var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); +var METAVERSE_SERVER_URL = Account.metaverseServerURL; var REZZING_SOUND = SoundCache.getSound(Script.resolvePath("../assets/sounds/rezzing.wav")); // Event bridge messages. @@ -137,7 +138,6 @@ function onUsernameChanged() { } } -var userHasUpdates = false; function sendCommerceSettings() { ui.sendToHtml({ type: "marketplaces", @@ -147,7 +147,7 @@ function sendCommerceSettings() { userIsLoggedIn: Account.loggedIn, walletNeedsSetup: Wallet.walletStatus === 1, metaverseServerURL: Account.metaverseServerURL, - messagesWaiting: userHasUpdates + messagesWaiting: shouldShowDot } }); } @@ -756,7 +756,7 @@ function deleteSendAssetParticleEffect() { } sendAssetRecipient = null; } - + var savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview"); var UI_FADE_TIMEOUT_MS = 150; function maybeEnableHMDPreview() { @@ -768,6 +768,13 @@ function maybeEnableHMDPreview() { }, UI_FADE_TIMEOUT_MS); } +var resourceObjectsInTest = []; +function signalNewResourceObjectInTest(resourceObject) { + ui.tablet.sendToQml({ + method: "newResourceObjectInTest", + resourceObject: resourceObject }); +} + var onQmlMessageReceived = function onQmlMessageReceived(message) { if (message.messageSrc === "HTML") { return; @@ -817,8 +824,20 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { break; case 'checkout_rezClicked': case 'purchases_rezClicked': + case 'tester_rezClicked': rezEntity(message.itemHref, message.itemType); break; + case 'tester_newResourceObject': + var resourceObject = message.resourceObject; + resourceObjectsInTest[resourceObject.id] = resourceObject; + signalNewResourceObjectInTest(resourceObject); + break; + case 'tester_updateResourceObjectAssetType': + resourceObjectsInTest[message.objectId].assetType = message.assetType; + break; + case 'tester_deleteResourceObject': + delete resourceObjectsInTest[message.objectId]; + break; case 'header_marketplaceImageClicked': case 'purchases_backClicked': ui.open(message.referrerURL, MARKETPLACES_INJECT_SCRIPT_URL); @@ -841,10 +860,6 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { openLoginWindow(); break; case 'disableHmdPreview': - if (!savedDisablePreviewOption) { - savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview"); - } - if (!savedDisablePreviewOption) { DesktopPreviewProvider.setPreviewDisabledReason("SECURE_SCREEN"); Menu.setIsOptionChecked("Disable Preview", true); @@ -908,10 +923,9 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { removeOverlays(); } break; - case 'wallet_availableUpdatesReceived': case 'purchases_availableUpdatesReceived': - userHasUpdates = message.numUpdates > 0; - ui.messagesWaiting(userHasUpdates); + shouldShowDot = message.numUpdates > 0; + ui.messagesWaiting(shouldShowDot && !ui.isOpen); break; case 'purchases_updateWearables': var currentlyWornWearables = []; @@ -962,34 +976,65 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { } }; +function pushResourceObjectsInTest() { + var maxObjectId = -1; + for (var objectId in resourceObjectsInTest) { + signalNewResourceObjectInTest(resourceObjectsInTest[objectId]); + maxObjectId = (maxObjectId < objectId) ? parseInt(objectId) : maxObjectId; + } + // N.B. Thinking about removing the following sendToQml? Be sure + // that the marketplace item tester QML has heard from us, at least + // so that it can indicate to the user that all of the resoruce + // objects in test have been transmitted to it. + ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: maxObjectId + 1 }); +} + // Function Name: onTabletScreenChanged() // // Description: // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. -var onMarketplaceScreen = false; -var onWalletScreen = false; + var onCommerceScreen = false; var onInspectionCertificateScreen = false; +var onMarketplaceItemTesterScreen = false; +var onMarketplaceScreen = false; +var onWalletScreen = false; var onTabletScreenChanged = function onTabletScreenChanged(type, url) { ui.setCurrentVisibleScreenMetadata(type, url); onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; onInspectionCertificateScreen = type === "QML" && url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1; var onWalletScreenNow = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1; - var onCommerceScreenNow = type === "QML" && - (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH || - onInspectionCertificateScreen); + var onCommerceScreenNow = type === "QML" && ( + url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 || + url === MARKETPLACE_PURCHASES_QML_PATH || + url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); + var onMarketplaceItemTesterScreenNow = ( + url.indexOf(MARKETPLACE_ITEM_TESTER_QML_PATH) !== -1 || + url === MARKETPLACE_ITEM_TESTER_QML_PATH); - // exiting wallet or commerce screen - if ((!onWalletScreenNow && onWalletScreen) || (!onCommerceScreenNow && onCommerceScreen)) { + if ((!onWalletScreenNow && onWalletScreen) || + (!onCommerceScreenNow && onCommerceScreen) || + (!onMarketplaceItemTesterScreenNow && onMarketplaceScreen) + ) { + // exiting wallet, commerce, or marketplace item tester screen maybeEnableHMDPreview(); } onCommerceScreen = onCommerceScreenNow; onWalletScreen = onWalletScreenNow; - wireQmlEventBridge(onMarketplaceScreen || onCommerceScreen || onWalletScreen); + onMarketplaceItemTesterScreen = onMarketplaceItemTesterScreenNow; + + wireQmlEventBridge( + onMarketplaceScreen || + onCommerceScreen || + onWalletScreen || + onMarketplaceItemTesterScreen); if (url === MARKETPLACE_PURCHASES_QML_PATH) { + // FIXME? Is there a race condition here in which the event + // bridge may not be up yet? If so, Script.setTimeout(..., 750) + // may help avoid the condition. ui.tablet.sendToQml({ method: 'updatePurchases', referrerURL: referrerURL, @@ -1026,13 +1071,53 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { }); off(); } + + if (onMarketplaceItemTesterScreen) { + // Why setTimeout? The QML event bridge, wired above, requires a + // variable amount of time to come up, in practice less than + // 750ms. + Script.setTimeout(pushResourceObjectsInTest, 750); + } + console.debug(ui.buttonName + " app reports: Tablet screen changed.\nNew screen type: " + type + "\nNew screen URL: " + url + "\nCurrent app open status: " + ui.isOpen + "\n"); }; -// -// Manage the connection between the button and the window. -// +function notificationDataProcessPage(data) { + return data.data.updates; +} + +var shouldShowDot = false; +function notificationPollCallback(updatesArray) { + shouldShowDot = shouldShowDot || updatesArray.length > 0; + ui.messagesWaiting(shouldShowDot && !ui.isOpen); + + if (updatesArray.length > 0) { + var message; + if (!ui.notificationInitialCallbackMade) { + message = updatesArray.length + " of your purchased items " + + (updatesArray.length === 1 ? "has an update " : "have updates ") + + "available. Open MARKET to update."; + ui.notificationDisplayBanner(message); + + ui.notificationPollCaresAboutSince = true; + } else { + for (var i = 0; i < updatesArray.length; i++) { + message = "Update available for \"" + + updatesArray[i].base_item_title + "\"." + + "Open MARKET to update."; + ui.notificationDisplayBanner(message); + } + } + } +} + +function isReturnedDataEmpty(data) { + var historyArray = data.data.updates; + return historyArray.length === 0; +} + + var BUTTON_NAME = "MARKET"; var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace"; var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. @@ -1044,7 +1129,13 @@ function startup() { inject: MARKETPLACES_INJECT_SCRIPT_URL, home: MARKETPLACE_URL_INITIAL, onScreenChanged: onTabletScreenChanged, - onMessage: onQmlMessageReceived + onMessage: onQmlMessageReceived, + notificationPollEndpoint: "/api/v1/commerce/available_updates?per_page=10", + notificationPollTimeoutMs: 300000, + notificationDataProcessPage: notificationDataProcessPage, + notificationPollCallback: notificationPollCallback, + notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, + notificationPollCaresAboutSince: false // Changes to true after first poll }); ContextOverlay.contextOverlayClicked.connect(openInspectionCertificateQML); Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 85898c28fb..a2ebae1a33 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -823,46 +823,40 @@ function notificationDataProcessPage(data) { } var shouldShowDot = false; -var storedOnlineUsersArray = []; +var pingPong = false; +var storedOnlineUsers = {}; function notificationPollCallback(connectionsArray) { // // START logic for handling online/offline user changes // - var i, j; - var newlyOnlineConnectionsArray = []; - for (i = 0; i < connectionsArray.length; i++) { - var currentUser = connectionsArray[i]; + pingPong = !pingPong; + var newOnlineUsers = 0; + var message; - if (connectionsArray[i].online) { - var indexOfStoredOnlineUser = -1; - for (j = 0; j < storedOnlineUsersArray.length; j++) { - if (currentUser.username === storedOnlineUsersArray[j].username) { - indexOfStoredOnlineUser = j; - break; - } - } - // If the user record isn't already presesnt inside `storedOnlineUsersArray`... - if (indexOfStoredOnlineUser < 0) { - storedOnlineUsersArray.push(currentUser); - newlyOnlineConnectionsArray.push(currentUser); - } - } else { - var indexOfOfflineUser = -1; - for (j = 0; j < storedOnlineUsersArray.length; j++) { - if (currentUser.username === storedOnlineUsersArray[j].username) { - indexOfOfflineUser = j; - break; - } - } - if (indexOfOfflineUser >= 0) { - storedOnlineUsersArray.splice(indexOfOfflineUser); - } + connectionsArray.forEach(function (user) { + var stored = storedOnlineUsers[user.username]; + var storedOrNew = stored || user; + storedOrNew.pingPong = pingPong; + if (stored) { + return; + } + + newOnlineUsers++; + storedOnlineUsers[user.username] = user; + + if (!ui.isOpen && ui.notificationInitialCallbackMade) { + message = user.username + " is available in " + + user.location.root.name + ". Open PEOPLE to join them."; + ui.notificationDisplayBanner(message); + } + }); + var key; + for (key in storedOnlineUsers) { + if (storedOnlineUsers[key].pingPong !== pingPong) { + delete storedOnlineUsers[key]; } } - // If there's new data, the light should turn on. - // If the light is already on and you have connections online, the light should stay on. - // In all other cases, the light should turn off or stay off. - shouldShowDot = newlyOnlineConnectionsArray.length > 0 || (storedOnlineUsersArray.length > 0 && shouldShowDot); + shouldShowDot = newOnlineUsers > 0 || (Object.keys(storedOnlineUsers).length > 0 && shouldShowDot); // // END logic for handling online/offline user changes // @@ -874,19 +868,10 @@ function notificationPollCallback(connectionsArray) { shouldShowDot: shouldShowDot }); - if (newlyOnlineConnectionsArray.length > 0) { - var message; - if (!ui.notificationInitialCallbackMade) { - message = newlyOnlineConnectionsArray.length + " of your connections " + - (newlyOnlineConnectionsArray.length === 1 ? "is" : "are") + " online. Open PEOPLE to join them!"; - ui.notificationDisplayBanner(message); - } else { - for (i = 0; i < newlyOnlineConnectionsArray.length; i++) { - message = newlyOnlineConnectionsArray[i].username + " is available in " + - newlyOnlineConnectionsArray[i].location.root.name + ". Open PEOPLE to join them!"; - ui.notificationDisplayBanner(message); - } - } + if (newOnlineUsers > 0 && !ui.notificationInitialCallbackMade) { + message = newOnlineUsers + " of your connections " + + (newOnlineUsers === 1 ? "is" : "are") + " available online. Open PEOPLE to join them."; + ui.notificationDisplayBanner(message); } } } @@ -904,7 +889,7 @@ function startup() { onOpened: palOpened, onClosed: off, onMessage: fromQml, - notificationPollEndpoint: "/api/v1/users?filter=connections&per_page=10", + notificationPollEndpoint: "/api/v1/users?filter=connections&status=online&per_page=10", notificationPollTimeoutMs: 60000, notificationDataProcessPage: notificationDataProcessPage, notificationPollCallback: notificationPollCallback, diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 804f838d04..6d8ba3a927 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -15,118 +15,121 @@ // (function () { // BEGIN LOCAL_SCOPE -var request = Script.require('request').request; var AppUi = Script.require('appUi'); -var DEBUG = false; -function debug() { - if (!DEBUG) { - return; - } - print('tablet-goto.js:', [].map.call(arguments, JSON.stringify)); -} - -var stories = {}, pingPong = false; -function expire(id) { - var options = { - uri: Account.metaverseServerURL + '/api/v1/user_stories/' + id, - method: 'PUT', - json: true, - body: {expire: "true"} - }; - request(options, function (error, response) { - debug('expired story', options, 'error:', error, 'response:', response); - if (error || (response.status !== 'success')) { - print("ERROR expiring story: ", error || response.status); - } - }); -} -var PER_PAGE_DEBUG = 10; -var PER_PAGE_NORMAL = 100; -function pollForAnnouncements() { - // We could bail now if !Account.isLoggedIn(), but what if we someday have system-wide announcments? - var actions = 'announcement'; - var count = DEBUG ? PER_PAGE_DEBUG : PER_PAGE_NORMAL; - var options = [ - 'now=' + new Date().toISOString(), - 'include_actions=' + actions, - 'restriction=' + (Account.isLoggedIn() ? 'open,hifi' : 'open'), - 'require_online=true', - 'protocol=' + encodeURIComponent(Window.protocolSignature()), - 'per_page=' + count - ]; - var url = Account.metaverseServerURL + '/api/v1/user_stories?' + options.join('&'); - request({ - uri: url - }, function (error, data) { - debug(url, error, data); - if (error || (data.status !== 'success')) { - print("Error: unable to get", url, error || data.status); - return; - } - var didNotify = false, key; - pingPong = !pingPong; - data.user_stories.forEach(function (story) { - var stored = stories[story.id], storedOrNew = stored || story; - debug('story exists:', !!stored, storedOrNew); - if ((storedOrNew.username === Account.username) && (storedOrNew.place_name !== location.placename)) { - if (storedOrNew.audience === 'for_connections') { // Only expire if we haven't already done so. - expire(story.id); - } - return; // before marking - } - storedOrNew.pingPong = pingPong; - if (stored) { // already seen - return; - } - stories[story.id] = story; - var message = story.username + " " + story.action_string + " in " + - story.place_name + ". Open GOTO to join them."; - Window.displayAnnouncement(message); - didNotify = true; - }); - for (key in stories) { // Any story we were tracking that was not marked, has expired. - if (stories[key].pingPong !== pingPong) { - debug('removing story', key); - delete stories[key]; - } - } - if (didNotify) { - ui.messagesWaiting(true); - if (HMD.isHandControllerAvailable()) { - var STRENGTH = 1.0, DURATION_MS = 60, HAND = 2; // both hands - Controller.triggerHapticPulse(STRENGTH, DURATION_MS, HAND); - } - } else if (!Object.keys(stories).length) { // If there's nothing being tracked, then any messageWaiting has expired. - ui.messagesWaiting(false); - } - }); -} -var MS_PER_SEC = 1000; -var DEBUG_POLL_TIME_SEC = 10; -var NORMAL_POLL_TIME_SEC = 60; -var ANNOUNCEMENTS_POLL_TIME_MS = (DEBUG ? DEBUG_POLL_TIME_SEC : NORMAL_POLL_TIME_SEC) * MS_PER_SEC; -var pollTimer = Script.setInterval(pollForAnnouncements, ANNOUNCEMENTS_POLL_TIME_MS); function gotoOpened() { - ui.messagesWaiting(false); + shouldShowDot = false; + ui.messagesWaiting(shouldShowDot); +} + +function notificationDataProcessPage(data) { + return data.user_stories; +} + +var shouldShowDot = false; +var pingPong = false; +var storedAnnouncements = {}; +var storedFeaturedStories = {}; +var message; +function notificationPollCallback(userStoriesArray) { + // + // START logic for keeping track of new info + // + pingPong = !pingPong; + var totalNewStories = 0; + var shouldNotifyIndividually = !ui.isOpen && ui.notificationInitialCallbackMade; + userStoriesArray.forEach(function (story) { + if (story.audience !== "for_connections" && + story.audience !== "for_feed") { + return; + } + + var stored = storedAnnouncements[story.id] || storedFeaturedStories[story.id]; + var storedOrNew = stored || story; + storedOrNew.pingPong = pingPong; + if (stored) { + return; + } + + totalNewStories++; + + if (story.audience === "for_connections") { + storedAnnouncements[story.id] = story; + + if (shouldNotifyIndividually) { + message = story.username + " says something is happening in " + + story.place_name + ". Open GOTO to join them."; + ui.notificationDisplayBanner(message); + } + } else if (story.audience === "for_feed") { + storedFeaturedStories[story.id] = story; + + if (shouldNotifyIndividually) { + message = story.username + " invites you to an event in " + + story.place_name + ". Open GOTO to join them."; + ui.notificationDisplayBanner(message); + } + } + }); + var key; + for (key in storedAnnouncements) { + if (storedAnnouncements[key].pingPong !== pingPong) { + delete storedAnnouncements[key]; + } + } + for (key in storedFeaturedStories) { + if (storedFeaturedStories[key].pingPong !== pingPong) { + delete storedFeaturedStories[key]; + } + } + // + // END logic for keeping track of new info + // + + var totalStories = Object.keys(storedAnnouncements).length + + Object.keys(storedFeaturedStories).length; + shouldShowDot = totalNewStories > 0 || (totalStories > 0 && shouldShowDot); + ui.messagesWaiting(shouldShowDot && !ui.isOpen); + + if (totalStories > 0 && !ui.isOpen && !ui.notificationInitialCallbackMade) { + message = "There " + (totalStories === 1 ? "is " : "are ") + totalStories + " event" + + (totalStories === 1 ? "" : "s") + " to know about. " + + "Open GOTO to see " + (totalStories === 1 ? "it" : "them") + "."; + ui.notificationDisplayBanner(message); + } +} + +function isReturnedDataEmpty(data) { + var storiesArray = data.user_stories; + return storiesArray.length === 0; } var ui; var GOTO_QML_SOURCE = "hifi/tablet/TabletAddressDialog.qml"; var BUTTON_NAME = "GOTO"; function startup() { + var options = [ + 'include_actions=announcement', + 'restriction=open,hifi', + 'require_online=true', + 'protocol=' + encodeURIComponent(Window.protocolSignature()), + 'per_page=10' + ]; + var endpoint = '/api/v1/user_stories?' + options.join('&'); + ui = new AppUi({ buttonName: BUTTON_NAME, sortOrder: 8, onOpened: gotoOpened, - home: GOTO_QML_SOURCE + home: GOTO_QML_SOURCE, + notificationPollEndpoint: endpoint, + notificationPollTimeoutMs: 60000, + notificationDataProcessPage: notificationDataProcessPage, + notificationPollCallback: notificationPollCallback, + notificationPollStopPaginatingConditionMet: isReturnedDataEmpty, + notificationPollCaresAboutSince: false }); } -function shutdown() { - Script.clearInterval(pollTimer); -} - startup(); -Script.scriptEnding.connect(shutdown); }()); // END LOCAL_SCOPE diff --git a/server-console/CMakeLists.txt b/server-console/CMakeLists.txt index df51409151..8510441b43 100644 --- a/server-console/CMakeLists.txt +++ b/server-console/CMakeLists.txt @@ -36,6 +36,7 @@ if (APPLE) PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_CONSOLE_FOLDER}" DESTINATION ${CONSOLE_INSTALL_DIR} COMPONENT ${SERVER_COMPONENT} + COMPONENT ${CLIENT_COMPONENT} ) install( PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/${PACKAGED_CONSOLE_FOLDER}" @@ -49,6 +50,7 @@ elseif (WIN32) DIRECTORY "${CONSOLE_DESTINATION}/" DESTINATION ${CONSOLE_INSTALL_DIR} COMPONENT ${SERVER_COMPONENT} + COMPONENT ${CLIENT_COMPONENT} ) install( DIRECTORY "${CONSOLE_DESTINATION}/" diff --git a/server-console/src/modules/hf-notifications.js b/server-console/src/modules/hf-notifications.js index ab4bd44486..2953075523 100644 --- a/server-console/src/modules/hf-notifications.js +++ b/server-console/src/modules/hf-notifications.js @@ -16,6 +16,7 @@ const WALLET_NOTIFICATION_POLL_TIME_MS = 600 * 1000; const MARKETPLACE_NOTIFICATION_POLL_TIME_MS = 600 * 1000; const OSX_CLICK_DELAY_TIMEOUT = 500; + const METAVERSE_SERVER_URL= process.env.HIFI_METAVERSE_URL ? process.env.HIFI_METAVERSE_URL : 'https://metaverse.highfidelity.com' const STORIES_URL= '/api/v1/user_stories'; const USERS_URL= '/api/v1/users';