diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index 288652715a..268aba62d6 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -33,7 +33,7 @@ uint64_t AvatarMixerClientData::getLastOtherAvatarEncodeTime(QUuid otherAvatar) return 0; } -void AvatarMixerClientData::setLastOtherAvatarEncodeTime(const QUuid& otherAvatar, const uint64_t& time) { +void AvatarMixerClientData::setLastOtherAvatarEncodeTime(const QUuid& otherAvatar, uint64_t time) { std::unordered_map::iterator itr = _lastOtherAvatarEncodeTime.find(otherAvatar); if (itr != _lastOtherAvatarEncodeTime.end()) { itr->second = time; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 7a7210a0e8..3c2e660cbc 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -113,7 +113,7 @@ public: ViewFrustum getViewFrustum() const { return _currentViewFrustum; } uint64_t getLastOtherAvatarEncodeTime(QUuid otherAvatar) const; - void setLastOtherAvatarEncodeTime(const QUuid& otherAvatar, const uint64_t& time); + void setLastOtherAvatarEncodeTime(const QUuid& otherAvatar, uint64_t time); QVector& getLastOtherAvatarSentJoints(QUuid otherAvatar) { auto& lastOtherAvatarSentJoints = _lastOtherAvatarSentJoints[otherAvatar]; diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index 1255c18e71..d242b393bf 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -105,8 +105,6 @@ EntityScriptServer::~EntityScriptServer() { static const QString ENTITY_SCRIPT_SERVER_LOGGING_NAME = "entity-script-server"; void EntityScriptServer::handleReloadEntityServerScriptPacket(QSharedPointer message, SharedNodePointer senderNode) { - // These are temporary checks until we can ensure that nodes eventually disconnect if the Domain Server stops telling them - // about each other. if (senderNode->getCanRez() || senderNode->getCanRezTmp() || senderNode->getCanRezCertified() || senderNode->getCanRezTmpCertified()) { auto entityID = QUuid::fromRfc4122(message->read(NUM_BYTES_RFC4122_UUID)); @@ -119,8 +117,6 @@ void EntityScriptServer::handleReloadEntityServerScriptPacket(QSharedPointer message, SharedNodePointer senderNode) { - // These are temporary checks until we can ensure that nodes eventually disconnect if the Domain Server stops telling them - // about each other. if (senderNode->getCanRez() || senderNode->getCanRezTmp() || senderNode->getCanRezCertified() || senderNode->getCanRezTmpCertified()) { MessageID messageID; message->readPrimitive(&messageID); @@ -190,15 +186,14 @@ void EntityScriptServer::updateEntityPPS() { } void EntityScriptServer::handleEntityServerScriptLogPacket(QSharedPointer message, SharedNodePointer senderNode) { - // These are temporary checks until we can ensure that nodes eventually disconnect if the Domain Server stops telling them - // about each other. + bool canRezAny = senderNode->getCanRez() || senderNode->getCanRezTmp() || senderNode->getCanRezCertified() || senderNode->getCanRezTmpCertified(); bool enable = false; message->readPrimitive(&enable); auto senderUUID = senderNode->getUUID(); auto it = _logListeners.find(senderUUID); - if (enable && senderNode->getCanRez()) { + if (enable && canRezAny) { if (it == std::end(_logListeners)) { _logListeners.insert(senderUUID); qCInfo(entity_script_server) << "Node" << senderUUID << "subscribed to log stream"; diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 197ac7eac2..dbf2907cc0 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -1042,41 +1042,7 @@ void DomainServer::processListRequestPacket(QSharedPointer mess bool DomainServer::isInInterestSet(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB) { auto nodeAData = static_cast(nodeA->getLinkedData()); - auto nodeBData = static_cast(nodeB->getLinkedData()); - - // if we have no linked data for node A then B can't possibly be in the interest set - if (!nodeAData) { - return false; - } - - // first check if the general interest set A contains the type for B - if (nodeAData->getNodeInterestSet().contains(nodeB->getType())) { - // given that there is a match in the general interest set, do any special checks - - // (1/19/17) Agents only need to connect to Entity Script Servers to perform administrative tasks - // related to entity server scripts. Only agents with rez permissions should be doing that, so - // if the agent does not have those permissions, we do not want them and the server to incur the - // overhead of connecting to one another. Additionally we exclude agents that do not care about the - // Entity Script Server and won't attempt to connect to it. - - bool isAgentWithoutRights = nodeA->getType() == NodeType::Agent - && nodeB->getType() == NodeType::EntityScriptServer - && !nodeA->getCanRez() && !nodeA->getCanRezTmp() - && !nodeA->getCanRezCertified() && !nodeA->getCanRezTmpCertified(); - - if (isAgentWithoutRights) { - return false; - } - - bool isScriptServerForIneffectiveAgent = - (nodeA->getType() == NodeType::EntityScriptServer && nodeB->getType() == NodeType::Agent) - && ((nodeBData && !nodeBData->getNodeInterestSet().contains(NodeType::EntityScriptServer)) - || (!nodeB->getCanRez() && !nodeB->getCanRezTmp() && !nodeB->getCanRezCertified() && !nodeB->getCanRezTmpCertified())); - - return !isScriptServerForIneffectiveAgent; - } else { - return false; - } + return nodeAData && nodeAData->getNodeInterestSet().contains(nodeB->getType()); } unsigned int DomainServer::countConnectedUsers() { @@ -3476,4 +3442,4 @@ void DomainServer::handleOctreeFileReplacementRequest(QSharedPointergetCanReplaceContent()) { handleOctreeFileReplacement(message->readAll()); } -} \ No newline at end of file +} diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controls-uit/TextField.qml index f94541897b..6743d08275 100644 --- a/interface/resources/qml/controls-uit/TextField.qml +++ b/interface/resources/qml/controls-uit/TextField.qml @@ -163,10 +163,18 @@ TextField { text: textField.label colorScheme: textField.colorScheme anchors.left: parent.left - anchors.right: parent.right + + Binding on anchors.right { + when: parent.right + value: parent.right + } + Binding on wrapMode { + when: parent.right + value: Text.WordWrap + } + anchors.bottom: parent.top anchors.bottomMargin: 3 - wrapMode: Text.WordWrap visible: label != "" } } diff --git a/interface/resources/qml/hifi/AssetServer.qml b/interface/resources/qml/hifi/AssetServer.qml index 1ff954feff..526ea6aad0 100644 --- a/interface/resources/qml/hifi/AssetServer.qml +++ b/interface/resources/qml/hifi/AssetServer.qml @@ -39,6 +39,7 @@ Windows.ScrollingWindow { property var assetMappingsModel: Assets.mappingModel; property var currentDirectory; property var selectedItemCount: treeView.selection.selectedIndexes.length; + property int updatesCount: 0; // this is used for notifying model-dependent bindings about model updates Settings { category: "Overlay.AssetServer" @@ -51,6 +52,9 @@ Windows.ScrollingWindow { ApplicationInterface.uploadRequest.connect(uploadClicked); assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError); assetMappingsModel.autoRefreshEnabled = true; + assetMappingsModel.updated.connect(function() { + ++updatesCount; + }); reload(); } @@ -852,12 +856,17 @@ Windows.ScrollingWindow { checked = Qt.binding(isChecked); } + function getStatus() { + // kind of hack for ensuring getStatus() will be re-evaluated on updatesCount changes + return updatesCount, assetProxyModel.data(treeView.selection.currentIndex, 0x105); + } + function isEnabled() { if (!treeView.selection.hasSelection) { return false; } - var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + var status = getStatus(); if (status === "--") { return false; } @@ -882,9 +891,9 @@ Windows.ScrollingWindow { return false; } - var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); - return isEnabled() && status !== "Not Baked"; - } + var status = getStatus(); + return isEnabled() && status !== "Not Baked"; + } } Item { diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml index 91d2ab9f7f..e74b0fa9dc 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml @@ -24,6 +24,18 @@ Item { HifiConstants { id: hifi; } id: root; + + // This will cause a bug -- if you bring up passphrase selection in HUD mode while + // in HMD while having HMD preview enabled, then move, then finish passphrase selection, + // HMD preview will stay off. + // TODO: Fix this unlikely bug + onVisibleChanged: { + if (visible) { + sendSignalToWallet({method: 'disableHmdPreview'}); + } else { + sendSignalToWallet({method: 'maybeEnableHmdPreview'}); + } + } // Username Text RalewayRegular { diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml index 1fa9054d69..7c38406697 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml @@ -68,10 +68,6 @@ Item { propagateComposedEvents: false; hoverEnabled: true; } - - Component.onDestruction: { - sendSignalToParent({method: 'maybeEnableHmdPreview'}); - } // This will cause a bug -- if you bring up passphrase selection in HUD mode while // in HMD while having HMD preview enabled, then move, then finish passphrase selection, diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index 50bea2a3cf..5fd6b01d18 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -61,9 +61,6 @@ Item { if (root.shouldImmediatelyFocus) { focusFirstTextField(); } - sendMessageToLightbox({method: 'disableHmdPreview'}); - } else { - sendMessageToLightbox({method: 'maybeEnableHmdPreview'}); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml index 86a4220b74..0d7fe9ed18 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageChange.qml @@ -44,6 +44,17 @@ Item { } } + // This will cause a bug -- if you bring up security image selection in HUD mode while + // in HMD while having HMD preview enabled, then move, then finish passphrase selection, + // HMD preview will stay off. + // TODO: Fix this unlikely bug + onVisibleChanged: { + if (visible) { + sendSignalToWallet({method: 'disableHmdPreview'}); + } else { + sendSignalToWallet({method: 'maybeEnableHmdPreview'}); + } + } // Security Image Item { diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml index 56b78c5865..85fc0db3be 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml @@ -25,18 +25,6 @@ Item { id: root; property alias currentIndex: securityImageGrid.currentIndex; - - // This will cause a bug -- if you bring up security image selection in HUD mode while - // in HMD while having HMD preview enabled, then move, then finish passphrase selection, - // HMD preview will stay off. - // TODO: Fix this unlikely bug - onVisibleChanged: { - if (visible) { - sendSignalToWallet({method: 'disableHmdPreview'}); - } else { - sendSignalToWallet({method: 'maybeEnableHmdPreview'}); - } - } SecurityImageModel { id: gridModel; diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index b8b34dc395..b2e7daa066 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -237,7 +237,7 @@ Rectangle { } else { sendToScript(msg); } - } else if (msg.method === 'maybeEnableHmdPreview') { + } else { sendToScript(msg); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index bad592067c..6956252ee0 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -76,6 +76,12 @@ Item { var currentStepNumber = root.activeView.substring(5); UserActivityLogger.commerceWalletSetupProgress(timestamp, root.setupAttemptID, Math.round((timestamp - root.startingTimestamp)/1000), currentStepNumber, root.setupStepNames[currentStepNumber - 1]); + + if (root.activeView === "step_2" || root.activeView === "step_3") { + sendSignalToWallet({method: 'disableHmdPreview'}); + } else { + sendSignalToWallet({method: 'maybeEnableHmdPreview'}); + } } // @@ -441,7 +447,7 @@ Item { } Item { id: choosePassphraseContainer; - visible: root.hasShownSecurityImageTip && root.activeView === "step_3"; + visible: root.activeView === "step_3"; // Anchors anchors.top: titleBarContainer.bottom; anchors.topMargin: 30; @@ -451,10 +457,7 @@ Item { onVisibleChanged: { if (visible) { - sendSignalToWallet({method: 'disableHmdPreview'}); Commerce.getWalletAuthenticatedStatus(); - } else { - sendSignalToWallet({method: 'maybeEnableHmdPreview'}); } } diff --git a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml index 138eb5c6f8..6bf8f8a5d5 100644 --- a/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml +++ b/interface/resources/qml/hifi/dialogs/TabletAssetServer.qml @@ -40,6 +40,7 @@ Rectangle { property var assetMappingsModel: Assets.mappingModel; property var currentDirectory; property var selectedItemCount: treeView.selection.selectedIndexes.length; + property int updatesCount: 0; // this is used for notifying model-dependent bindings about model updates Settings { category: "Overlay.AssetServer" @@ -51,6 +52,9 @@ Rectangle { ApplicationInterface.uploadRequest.connect(uploadClicked); assetMappingsModel.errorGettingMappings.connect(handleGetMappingsError); assetMappingsModel.autoRefreshEnabled = true; + assetMappingsModel.updated.connect(function() { + ++updatesCount; + }); reload(); } @@ -850,12 +854,17 @@ Rectangle { checked = Qt.binding(isChecked); } + function getStatus() { + // kind of hack for ensuring getStatus() will be re-evaluated on updatesCount changes + return updatesCount, assetProxyModel.data(treeView.selection.currentIndex, 0x105); + } + function isEnabled() { if (!treeView.selection.hasSelection) { return false; } - var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + var status = getStatus(); if (status === "--") { return false; } @@ -880,7 +889,7 @@ Rectangle { return false; } - var status = assetProxyModel.data(treeView.selection.currentIndex, 0x105); + var status = getStatus(); return isEnabled() && status !== "Not Baked"; } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 6209b828fc..4b64e8a0c5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4679,7 +4679,7 @@ void Application::init() { auto entityScriptingInterface = DependencyManager::get(); // connect the _entityCollisionSystem to our EntityTreeRenderer since that's what handles running entity scripts - connect(_entitySimulation.get(), &EntitySimulation::entityCollisionWithEntity, + connect(_entitySimulation.get(), &PhysicalEntitySimulation::entityCollisionWithEntity, getEntities().data(), &EntityTreeRenderer::entityCollisionWithEntity); // connect the _entities (EntityTreeRenderer) to our script engine's EntityScriptingInterface for firing @@ -5274,11 +5274,13 @@ void Application::update(float deltaTime) { { PROFILE_RANGE(simulation_physics, "PreStep"); PerformanceTimer perfTimer("preStep)"); - static VectorOfMotionStates motionStates; - _entitySimulation->getObjectsToRemoveFromPhysics(motionStates); - _physicsEngine->removeObjects(motionStates); - _entitySimulation->deleteObjectsRemovedFromPhysics(); + { + const VectorOfMotionStates& motionStates = _entitySimulation->getObjectsToRemoveFromPhysics(); + _physicsEngine->removeObjects(motionStates); + _entitySimulation->deleteObjectsRemovedFromPhysics(); + } + VectorOfMotionStates motionStates; getEntities()->getTree()->withReadLock([&] { _entitySimulation->getObjectsToAddToPhysics(motionStates); _physicsEngine->addObjects(motionStates); @@ -5292,7 +5294,7 @@ void Application::update(float deltaTime) { _entitySimulation->applyDynamicChanges(); - avatarManager->getObjectsToRemoveFromPhysics(motionStates); + avatarManager->getObjectsToRemoveFromPhysics(motionStates); _physicsEngine->removeObjects(motionStates); avatarManager->getObjectsToAddToPhysics(motionStates); _physicsEngine->addObjects(motionStates); @@ -6244,8 +6246,9 @@ bool Application::canAcceptURL(const QString& urlString) const { bool Application::acceptURL(const QString& urlString, bool defaultUpload) { QUrl url(urlString); - if (isDomainURL(url)) { - // this is a URL for a domain, either hifi:// or serverless - have the AddressManager handle it + + if (url.scheme() == URL_SCHEME_HIFI) { + // this is a hifi URL - have the AddressManager handle it QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", Qt::AutoConnection, Q_ARG(const QString&, urlString)); return true; diff --git a/interface/src/LODManager.cpp b/interface/src/LODManager.cpp index 73408377c0..d7d73e962a 100644 --- a/interface/src/LODManager.cpp +++ b/interface/src/LODManager.cpp @@ -86,10 +86,6 @@ void LODManager::autoAdjustLOD(float realTimeDelta) { if (_octreeSizeScale < ADJUST_LOD_MIN_SIZE_SCALE) { _octreeSizeScale = ADJUST_LOD_MIN_SIZE_SCALE; } - qCDebug(interfaceapp) << "adjusting LOD DOWN" - << "fps =" << currentFPS - << "targetFPS =" << getLODDecreaseFPS() - << "octreeSizeScale =" << _octreeSizeScale; emit LODDecreased(); // Assuming the LOD adjustment will work: we optimistically reset _avgRenderTime // to provide an FPS just above the decrease threshold. It will drift close to its @@ -111,10 +107,6 @@ void LODManager::autoAdjustLOD(float realTimeDelta) { if (_octreeSizeScale > ADJUST_LOD_MAX_SIZE_SCALE) { _octreeSizeScale = ADJUST_LOD_MAX_SIZE_SCALE; } - qCDebug(interfaceapp) << "adjusting LOD UP" - << "fps =" << currentFPS - << "targetFPS =" << getLODDecreaseFPS() - << "octreeSizeScale =" << _octreeSizeScale; emit LODIncreased(); // Assuming the LOD adjustment will work: we optimistically reset _avgRenderTime // to provide an FPS just below the increase threshold. It will drift close to its diff --git a/interface/src/scripting/SelectionScriptingInterface.cpp b/interface/src/scripting/SelectionScriptingInterface.cpp index 233e61c8ae..f8a62e848c 100644 --- a/interface/src/scripting/SelectionScriptingInterface.cpp +++ b/interface/src/scripting/SelectionScriptingInterface.cpp @@ -110,7 +110,6 @@ bool SelectionScriptingInterface::enableListHighlight(const QString& listName, c } if (!(*highlightStyle).isBoundToList()) { - setupHandler(listName); (*highlightStyle).setBoundToList(true); } @@ -172,6 +171,18 @@ render::HighlightStyle SelectionScriptingInterface::getHighlightStyle(const QStr } } +bool SelectionScriptingInterface::enableListToScene(const QString& listName) { + setupHandler(listName); + + return true; +} + +bool SelectionScriptingInterface::disableListToScene(const QString& listName) { + removeHandler(listName); + + return true; +} + template bool SelectionScriptingInterface::addToGameplayObjects(const QString& listName, T idToAdd) { { QWriteLocker lock(&_selectionListsLock); @@ -303,6 +314,15 @@ void SelectionScriptingInterface::setupHandler(const QString& selectionName) { (*handler)->initialize(selectionName); } +void SelectionScriptingInterface::removeHandler(const QString& selectionName) { + QWriteLocker lock(&_selectionHandlersLock); + auto handler = _handlerMap.find(selectionName); + if (handler != _handlerMap.end()) { + delete handler.value(); + _handlerMap.erase(handler); + } +} + void SelectionScriptingInterface::onSelectedItemsListChanged(const QString& listName) { { QWriteLocker lock(&_selectionHandlersLock); diff --git a/interface/src/scripting/SelectionScriptingInterface.h b/interface/src/scripting/SelectionScriptingInterface.h index 8295375870..3046ac371e 100644 --- a/interface/src/scripting/SelectionScriptingInterface.h +++ b/interface/src/scripting/SelectionScriptingInterface.h @@ -160,13 +160,14 @@ public: * If the Selection doesn't exist, it will be created. * All objects in the list will be displayed with the highlight effect as specified from the highlightStyle. * The function can be called several times with different values in the style to modify it. - * + * * @function Selection.enableListHighlight * @param listName {string} name of the selection * @param highlightStyle {jsObject} highlight style fields (see Selection.getListHighlightStyle for a detailed description of the highlightStyle). * @returns {bool} true if the selection was successfully enabled for highlight. */ Q_INVOKABLE bool enableListHighlight(const QString& listName, const QVariantMap& highlightStyle); + /**jsdoc * Disable highlighting for the named selection. * If the Selection doesn't exist or wasn't enabled for highliting then nothing happens simply returning false. @@ -175,7 +176,27 @@ public: * @param listName {string} name of the selection * @returns {bool} true if the selection was successfully disabled for highlight, false otherwise. */ - Q_INVOKABLE bool disableListHighlight(const QString& listName); + Q_INVOKABLE bool disableListHighlight(const QString& listName); + /**jsdoc + * Enable scene selection for the named selection. + * If the Selection doesn't exist, it will be created. + * All objects in the list will be sent to a scene selection. + * + * @function Selection.enableListToScene + * @param listName {string} name of the selection + * @returns {bool} true if the selection was successfully enabled on the scene. + */ + Q_INVOKABLE bool enableListToScene(const QString& listName); + /**jsdoc + * Disable scene selection for the named selection. + * If the Selection doesn't exist or wasn't enabled on the scene then nothing happens simply returning false. + * + * @function Selection.disableListToScene + * @param listName {string} name of the selection + * @returns {bool} true if the selection was successfully disabled on the scene, false otherwise. + */ + Q_INVOKABLE bool disableListToScene(const QString& listName); + /**jsdoc * Query the highlight style values for the named selection. * If the Selection doesn't exist or hasn't been highlight enabled yet, it will return an empty object. @@ -223,7 +244,7 @@ private: template bool removeFromGameplayObjects(const QString& listName, T idToRemove); void setupHandler(const QString& selectionName); - + void removeHandler(const QString& selectionName); }; diff --git a/interface/src/ui/SnapshotUploader.cpp b/interface/src/ui/SnapshotUploader.cpp index 3408cb8512..37505db629 100644 --- a/interface/src/ui/SnapshotUploader.cpp +++ b/interface/src/ui/SnapshotUploader.cpp @@ -65,7 +65,7 @@ void SnapshotUploader::uploadSuccess(QNetworkReply& reply) { } else { emit DependencyManager::get()->snapshotShared(true, contents); - delete this; + this->deleteLater(); } } @@ -75,23 +75,27 @@ void SnapshotUploader::uploadFailure(QNetworkReply& reply) { if (replyString.size() == 0) { replyString = reply.errorString(); } + replyString = replyString.left(1000); // Only print first 1000 characters of error + qDebug() << "Snapshot upload reply error (truncated):" << replyString; emit DependencyManager::get()->snapshotShared(true, replyString); // maybe someday include _inWorldLocation, _filename? - delete this; + this->deleteLater(); } void SnapshotUploader::createStorySuccess(QNetworkReply& reply) { QString replyString = reply.readAll(); emit DependencyManager::get()->snapshotShared(false, replyString); - delete this; + this->deleteLater(); } void SnapshotUploader::createStoryFailure(QNetworkReply& reply) { QString replyString = reply.readAll(); - qDebug() << "Error " << reply.errorString() << " uploading snapshot " << _pathname << " from " << _inWorldLocation; + qDebug() << "Error " << reply.errorString() << " uploading snapshot story " << _pathname << " from " << _inWorldLocation; if (replyString.size() == 0) { replyString = reply.errorString(); } + replyString = replyString.left(1000); // Only print first 1000 characters of error + qDebug() << "Snapshot story upload reply error (truncated):" << replyString; emit DependencyManager::get()->snapshotShared(true, replyString); - delete this; + this->deleteLater(); } diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index dd05e5c6a8..aca186a589 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -50,7 +50,9 @@ ContextOverlayInterface::ContextOverlayInterface() { _entityPropertyFlags += PROP_OWNING_AVATAR_ID; auto entityScriptingInterface = DependencyManager::get().data(); - connect(entityScriptingInterface, &EntityScriptingInterface::mousePressOnEntity, this, &ContextOverlayInterface::createOrDestroyContextOverlay); + connect(entityScriptingInterface, &EntityScriptingInterface::clickDownOnEntity, this, &ContextOverlayInterface::clickDownOnEntity); + connect(entityScriptingInterface, &EntityScriptingInterface::holdingClickOnEntity, this, &ContextOverlayInterface::holdingClickOnEntity); + connect(entityScriptingInterface, &EntityScriptingInterface::mouseReleaseOnEntity, this, &ContextOverlayInterface::mouseReleaseOnEntity); connect(entityScriptingInterface, &EntityScriptingInterface::hoverEnterEntity, this, &ContextOverlayInterface::contextOverlays_hoverEnterEntity); connect(entityScriptingInterface, &EntityScriptingInterface::hoverLeaveEntity, this, &ContextOverlayInterface::contextOverlays_hoverLeaveEntity); connect(_tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system"), &TabletProxy::tabletShownChanged, this, [&]() { @@ -97,6 +99,31 @@ void ContextOverlayInterface::setEnabled(bool enabled) { _enabled = enabled; } +void ContextOverlayInterface::clickDownOnEntity(const EntityItemID& entityItemID, const PointerEvent& event) { + if (_enabled && event.getButton() == PointerEvent::SecondaryButton && contextOverlayFilterPassed(entityItemID)) { + _mouseDownEntity = entityItemID; + _mouseDownEntityTimestamp = usecTimestampNow(); + } else { + if (!_currentEntityWithContextOverlay.isNull()) { + disableEntityHighlight(_currentEntityWithContextOverlay); + destroyContextOverlay(_currentEntityWithContextOverlay, event); + } + } +} + +static const float CONTEXT_OVERLAY_CLICK_HOLD_TIME_MSEC = 400.0f; +void ContextOverlayInterface::holdingClickOnEntity(const EntityItemID& entityItemID, const PointerEvent& event) { + if (!_mouseDownEntity.isNull() && ((usecTimestampNow() - _mouseDownEntityTimestamp) > (CONTEXT_OVERLAY_CLICK_HOLD_TIME_MSEC * USECS_PER_MSEC))) { + _mouseDownEntity = EntityItemID(); + } +} + +void ContextOverlayInterface::mouseReleaseOnEntity(const EntityItemID& entityItemID, const PointerEvent& event) { + if (_enabled && event.getButton() == PointerEvent::SecondaryButton && contextOverlayFilterPassed(entityItemID) && _mouseDownEntity == entityItemID) { + createOrDestroyContextOverlay(entityItemID, event); + } +} + bool ContextOverlayInterface::createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event) { if (_enabled && event.getButton() == PointerEvent::SecondaryButton) { if (contextOverlayFilterPassed(entityItemID)) { diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index fcdf2d5820..b80a3a70fb 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -64,6 +64,10 @@ signals: void contextOverlayClicked(const QUuid& currentEntityWithContextOverlay); public slots: + void clickDownOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); + void holdingClickOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); + void mouseReleaseOnEntity(const EntityItemID& entityItemID, const PointerEvent& event); + bool createOrDestroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); bool destroyContextOverlay(const EntityItemID& entityItemID, const PointerEvent& event); bool destroyContextOverlay(const EntityItemID& entityItemID); @@ -84,6 +88,8 @@ private: }; bool _verboseLogging{ true }; bool _enabled { true }; + EntityItemID _mouseDownEntity{}; + quint64 _mouseDownEntityTimestamp; EntityItemID _currentEntityWithContextOverlay{}; EntityItemID _lastInspectedEntity{}; QString _entityMarketplaceID; diff --git a/libraries/controllers/src/controllers/impl/endpoints/JSEndpoint.cpp b/libraries/controllers/src/controllers/impl/endpoints/JSEndpoint.cpp index 7f0e80cbae..2f20cd82c6 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/JSEndpoint.cpp +++ b/libraries/controllers/src/controllers/impl/endpoints/JSEndpoint.cpp @@ -6,10 +6,43 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -// NOTE: we don't need to include this header unless/until we add additional symbols. -// By removing this header we prevent these warnings on windows: -// -// warning LNK4221: This object file does not define any previously undefined public symbols, -// so it will not be used by any link operation that consumes this library -// -//#include "JSEndpoint.h" \ No newline at end of file +#include "JSEndpoint.h" +#include "../../Logging.h" + +using namespace controller; + +QString formatException(const QJSValue& exception) { + QString note { "UncaughtException" }; + QString result; + + const auto message = exception.toString(); + const auto fileName = exception.property("fileName").toString(); + const auto lineNumber = exception.property("lineNumber").toString(); + const auto stacktrace = exception.property("stack").toString(); + + const QString SCRIPT_EXCEPTION_FORMAT = "[%0] %1 in %2:%3"; + const QString SCRIPT_BACKTRACE_SEP = "\n "; + + result = QString(SCRIPT_EXCEPTION_FORMAT).arg(note, message, fileName, lineNumber); + if (!stacktrace.isEmpty()) { + result += QString("\n[Backtrace]%1%2").arg(SCRIPT_BACKTRACE_SEP).arg(stacktrace); + } + return result; +} + +float JSEndpoint::peek() const { + QJSValue result = _callable.call(); + if (result.isError()) { + qCDebug(controllers).noquote() << formatException(result); + return 0.0f; + } else { + return (float)result.toNumber(); + } +} + +void JSEndpoint::apply(float newValue, const Pointer& source) { + QJSValue result = _callable.call(QJSValueList({ QJSValue(newValue) })); + if (result.isError()) { + qCDebug(controllers).noquote() << formatException(result); + } +} diff --git a/libraries/controllers/src/controllers/impl/endpoints/JSEndpoint.h b/libraries/controllers/src/controllers/impl/endpoints/JSEndpoint.h index 24d5ec93e9..4d179da8e6 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/JSEndpoint.h +++ b/libraries/controllers/src/controllers/impl/endpoints/JSEndpoint.h @@ -24,16 +24,11 @@ public: : Endpoint(Input::INVALID_INPUT), _callable(callable) { } - virtual float peek() const override { - return (float)const_cast(this)->_callable.call().toNumber(); - } - - virtual void apply(float newValue, const Pointer& source) override { - _callable.call(QJSValueList({ QJSValue(newValue) })); - } + virtual float peek() const override; + virtual void apply(float newValue, const Pointer& source) override; private: - QJSValue _callable; + mutable QJSValue _callable; }; } diff --git a/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.cpp b/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.cpp index 3e7fde347e..e2c48d776f 100644 --- a/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.cpp +++ b/libraries/controllers/src/controllers/impl/endpoints/ScriptEndpoint.cpp @@ -7,6 +7,7 @@ // #include "ScriptEndpoint.h" +#include "../../Logging.h" #include @@ -14,6 +15,25 @@ using namespace controller; +QString formatException(const QScriptValue& exception) { + QString note { "UncaughtException" }; + QString result; + + const auto message = exception.toString(); + const auto fileName = exception.property("fileName").toString(); + const auto lineNumber = exception.property("lineNumber").toString(); + const auto stacktrace = exception.property("stack").toString(); + + const QString SCRIPT_EXCEPTION_FORMAT = "[%0] %1 in %2:%3"; + const QString SCRIPT_BACKTRACE_SEP = "\n "; + + result = QString(SCRIPT_EXCEPTION_FORMAT).arg(note, message, fileName, lineNumber); + if (!stacktrace.isEmpty()) { + result += QString("\n[Backtrace]%1%2").arg(SCRIPT_BACKTRACE_SEP).arg(stacktrace); + } + return result; +} + float ScriptEndpoint::peek() const { const_cast(this)->updateValue(); return _lastValueRead; @@ -26,10 +46,11 @@ void ScriptEndpoint::updateValue() { } QScriptValue result = _callable.call(); - - // If the callable ever returns a non-number, we assume it's a pose - // and start reporting ourselves as a pose. - if (result.isNumber()) { + if (result.isError()) { + // print JavaScript exception + qCDebug(controllers).noquote() << formatException(result); + _lastValueRead = 0.0f; + } else if (result.isNumber()) { _lastValueRead = (float)_callable.call().toNumber(); } else { Pose::fromScriptValue(result, _lastPoseRead); @@ -52,8 +73,12 @@ void ScriptEndpoint::internalApply(float value, int sourceID) { Q_ARG(int, sourceID)); return; } - _callable.call(QScriptValue(), + QScriptValue result = _callable.call(QScriptValue(), QScriptValueList({ QScriptValue(value), QScriptValue(sourceID) })); + if (result.isError()) { + // print JavaScript exception + qCDebug(controllers).noquote() << formatException(result); + } } Pose ScriptEndpoint::peekPose() const { @@ -67,6 +92,10 @@ void ScriptEndpoint::updatePose() { return; } QScriptValue result = _callable.call(); + if (result.isError()) { + // print JavaScript exception + qCDebug(controllers).noquote() << formatException(result); + } Pose::fromScriptValue(result, _lastPoseRead); } @@ -85,6 +114,10 @@ void ScriptEndpoint::internalApply(const Pose& newPose, int sourceID) { Q_ARG(int, sourceID)); return; } - _callable.call(QScriptValue(), + QScriptValue result = _callable.call(QScriptValue(), QScriptValueList({ Pose::toScriptValue(_callable.engine(), newPose), QScriptValue(sourceID) })); + if (result.isError()) { + // print JavaScript exception + qCDebug(controllers).noquote() << formatException(result); + } } diff --git a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp index b05854da4e..feb88bed4b 100644 --- a/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableShapeEntityItem.cpp @@ -131,6 +131,8 @@ ItemKey ShapeEntityRenderer::getKey() { withReadLock([&] { if (isTransparent()) { builder.withTransparent(); + } else if (_canCastShadow) { + builder.withShadowCaster(); } }); diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 5d056e17d8..aab8777862 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -1911,7 +1911,7 @@ void EntityItem::computeCollisionGroupAndFinalMask(int16_t& group, int16_t& mask } } -void EntityItem::setSimulationOwner(const QUuid& id, quint8 priority) { +void EntityItem::setSimulationOwner(const QUuid& id, uint8_t priority) { if (wantTerseEditLogging() && (id != _simulationOwner.getID() || priority != _simulationOwner.getPriority())) { qCDebug(entities) << "sim ownership for" << getDebugName() << "is now" << id << priority; } @@ -1942,7 +1942,7 @@ void EntityItem::clearSimulationOwnership() { } -void EntityItem::setPendingOwnershipPriority(quint8 priority, const quint64& timestamp) { +void EntityItem::setPendingOwnershipPriority(uint8_t priority, const quint64& timestamp) { _simulationOwner.setPendingPriority(priority, timestamp); } @@ -2962,13 +2962,6 @@ void EntityItem::retrieveMarketplacePublicKey() { } void EntityItem::preDelete() { - // clear out any left-over actions - EntityTreeElementPointer element = _element; // use local copy of _element for logic below - EntityTreePointer entityTree = element ? element->getTree() : nullptr; - EntitySimulationPointer simulation = entityTree ? entityTree->getSimulation() : nullptr; - if (simulation) { - clearActions(simulation); - } } void EntityItem::addMaterial(graphics::MaterialLayer material, const std::string& parentMaterialName) { diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 0303964e18..ebbeaaa254 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -304,14 +304,14 @@ public: // FIXME not thread safe? const SimulationOwner& getSimulationOwner() const { return _simulationOwner; } - void setSimulationOwner(const QUuid& id, quint8 priority); + void setSimulationOwner(const QUuid& id, uint8_t priority); void setSimulationOwner(const SimulationOwner& owner); - void promoteSimulationPriority(quint8 priority); + void promoteSimulationPriority(uint8_t priority); - quint8 getSimulationPriority() const { return _simulationOwner.getPriority(); } + uint8_t getSimulationPriority() const { return _simulationOwner.getPriority(); } QUuid getSimulatorID() const { return _simulationOwner.getID(); } void clearSimulationOwnership(); - void setPendingOwnershipPriority(quint8 priority, const quint64& timestamp); + void setPendingOwnershipPriority(uint8_t priority, const quint64& timestamp); uint8_t getPendingOwnershipPriority() const { return _simulationOwner.getPendingPriority(); } void rememberHasSimulationOwnershipBid() const; diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 9e5d6ddc79..0568a859ab 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -326,7 +326,7 @@ public: void clearSimulationOwner(); void setSimulationOwner(const QUuid& id, uint8_t priority); void setSimulationOwner(const QByteArray& data); - void promoteSimulationPriority(quint8 priority) { _simulationOwner.promotePriority(priority); } + void promoteSimulationPriority(uint8_t priority) { _simulationOwner.promotePriority(priority); } void setActionDataDirty() { _actionDataChanged = true; } diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 7e15e78624..2e9b386ba5 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -254,17 +254,6 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties propertiesWithSimID = convertPropertiesFromScriptSemantics(propertiesWithSimID, scalesWithParent); propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged()); - auto dimensions = propertiesWithSimID.getDimensions(); - float volume = dimensions.x * dimensions.y * dimensions.z; - auto density = propertiesWithSimID.getDensity(); - auto newVelocity = propertiesWithSimID.getVelocity().length(); - float cost = calculateCost(density * volume, 0, newVelocity); - cost *= costMultiplier; - - if (cost > _currentAvatarEnergy) { - return QUuid(); - } - EntityItemID id = EntityItemID(QUuid::createUuid()); // If we have a local entity tree set, then also update it. @@ -295,9 +284,7 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties // queue the packet if (success) { - emit debitEnergySource(cost); queueEntityMessage(PacketType::EntityAdd, id, propertiesWithSimID); - return id; } else { return QUuid(); @@ -378,27 +365,9 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& EntityItemProperties properties = scriptSideProperties; - auto dimensions = properties.getDimensions(); - float volume = dimensions.x * dimensions.y * dimensions.z; - auto density = properties.getDensity(); - auto newVelocity = properties.getVelocity().length(); - float oldVelocity = { 0.0f }; - EntityItemID entityID(id); if (!_entityTree) { queueEntityMessage(PacketType::EntityEdit, entityID, properties); - - //if there is no local entity entity tree, no existing velocity, use 0. - float cost = calculateCost(density * volume, oldVelocity, newVelocity); - cost *= costMultiplier; - - if (cost > _currentAvatarEnergy) { - return QUuid(); - } else { - //debit the avatar energy and continue - emit debitEnergySource(cost); - } - return id; } // If we have a local entity tree set, then also update it. @@ -420,9 +389,6 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& // All of parentID, parentJointIndex, position, rotation are needed to make sense of any of them. // If any of these changed, pull any missing properties from the entity. - //existing entity, retrieve old velocity for check down below - oldVelocity = entity->getWorldVelocity().length(); - if (!scriptSideProperties.parentIDChanged()) { properties.setParentID(entity->getParentID()); } @@ -442,23 +408,11 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties& properties.setClientOnly(entity->getClientOnly()); properties.setOwningAvatarID(entity->getOwningAvatarID()); properties = convertPropertiesFromScriptSemantics(properties, properties.getScalesWithParent()); - - float cost = calculateCost(density * volume, oldVelocity, newVelocity); - cost *= costMultiplier; - - if (cost > _currentAvatarEnergy) { - updatedEntity = false; - } else { - //debit the avatar energy and continue - updatedEntity = _entityTree->updateEntity(entityID, properties); - if (updatedEntity) { - emit debitEnergySource(cost); - } - } + updatedEntity = _entityTree->updateEntity(entityID, properties); }); // FIXME: We need to figure out a better way to handle this. Allowing these edits to go through potentially - // breaks avatar energy and entities that are parented. + // breaks entities that are parented. // // To handle cases where a script needs to edit an entity with a _known_ entity id but doesn't exist // in the local entity tree, we need to allow those edits to go through to the server. @@ -577,21 +531,6 @@ void EntityScriptingInterface::deleteEntity(QUuid id) { return; } - auto dimensions = entity->getScaledDimensions(); - float volume = dimensions.x * dimensions.y * dimensions.z; - auto density = entity->getDensity(); - auto velocity = entity->getWorldVelocity().length(); - float cost = calculateCost(density * volume, velocity, 0); - cost *= costMultiplier; - - if (cost > _currentAvatarEnergy) { - shouldDelete = false; - return; - } else { - //debit the avatar energy and continue - emit debitEnergySource(cost); - } - if (entity->getLocked()) { shouldDelete = false; } else { @@ -1812,23 +1751,6 @@ void EntityScriptingInterface::emitScriptEvent(const EntityItemID& entityID, con } } -float EntityScriptingInterface::calculateCost(float mass, float oldVelocity, float newVelocity) { - return std::abs(mass * (newVelocity - oldVelocity)); -} - -void EntityScriptingInterface::setCurrentAvatarEnergy(float energy) { - // qCDebug(entities) << "NEW AVATAR ENERGY IN ENTITY SCRIPTING INTERFACE: " << energy; - _currentAvatarEnergy = energy; -} - -float EntityScriptingInterface::getCostMultiplier() { - return costMultiplier; -} - -void EntityScriptingInterface::setCostMultiplier(float value) { - costMultiplier = value; -} - // TODO move this someplace that makes more sense... bool EntityScriptingInterface::AABoxIntersectsCapsule(const glm::vec3& low, const glm::vec3& dimensions, const glm::vec3& start, const glm::vec3& end, float radius) { diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 9613a7a310..4c2a2a47b4 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -94,8 +94,6 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra * Interface has displayed and so knows about. * * @namespace Entities - * @property {number} currentAvatarEnergy - Deprecated - * @property {number} costMultiplier - Deprecated * @property {Uuid} keyboardFocusEntity - Get or set the {@link Entities.EntityType|Web} entity that has keyboard focus. * If no entity has keyboard focus, get returns null; set to null or {@link Uuid|Uuid.NULL} to * clear keyboard focus. @@ -104,8 +102,6 @@ void RayToEntityIntersectionResultFromScriptValue(const QScriptValue& object, Ra class EntityScriptingInterface : public OctreeScriptingInterface, public Dependency { Q_OBJECT - Q_PROPERTY(float currentAvatarEnergy READ getCurrentAvatarEnergy WRITE setCurrentAvatarEnergy) - Q_PROPERTY(float costMultiplier READ getCostMultiplier WRITE setCostMultiplier) Q_PROPERTY(QUuid keyboardFocusEntity READ getKeyboardFocusEntity WRITE setKeyboardFocusEntity) friend EntityPropertyMetadataRequest; @@ -126,7 +122,6 @@ public: void setEntityTree(EntityTreePointer modelTree); EntityTreePointer getEntityTree() { return _entityTree; } void setEntitiesScriptEngine(QSharedPointer engine); - float calculateCost(float mass, float oldVelocity, float newVelocity); void resetActivityTracking(); ActivityTracking getActivityTracking() const { return _activityTracking; } @@ -1834,14 +1829,6 @@ signals: */ void clearingEntities(); - /**jsdoc - * @function Entities.debitEnergySource - * @param {number} value - The amount to debit. - * @returns {Signal} - * @deprecated This function is deprecated and will soon be removed. - */ - void debitEnergySource(float value); - /**jsdoc * Triggered in when a script in a {@link Entities.EntityType|Web} entity's Web page script sends an event over the * script's EventBridge. @@ -1882,14 +1869,8 @@ private: QSharedPointer _entitiesScriptEngine; bool _bidOnSimulationOwnership { false }; - float _currentAvatarEnergy = { FLT_MAX }; - float getCurrentAvatarEnergy() { return _currentAvatarEnergy; } - void setCurrentAvatarEnergy(float energy); ActivityTracking _activityTracking; - float costMultiplier = { 0.01f }; - float getCostMultiplier(); - void setCostMultiplier(float value); }; #endif // hifi_EntityScriptingInterface_h diff --git a/libraries/entities/src/EntitySimulation.cpp b/libraries/entities/src/EntitySimulation.cpp index 36b0d8ab2d..d034ddedbe 100644 --- a/libraries/entities/src/EntitySimulation.cpp +++ b/libraries/entities/src/EntitySimulation.cpp @@ -20,7 +20,7 @@ void EntitySimulation::setEntityTree(EntityTreePointer tree) { if (_entityTree && _entityTree != tree) { _mortalEntities.clear(); - _nextExpiry = quint64(-1); + _nextExpiry = std::numeric_limits::max(); _entitiesToUpdate.clear(); _entitiesToSort.clear(); _simpleKinematicEntities.clear(); @@ -30,7 +30,7 @@ void EntitySimulation::setEntityTree(EntityTreePointer tree) { void EntitySimulation::updateEntities() { QMutexLocker lock(&_mutex); - quint64 now = usecTimestampNow(); + uint64_t now = usecTimestampNow(); // these methods may accumulate entries in _entitiesToBeDeleted expireMortalEntities(now); @@ -40,18 +40,14 @@ void EntitySimulation::updateEntities() { sortEntitiesThatMoved(); } -void EntitySimulation::takeEntitiesToDelete(VectorOfEntities& entitiesToDelete) { +void EntitySimulation::takeDeadEntities(SetOfEntities& entitiesToDelete) { QMutexLocker lock(&_mutex); - for (auto entity : _entitiesToDelete) { - // push this entity onto the external list - entitiesToDelete.push_back(entity); - } - _entitiesToDelete.clear(); + entitiesToDelete.swap(_deadEntities); + _deadEntities.clear(); } void EntitySimulation::removeEntityInternal(EntityItemPointer entity) { - QMutexLocker lock(&_mutex); - // remove from all internal lists except _entitiesToDelete + // remove from all internal lists except _deadEntities _mortalEntities.remove(entity); _entitiesToUpdate.remove(entity); _entitiesToSort.remove(entity); @@ -67,42 +63,23 @@ void EntitySimulation::prepareEntityForDelete(EntityItemPointer entity) { QMutexLocker lock(&_mutex); entity->clearActions(getThisPointer()); removeEntityInternal(entity); - _entitiesToDelete.insert(entity); - } -} - -void EntitySimulation::addEntityInternal(EntityItemPointer entity) { - if (entity->isMovingRelativeToParent() && !entity->getPhysicsInfo()) { - QMutexLocker lock(&_mutex); - _simpleKinematicEntities.insert(entity); - entity->setLastSimulated(usecTimestampNow()); - } -} - -void EntitySimulation::changeEntityInternal(EntityItemPointer entity) { - QMutexLocker lock(&_mutex); - if (entity->isMovingRelativeToParent() && !entity->getPhysicsInfo()) { - int numKinematicEntities = _simpleKinematicEntities.size(); - _simpleKinematicEntities.insert(entity); - if (numKinematicEntities != _simpleKinematicEntities.size()) { - entity->setLastSimulated(usecTimestampNow()); + if (entity->getElement()) { + _deadEntities.insert(entity); } - } else { - _simpleKinematicEntities.remove(entity); } } // protected -void EntitySimulation::expireMortalEntities(const quint64& now) { +void EntitySimulation::expireMortalEntities(uint64_t now) { if (now > _nextExpiry) { PROFILE_RANGE_EX(simulation_physics, "ExpireMortals", 0xffff00ff, (uint64_t)_mortalEntities.size()); // only search for expired entities if we expect to find one - _nextExpiry = quint64(-1); + _nextExpiry = std::numeric_limits::max(); QMutexLocker lock(&_mutex); SetOfEntities::iterator itemItr = _mortalEntities.begin(); while (itemItr != _mortalEntities.end()) { EntityItemPointer entity = *itemItr; - quint64 expiry = entity->getExpiry(); + uint64_t expiry = entity->getExpiry(); if (expiry < now) { itemItr = _mortalEntities.erase(itemItr); entity->die(); @@ -122,7 +99,7 @@ void EntitySimulation::expireMortalEntities(const quint64& now) { } // protected -void EntitySimulation::callUpdateOnEntitiesThatNeedIt(const quint64& now) { +void EntitySimulation::callUpdateOnEntitiesThatNeedIt(uint64_t now) { PerformanceTimer perfTimer("updatingEntities"); QMutexLocker lock(&_mutex); SetOfEntities::iterator itemItr = _entitiesToUpdate.begin(); @@ -176,7 +153,7 @@ void EntitySimulation::addEntity(EntityItemPointer entity) { entity->deserializeActions(); if (entity->isMortal()) { _mortalEntities.insert(entity); - quint64 expiry = entity->getExpiry(); + uint64_t expiry = entity->getExpiry(); if (expiry < _nextExpiry) { _nextExpiry = expiry; } @@ -207,7 +184,6 @@ void EntitySimulation::changeEntity(EntityItemPointer entity) { // Although it is not the responsibility of the EntitySimulation to sort the tree for EXTERNAL changes // it IS responsibile for triggering deletes for entities that leave the bounds of the domain, hence // we must check for that case here, however we rely on the change event to have set DIRTY_POSITION flag. - bool wasRemoved = false; uint32_t dirtyFlags = entity->getDirtyFlags(); if (dirtyFlags & Simulation::DIRTY_POSITION) { AACube domainBounds(glm::vec3((float)-HALF_TREE_SCALE), (float)TREE_SCALE); @@ -217,50 +193,45 @@ void EntitySimulation::changeEntity(EntityItemPointer entity) { qCDebug(entities) << "Entity " << entity->getEntityItemID() << " moved out of domain bounds."; entity->die(); prepareEntityForDelete(entity); - wasRemoved = true; + return; } } - if (!wasRemoved) { - if (dirtyFlags & Simulation::DIRTY_LIFETIME) { - if (entity->isMortal()) { - _mortalEntities.insert(entity); - quint64 expiry = entity->getExpiry(); - if (expiry < _nextExpiry) { - _nextExpiry = expiry; - } - } else { - _mortalEntities.remove(entity); + + if (dirtyFlags & Simulation::DIRTY_LIFETIME) { + if (entity->isMortal()) { + _mortalEntities.insert(entity); + uint64_t expiry = entity->getExpiry(); + if (expiry < _nextExpiry) { + _nextExpiry = expiry; } - entity->clearDirtyFlags(Simulation::DIRTY_LIFETIME); - } - if (entity->needsToCallUpdate()) { - _entitiesToUpdate.insert(entity); } else { - _entitiesToUpdate.remove(entity); + _mortalEntities.remove(entity); } - changeEntityInternal(entity); + entity->clearDirtyFlags(Simulation::DIRTY_LIFETIME); } + if (entity->needsToCallUpdate()) { + _entitiesToUpdate.insert(entity); + } else { + _entitiesToUpdate.remove(entity); + } + changeEntityInternal(entity); } void EntitySimulation::clearEntities() { QMutexLocker lock(&_mutex); _mortalEntities.clear(); - _nextExpiry = quint64(-1); + _nextExpiry = std::numeric_limits::max(); _entitiesToUpdate.clear(); _entitiesToSort.clear(); _simpleKinematicEntities.clear(); clearEntitiesInternal(); - for (auto entity : _allEntities) { - entity->setSimulated(false); - entity->die(); - } _allEntities.clear(); - _entitiesToDelete.clear(); + _deadEntities.clear(); } -void EntitySimulation::moveSimpleKinematics(const quint64& now) { +void EntitySimulation::moveSimpleKinematics(uint64_t now) { PROFILE_RANGE_EX(simulation_physics, "MoveSimples", 0xffff00ff, (uint64_t)_simpleKinematicEntities.size()); SetOfEntities::iterator itemItr = _simpleKinematicEntities.begin(); while (itemItr != _simpleKinematicEntities.end()) { diff --git a/libraries/entities/src/EntitySimulation.h b/libraries/entities/src/EntitySimulation.h index 1c633aa9dc..b19e1c33d3 100644 --- a/libraries/entities/src/EntitySimulation.h +++ b/libraries/entities/src/EntitySimulation.h @@ -12,6 +12,8 @@ #ifndef hifi_EntitySimulation_h #define hifi_EntitySimulation_h +#include + #include #include #include @@ -43,9 +45,8 @@ const int DIRTY_SIMULATION_FLAGS = Simulation::DIRTY_SIMULATOR_ID; class EntitySimulation : public QObject, public std::enable_shared_from_this { -Q_OBJECT public: - EntitySimulation() : _mutex(QMutex::Recursive), _entityTree(NULL), _nextExpiry(quint64(-1)) { } + EntitySimulation() : _mutex(QMutex::Recursive), _entityTree(NULL), _nextExpiry(std::numeric_limits::max()) { } virtual ~EntitySimulation() { setEntityTree(NULL); } inline EntitySimulationPointer getThisPointer() const { @@ -57,8 +58,6 @@ public: void updateEntities(); -// friend class EntityTree; - virtual void addDynamic(EntityDynamicPointer dynamic); virtual void removeDynamic(const QUuid dynamicID); virtual void removeDynamics(QList dynamicIDsToRemove); @@ -74,29 +73,26 @@ public: void clearEntities(); - void moveSimpleKinematics(const quint64& now); + void moveSimpleKinematics(uint64_t now); EntityTreePointer getEntityTree() { return _entityTree; } - virtual void takeEntitiesToDelete(VectorOfEntities& entitiesToDelete); + virtual void takeDeadEntities(SetOfEntities& entitiesToDelete); /// \param entity pointer to EntityItem that needs to be put on the entitiesToDelete list and removed from others. virtual void prepareEntityForDelete(EntityItemPointer entity); -signals: - void entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); - protected: // These pure virtual methods are protected because they are not to be called will-nilly. The base class // calls them in the right places. - virtual void updateEntitiesInternal(const quint64& now) = 0; - virtual void addEntityInternal(EntityItemPointer entity); - virtual void removeEntityInternal(EntityItemPointer entity) = 0; - virtual void changeEntityInternal(EntityItemPointer entity); + virtual void updateEntitiesInternal(uint64_t now) = 0; + virtual void addEntityInternal(EntityItemPointer entity) = 0; + virtual void removeEntityInternal(EntityItemPointer entity); + virtual void changeEntityInternal(EntityItemPointer entity) = 0; virtual void clearEntitiesInternal() = 0; - void expireMortalEntities(const quint64& now); - void callUpdateOnEntitiesThatNeedIt(const quint64& now); + void expireMortalEntities(uint64_t now); + void callUpdateOnEntitiesThatNeedIt(uint64_t now); virtual void sortEntitiesThatMoved(); QMutex _mutex{ QMutex::Recursive }; @@ -108,7 +104,7 @@ protected: QMutex _dynamicsMutex { QMutex::Recursive }; protected: - SetOfEntities _entitiesToDelete; // entities simulation decided needed to be deleted (EntityTree will actually delete) + SetOfEntities _deadEntities; private: void moveSimpleKinematics(); @@ -120,11 +116,10 @@ private: // An entity may be in more than one list. SetOfEntities _allEntities; // tracks all entities added the simulation SetOfEntities _mortalEntities; // entities that have an expiry - quint64 _nextExpiry; + uint64_t _nextExpiry; SetOfEntities _entitiesToUpdate; // entities that need to call EntityItem::update() - }; #endif // hifi_EntitySimulation_h diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 75f024d0b9..7f6a7087cf 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -94,7 +94,6 @@ OctreeElementPointer EntityTree::createNewElement(unsigned char* octalCode) { void EntityTree::eraseAllOctreeElements(bool createNewRoot) { emit clearingEntities(); - // this would be a good place to clean up our entities... if (_simulation) { _simulation->clearEntities(); } @@ -260,7 +259,7 @@ void EntityTree::postAddEntity(EntityItemPointer entity) { return; } } - + // check to see if we need to simulate this entity.. if (_simulation) { _simulation->addEntity(entity); @@ -425,8 +424,8 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti if (!childEntity) { continue; } - EntityTreeElementPointer containingElement = childEntity->getElement(); - if (!containingElement) { + EntityTreeElementPointer childContainingElement = childEntity->getElement(); + if (!childContainingElement) { continue; } @@ -440,7 +439,7 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti addToNeedsParentFixupList(childEntity); } - UpdateEntityOperator theChildOperator(getThisPointer(), containingElement, childEntity, queryCube); + UpdateEntityOperator theChildOperator(getThisPointer(), childContainingElement, childEntity, queryCube); recurseTreeWithOperator(&theChildOperator); foreach (SpatiallyNestablePointer childChild, childEntity->getChildren()) { if (childChild && childChild->getNestableType() == NestableType::Entity) { @@ -453,12 +452,13 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti uint32_t newFlags = entity->getDirtyFlags() & ~preFlags; if (newFlags) { - if (_simulation) { + if (entity->isSimulated()) { + assert((bool)_simulation); if (newFlags & DIRTY_SIMULATION_FLAGS) { _simulation->changeEntity(entity); } } else { - // normally the _simulation clears ALL updateFlags, but since there is none we do it explicitly + // normally the _simulation clears ALL dirtyFlags, but when not possible we do it explicitly entity->clearDirtyFlags(); } } @@ -469,7 +469,7 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti if (entityScriptBefore != entityScriptAfter || reload) { emitEntityScriptChanging(entity->getEntityItemID(), reload); // the entity script has changed } - } + } // TODO: this final containingElement check should eventually be removed (or wrapped in an #ifdef DEBUG). if (!entity->getElement()) { @@ -551,8 +551,6 @@ void EntityTree::setSimulation(EntitySimulationPointer simulation) { assert(simulation->getEntityTree().get() == this); } if (_simulation && _simulation != simulation) { - // It's important to clearEntities() on the simulation since taht will update each - // EntityItem::_simulationState correctly so as to not confuse the next _simulation. _simulation->clearEntities(); } _simulation = simulation; @@ -650,7 +648,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i emit deletingEntityPointer(existingEntity.get()); } - if (theOperator.getEntities().size() > 0) { + if (!theOperator.getEntities().empty()) { recurseTreeWithOperator(&theOperator); processRemovedEntities(theOperator); _isDirty = true; @@ -692,7 +690,7 @@ void EntityTree::processRemovedEntities(const DeleteEntityOperator& theOperator) trackDeletedEntity(theEntity->getEntityItemID()); } - if (_simulation) { + if (theEntity->isSimulated()) { _simulation->prepareEntityForDelete(theEntity); } } @@ -1688,7 +1686,7 @@ void EntityTree::releaseSceneEncodeData(OctreeElementExtraEncodeData* extraEncod } void EntityTree::entityChanged(EntityItemPointer entity) { - if (_simulation) { + if (entity->isSimulated()) { _simulation->changeEntity(entity); } } @@ -1802,13 +1800,13 @@ void EntityTree::update(bool simulate) { _simulation->updateEntities(); { PROFILE_RANGE(simulation_physics, "Deletes"); - VectorOfEntities pendingDeletes; - _simulation->takeEntitiesToDelete(pendingDeletes); - if (pendingDeletes.size() > 0) { + SetOfEntities deadEntities; + _simulation->takeDeadEntities(deadEntities); + if (!deadEntities.empty()) { // translate into list of ID's QSet idsToDelete; - for (auto entity : pendingDeletes) { + for (auto entity : deadEntities) { idsToDelete.insert(entity->getEntityItemID()); } diff --git a/libraries/entities/src/MaterialEntityItem.cpp b/libraries/entities/src/MaterialEntityItem.cpp index 137d6ef396..8b595bf69d 100644 --- a/libraries/entities/src/MaterialEntityItem.cpp +++ b/libraries/entities/src/MaterialEntityItem.cpp @@ -267,8 +267,11 @@ void MaterialEntityItem::setOwningAvatarID(const QUuid& owningAvatarID) { void MaterialEntityItem::removeMaterial() { graphics::MaterialPointer material = getMaterial(); + if (!material) { + return; + } QUuid parentID = getClientOnly() ? getOwningAvatarID() : getParentID(); - if (!material || parentID.isNull()) { + if (parentID.isNull()) { return; } @@ -336,4 +339,4 @@ void MaterialEntityItem::update(const quint64& now) { } EntityItem::update(now); -} \ No newline at end of file +} diff --git a/libraries/entities/src/SimpleEntitySimulation.cpp b/libraries/entities/src/SimpleEntitySimulation.cpp index 3203c2968c..a2aba0169d 100644 --- a/libraries/entities/src/SimpleEntitySimulation.cpp +++ b/libraries/entities/src/SimpleEntitySimulation.cpp @@ -18,7 +18,7 @@ #include "EntityItem.h" #include "EntitiesLogging.h" -const quint64 MAX_OWNERLESS_PERIOD = 2 * USECS_PER_SECOND; +const uint64_t MAX_OWNERLESS_PERIOD = 2 * USECS_PER_SECOND; void SimpleEntitySimulation::clearOwnership(const QUuid& ownerID) { QMutexLocker lock(&_mutex); @@ -33,7 +33,7 @@ void SimpleEntitySimulation::clearOwnership(const QUuid& ownerID) { if (entity->getDynamic() && entity->hasLocalVelocity()) { // it is still moving dynamically --> add to orphaned list _entitiesThatNeedSimulationOwner.insert(entity); - quint64 expiry = entity->getLastChangedOnServer() + MAX_OWNERLESS_PERIOD; + uint64_t expiry = entity->getLastChangedOnServer() + MAX_OWNERLESS_PERIOD; if (expiry < _nextOwnerlessExpiry) { _nextOwnerlessExpiry = expiry; } @@ -50,15 +50,15 @@ void SimpleEntitySimulation::clearOwnership(const QUuid& ownerID) { } } -void SimpleEntitySimulation::updateEntitiesInternal(const quint64& now) { +void SimpleEntitySimulation::updateEntitiesInternal(uint64_t now) { if (now > _nextOwnerlessExpiry) { // search for ownerless objects that have expired QMutexLocker lock(&_mutex); - _nextOwnerlessExpiry = -1; + _nextOwnerlessExpiry = std::numeric_limits::max(); SetOfEntities::iterator itemItr = _entitiesThatNeedSimulationOwner.begin(); while (itemItr != _entitiesThatNeedSimulationOwner.end()) { EntityItemPointer entity = *itemItr; - quint64 expiry = entity->getLastChangedOnServer() + MAX_OWNERLESS_PERIOD; + uint64_t expiry = entity->getLastChangedOnServer() + MAX_OWNERLESS_PERIOD; if (expiry < now) { // no simulators have volunteered ownership --> remove from list itemItr = _entitiesThatNeedSimulationOwner.erase(itemItr); @@ -85,14 +85,18 @@ void SimpleEntitySimulation::updateEntitiesInternal(const quint64& now) { } void SimpleEntitySimulation::addEntityInternal(EntityItemPointer entity) { - EntitySimulation::addEntityInternal(entity); + if (entity->isMovingRelativeToParent() && !entity->getPhysicsInfo()) { + QMutexLocker lock(&_mutex); + _simpleKinematicEntities.insert(entity); + entity->setLastSimulated(usecTimestampNow()); + } if (!entity->getSimulatorID().isNull()) { QMutexLocker lock(&_mutex); _entitiesWithSimulationOwner.insert(entity); } else if (entity->getDynamic() && entity->hasLocalVelocity()) { QMutexLocker lock(&_mutex); _entitiesThatNeedSimulationOwner.insert(entity); - quint64 expiry = entity->getLastChangedOnServer() + MAX_OWNERLESS_PERIOD; + uint64_t expiry = entity->getLastChangedOnServer() + MAX_OWNERLESS_PERIOD; if (expiry < _nextOwnerlessExpiry) { _nextOwnerlessExpiry = expiry; } @@ -101,19 +105,29 @@ void SimpleEntitySimulation::addEntityInternal(EntityItemPointer entity) { void SimpleEntitySimulation::removeEntityInternal(EntityItemPointer entity) { EntitySimulation::removeEntityInternal(entity); - QMutexLocker lock(&_mutex); _entitiesWithSimulationOwner.remove(entity); _entitiesThatNeedSimulationOwner.remove(entity); } void SimpleEntitySimulation::changeEntityInternal(EntityItemPointer entity) { - EntitySimulation::changeEntityInternal(entity); + { + QMutexLocker lock(&_mutex); + if (entity->isMovingRelativeToParent() && !entity->getPhysicsInfo()) { + int numKinematicEntities = _simpleKinematicEntities.size(); + _simpleKinematicEntities.insert(entity); + if (numKinematicEntities != _simpleKinematicEntities.size()) { + entity->setLastSimulated(usecTimestampNow()); + } + } else { + _simpleKinematicEntities.remove(entity); + } + } if (entity->getSimulatorID().isNull()) { QMutexLocker lock(&_mutex); _entitiesWithSimulationOwner.remove(entity); if (entity->getDynamic() && entity->hasLocalVelocity()) { _entitiesThatNeedSimulationOwner.insert(entity); - quint64 expiry = entity->getLastChangedOnServer() + MAX_OWNERLESS_PERIOD; + uint64_t expiry = entity->getLastChangedOnServer() + MAX_OWNERLESS_PERIOD; if (expiry < _nextOwnerlessExpiry) { _nextOwnerlessExpiry = expiry; } diff --git a/libraries/entities/src/SimpleEntitySimulation.h b/libraries/entities/src/SimpleEntitySimulation.h index 9c7c9a374e..95c996a920 100644 --- a/libraries/entities/src/SimpleEntitySimulation.h +++ b/libraries/entities/src/SimpleEntitySimulation.h @@ -28,7 +28,7 @@ public: void clearOwnership(const QUuid& ownerID); protected: - virtual void updateEntitiesInternal(const quint64& now) override; + virtual void updateEntitiesInternal(uint64_t now) override; virtual void addEntityInternal(EntityItemPointer entity) override; virtual void removeEntityInternal(EntityItemPointer entity) override; virtual void changeEntityInternal(EntityItemPointer entity) override; @@ -38,7 +38,7 @@ protected: SetOfEntities _entitiesWithSimulationOwner; SetOfEntities _entitiesThatNeedSimulationOwner; - quint64 _nextOwnerlessExpiry { 0 }; + uint64_t _nextOwnerlessExpiry { 0 }; }; #endif // hifi_SimpleEntitySimulation_h diff --git a/libraries/entities/src/SimulationOwner.cpp b/libraries/entities/src/SimulationOwner.cpp index 4398582673..b312c6deb9 100644 --- a/libraries/entities/src/SimulationOwner.cpp +++ b/libraries/entities/src/SimulationOwner.cpp @@ -16,9 +16,9 @@ #include -const quint8 PENDING_STATE_NOTHING = 0; -const quint8 PENDING_STATE_TAKE = 1; -const quint8 PENDING_STATE_RELEASE = 2; +const uint8_t PENDING_STATE_NOTHING = 0; +const uint8_t PENDING_STATE_TAKE = 1; +const uint8_t PENDING_STATE_RELEASE = 2; // static const int SimulationOwner::NUM_BYTES_ENCODED = NUM_BYTES_RFC4122_UUID + 1; @@ -33,7 +33,7 @@ SimulationOwner::SimulationOwner() : { } -SimulationOwner::SimulationOwner(const QUuid& id, quint8 priority) : +SimulationOwner::SimulationOwner(const QUuid& id, uint8_t priority) : _id(id), _expiry(0), _pendingBidTimestamp(0), @@ -67,11 +67,11 @@ void SimulationOwner::clear() { _pendingState = PENDING_STATE_NOTHING; } -void SimulationOwner::setPriority(quint8 priority) { +void SimulationOwner::setPriority(uint8_t priority) { _priority = priority; } -void SimulationOwner::promotePriority(quint8 priority) { +void SimulationOwner::promotePriority(uint8_t priority) { if (priority > _priority) { _priority = priority; } @@ -89,7 +89,7 @@ bool SimulationOwner::setID(const QUuid& id) { return false; } -bool SimulationOwner::set(const QUuid& id, quint8 priority) { +bool SimulationOwner::set(const QUuid& id, uint8_t priority) { uint8_t oldPriority = _priority; setPriority(priority); return setID(id) || oldPriority != _priority; @@ -101,22 +101,22 @@ bool SimulationOwner::set(const SimulationOwner& owner) { return setID(owner._id) || oldPriority != _priority; } -void SimulationOwner::setPendingPriority(quint8 priority, const quint64& timestamp) { +void SimulationOwner::setPendingPriority(uint8_t priority, uint64_t timestamp) { _pendingBidPriority = priority; _pendingBidTimestamp = timestamp; _pendingState = (_pendingBidPriority == 0) ? PENDING_STATE_RELEASE : PENDING_STATE_TAKE; } void SimulationOwner::updateExpiry() { - const quint64 OWNERSHIP_LOCKOUT_EXPIRY = USECS_PER_SECOND / 5; + const uint64_t OWNERSHIP_LOCKOUT_EXPIRY = 200 * USECS_PER_MSEC; _expiry = usecTimestampNow() + OWNERSHIP_LOCKOUT_EXPIRY; } -bool SimulationOwner::pendingRelease(const quint64& timestamp) { +bool SimulationOwner::pendingRelease(uint64_t timestamp) { return _pendingBidPriority == 0 && _pendingState == PENDING_STATE_RELEASE && _pendingBidTimestamp >= timestamp; } -bool SimulationOwner::pendingTake(const quint64& timestamp) { +bool SimulationOwner::pendingTake(uint64_t timestamp) { return _pendingBidPriority > 0 && _pendingState == PENDING_STATE_TAKE && _pendingBidTimestamp >= timestamp; } @@ -142,7 +142,7 @@ void SimulationOwner::test() { { // test set constructor QUuid id = QUuid::createUuid(); - quint8 priority = 128; + uint8_t priority = 128; SimulationOwner simOwner(id, priority); if (simOwner.isNull()) { std::cout << __FILE__ << ":" << __LINE__ << " ERROR : SimulationOwner should NOT be NULL" << std::endl; @@ -164,7 +164,7 @@ void SimulationOwner::test() { { // test set() QUuid id = QUuid::createUuid(); - quint8 priority = 1; + uint8_t priority = 1; SimulationOwner simOwner; simOwner.set(id, priority); if (simOwner.isNull()) { diff --git a/libraries/entities/src/SimulationOwner.h b/libraries/entities/src/SimulationOwner.h index 94ed1d9a08..cc2069dcc8 100644 --- a/libraries/entities/src/SimulationOwner.h +++ b/libraries/entities/src/SimulationOwner.h @@ -18,20 +18,88 @@ #include #include -// Simulation observers will bid to simulate unowned active objects at the lowest possible priority -// which is VOLUNTEER. If the server accepts a VOLUNTEER bid it will automatically bump it -// to RECRUIT priority so that other volunteers don't accidentally take over. -const quint8 VOLUNTEER_SIMULATION_PRIORITY = 0x01; -const quint8 RECRUIT_SIMULATION_PRIORITY = VOLUNTEER_SIMULATION_PRIORITY + 1; +// HighFidelity uses a distributed physics simulation where multiple "participants" simulate portions +// of the same world. When portions overlap only one participant is allowed to be the authority for any +// particular object. For a simulated entity the authoritative participant is called the simulation "owner" and +// their duty is to send transform/velocity updates for the entity to the central entity-server. +// The entity-server relays updates to other participants who apply them as "state synchronization" +// to their own simulation. +// +// Participants acquire ownership by sending a "bid" to the entity-server. The bid is a properties update: +// { +// "simulationOwner": { "ownerID" : sessionID, "priority" : priority }, +// transform/velocity properties +// } +// +// The entity-server is the authority as to who owns what and may reject a bid. +// The rules for handling a bid are as follows: +// +// (1) A bid may be refused for special ownership restrictions, but otherwise... +// +// (2) A bid at higher priority is accepted +// +// (3) A bid at equal priority is rejected if receieved within a grace-period (200msec) +// of the last ownership transition, otherwise it is accepted +// +// (4) The current owner is the only participant allowed to clear ownership (entity-server can override). +// +// (5) The current owner is the only participant allowed to adjust priority (entity-server can override). +// +// (6) If an owner does not update the transform or velocities of an owned entity within some period +// (5 seconds) then ownership is cleared and the entity's velocities are zeroed. This to handle +// the case when an owner drops off the network. +// +// The priority of a participant's bid depends on how "interested" it is in the entity's motion. The rules +// for bidding are as follows: +// +// (7) A participant (almost) never assumes that a bid is accepted by the entity-server. It packs the +// simulation owner and priority as if they really did change but doesn't actually modify them +// locally. Thus, if the bid packet is lost the participant will re-send after some period. +// The participant only updates its knowledge of who owns what when it recieves an update from the +// entity-server. An exception is when the participant creates a moving entity: it assumes it starts +// off owning any moving entities it creates. +// +// (8) When an unowned entity becomes active in the physics simulation the participant will +// start a timer and if the entity is still unowned after some period (0.5 seconds) +// it will bid at priority = VOLUNTEER (=2). The entity-server never grants ownership at VOLUNTEER +// priority: when a VOLUNTEER bid is accepted the entity-server always promotes the priority to +// RECRUIT (VOLUNTEER + 1); this to avoid a race-condition which might rapidly transition ownership +// when multiple participants (with variable ping-times to the server) bid simultaneously for a +// recently activated entity. +// +// (9) When a participant changes an entity's transform/velocity it will bid at priority = POKE (=127) +// +// (10) When an entity touches MyAvatar the participant it will bid at priority = POKE. +// +// (11) When a participant grabs an entity it will bid at priority = GRAB (=128). +// +// (12) When entityA, locally owned at priority = N, collides with an unowned entityB the owner will +// also bid for entityB at priority = N-1 (or VOLUNTEER, whichever is larger). +// +// (13) When an entity comes to rest and is deactivated in the physics simulation the owner will +// send an update to: clear their ownerhsip, set priority to zero, and set the object's +// velocities to be zero. As per a normal bid, the owner does NOT assume that its ownership +// has been cleared until it hears from the entity-server. This, if the packet is lost the +// owner will re-send after some period. +// +// (14) When an entity's ownership priority drops below VOLUNTEER other participants may bid for it +// immediately at priority = VOLUNTEER. +// +// (15) When an entity is still active but the owner no longer wants to own it, it will drop its priority +// to YIELD (=1, less than VOLUNTEER) thereby signalling to other participants to bid for it. +// +const uint8_t YIELD_SIMULATION_PRIORITY = 1; +const uint8_t VOLUNTEER_SIMULATION_PRIORITY = YIELD_SIMULATION_PRIORITY + 1; +const uint8_t RECRUIT_SIMULATION_PRIORITY = VOLUNTEER_SIMULATION_PRIORITY + 1; // When poking objects with scripts an observer will bid at SCRIPT_EDIT priority. -const quint8 SCRIPT_GRAB_SIMULATION_PRIORITY = 0x80; -const quint8 SCRIPT_POKE_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY - 1; -const quint8 AVATAR_ENTITY_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY + 1; +const uint8_t SCRIPT_GRAB_SIMULATION_PRIORITY = 128; +const uint8_t SCRIPT_POKE_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY - 1; +const uint8_t AVATAR_ENTITY_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY + 1; // PERSONAL priority (needs a better name) is the level at which a simulation observer owns its own avatar // which really just means: things that collide with it will be bid at a priority level one lower -const quint8 PERSONAL_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY; +const uint8_t PERSONAL_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY; class SimulationOwner { @@ -39,25 +107,25 @@ public: static const int NUM_BYTES_ENCODED; SimulationOwner(); - SimulationOwner(const QUuid& id, quint8 priority); + SimulationOwner(const QUuid& id, uint8_t priority); const QUuid& getID() const { return _id; } - const quint64& getExpiry() const { return _expiry; } - quint8 getPriority() const { return _priority; } + const uint64_t& getExpiry() const { return _expiry; } + uint8_t getPriority() const { return _priority; } QByteArray toByteArray() const; bool fromByteArray(const QByteArray& data); void clear(); - void setPriority(quint8 priority); - void promotePriority(quint8 priority); + void setPriority(uint8_t priority); + void promotePriority(uint8_t priority); // return true if id is changed bool setID(const QUuid& id); - bool set(const QUuid& id, quint8 priority); + bool set(const QUuid& id, uint8_t priority); bool set(const SimulationOwner& owner); - void setPendingPriority(quint8 priority, const quint64& timestamp); + void setPendingPriority(uint8_t priority, uint64_t timestamp); bool isNull() const { return _id.isNull(); } bool matchesValidID(const QUuid& id) const { return _id == id && !_id.isNull(); } @@ -67,11 +135,11 @@ public: bool hasExpired() const { return usecTimestampNow() > _expiry; } uint8_t getPendingPriority() const { return _pendingBidPriority; } - bool pendingRelease(const quint64& timestamp); // return true if valid pending RELEASE - bool pendingTake(const quint64& timestamp); // return true if valid pending TAKE + bool pendingRelease(uint64_t timestamp); // return true if valid pending RELEASE + bool pendingTake(uint64_t timestamp); // return true if valid pending TAKE void clearCurrentOwner(); - bool operator>=(quint8 priority) const { return _priority >= priority; } + bool operator>=(uint8_t priority) const { return _priority >= priority; } bool operator==(const SimulationOwner& other) { return (_id == other._id && _priority == other._priority); } bool operator!=(const SimulationOwner& other); @@ -84,11 +152,11 @@ public: private: QUuid _id; // owner - quint64 _expiry; // time when ownership can transition at equal priority - quint64 _pendingBidTimestamp; // time when pending bid was set - quint8 _priority; // priority of current owner - quint8 _pendingBidPriority; // priority at which we'd like to own it - quint8 _pendingState; // NOTHING, TAKE, or RELEASE + uint64_t _expiry; // time when ownership can transition at equal priority + uint64_t _pendingBidTimestamp; // time when pending bid was set + uint8_t _priority; // priority of current owner + uint8_t _pendingBidPriority; // priority at which we'd like to own it + uint8_t _pendingState; // NOTHING, TAKE, or RELEASE }; diff --git a/libraries/entities/src/UpdateEntityOperator.cpp b/libraries/entities/src/UpdateEntityOperator.cpp index fa7e5ca38f..32bd2f06ba 100644 --- a/libraries/entities/src/UpdateEntityOperator.cpp +++ b/libraries/entities/src/UpdateEntityOperator.cpp @@ -288,7 +288,7 @@ OctreeElementPointer UpdateEntityOperator::possiblyCreateChildAt(const OctreeEle int indexOfChildContainingNewEntity = element->getMyChildContaining(_newEntityBox); if (childIndex == indexOfChildContainingNewEntity) { - return element->addChildAtIndex(childIndex);; + return element->addChildAtIndex(childIndex); } } } diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 420da5a1e0..6a25700ce8 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -26,12 +26,7 @@ #ifdef WANT_DEBUG_ENTITY_TREE_LOCKS #include "EntityTree.h" -#endif -const uint8_t LOOPS_FOR_SIMULATION_ORPHAN = 50; -const quint64 USECS_BETWEEN_OWNERSHIP_BIDS = USECS_PER_SECOND / 5; - -#ifdef WANT_DEBUG_ENTITY_TREE_LOCKS bool EntityMotionState::entityTreeIsLocked() const { EntityTreeElementPointer element = _entity->getElement(); EntityTreePointer tree = element ? element->getTree() : nullptr; @@ -46,11 +41,13 @@ bool entityTreeIsLocked() { } #endif +const uint8_t LOOPS_FOR_SIMULATION_ORPHAN = 50; +const quint64 USECS_BETWEEN_OWNERSHIP_BIDS = USECS_PER_SECOND / 5; + EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer entity) : ObjectMotionState(nullptr), - _entityPtr(entity), - _entity(entity.get()), + _entity(entity), _serverPosition(0.0f), _serverRotation(), _serverVelocity(0.0f), @@ -60,7 +57,7 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer _serverActionData(QByteArray()), _lastVelocity(0.0f), _measuredAcceleration(0.0f), - _nextOwnershipBid(0), + _nextBidExpiry(0), _measuredDeltaTime(0.0f), _lastMeasureStep(0), _lastStep(0), @@ -68,6 +65,12 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer _accelerationNearlyGravityCount(0), _numInactiveUpdates(1) { + // Why is _numInactiveUpdates initialied to 1? + // Because: when an entity is first created by a LOCAL operatioin its local simulation ownership is assumed, + // which causes it to be immediately placed on the 'owned' list, but in this case an "update" already just + // went out for the object's creation and there is no need to send another. By initializing _numInactiveUpdates + // to 1 here we trick remoteSimulationOutOfSync() to return "false" the first time through for this case. + _type = MOTIONSTATE_TYPE_ENTITY; assert(_entity); assert(entityTreeIsLocked()); @@ -76,77 +79,89 @@ EntityMotionState::EntityMotionState(btCollisionShape* shape, EntityItemPointer // rather than pass the legit shape pointer to the ObjectMotionState ctor above. setShape(shape); - _outgoingPriority = _entity->getPendingOwnershipPriority(); -} - -EntityMotionState::~EntityMotionState() { - assert(_entity); - _entity = nullptr; -} - -void EntityMotionState::updateServerPhysicsVariables() { - assert(entityTreeIsLocked()); - if (isLocallyOwned()) { - // don't slam these values if we are the simulation owner - return; + _bidPriority = _entity->getPendingOwnershipPriority(); + if (_entity->getClientOnly() && _entity->getOwningAvatarID() != Physics::getSessionUUID()) { + // client-only entities are always thus, so we cache this fact in _ownershipState + _ownershipState = EntityMotionState::OwnershipState::Unownable; } Transform localTransform; _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); - _serverVariablesSet = true; _serverPosition = localTransform.getTranslation(); _serverRotation = localTransform.getRotation(); _serverAcceleration = _entity->getAcceleration(); _serverActionData = _entity->getDynamicData(); + +} + +EntityMotionState::~EntityMotionState() { + if (_entity) { + assert(_entity->getPhysicsInfo() == this); + _entity->setPhysicsInfo(nullptr); + _entity.reset(); + } +} + +void EntityMotionState::updateServerPhysicsVariables() { + if (_ownershipState != EntityMotionState::OwnershipState::LocallyOwned) { + // only slam these values if we are NOT the simulation owner + Transform localTransform; + _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); + _serverPosition = localTransform.getTranslation(); + _serverRotation = localTransform.getRotation(); + _serverAcceleration = _entity->getAcceleration(); + _serverActionData = _entity->getDynamicData(); + _lastStep = ObjectMotionState::getWorldSimulationStep(); + } } void EntityMotionState::handleDeactivation() { - if (_serverVariablesSet) { - // copy _server data to entity - Transform localTransform = _entity->getLocalTransform(); - localTransform.setTranslation(_serverPosition); - localTransform.setRotation(_serverRotation); - _entity->setLocalTransformAndVelocities(localTransform, ENTITY_ITEM_ZERO_VEC3, ENTITY_ITEM_ZERO_VEC3); - // and also to RigidBody - btTransform worldTrans; - worldTrans.setOrigin(glmToBullet(_entity->getWorldPosition())); - worldTrans.setRotation(glmToBullet(_entity->getWorldOrientation())); - _body->setWorldTransform(worldTrans); - // no need to update velocities... should already be zero - } + // copy _server data to entity + Transform localTransform = _entity->getLocalTransform(); + localTransform.setTranslation(_serverPosition); + localTransform.setRotation(_serverRotation); + _entity->setLocalTransformAndVelocities(localTransform, ENTITY_ITEM_ZERO_VEC3, ENTITY_ITEM_ZERO_VEC3); + // and also to RigidBody + btTransform worldTrans; + worldTrans.setOrigin(glmToBullet(_entity->getWorldPosition())); + worldTrans.setRotation(glmToBullet(_entity->getWorldOrientation())); + _body->setWorldTransform(worldTrans); + // no need to update velocities... should already be zero } // virtual void EntityMotionState::handleEasyChanges(uint32_t& flags) { - assert(_entity); assert(entityTreeIsLocked()); updateServerPhysicsVariables(); ObjectMotionState::handleEasyChanges(flags); if (flags & Simulation::DIRTY_SIMULATOR_ID) { if (_entity->getSimulatorID().isNull()) { - // simulation ownership has been removed by an external simulator + // simulation ownership has been removed if (glm::length2(_entity->getWorldVelocity()) == 0.0f) { - // this object is coming to rest --> clear the ACTIVATION flag and _outgoingPriority + // this object is coming to rest --> clear the ACTIVATION flag and _bidPriority flags &= ~Simulation::DIRTY_PHYSICS_ACTIVATION; _body->setActivationState(WANTS_DEACTIVATION); - _outgoingPriority = 0; + _bidPriority = 0; const float ACTIVATION_EXPIRY = 3.0f; // something larger than the 2.0 hard coded in Bullet _body->setDeactivationTime(ACTIVATION_EXPIRY); } else { // disowned object is still moving --> start timer for ownership bid // TODO? put a delay in here proportional to distance from object? - upgradeOutgoingPriority(VOLUNTEER_SIMULATION_PRIORITY); - _nextOwnershipBid = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS; + upgradeBidPriority(VOLUNTEER_SIMULATION_PRIORITY); + _nextBidExpiry = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS; } _loopsWithoutOwner = 0; _numInactiveUpdates = 0; } else if (isLocallyOwned()) { // we just inherited ownership, make sure our desired priority matches what we have - upgradeOutgoingPriority(_entity->getSimulationPriority()); + upgradeBidPriority(_entity->getSimulationPriority()); } else { - _outgoingPriority = 0; - _nextOwnershipBid = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS; + // the entity is owned by someone else, so we clear _bidPriority here + // but _bidPriority may be updated to non-zero value if this object interacts with locally owned simulation + // in which case we may try to bid again + _bidPriority = 0; + _nextBidExpiry = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS; _numInactiveUpdates = 0; } } @@ -155,10 +170,10 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { // (1) we own it but may need to change the priority OR... // (2) we don't own it but should bid (because a local script has been changing physics properties) uint8_t newPriority = isLocallyOwned() ? _entity->getSimulationOwner().getPriority() : _entity->getSimulationOwner().getPendingPriority(); - upgradeOutgoingPriority(newPriority); + upgradeBidPriority(newPriority); // reset bid expiry so that we bid ASAP - _nextOwnershipBid = 0; + _nextBidExpiry = 0; } if ((flags & Simulation::DIRTY_PHYSICS_ACTIVATION) && !_body->isActive()) { if (_body->isKinematicObject()) { @@ -175,7 +190,6 @@ void EntityMotionState::handleEasyChanges(uint32_t& flags) { // virtual bool EntityMotionState::handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) { - assert(_entity); updateServerPhysicsVariables(); return ObjectMotionState::handleHardAndEasyChanges(flags, engine); } @@ -253,7 +267,6 @@ void EntityMotionState::getWorldTransform(btTransform& worldTrans) const { // This callback is invoked by the physics simulation at the end of each simulation step... // iff the corresponding RigidBody is DYNAMIC and ACTIVE. void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { - assert(_entity); assert(entityTreeIsLocked()); measureBodyAcceleration(); @@ -285,21 +298,10 @@ void EntityMotionState::setWorldTransform(const btTransform& worldTrans) { if (_entity->getSimulatorID().isNull()) { _loopsWithoutOwner++; - if (_loopsWithoutOwner > LOOPS_FOR_SIMULATION_ORPHAN && usecTimestampNow() > _nextOwnershipBid) { - upgradeOutgoingPriority(VOLUNTEER_SIMULATION_PRIORITY); + if (_loopsWithoutOwner > LOOPS_FOR_SIMULATION_ORPHAN && usecTimestampNow() > _nextBidExpiry) { + upgradeBidPriority(VOLUNTEER_SIMULATION_PRIORITY); } } - - #ifdef WANT_DEBUG - quint64 now = usecTimestampNow(); - qCDebug(physics) << "EntityMotionState::setWorldTransform()... changed entity:" << _entity->getEntityItemID(); - qCDebug(physics) << " last edited:" << _entity->getLastEdited() - << formatUsecTime(now - _entity->getLastEdited()) << "ago"; - qCDebug(physics) << " last simulated:" << _entity->getLastSimulated() - << formatUsecTime(now - _entity->getLastSimulated()) << "ago"; - qCDebug(physics) << " last updated:" << _entity->getLastUpdated() - << formatUsecTime(now - _entity->getLastUpdated()) << "ago"; - #endif } @@ -323,19 +325,13 @@ void EntityMotionState::setShape(const btCollisionShape* shape) { } } -bool EntityMotionState::isCandidateForOwnership() const { - assert(_body); - assert(_entity); - assert(entityTreeIsLocked()); - return _outgoingPriority != 0 - || isLocallyOwned() - || _entity->dynamicDataNeedsTransmit(); -} - bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { - DETAILED_PROFILE_RANGE(simulation_physics, "CheckOutOfSync"); // NOTE: we only get here if we think we own the simulation - assert(_body); + DETAILED_PROFILE_RANGE(simulation_physics, "CheckOutOfSync"); + + // Since we own the simulation: make sure _bidPriority is not less than current owned priority + // because: an _bidPriority of zero indicates that we should drop ownership when we have it. + upgradeBidPriority(_entity->getSimulationPriority()); bool parentTransformSuccess; Transform localToWorld = _entity->getParentTransform(parentTransformSuccess); @@ -347,27 +343,6 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { worldVelocityToLocal.setTranslation(glm::vec3(0.0f)); } - // if we've never checked before, our _lastStep will be 0, and we need to initialize our state - if (_lastStep == 0) { - btTransform xform = _body->getWorldTransform(); - _serverVariablesSet = true; - _serverPosition = worldToLocal.transform(bulletToGLM(xform.getOrigin())); - _serverRotation = worldToLocal.getRotation() * bulletToGLM(xform.getRotation()); - _serverVelocity = worldVelocityToLocal.transform(getBodyLinearVelocityGTSigma()); - _serverAcceleration = Vectors::ZERO; - _serverAngularVelocity = worldVelocityToLocal.transform(bulletToGLM(_body->getAngularVelocity())); - _lastStep = simulationStep; - _serverActionData = _entity->getDynamicData(); - _numInactiveUpdates = 1; - return false; - } - - #ifdef WANT_DEBUG - glm::vec3 wasPosition = _serverPosition; - glm::quat wasRotation = _serverRotation; - glm::vec3 wasAngularVelocity = _serverAngularVelocity; - #endif - int numSteps = simulationStep - _lastStep; float dt = (float)(numSteps) * PHYSICS_ENGINE_FIXED_SUBSTEP; @@ -378,9 +353,9 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { _entity->clearSimulationOwnership(); return false; } - // we resend the inactive update every INACTIVE_UPDATE_PERIOD - // until it is removed from the outgoing updates - // (which happens when we don't own the simulation and it isn't touching our simulation) + // we resend the inactive update with a growing delay: every INACTIVE_UPDATE_PERIOD * _numInactiveUpdates + // until it is removed from the owned list + // (which happens when we no longer own the simulation) const float INACTIVE_UPDATE_PERIOD = 0.5f; return (dt > INACTIVE_UPDATE_PERIOD * (float)_numInactiveUpdates); } @@ -411,14 +386,10 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { if (_entity->dynamicDataNeedsTransmit()) { uint8_t priority = _entity->hasActions() ? SCRIPT_GRAB_SIMULATION_PRIORITY : SCRIPT_POKE_SIMULATION_PRIORITY; - upgradeOutgoingPriority(priority); + upgradeBidPriority(priority); return true; } - if (_entity->shouldSuppressLocationEdits()) { - return false; - } - // Else we measure the error between current and extrapolated transform (according to expected behavior // of remote EntitySimulation) and return true if the error is significant. @@ -440,13 +411,6 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { const float MIN_ERROR_RATIO_SQUARED = 0.0025f; // corresponds to 5% error in 1 second const float MIN_SPEED_SQUARED = 1.0e-6f; // corresponds to 1mm/sec if (speed2 < MIN_SPEED_SQUARED || dx2 / speed2 > MIN_ERROR_RATIO_SQUARED) { - #ifdef WANT_DEBUG - qCDebug(physics) << ".... (dx2 > MAX_POSITION_ERROR_SQUARED) ...."; - qCDebug(physics) << "wasPosition:" << wasPosition; - qCDebug(physics) << "bullet position:" << position; - qCDebug(physics) << "_serverPosition:" << _serverPosition; - qCDebug(physics) << "dx2:" << dx2; - #endif return true; } } @@ -466,22 +430,6 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) { const float MIN_ROTATION_DOT = 0.99999f; // This corresponds to about 0.5 degrees of rotation glm::quat actualRotation = worldToLocal.getRotation() * bulletToGLM(worldTrans.getRotation()); - #ifdef WANT_DEBUG - if ((fabsf(glm::dot(actualRotation, _serverRotation)) < MIN_ROTATION_DOT)) { - qCDebug(physics) << ".... ((fabsf(glm::dot(actualRotation, _serverRotation)) < MIN_ROTATION_DOT)) ...."; - - qCDebug(physics) << "wasAngularVelocity:" << wasAngularVelocity; - qCDebug(physics) << "_serverAngularVelocity:" << _serverAngularVelocity; - - qCDebug(physics) << "length wasAngularVelocity:" << glm::length(wasAngularVelocity); - qCDebug(physics) << "length _serverAngularVelocity:" << glm::length(_serverAngularVelocity); - - qCDebug(physics) << "wasRotation:" << wasRotation; - qCDebug(physics) << "bullet actualRotation:" << actualRotation; - qCDebug(physics) << "_serverRotation:" << _serverRotation; - } - #endif - return (fabsf(glm::dot(actualRotation, _serverRotation)) < MIN_ROTATION_DOT); } @@ -489,14 +437,10 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { DETAILED_PROFILE_RANGE(simulation_physics, "ShouldSend"); // NOTE: we expect _entity and _body to be valid in this context, since shouldSendUpdate() is only called // after doesNotNeedToSendUpdate() returns false and that call should return 'true' if _entity or _body are NULL. - assert(_entity); - assert(_body); assert(entityTreeIsLocked()); - if (_entity->getClientOnly() && _entity->getOwningAvatarID() != Physics::getSessionUUID()) { - // don't send updates for someone else's avatarEntities - return false; - } + // this case is prevented by setting _ownershipState to UNOWNABLE in EntityMotionState::ctor + assert(!(_entity->getClientOnly() && _entity->getOwningAvatarID() != Physics::getSessionUUID())); if (_entity->dynamicDataNeedsTransmit() || _entity->queryAACubeNeedsUpdate()) { return true; @@ -506,44 +450,19 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) { return false; } - if (!isLocallyOwned()) { - // we don't own the simulation - - // NOTE: we do not volunteer to own kinematic or static objects - uint8_t volunteerPriority = _body->isStaticOrKinematicObject() ? VOLUNTEER_SIMULATION_PRIORITY : 0; - - bool shouldBid = _outgoingPriority > volunteerPriority && // but we would like to own it AND - usecTimestampNow() > _nextOwnershipBid; // it is time to bid again - if (shouldBid && _outgoingPriority < _entity->getSimulationPriority()) { - // we are insufficiently interested so clear _outgoingPriority - // and reset the bid expiry - _outgoingPriority = 0; - _nextOwnershipBid = usecTimestampNow() + USECS_BETWEEN_OWNERSHIP_BIDS; - } - return shouldBid; - } else { - // When we own the simulation: make sure _outgoingPriority is not less than current owned priority - // because: an _outgoingPriority of zero indicates that we should drop ownership when we have it. - upgradeOutgoingPriority(_entity->getSimulationPriority()); - } - return remoteSimulationOutOfSync(simulationStep); } -void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_t step) { - DETAILED_PROFILE_RANGE(simulation_physics, "Send"); - assert(_entity); - assert(entityTreeIsLocked()); - +void EntityMotionState::updateSendVelocities() { if (!_body->isActive()) { // make sure all derivatives are zero - zeroCleanObjectVelocities(); - _numInactiveUpdates++; + clearObjectVelocities(); + _numInactiveUpdates = 1; } else { glm::vec3 gravity = _entity->getGravity(); - // if this entity has been accelerated at close to gravity for a certain number of simulation-steps, let - // the entity server's estimates include gravity. + // if this entity has been accelerated at close to gravity for a certain number of simulation-steps + // let the entity server's estimates include gravity. const uint8_t STEPS_TO_DECIDE_BALLISTIC = 4; if (_accelerationNearlyGravityCount >= STEPS_TO_DECIDE_BALLISTIC) { _entity->setAcceleration(gravity); @@ -564,13 +483,82 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ if (movingSlowly) { // velocities might not be zero, but we'll fake them as such, which will hopefully help convince // other simulating observers to deactivate their own copies - zeroCleanObjectVelocities(); + clearObjectVelocities(); } } _numInactiveUpdates = 0; } +} - // remember properties for local server prediction +void EntityMotionState::sendBid(OctreeEditPacketSender* packetSender, uint32_t step) { + DETAILED_PROFILE_RANGE(simulation_physics, "Bid"); + assert(entityTreeIsLocked()); + + updateSendVelocities(); + + EntityItemProperties properties; + Transform localTransform; + glm::vec3 linearVelocity; + glm::vec3 angularVelocity; + _entity->getLocalTransformAndVelocities(localTransform, linearVelocity, angularVelocity); + properties.setPosition(localTransform.getTranslation()); + properties.setRotation(localTransform.getRotation()); + properties.setVelocity(linearVelocity); + properties.setAcceleration(_entity->getAcceleration()); + properties.setAngularVelocity(angularVelocity); + if (_entity->dynamicDataNeedsTransmit()) { + _entity->setDynamicDataNeedsTransmit(false); + properties.setActionData(_entity->getDynamicData()); + } + + if (_entity->updateQueryAACube()) { + // due to parenting, the server may not know where something is in world-space, so include the bounding cube. + properties.setQueryAACube(_entity->getQueryAACube()); + } + + // set the LastEdited of the properties but NOT the entity itself + quint64 now = usecTimestampNow(); + properties.setLastEdited(now); + + // we don't own the simulation for this entity yet, but we're sending a bid for it + uint8_t bidPriority = glm::max(_bidPriority, VOLUNTEER_SIMULATION_PRIORITY); + properties.setSimulationOwner(Physics::getSessionUUID(), bidPriority); + // copy _bidPriority into pendingPriority... + _entity->setPendingOwnershipPriority(_bidPriority, now); + // don't forget to remember that we have made a bid + _entity->rememberHasSimulationOwnershipBid(); + + EntityTreeElementPointer element = _entity->getElement(); + EntityTreePointer tree = element ? element->getTree() : nullptr; + + properties.setClientOnly(_entity->getClientOnly()); + properties.setOwningAvatarID(_entity->getOwningAvatarID()); + + EntityItemID id(_entity->getID()); + EntityEditPacketSender* entityPacketSender = static_cast(packetSender); + entityPacketSender->queueEditEntityMessage(PacketType::EntityPhysics, tree, id, properties); + _entity->setLastBroadcast(now); // for debug/physics status icons + + // NOTE: we don't descend to children for ownership bid. Instead, if we win ownership of the parent + // then in sendUpdate() we'll walk descendents and send updates for their QueryAACubes if necessary. + + _lastStep = step; + _nextBidExpiry = now + USECS_BETWEEN_OWNERSHIP_BIDS; + + // finally: clear _bidPriority + // which will may get promoted before next bid + // or maybe we'll win simulation ownership + _bidPriority = 0; +} + +void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_t step) { + DETAILED_PROFILE_RANGE(simulation_physics, "Send"); + assert(entityTreeIsLocked()); + assert(isLocallyOwned()); + + updateSendVelocities(); + + // remember _serverFoo data for local prediction of server state Transform localTransform; _entity->getLocalTransformAndVelocities(localTransform, _serverVelocity, _serverAngularVelocity); _serverPosition = localTransform.getTranslation(); @@ -579,11 +567,8 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ _serverActionData = _entity->getDynamicData(); EntityItemProperties properties; - - // explicitly set the properties that changed so that they will be packed properties.setPosition(_entity->getLocalPosition()); properties.setRotation(_entity->getLocalOrientation()); - properties.setVelocity(_serverVelocity); properties.setAcceleration(_serverAcceleration); properties.setAngularVelocity(_serverAngularVelocity); @@ -592,61 +577,35 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ properties.setActionData(_serverActionData); } - if (properties.transformChanged()) { - if (_entity->updateQueryAACube()) { - // due to parenting, the server may not know where something is in world-space, so include the bounding cube. - properties.setQueryAACube(_entity->getQueryAACube()); - } + if (_entity->updateQueryAACube()) { + // due to parenting, the server may not know where something is in world-space, so include the bounding cube. + properties.setQueryAACube(_entity->getQueryAACube()); } // set the LastEdited of the properties but NOT the entity itself quint64 now = usecTimestampNow(); properties.setLastEdited(now); - #ifdef WANT_DEBUG - quint64 lastSimulated = _entity->getLastSimulated(); - qCDebug(physics) << "EntityMotionState::sendUpdate()"; - qCDebug(physics) << " EntityItemId:" << _entity->getEntityItemID() - << "---------------------------------------------"; - qCDebug(physics) << " lastSimulated:" << debugTime(lastSimulated, now); - #endif //def WANT_DEBUG - if (_numInactiveUpdates > 0) { - // we own the simulation but the entity has stopped so we tell the server we're clearing simulatorID + // the entity is stopped and inactive so we tell the server we're clearing simulatorID // but we remember we do still own it... and rely on the server to tell us we don't properties.clearSimulationOwner(); - _outgoingPriority = 0; - _entity->setPendingOwnershipPriority(_outgoingPriority, now); - } else if (!isLocallyOwned()) { - // we don't own the simulation for this entity yet, but we're sending a bid for it - quint8 bidPriority = glm::max(_outgoingPriority, VOLUNTEER_SIMULATION_PRIORITY); - properties.setSimulationOwner(Physics::getSessionUUID(), bidPriority); - _nextOwnershipBid = now + USECS_BETWEEN_OWNERSHIP_BIDS; - // copy _outgoingPriority into pendingPriority... - _entity->setPendingOwnershipPriority(_outgoingPriority, now); - // don't forget to remember that we have made a bid - _entity->rememberHasSimulationOwnershipBid(); - // ...then reset _outgoingPriority - _outgoingPriority = 0; - // _outgoingPrioriuty will be re-computed before next bid, - // or will be set to agree with ownership priority should we win the bid - } else if (_outgoingPriority != _entity->getSimulationPriority()) { - // we own the simulation but our desired priority has changed - if (_outgoingPriority == 0) { + _bidPriority = 0; + _entity->setPendingOwnershipPriority(_bidPriority, now); + } else if (_bidPriority != _entity->getSimulationPriority()) { + // our desired priority has changed + if (_bidPriority == 0) { // we should release ownership properties.clearSimulationOwner(); } else { // we just need to change the priority - properties.setSimulationOwner(Physics::getSessionUUID(), _outgoingPriority); + properties.setSimulationOwner(Physics::getSessionUUID(), _bidPriority); } - _entity->setPendingOwnershipPriority(_outgoingPriority, now); + _entity->setPendingOwnershipPriority(_bidPriority, now); } EntityItemID id(_entity->getID()); EntityEditPacketSender* entityPacketSender = static_cast(packetSender); - #ifdef WANT_DEBUG - qCDebug(physics) << "EntityMotionState::sendUpdate()... calling queueEditEntityMessage()..."; - #endif EntityTreeElementPointer element = _entity->getElement(); EntityTreePointer tree = element ? element->getTree() : nullptr; @@ -655,7 +614,7 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ properties.setOwningAvatarID(_entity->getOwningAvatarID()); entityPacketSender->queueEditEntityMessage(PacketType::EntityPhysics, tree, id, properties); - _entity->setLastBroadcast(now); + _entity->setLastBroadcast(now); // for debug/physics status icons // if we've moved an entity with children, check/update the queryAACube of all descendents and tell the server // if they've changed. @@ -666,13 +625,12 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_ EntityItemProperties newQueryCubeProperties; newQueryCubeProperties.setQueryAACube(descendant->getQueryAACube()); newQueryCubeProperties.setLastEdited(properties.getLastEdited()); - newQueryCubeProperties.setClientOnly(entityDescendant->getClientOnly()); newQueryCubeProperties.setOwningAvatarID(entityDescendant->getOwningAvatarID()); entityPacketSender->queueEditEntityMessage(PacketType::EntityPhysics, tree, descendant->getID(), newQueryCubeProperties); - entityDescendant->setLastBroadcast(now); + entityDescendant->setLastBroadcast(now); // for debug/physics status icons } } }); @@ -723,6 +681,10 @@ uint8_t EntityMotionState::getSimulationPriority() const { return _entity->getSimulationPriority(); } +void EntityMotionState::slaveBidPriority() { + upgradeBidPriority(_entity->getSimulationPriority()); +} + // virtual QUuid EntityMotionState::getSimulatorID() const { assert(entityTreeIsLocked()); @@ -731,7 +693,7 @@ QUuid EntityMotionState::getSimulatorID() const { void EntityMotionState::bump(uint8_t priority) { assert(priority != 0); - upgradeOutgoingPriority(glm::max(VOLUNTEER_SIMULATION_PRIORITY, --priority)); + upgradeBidPriority(glm::max(VOLUNTEER_SIMULATION_PRIORITY, --priority)); } void EntityMotionState::resetMeasuredBodyAcceleration() { @@ -755,13 +717,14 @@ void EntityMotionState::measureBodyAcceleration() { _lastMeasureStep = thisStep; _measuredDeltaTime = dt; - // Note: the integration equation for velocity uses damping: v1 = (v0 + a * dt) * (1 - D)^dt + // Note: the integration equation for velocity uses damping (D): v1 = (v0 + a * dt) * (1 - D)^dt // hence the equation for acceleration is: a = (v1 / (1 - D)^dt - v0) / dt glm::vec3 velocity = getBodyLinearVelocityGTSigma(); _measuredAcceleration = (velocity / powf(1.0f - _body->getLinearDamping(), dt) - _lastVelocity) * invDt; _lastVelocity = velocity; if (numSubsteps > PHYSICS_ENGINE_MAX_NUM_SUBSTEPS) { + // we fall in here when _lastMeasureStep is old: the body has just become active _loopsWithoutOwner = 0; _lastStep = ObjectMotionState::getWorldSimulationStep(); _numInactiveUpdates = 0; @@ -805,24 +768,44 @@ QString EntityMotionState::getName() const { // virtual void EntityMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const { - assert(_entity); _entity->computeCollisionGroupAndFinalMask(group, mask); } +bool EntityMotionState::shouldSendBid() { + if (_bidPriority >= glm::max(_entity->getSimulationPriority(), VOLUNTEER_SIMULATION_PRIORITY)) { + return true; + } else { + // NOTE: this 'else' case has a side-effect: it clears _bidPriority + // which may be updated next simulation step (via collision or script event) + _bidPriority = 0; + return false; + } +} + bool EntityMotionState::isLocallyOwned() const { return _entity->getSimulatorID() == Physics::getSessionUUID(); } -bool EntityMotionState::shouldBeLocallyOwned() const { - return (_outgoingPriority > VOLUNTEER_SIMULATION_PRIORITY && _outgoingPriority > _entity->getSimulationPriority()) || +bool EntityMotionState::isLocallyOwnedOrShouldBe() const { + return (_bidPriority > VOLUNTEER_SIMULATION_PRIORITY && _bidPriority > _entity->getSimulationPriority()) || _entity->getSimulatorID() == Physics::getSessionUUID(); } -void EntityMotionState::upgradeOutgoingPriority(uint8_t priority) { - _outgoingPriority = glm::max(_outgoingPriority, priority); +void EntityMotionState::initForBid() { + assert(_ownershipState != EntityMotionState::OwnershipState::Unownable); + _ownershipState = EntityMotionState::OwnershipState::PendingBid; } -void EntityMotionState::zeroCleanObjectVelocities() const { +void EntityMotionState::initForOwned() { + assert(_ownershipState != EntityMotionState::OwnershipState::Unownable); + _ownershipState = EntityMotionState::OwnershipState::LocallyOwned; +} + +void EntityMotionState::upgradeBidPriority(uint8_t priority) { + _bidPriority = glm::max(_bidPriority, priority); +} + +void EntityMotionState::clearObjectVelocities() const { // If transform or velocities are flagged as dirty it means a network or scripted change // occured between the beginning and end of the stepSimulation() and we DON'T want to apply // these physics simulation results. diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index 784273d600..807acbfe80 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -24,11 +24,17 @@ class EntityMotionState : public ObjectMotionState { public: + enum class OwnershipState { + NotLocallyOwned = 0, + PendingBid, + LocallyOwned, + Unownable + }; + EntityMotionState() = delete; EntityMotionState(btCollisionShape* shape, EntityItemPointer item); virtual ~EntityMotionState(); - void updateServerPhysicsVariables(); void handleDeactivation(); virtual void handleEasyChanges(uint32_t& flags) override; virtual bool handleHardAndEasyChanges(uint32_t& flags, PhysicsEngine* engine) override; @@ -44,9 +50,8 @@ public: // this relays outgoing position/rotation to the EntityItem virtual void setWorldTransform(const btTransform& worldTrans) override; - bool isCandidateForOwnership() const; - bool remoteSimulationOutOfSync(uint32_t simulationStep); bool shouldSendUpdate(uint32_t simulationStep); + void sendBid(OctreeEditPacketSender* packetSender, uint32_t step); void sendUpdate(OctreeEditPacketSender* packetSender, uint32_t step); virtual uint32_t getIncomingDirtyFlags() override; @@ -70,7 +75,9 @@ public: virtual QUuid getSimulatorID() const override; virtual void bump(uint8_t priority) override; - EntityItemPointer getEntity() const { return _entityPtr.lock(); } + // getEntity() returns a smart-pointer by reference because it is only ever used + // to insert into lists of smart pointers, and the lists will make their own copies + const EntityItemPointer& getEntity() const { return _entity; } void resetMeasuredBodyAcceleration(); void measureBodyAcceleration(); @@ -79,15 +86,29 @@ public: virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override; + bool shouldSendBid(); bool isLocallyOwned() const override; - bool shouldBeLocallyOwned() const override; + bool isLocallyOwnedOrShouldBe() const override; // aka shouldEmitCollisionEvents() friend class PhysicalEntitySimulation; + OwnershipState getOwnershipState() const { return _ownershipState; } protected: - // changes _outgoingPriority only if priority is larger - void upgradeOutgoingPriority(uint8_t priority); - void zeroCleanObjectVelocities() const; + void updateSendVelocities(); + uint64_t getNextBidExpiry() const { return _nextBidExpiry; } + void initForBid(); + void initForOwned(); + void clearOwnershipState() { _ownershipState = OwnershipState::NotLocallyOwned; } + void updateServerPhysicsVariables(); + bool remoteSimulationOutOfSync(uint32_t simulationStep); + + // changes _bidPriority only if priority is larger + void upgradeBidPriority(uint8_t priority); + + // upgradeBidPriority to value stored in _entity + void slaveBidPriority(); + + void clearObjectVelocities() const; #ifdef WANT_DEBUG_ENTITY_TREE_LOCKS bool entityTreeIsLocked() const; @@ -98,17 +119,21 @@ protected: void setShape(const btCollisionShape* shape) override; void setMotionType(PhysicsMotionType motionType) override; - // In the glorious future (when entities lib depends on physics lib) the EntityMotionState will be - // properly "owned" by the EntityItem and will be deleted by it in the dtor. In pursuit of that - // state of affairs we can't keep a real EntityItemPointer as data member (it would produce a - // recursive dependency). Instead we keep a EntityItemWeakPointer to break that dependency while - // still granting us the capability to generate EntityItemPointers as necessary (for external data - // structures that use the MotionState to get to the EntityItem). - EntityItemWeakPointer _entityPtr; - // Meanwhile we also keep a raw EntityItem* for internal stuff where the pointer is guaranteed valid. - EntityItem* _entity; + // EntityMotionState keeps a SharedPointer to its EntityItem which is only set in the CTOR + // and is only cleared in the DTOR + EntityItemPointer _entity; - bool _serverVariablesSet { false }; + // These "_serverFoo" variables represent what we think the server knows. + // They are used in two different modes: + // + // (1) For remotely owned simulation: we store the last values recieved from the server. + // When the body comes to rest and goes inactive we slam its final transforms to agree with the last server + // update. This to reduce state synchronization errors when the local simulation deviated from remote. + // + // (2) For locally owned simulation: we store the last values sent to the server, integrated forward over time + // according to how we think the server doing it. We calculate the error between the true local transform + // and the remote to decide when to send another update. + // glm::vec3 _serverPosition; // in simulation-frame (not world-frame) glm::quat _serverRotation; glm::vec3 _serverVelocity; @@ -119,16 +144,18 @@ protected: glm::vec3 _lastVelocity; glm::vec3 _measuredAcceleration; - quint64 _nextOwnershipBid { 0 }; + quint64 _nextBidExpiry { 0 }; float _measuredDeltaTime; uint32_t _lastMeasureStep; uint32_t _lastStep; // last step of server extrapolation + OwnershipState _ownershipState { OwnershipState::NotLocallyOwned }; uint8_t _loopsWithoutOwner; mutable uint8_t _accelerationNearlyGravityCount; uint8_t _numInactiveUpdates { 1 }; - uint8_t _outgoingPriority { 0 }; + uint8_t _bidPriority { 0 }; + bool _serverVariablesSet { false }; }; #endif // hifi_EntityMotionState_h diff --git a/libraries/physics/src/ObjectMotionState.h b/libraries/physics/src/ObjectMotionState.h index 7f583ca9ca..fbda9366fc 100644 --- a/libraries/physics/src/ObjectMotionState.h +++ b/libraries/physics/src/ObjectMotionState.h @@ -148,9 +148,9 @@ public: virtual const QUuid getObjectID() const = 0; - virtual quint8 getSimulationPriority() const { return 0; } + virtual uint8_t getSimulationPriority() const { return 0; } virtual QUuid getSimulatorID() const = 0; - virtual void bump(quint8 priority) {} + virtual void bump(uint8_t priority) {} virtual QString getName() const { return ""; } @@ -164,7 +164,7 @@ public: void clearInternalKinematicChanges() { _hasInternalKinematicChanges = false; } virtual bool isLocallyOwned() const { return false; } - virtual bool shouldBeLocallyOwned() const { return false; } + virtual bool isLocallyOwnedOrShouldBe() const { return false; } // aka shouldEmitCollisionEvents() friend class PhysicsEngine; diff --git a/libraries/physics/src/PhysicalEntitySimulation.cpp b/libraries/physics/src/PhysicalEntitySimulation.cpp index e4ba47e205..d799577fc2 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.cpp +++ b/libraries/physics/src/PhysicalEntitySimulation.cpp @@ -40,7 +40,7 @@ void PhysicalEntitySimulation::init( } // begin EntitySimulation overrides -void PhysicalEntitySimulation::updateEntitiesInternal(const quint64& now) { +void PhysicalEntitySimulation::updateEntitiesInternal(uint64_t now) { // Do nothing here because the "internal" update the PhysicsEngine::stepSimualtion() which is done elsewhere. } @@ -61,33 +61,58 @@ void PhysicalEntitySimulation::addEntityInternal(EntityItemPointer entity) { void PhysicalEntitySimulation::removeEntityInternal(EntityItemPointer entity) { if (entity->isSimulated()) { EntitySimulation::removeEntityInternal(entity); - QMutexLocker lock(&_mutex); _entitiesToAddToPhysics.remove(entity); EntityMotionState* motionState = static_cast(entity->getPhysicsInfo()); if (motionState) { - _outgoingChanges.remove(motionState); + removeOwnershipData(motionState); _entitiesToRemoveFromPhysics.insert(entity); - } else { - _entitiesToDelete.insert(entity); + } else if (entity->isDead() && entity->getElement()) { + _deadEntities.insert(entity); } } } -void PhysicalEntitySimulation::takeEntitiesToDelete(VectorOfEntities& entitiesToDelete) { - QMutexLocker lock(&_mutex); - for (auto entity : _entitiesToDelete) { - // this entity is still in its tree, so we insert into the external list - entitiesToDelete.push_back(entity); +void PhysicalEntitySimulation::removeOwnershipData(EntityMotionState* motionState) { + assert(motionState); + if (motionState->getOwnershipState() == EntityMotionState::OwnershipState::LocallyOwned) { + for (uint32_t i = 0; i < _owned.size(); ++i) { + if (_owned[i] == motionState) { + _owned[i]->clearOwnershipState(); + _owned.remove(i); + } + } + } else if (motionState->getOwnershipState() == EntityMotionState::OwnershipState::PendingBid) { + for (uint32_t i = 0; i < _bids.size(); ++i) { + if (_bids[i] == motionState) { + _bids[i]->clearOwnershipState(); + _bids.remove(i); + } + } + } +} - // Someday when we invert the entities/physics lib dependencies we can let EntityItem delete its own PhysicsInfo - // rather than do it here +void PhysicalEntitySimulation::clearOwnershipData() { + for (uint32_t i = 0; i < _owned.size(); ++i) { + _owned[i]->clearOwnershipState(); + } + _owned.clear(); + for (uint32_t i = 0; i < _bids.size(); ++i) { + _bids[i]->clearOwnershipState(); + } + _bids.clear(); +} + +void PhysicalEntitySimulation::takeDeadEntities(SetOfEntities& deadEntities) { + QMutexLocker lock(&_mutex); + for (auto entity : _deadEntities) { EntityMotionState* motionState = static_cast(entity->getPhysicsInfo()); if (motionState) { _entitiesToRemoveFromPhysics.insert(entity); } } - _entitiesToDelete.clear(); + _deadEntities.swap(deadEntities); + _deadEntities.clear(); } void PhysicalEntitySimulation::changeEntityInternal(EntityItemPointer entity) { @@ -98,15 +123,15 @@ void PhysicalEntitySimulation::changeEntityInternal(EntityItemPointer entity) { if (motionState) { if (!entity->shouldBePhysical()) { // the entity should be removed from the physical simulation - _pendingChanges.remove(motionState); + _incomingChanges.remove(motionState); _physicalObjects.remove(motionState); - _outgoingChanges.remove(motionState); + removeOwnershipData(motionState); _entitiesToRemoveFromPhysics.insert(entity); if (entity->isMovingRelativeToParent()) { _simpleKinematicEntities.insert(entity); } } else { - _pendingChanges.insert(motionState); + _incomingChanges.insert(motionState); } } else if (entity->shouldBePhysical()) { // The intent is for this object to be in the PhysicsEngine, but it has no MotionState yet. @@ -125,80 +150,69 @@ void PhysicalEntitySimulation::clearEntitiesInternal() { // while it is in the middle of a simulation step. As it is, we're probably in shutdown mode // anyway, so maybe the simulation was already properly shutdown? Cross our fingers... - // copy everything into _entitiesToDelete - for (auto stateItr : _physicalObjects) { - EntityMotionState* motionState = static_cast(&(*stateItr)); - _entitiesToDelete.insert(motionState->getEntity()); - } - - // then remove the objects (aka MotionStates) from physics + // remove the objects (aka MotionStates) from physics _physicsEngine->removeSetOfObjects(_physicalObjects); // delete the MotionStates - // TODO: after we invert the entities/physics lib dependencies we will let EntityItem delete - // its own PhysicsInfo rather than do it here - for (auto entity : _entitiesToDelete) { - EntityMotionState* motionState = static_cast(entity->getPhysicsInfo()); - if (motionState) { - entity->setPhysicsInfo(nullptr); - delete motionState; - } + for (auto stateItr : _physicalObjects) { + EntityMotionState* motionState = static_cast(&(*stateItr)); + assert(motionState); + EntityItemPointer entity = motionState->getEntity(); + entity->setPhysicsInfo(nullptr); + // TODO: someday when we invert the entities/physics lib dependencies we can let EntityItem delete its own PhysicsInfo + // until then we must do it here + delete motionState; } - - // finally clear all lists maintained by this class _physicalObjects.clear(); + + // clear all other lists specific to this derived class + clearOwnershipData(); _entitiesToRemoveFromPhysics.clear(); - _entitiesToRelease.clear(); _entitiesToAddToPhysics.clear(); - _pendingChanges.clear(); - _outgoingChanges.clear(); + _incomingChanges.clear(); } // virtual void PhysicalEntitySimulation::prepareEntityForDelete(EntityItemPointer entity) { assert(entity); assert(entity->isDead()); + QMutexLocker lock(&_mutex); entity->clearActions(getThisPointer()); removeEntityInternal(entity); } // end EntitySimulation overrides -void PhysicalEntitySimulation::getObjectsToRemoveFromPhysics(VectorOfMotionStates& result) { - result.clear(); +const VectorOfMotionStates& PhysicalEntitySimulation::getObjectsToRemoveFromPhysics() { QMutexLocker lock(&_mutex); for (auto entity: _entitiesToRemoveFromPhysics) { - // make sure it isn't on any side lists - _entitiesToAddToPhysics.remove(entity); - EntityMotionState* motionState = static_cast(entity->getPhysicsInfo()); - if (motionState) { - _pendingChanges.remove(motionState); - _outgoingChanges.remove(motionState); - _physicalObjects.remove(motionState); - result.push_back(motionState); - _entitiesToRelease.insert(entity); + assert(motionState); + + _entitiesToAddToPhysics.remove(entity); + if (entity->isDead() && entity->getElement()) { + _deadEntities.insert(entity); } - if (entity->isDead()) { - _entitiesToDelete.insert(entity); - } + _incomingChanges.remove(motionState); + removeOwnershipData(motionState); + _physicalObjects.remove(motionState); + + // remember this motionState and delete it later (after removing its RigidBody from the PhysicsEngine) + _objectsToDelete.push_back(motionState); } _entitiesToRemoveFromPhysics.clear(); + return _objectsToDelete; } void PhysicalEntitySimulation::deleteObjectsRemovedFromPhysics() { QMutexLocker lock(&_mutex); - for (auto entity: _entitiesToRelease) { - EntityMotionState* motionState = static_cast(entity->getPhysicsInfo()); - assert(motionState); - entity->setPhysicsInfo(nullptr); + for (auto motionState : _objectsToDelete) { + // someday when we invert the entities/physics lib dependencies we can let EntityItem delete its own PhysicsInfo + // until then we must do it here + // NOTE: a reference to the EntityItemPointer is released in the EntityMotionState::dtor delete motionState; - - if (entity->isDead()) { - _entitiesToDelete.insert(entity); - } } - _entitiesToRelease.clear(); + _objectsToDelete.clear(); } void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& result) { @@ -248,18 +262,18 @@ void PhysicalEntitySimulation::getObjectsToAddToPhysics(VectorOfMotionStates& re void PhysicalEntitySimulation::setObjectsToChange(const VectorOfMotionStates& objectsToChange) { QMutexLocker lock(&_mutex); for (auto object : objectsToChange) { - _pendingChanges.insert(static_cast(object)); + _incomingChanges.insert(static_cast(object)); } } void PhysicalEntitySimulation::getObjectsToChange(VectorOfMotionStates& result) { result.clear(); QMutexLocker lock(&_mutex); - for (auto stateItr : _pendingChanges) { + for (auto stateItr : _incomingChanges) { EntityMotionState* motionState = &(*stateItr); result.push_back(motionState); } - _pendingChanges.clear(); + _incomingChanges.clear(); } void PhysicalEntitySimulation::handleDeactivatedMotionStates(const VectorOfMotionStates& motionStates) { @@ -279,20 +293,22 @@ void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionSta PROFILE_RANGE_EX(simulation_physics, "ChangedEntities", 0x00000000, (uint64_t)motionStates.size()); QMutexLocker lock(&_mutex); - // walk the motionStates looking for those that correspond to entities - { - PROFILE_RANGE_EX(simulation_physics, "Filter", 0x00000000, (uint64_t)motionStates.size()); - for (auto stateItr : motionStates) { - ObjectMotionState* state = &(*stateItr); - assert(state); - if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { - EntityMotionState* entityState = static_cast(state); - EntityItemPointer entity = entityState->getEntity(); - assert(entity.get()); - if (entityState->isCandidateForOwnership()) { - _outgoingChanges.insert(entityState); + for (auto stateItr : motionStates) { + ObjectMotionState* state = &(*stateItr); + assert(state); + if (state->getType() == MOTIONSTATE_TYPE_ENTITY) { + EntityMotionState* entityState = static_cast(state); + _entitiesToSort.insert(entityState->getEntity()); + if (entityState->getOwnershipState() == EntityMotionState::OwnershipState::NotLocallyOwned) { + // NOTE: entityState->getOwnershipState() reflects what ownership list (_bids or _owned) it is in + // and is distinct from entityState->isLocallyOwned() which checks the simulation ownership + // properties of the corresponding EntityItem. It is possible for the two states to be out + // of sync. In fact, we're trying to put them back into sync here. + if (entityState->isLocallyOwned()) { + addOwnership(entityState); + } else if (entityState->shouldSendBid()) { + addOwnershipBid(entityState); } - _entitiesToSort.insert(entity); } } } @@ -302,26 +318,78 @@ void PhysicalEntitySimulation::handleChangedMotionStates(const VectorOfMotionSta _lastStepSendPackets = numSubsteps; if (Physics::getSessionUUID().isNull()) { - // usually don't get here, but if so --> nothing to do - _outgoingChanges.clear(); - return; + // usually don't get here, but if so clear all ownership + clearOwnershipData(); } + // send updates before bids, because this simplifies the logic thasuccessful bids will immediately send an update when added to the 'owned' list + sendOwnedUpdates(numSubsteps); + sendOwnershipBids(numSubsteps); + } +} - // look for entities to prune or update - PROFILE_RANGE_EX(simulation_physics, "Prune/Send", 0x00000000, (uint64_t)_outgoingChanges.size()); - QSet::iterator stateItr = _outgoingChanges.begin(); - while (stateItr != _outgoingChanges.end()) { - EntityMotionState* state = *stateItr; - if (!state->isCandidateForOwnership()) { - // prune - stateItr = _outgoingChanges.erase(stateItr); - } else if (state->shouldSendUpdate(numSubsteps)) { - // update - state->sendUpdate(_entityPacketSender, numSubsteps); - ++stateItr; - } else { - ++stateItr; +void PhysicalEntitySimulation::addOwnershipBid(EntityMotionState* motionState) { + motionState->initForBid(); + motionState->sendBid(_entityPacketSender, _physicsEngine->getNumSubsteps()); + _bids.push_back(motionState); + _nextBidExpiry = glm::min(_nextBidExpiry, motionState->getNextBidExpiry()); +} + +void PhysicalEntitySimulation::addOwnership(EntityMotionState* motionState) { + motionState->initForOwned(); + _owned.push_back(motionState); +} + +void PhysicalEntitySimulation::sendOwnershipBids(uint32_t numSubsteps) { + uint64_t now = usecTimestampNow(); + if (now > _nextBidExpiry) { + PROFILE_RANGE_EX(simulation_physics, "Bid", 0x00000000, (uint64_t)_bids.size()); + _nextBidExpiry = std::numeric_limits::max(); + uint32_t i = 0; + while (i < _bids.size()) { + bool removeBid = false; + if (_bids[i]->isLocallyOwned()) { + // when an object transitions from 'bid' to 'owned' we are changing the "mode" of data stored + // in the EntityMotionState::_serverFoo variables (please see comments in EntityMotionState.h) + // therefore we need to immediately send an update so that the values stored are what we're + // "telling" the server rather than what we've been "hearing" from the server. + _bids[i]->slaveBidPriority(); + _bids[i]->sendUpdate(_entityPacketSender, numSubsteps); + + addOwnership(_bids[i]); + removeBid = true; + } else if (!_bids[i]->shouldSendBid()) { + removeBid = true; + _bids[i]->clearOwnershipState(); } + if (removeBid) { + _bids.remove(i); + } else { + if (now > _bids[i]->getNextBidExpiry()) { + _bids[i]->sendBid(_entityPacketSender, numSubsteps); + _nextBidExpiry = glm::min(_nextBidExpiry, _bids[i]->getNextBidExpiry()); + } + ++i; + } + } + } +} + +void PhysicalEntitySimulation::sendOwnedUpdates(uint32_t numSubsteps) { + PROFILE_RANGE_EX(simulation_physics, "Update", 0x00000000, (uint64_t)_owned.size()); + uint32_t i = 0; + while (i < _owned.size()) { + if (!_owned[i]->isLocallyOwned()) { + if (_owned[i]->shouldSendBid()) { + addOwnershipBid(_owned[i]); + } else { + _owned[i]->clearOwnershipState(); + } + _owned.remove(i); + } else { + if (_owned[i]->shouldSendUpdate(numSubsteps)) { + _owned[i]->sendUpdate(_entityPacketSender, numSubsteps); + } + ++i; } } } @@ -336,7 +404,6 @@ void PhysicalEntitySimulation::handleCollisionEvents(const CollisionEvents& coll } } - void PhysicalEntitySimulation::addDynamic(EntityDynamicPointer dynamic) { if (_physicsEngine) { // FIXME put fine grain locking into _physicsEngine diff --git a/libraries/physics/src/PhysicalEntitySimulation.h b/libraries/physics/src/PhysicalEntitySimulation.h index b9acf4cace..7b6fe221fb 100644 --- a/libraries/physics/src/PhysicalEntitySimulation.h +++ b/libraries/physics/src/PhysicalEntitySimulation.h @@ -27,7 +27,19 @@ class PhysicalEntitySimulation; using PhysicalEntitySimulationPointer = std::shared_ptr; using SetOfEntityMotionStates = QSet; +class VectorOfEntityMotionStates: public std::vector { +public: + void remove(uint32_t index) { + assert(index < size()); + if (index < size() - 1) { + (*this)[index] = back(); + } + pop_back(); + } +}; + class PhysicalEntitySimulation : public EntitySimulation { + Q_OBJECT public: PhysicalEntitySimulation(); ~PhysicalEntitySimulation(); @@ -37,21 +49,28 @@ public: virtual void addDynamic(EntityDynamicPointer dynamic) override; virtual void applyDynamicChanges() override; - virtual void takeEntitiesToDelete(VectorOfEntities& entitiesToDelete) override; + virtual void takeDeadEntities(SetOfEntities& deadEntities) override; + +signals: + void entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); protected: // only called by EntitySimulation // overrides for EntitySimulation - virtual void updateEntitiesInternal(const quint64& now) override; + virtual void updateEntitiesInternal(uint64_t now) override; virtual void addEntityInternal(EntityItemPointer entity) override; virtual void removeEntityInternal(EntityItemPointer entity) override; virtual void changeEntityInternal(EntityItemPointer entity) override; virtual void clearEntitiesInternal() override; + void removeOwnershipData(EntityMotionState* motionState); + void clearOwnershipData(); + public: virtual void prepareEntityForDelete(EntityItemPointer entity) override; - void getObjectsToRemoveFromPhysics(VectorOfMotionStates& result); + const VectorOfMotionStates& getObjectsToRemoveFromPhysics(); void deleteObjectsRemovedFromPhysics(); + void getObjectsToAddToPhysics(VectorOfMotionStates& result); void setObjectsToChange(const VectorOfMotionStates& objectsToChange); void getObjectsToChange(VectorOfMotionStates& result); @@ -62,19 +81,28 @@ public: EntityEditPacketSender* getPacketSender() { return _entityPacketSender; } -private: - SetOfEntities _entitiesToRemoveFromPhysics; - SetOfEntities _entitiesToRelease; - SetOfEntities _entitiesToAddToPhysics; + void addOwnershipBid(EntityMotionState* motionState); + void addOwnership(EntityMotionState* motionState); + void sendOwnershipBids(uint32_t numSubsteps); + void sendOwnedUpdates(uint32_t numSubsteps); - SetOfEntityMotionStates _pendingChanges; // EntityMotionStates already in PhysicsEngine that need their physics changed - SetOfEntityMotionStates _outgoingChanges; // EntityMotionStates for which we may need to send updates to entity-server +private: + SetOfEntities _entitiesToAddToPhysics; + SetOfEntities _entitiesToRemoveFromPhysics; + + VectorOfMotionStates _objectsToDelete; + + SetOfEntityMotionStates _incomingChanges; // EntityMotionStates that have changed from external sources + // and need their RigidBodies updated SetOfMotionStates _physicalObjects; // MotionStates of entities in PhysicsEngine PhysicsEnginePointer _physicsEngine = nullptr; EntityEditPacketSender* _entityPacketSender = nullptr; + VectorOfEntityMotionStates _owned; + VectorOfEntityMotionStates _bids; + uint64_t _nextBidExpiry; uint32_t _lastStepSendPackets { 0 }; }; diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index 2d84f0cef1..1d4c385f07 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -571,7 +571,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { // modify the logic below. // // We only create events when at least one of the objects is (or should be) owned in the local simulation. - if (motionStateA && (motionStateA->shouldBeLocallyOwned())) { + if (motionStateA && (motionStateA->isLocallyOwnedOrShouldBe())) { QUuid idA = motionStateA->getObjectID(); QUuid idB; if (motionStateB) { @@ -582,7 +582,7 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { (motionStateB ? motionStateB->getObjectLinearVelocityChange() : glm::vec3(0.0f)); glm::vec3 penetration = bulletToGLM(contact.distance * contact.normalWorldOnB); _collisionEvents.push_back(Collision(type, idA, idB, position, penetration, velocityChange)); - } else if (motionStateB && (motionStateB->shouldBeLocallyOwned())) { + } else if (motionStateB && (motionStateB->isLocallyOwnedOrShouldBe())) { QUuid idB = motionStateB->getObjectID(); QUuid idA; if (motionStateA) { diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index 4a9b69c099..ba5036ad68 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -380,6 +380,8 @@ void Antialiasing::run(const render::RenderContextPointer& renderContext, const batch.setResourceTexture(AntialiasingPass_VelocityMapSlot, nullptr); batch.setResourceTexture(AntialiasingPass_NextMapSlot, nullptr); }); + + args->popViewFrustum(); } @@ -520,7 +522,7 @@ void JitterSample::run(const render::RenderContextPointer& renderContext) { viewFrustum.setProjection(projMat); viewFrustum.calculate(); - args->setViewFrustum(viewFrustum); + args->pushViewFrustum(viewFrustum); } else { mat4 projMats[2]; args->_context->getStereoProjections(projMats); @@ -538,4 +540,4 @@ void JitterSample::run(const render::RenderContextPointer& renderContext) { } -#endif \ No newline at end of file +#endif diff --git a/libraries/render-utils/src/FadeEffectJobs.cpp b/libraries/render-utils/src/FadeEffectJobs.cpp index da3f8dddc0..1e2ea2bc15 100644 --- a/libraries/render-utils/src/FadeEffectJobs.cpp +++ b/libraries/render-utils/src/FadeEffectJobs.cpp @@ -39,43 +39,52 @@ void FadeEditJob::run(const render::RenderContextPointer& renderContext, const F auto scene = renderContext->_scene; if (_isEditEnabled) { - float minIsectDistance = std::numeric_limits::max(); - auto& itemBounds = inputs.get0(); - auto editedItem = findNearestItem(renderContext, itemBounds, minIsectDistance); - render::Transaction transaction; - bool hasTransaction{ false }; + static const std::string selectionName("TransitionEdit"); + auto scene = renderContext->_scene; + if (!scene->isSelectionEmpty(selectionName)) { + auto selection = scene->getSelection(selectionName); + auto editedItem = selection.getItems().front(); + render::Transaction transaction; + bool hasTransaction{ false }; - if (editedItem != _editedItem && render::Item::isValidID(_editedItem)) { - // Remove transition from previously edited item as we've changed edited item - hasTransaction = true; + if (editedItem != _editedItem && render::Item::isValidID(_editedItem)) { + // Remove transition from previously edited item as we've changed edited item + hasTransaction = true; + transaction.removeTransitionFromItem(_editedItem); + } + _editedItem = editedItem; + + if (render::Item::isValidID(_editedItem)) { + static const render::Transition::Type categoryToTransition[FADE_CATEGORY_COUNT] = { + render::Transition::ELEMENT_ENTER_DOMAIN, + render::Transition::BUBBLE_ISECT_OWNER, + render::Transition::BUBBLE_ISECT_TRESPASSER, + render::Transition::USER_ENTER_DOMAIN, + render::Transition::AVATAR_CHANGE + }; + + auto transitionType = categoryToTransition[inputs.get1()]; + + transaction.queryTransitionOnItem(_editedItem, [transitionType, scene](render::ItemID id, const render::Transition* transition) { + if (transition == nullptr || transition->isFinished || transition->eventType != transitionType) { + // Relaunch transition + render::Transaction transaction; + transaction.addTransitionToItem(id, transitionType); + scene->enqueueTransaction(transaction); + } + }); + hasTransaction = true; + } + + if (hasTransaction) { + scene->enqueueTransaction(transaction); + } + } else if (render::Item::isValidID(_editedItem)) { + // Remove transition from previously edited item as we've disabled fade edition + render::Transaction transaction; transaction.removeTransitionFromItem(_editedItem); - } - _editedItem = editedItem; - - if (render::Item::isValidID(_editedItem)) { - static const render::Transition::Type categoryToTransition[FADE_CATEGORY_COUNT] = { - render::Transition::ELEMENT_ENTER_DOMAIN, - render::Transition::BUBBLE_ISECT_OWNER, - render::Transition::BUBBLE_ISECT_TRESPASSER, - render::Transition::USER_ENTER_DOMAIN, - render::Transition::AVATAR_CHANGE - }; - - auto transitionType = categoryToTransition[inputs.get1()]; - - transaction.queryTransitionOnItem(_editedItem, [transitionType, scene](render::ItemID id, const render::Transition* transition) { - if (transition == nullptr || transition->isFinished || transition->eventType!=transitionType) { - // Relaunch transition - render::Transaction transaction; - transaction.addTransitionToItem(id, transitionType); - scene->enqueueTransaction(transaction); - } - }); - hasTransaction = true; - } - - if (hasTransaction) { scene->enqueueTransaction(transaction); + _editedItem = render::Item::INVALID_ITEM_ID; } } else if (render::Item::isValidID(_editedItem)) { @@ -87,28 +96,6 @@ void FadeEditJob::run(const render::RenderContextPointer& renderContext, const F } } -render::ItemID FadeEditJob::findNearestItem(const render::RenderContextPointer& renderContext, const render::ItemBounds& inputs, float& minIsectDistance) const { - const glm::vec3 rayOrigin = renderContext->args->getViewFrustum().getPosition(); - const glm::vec3 rayDirection = renderContext->args->getViewFrustum().getDirection(); - BoxFace face; - glm::vec3 normal; - float isectDistance; - render::ItemID nearestItem = render::Item::INVALID_ITEM_ID; - const float minDistance = 1.f; - const float maxDistance = 50.f; - - for (const auto& itemBound : inputs) { - if (!itemBound.bound.contains(rayOrigin) && itemBound.bound.findRayIntersection(rayOrigin, rayDirection, isectDistance, face, normal)) { - auto& item = renderContext->_scene->getItem(itemBound.id); - if (item.getKey().isWorldSpace() && isectDistance>minDistance && isectDistance < minIsectDistance && isectDistancemanualFade && (state.threshold != jobConfig->threshold)) { + if (isFirstItem && (state.threshold != jobConfig->threshold)) { jobConfig->setProperty("threshold", state.threshold); isFirstItem = false; } diff --git a/libraries/render-utils/src/FadeEffectJobs.h b/libraries/render-utils/src/FadeEffectJobs.h index 783f026bcd..449995dba5 100644 --- a/libraries/render-utils/src/FadeEffectJobs.h +++ b/libraries/render-utils/src/FadeEffectJobs.h @@ -160,8 +160,8 @@ public: float manualThreshold{ 0.f }; bool manualFade{ false }; - Q_INVOKABLE void save() const; - Q_INVOKABLE void load(); + Q_INVOKABLE void save(const QString& filePath) const; + Q_INVOKABLE void load(const QString& filePath); static QString eventNames[FADE_CATEGORY_COUNT]; @@ -190,7 +190,6 @@ private: bool _isEditEnabled{ false }; render::ItemID _editedItem{ render::Item::INVALID_ITEM_ID }; - render::ItemID findNearestItem(const render::RenderContextPointer& renderContext, const render::ItemBounds& inputs, float& minIsectDistance) const; }; class FadeJob { diff --git a/libraries/shared/src/OctalCode.cpp b/libraries/shared/src/OctalCode.cpp index 1a0a19bf44..ae4338be6f 100644 --- a/libraries/shared/src/OctalCode.cpp +++ b/libraries/shared/src/OctalCode.cpp @@ -46,7 +46,7 @@ void printOctalCode(const unsigned char* octalCode) { } char sectionValue(const unsigned char* startByte, char startIndexInByte) { - char rightShift = 8 - startIndexInByte - 3; + int8_t rightShift = 8 - startIndexInByte - 3; if (rightShift < 0) { return ((startByte[0] << -rightShift) & 7) + (startByte[1] >> (8 + rightShift)); @@ -73,7 +73,7 @@ int branchIndexWithDescendant(const unsigned char* ancestorOctalCode, const unsi return sectionValue(descendantOctalCode + 1 + (branchStartBit / 8), branchStartBit % 8); } -unsigned char* childOctalCode(const unsigned char* parentOctalCode, char childNumber) { +unsigned char* childOctalCode(const unsigned char* parentOctalCode, int childNumber) { // find the length (in number of three bit code sequences) // in the parent @@ -111,7 +111,7 @@ unsigned char* childOctalCode(const unsigned char* parentOctalCode, char childNu // calculate the amount of left shift required // this will be -1 or -2 if there's wrap - char leftShift = 8 - (startBit % 8) - 3; + int8_t leftShift = 8 - (startBit % 8) - 3; if (leftShift < 0) { // we have a wrap-around to accomodate diff --git a/libraries/shared/src/OctalCode.h b/libraries/shared/src/OctalCode.h index a0d86f32d2..89c5e6d74e 100644 --- a/libraries/shared/src/OctalCode.h +++ b/libraries/shared/src/OctalCode.h @@ -30,7 +30,7 @@ using OctalCodePtrList = std::vector; void printOctalCode(const unsigned char* octalCode); size_t bytesRequiredForCodeLength(unsigned char threeBitCodes); int branchIndexWithDescendant(const unsigned char* ancestorOctalCode, const unsigned char* descendantOctalCode); -unsigned char* childOctalCode(const unsigned char* parentOctalCode, char childNumber); +unsigned char* childOctalCode(const unsigned char* parentOctalCode, int childNumber); const int OVERFLOWED_OCTCODE_BUFFER = -1; const int UNKNOWN_OCTCODE_LENGTH = -2; diff --git a/scripts/developer/utilities/render/debugTransition.js b/scripts/developer/utilities/render/debugTransition.js index 161ff3f5d4..450b2e3ac9 100644 --- a/scripts/developer/utilities/render/debugTransition.js +++ b/scripts/developer/utilities/render/debugTransition.js @@ -1,5 +1,3 @@ -"use strict"; - // // debugTransition.js // developer/utilities/render @@ -12,12 +10,17 @@ // (function() { + "use strict"; + var TABLET_BUTTON_NAME = "Transition"; var QMLAPP_URL = Script.resolvePath("./transition.qml"); var ICON_URL = Script.resolvePath("../../../system/assets/images/transition-i.svg"); var ACTIVE_ICON_URL = Script.resolvePath("../../../system/assets/images/transition-a.svg"); - + Script.include([ + Script.resolvePath("../../../system/libraries/stringHelpers.js"), + ]); + var onScreen = false; function onClicked() { @@ -37,6 +40,71 @@ var hasEventBridge = false; + function enableSphereVisualization() { + if (gradientSphere==undefined) { + gradientSphere = Overlays.addOverlay("sphere", { + position: MyAvatar.position, + rotation: Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0), + dimensions: { x: 1.0, y: 1.0, z: 1.0 }, + color: { red: 100, green: 150, blue: 255}, + alpha: 0.2, + solid: false + }); + } + if (noiseSphere==undefined) { + noiseSphere = Overlays.addOverlay("sphere", { + position: MyAvatar.position, + rotation: Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0), + dimensions: { x: 1.0, y: 1.0, z: 1.0 }, + color: { red: 255, green: 150, blue: 100}, + alpha: 0.2, + solid: false + }); + } + } + + function disableSphereVisualization() { + Overlays.deleteOverlay(noiseSphere); + Overlays.deleteOverlay(gradientSphere); + noiseSphere = undefined + gradientSphere = undefined + } + + // Create a Laser pointer used to pick and add objects to selections + var END_DIMENSIONS = { x: 0.05, y: 0.05, z: 0.05 }; + var COLOR1 = {red: 255, green: 0, blue: 0}; + var COLOR2 = {red: 0, green: 255, blue: 0}; + var end1 = { + type: "sphere", + dimensions: END_DIMENSIONS, + color: COLOR1, + ignoreRayIntersection: true + } + var end2 = { + type: "sphere", + dimensions: END_DIMENSIONS, + color: COLOR2, + ignoreRayIntersection: true + } + var laser + + function enablePointer() { + laser = Pointers.createPointer(PickType.Ray, { + joint: "Mouse", + filter: Picks.PICK_ENTITIES, + renderStates: [{name: "one", end: end1}], + defaultRenderStates: [{name: "one", end: end2, distance: 2.0}], + enabled: true + }); + Pointers.setRenderState(laser, "one"); + Pointers.enablePointer(laser) + } + + function disablePointer() { + Pointers.disablePointer(laser) + Pointers.removePointer(laser); + } + function wireEventBridge(on) { if (!tablet) { print("Warning in wireEventBridge(): 'tablet' undefined!"); @@ -46,11 +114,15 @@ if (!hasEventBridge) { tablet.fromQml.connect(fromQml); hasEventBridge = true; + enablePointer(); + Render.getConfig("RenderMainView.FadeEdit")["editFade"] = true } } else { if (hasEventBridge) { tablet.fromQml.disconnect(fromQml); hasEventBridge = false; + disablePointer(); + Render.getConfig("RenderMainView.FadeEdit")["editFade"] = false } } } @@ -66,12 +138,87 @@ wireEventBridge(onScreen); } + var isEditEnabled = false + var noiseSphere + var gradientSphere + var selectedEntity + var editedCategory + + var FADE_MIN_SCALE = 0.001 + var FADE_MAX_SCALE = 10000.0 + + function parameterToValuePow(parameter, minValue, maxOverMinValue) { + return minValue * Math.pow(maxOverMinValue, parameter); + //return parameter + } + + function update(dt) { + var gradientProperties = Entities.getEntityProperties(selectedEntity, ["position", "dimensions"]); + if (gradientProperties!=undefined) { + var pos = gradientProperties.position + if (pos!=undefined) { + var config = Render.getConfig("RenderMainView.Fade") + //print("Center at "+pos.x+" "+pos.y+" "+pos.z) + var dim = {x:config.baseSizeX, y:config.baseSizeY, z:config.baseSizeZ} + dim.x = parameterToValuePow(dim.x, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + dim.y = parameterToValuePow(dim.y, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + dim.z = parameterToValuePow(dim.z, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + if (editedCategory==4 || editedCategory==5) { + dim.y = gradientProperties.dimensions.y + pos.y = pos.y - gradientProperties.dimensions.y/2 + } + Overlays.editOverlay(gradientSphere, { position: pos, dimensions: dim, alpha: config.baseLevel }) + dim.x = parameterToValuePow(config.noiseSizeX, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + dim.y = parameterToValuePow(config.noiseSizeY, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + dim.z = parameterToValuePow(config.noiseSizeZ, FADE_MIN_SCALE, FADE_MAX_SCALE/ FADE_MIN_SCALE) + Overlays.editOverlay(noiseSphere, { position: pos, dimensions: dim, alpha: config.noiseLevel }) + } + } + } + + Script.update.connect(update); + + function loadConfiguration(fileUrl) { + var config = Render.getConfig("RenderMainView.Fade") + config.load(fileUrl) + } + + function saveConfiguration(fileUrl) { + var config = Render.getConfig("RenderMainView.Fade") + config.save(fileUrl) + } + function fromQml(message) { + tokens = message.split('*') + //print("Received '"+message+"' from transition.qml") + command = tokens[0].toLowerCase() + if (command=="category") { + editedCategory = parseInt(tokens[1]) + } else if (command=="save") { + var filePath = tokens[1] + print("Raw token = "+filePath) + if (filePath.startsWith("file:///")) { + filePath = filePath.substr(8) + print("Saving configuration to "+filePath) + saveConfiguration(filePath) + } else { + print("Configurations can only be saved to local files") + } + } else if (command=="load") { + var filePath = tokens[1] + if (filePath.startsWith("file:///")) { + filePath = filePath.substr(8) + print("Loading configuration from "+filePath) + loadConfiguration(filePath) + } else { + print("Configurations can only be loaded from local files") + } + } } button.clicked.connect(onClicked); tablet.screenChanged.connect(onScreenChanged); - + Script.scriptEnding.connect(function () { if (onScreen) { tablet.gotoHomeScreen(); @@ -81,4 +228,28 @@ tablet.removeButton(button); }); + + var currentSelectionName = "" + var SelectionList = "TransitionEdit" + + Selection.enableListToScene(SelectionList) + Selection.clearSelectedItemsList(SelectionList) + + Entities.clickDownOnEntity.connect(function (id, event) { + if (selectedEntity) { + Selection.removeFromSelectedItemsList(SelectionList, "entity", selectedEntity) + } + selectedEntity = id + Selection.addToSelectedItemsList(SelectionList, "entity", selectedEntity) + update() + }) + + function cleanup() { + disablePointer(); + Selection.removeListFromMap(SelectionList) + Selection.disableListToScene(SelectionList); + Overlays.deleteOverlay(noiseSphere); + Overlays.deleteOverlay(gradientSphere); + } + Script.scriptEnding.connect(cleanup); }()); \ No newline at end of file diff --git a/scripts/developer/utilities/render/transition.qml b/scripts/developer/utilities/render/transition.qml index e83a85f8ed..f74468a273 100644 --- a/scripts/developer/utilities/render/transition.qml +++ b/scripts/developer/utilities/render/transition.qml @@ -11,6 +11,7 @@ import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 +import QtQuick.Dialogs 1.0 import "qrc:///qml/styles-uit" import "qrc:///qml/controls-uit" as HifiControls @@ -22,11 +23,31 @@ Rectangle { id: root anchors.margins: hifi.dimensions.contentMargin.x + signal sendToScript(var message); + color: hifi.colors.baseGray; property var config: Render.getConfig("RenderMainView.Fade"); property var configEdit: Render.getConfig("RenderMainView.FadeEdit"); + FileDialog { + id: fileDialog + title: "Please choose a file" + folder: shortcuts.documents + nameFilters: [ "JSON files (*.json)", "All files (*)" ] + onAccepted: { + root.sendToScript(title.split(" ")[0]+"*"+fileUrl.toString()) + // This is a hack to be sure the widgets below properly reflect the change of category: delete the Component + // by setting the loader source to Null and then recreate it 500ms later + paramWidgetLoader.sourceComponent = undefined; + postpone.interval = 500 + postpone.start() + } + onRejected: { + } + Component.onCompleted: visible = false + } + ColumnLayout { spacing: 3 anchors.left: parent.left @@ -37,23 +58,14 @@ Rectangle { } RowLayout { - spacing: 20 + spacing: 8 Layout.fillWidth: true id: root_col - HifiControls.CheckBox { - anchors.verticalCenter: parent.verticalCenter - boxSize: 20 - text: "Edit" - checked: root.configEdit["editFade"] - onCheckedChanged: { - root.configEdit["editFade"] = checked; - Render.getConfig("RenderMainView.DrawFadedOpaqueBounds").enabled = checked; - } - } HifiControls.ComboBox { anchors.verticalCenter: parent.verticalCenter - Layout.fillWidth: true + anchors.left : parent.left + width: 300 id: categoryBox model: ["Elements enter/leave domain", "Bubble isect. - Owner POV", "Bubble isect. - Trespasser POV", "Another user leaves/arrives", "Changing an avatar"] Timer { @@ -61,17 +73,48 @@ Rectangle { interval: 100; running: false; repeat: false onTriggered: { paramWidgetLoader.sourceComponent = paramWidgets + var isTimeBased = categoryBox.currentIndex==0 || categoryBox.currentIndex==3 + paramWidgetLoader.item.isTimeBased = isTimeBased } } onCurrentIndexChanged: { + var descriptions = [ + "Time based threshold, gradients centered on object", + "Fixed threshold, gradients centered on owner avatar", + "Position based threshold (increases when trespasser moves closer to avatar), gradients centered on trespasser avatar", + "Time based threshold, gradients centered on bottom of object", + "UNSUPPORTED" + ] + + description.text = descriptions[currentIndex] root.config["editedCategory"] = currentIndex; // This is a hack to be sure the widgets below properly reflect the change of category: delete the Component // by setting the loader source to Null and then recreate it 100ms later paramWidgetLoader.sourceComponent = undefined; postpone.interval = 100 postpone.start() + root.sendToScript("category*"+currentIndex) } } + HifiControls.Button { + action: saveAction + Layout.fillWidth: true + anchors.top: parent.top + anchors.bottom: parent.bottom + } + HifiControls.Button { + action: loadAction + Layout.fillWidth: true + anchors.top: parent.top + anchors.bottom: parent.bottom + } + } + + HifiControls.Label { + id: description + text: "..." + Layout.fillWidth: true + wrapMode: Text.WordWrap } RowLayout { @@ -104,19 +147,18 @@ Rectangle { id: saveAction text: "Save" onTriggered: { - root.config.save() + fileDialog.title = "Save configuration..." + fileDialog.selectExisting = false + fileDialog.open() } } Action { id: loadAction text: "Load" onTriggered: { - root.config.load() - // This is a hack to be sure the widgets below properly reflect the change of category: delete the Component - // by setting the loader source to Null and then recreate it 500ms later - paramWidgetLoader.sourceComponent = undefined; - postpone.interval = 500 - postpone.start() + fileDialog.title = "Load configuration..." + fileDialog.selectExisting = true + fileDialog.open() } } @@ -128,13 +170,8 @@ Rectangle { ColumnLayout { spacing: 3 width: root_col.width + property bool isTimeBased - HifiControls.CheckBox { - text: "Invert" - boxSize: 20 - checked: root.config["isInverted"] - onCheckedChanged: { root.config["isInverted"] = checked } - } RowLayout { Layout.fillWidth: true @@ -196,16 +233,32 @@ Rectangle { } } - - ConfigSlider { + RowLayout { + spacing: 20 height: 36 - label: "Edge Width" - integral: false - config: root.config - property: "edgeWidth" - max: 1.0 - min: 0.0 + + HifiControls.CheckBox { + text: "Invert gradient" + anchors.verticalCenter: parent.verticalCenter + boxSize: 20 + checked: root.config["isInverted"] + onCheckedChanged: { root.config["isInverted"] = checked } + } + ConfigSlider { + anchors.left: undefined + anchors.verticalCenter: parent.verticalCenter + height: 36 + width: 300 + label: "Edge Width" + integral: false + config: root.config + property: "edgeWidth" + max: 1.0 + min: 0.0 + } } + + RowLayout { Layout.fillWidth: true @@ -278,6 +331,8 @@ Rectangle { Layout.fillWidth: true ConfigSlider { + enabled: isTimeBased + opacity: isTimeBased ? 1.0 : 0.0 anchors.left: undefined anchors.right: undefined Layout.fillWidth: true @@ -290,6 +345,8 @@ Rectangle { min: 0.1 } HifiControls.ComboBox { + enabled: isTimeBased + opacity: isTimeBased ? 1.0 : 0.0 Layout.fillWidth: true model: ["Linear", "Ease In", "Ease Out", "Ease In / Out"] currentIndex: root.config["timing"] @@ -364,20 +421,6 @@ Rectangle { id: paramWidgetLoader sourceComponent: paramWidgets } - - Row { - anchors.left: parent.left - anchors.right: parent.right - - Button { - action: saveAction - } - Button { - action: loadAction - } - } - - } } \ No newline at end of file diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index b7fdacfe38..cc884cdbc7 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -296,6 +296,7 @@ Script.include("/~/system/libraries/Xform.js"); this.actionID = null; this.grabbedThingID = null; this.targetObject = null; + this.potentialEntityWithContextOverlay = false; }; this.updateRecommendedArea = function() { diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index fced5fc4e9..b284cab345 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1618,8 +1618,18 @@ SelectionDisplay = (function() { grid.snapToGrid(Vec3.sum(cornerPosition, vector), constrainMajorOnly), cornerPosition); - for (var i = 0; i < SelectionManager.selections.length; i++) { - var properties = SelectionManager.savedProperties[SelectionManager.selections[i]]; + // editing a parent will cause all the children to automatically follow along, so don't + // edit any entity who has an ancestor in SelectionManager.selections + var toMove = SelectionManager.selections.filter(function (selection) { + if (SelectionManager.selections.indexOf(SelectionManager.savedProperties[selection].parentID) >= 0) { + return false; // a parent is also being moved, so don't issue an edit for this entity + } else { + return true; + } + }); + + for (var i = 0; i < toMove.length; i++) { + var properties = SelectionManager.savedProperties[toMove[i]]; if (!properties) { continue; } @@ -1628,7 +1638,7 @@ SelectionDisplay = (function() { y: 0, z: vector.z }); - Entities.editEntity(SelectionManager.selections[i], { + Entities.editEntity(toMove[i], { position: newPosition }); @@ -1653,11 +1663,11 @@ SelectionDisplay = (function() { mode: mode, onBegin: function(event, pickRay, pickResult) { if (direction === TRANSLATE_DIRECTION.X) { - pickNormal = { x:0, y:0, z:1 }; + pickNormal = { x:0, y:1, z:1 }; } else if (direction === TRANSLATE_DIRECTION.Y) { - pickNormal = { x:1, y:0, z:0 }; + pickNormal = { x:1, y:0, z:1 }; } else if (direction === TRANSLATE_DIRECTION.Z) { - pickNormal = { x:0, y:1, z:0 }; + pickNormal = { x:1, y:1, z:0 }; } var rotation = spaceMode === SPACE_LOCAL ? SelectionManager.localRotation : SelectionManager.worldRotation; @@ -1701,7 +1711,6 @@ SelectionDisplay = (function() { onMove: function(event) { pickRay = generalComputePickRay(event.x, event.y); - // translate mode left/right based on view toward entity var newIntersection = rayPlaneIntersection(pickRay, SelectionManager.worldPosition, pickNormal); var vector = Vec3.subtract(newIntersection, lastPick); @@ -1719,7 +1728,7 @@ SelectionDisplay = (function() { var dotVector = Vec3.dot(vector, projectionVector); vector = Vec3.multiply(dotVector, projectionVector); vector = grid.snapToGrid(vector); - + var wantDebug = false; if (wantDebug) { print("translateUpDown... "); @@ -1727,9 +1736,19 @@ SelectionDisplay = (function() { Vec3.print(" newIntersection:", newIntersection); Vec3.print(" vector:", vector); } - - for (var i = 0; i < SelectionManager.selections.length; i++) { - var id = SelectionManager.selections[i]; + + // editing a parent will cause all the children to automatically follow along, so don't + // edit any entity who has an ancestor in SelectionManager.selections + var toMove = SelectionManager.selections.filter(function (selection) { + if (SelectionManager.selections.indexOf(SelectionManager.savedProperties[selection].parentID) >= 0) { + return false; // a parent is also being moved, so don't issue an edit for this entity + } else { + return true; + } + }); + + for (var i = 0; i < toMove.length; i++) { + var id = toMove[i]; var properties = SelectionManager.savedProperties[id]; var newPosition = Vec3.sum(properties.position, vector); Entities.editEntity(id, { position: newPosition }); @@ -2017,10 +2036,10 @@ SelectionDisplay = (function() { vector = grid.snapToSpacing(vector); var changeInDimensions = Vec3.multiply(NEGATE_VECTOR, vec3Mult(localSigns, vector)); - if (directionEnum === STRETCH_DIRECTION.ALL) { - var toCameraDistance = getDistanceToCamera(position); - var dimensionsMultiple = toCameraDistance * STRETCH_DIRECTION_ALL_CAMERA_DISTANCE_MULTIPLE; - changeInDimensions = Vec3.multiply(changeInDimensions, dimensionsMultiple); + if (directionEnum === STRETCH_DIRECTION.ALL) { + var toCameraDistance = getDistanceToCamera(position); + var dimensionsMultiple = toCameraDistance * STRETCH_DIRECTION_ALL_CAMERA_DISTANCE_MULTIPLE; + changeInDimensions = Vec3.multiply(changeInDimensions, dimensionsMultiple); } var newDimensions; @@ -2166,8 +2185,19 @@ SelectionDisplay = (function() { // the selections center point. Otherwise, the rotation will be around the entities // registration point which does not need repositioning. var reposition = (SelectionManager.selections.length > 1); - for (var i = 0; i < SelectionManager.selections.length; i++) { - var entityID = SelectionManager.selections[i]; + + // editing a parent will cause all the children to automatically follow along, so don't + // edit any entity who has an ancestor in SelectionManager.selections + var toRotate = SelectionManager.selections.filter(function (selection) { + if (SelectionManager.selections.indexOf(SelectionManager.savedProperties[selection].parentID) >= 0) { + return false; // a parent is also being moved, so don't issue an edit for this entity + } else { + return true; + } + }); + + for (var i = 0; i < toRotate.length; i++) { + var entityID = toRotate[i]; var initialProperties = SelectionManager.savedProperties[entityID]; var newProperties = { diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index 728760c1e7..ba37f6ee4e 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -1,6 +1,6 @@ "use strict"; /*jslint vars:true, plusplus:true, forin:true*/ -/*global Script, Settings, Window, Controller, Overlays, SoundArray, LODManager, MyAvatar, Tablet, Camera, HMD, Menu, Quat, Vec3*/ +/*global Script, Settings, Window, Controller, Overlays, SoundArray, MyAvatar, Tablet, Camera, HMD, Menu, Quat, Vec3*/ // // notifications.js // Version 0.801 @@ -84,21 +84,18 @@ var NOTIFICATION_MENU_ITEM_POST = " Notifications"; var PLAY_NOTIFICATION_SOUNDS_SETTING = "play_notification_sounds"; var PLAY_NOTIFICATION_SOUNDS_TYPE_SETTING_PRE = "play_notification_sounds_type_"; - var lodTextID = false; - var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications" + var NOTIFICATIONS_MESSAGE_CHANNEL = "Hifi-Notifications"; var NotificationType = { UNKNOWN: 0, SNAPSHOT: 1, - LOD_WARNING: 2, - CONNECTION_REFUSED: 3, - EDIT_ERROR: 4, - TABLET: 5, - CONNECTION: 6, - WALLET: 7, + CONNECTION_REFUSED: 2, + EDIT_ERROR: 3, + TABLET: 4, + CONNECTION: 5, + WALLET: 6, properties: [ { text: "Snapshot" }, - { text: "Level of Detail" }, { text: "Connection Refused" }, { text: "Edit error" }, { text: "Tablet" }, @@ -153,10 +150,6 @@ // This handles the final dismissal of a notification after fading function dismiss(firstNoteOut, firstButOut, firstOut) { - if (firstNoteOut === lodTextID) { - lodTextID = false; - } - Overlays.deleteOverlay(firstNoteOut); Overlays.deleteOverlay(firstButOut); notifications.splice(firstOut, 1); @@ -418,9 +411,6 @@ function deleteNotification(index) { var notificationTextID = notifications[index]; - if (notificationTextID === lodTextID) { - lodTextID = false; - } Overlays.deleteOverlay(notificationTextID); Overlays.deleteOverlay(buttons[index]); notifications.splice(index, 1); @@ -674,20 +664,6 @@ } } - LODManager.LODDecreased.connect(function () { - var warningText = "\n" + - "Due to the complexity of the content, the \n" + - "level of detail has been decreased. " + - "You can now see: \n" + - LODManager.getLODFeedbackText(); - - if (lodTextID === false) { - lodTextID = createNotification(warningText, NotificationType.LOD_WARNING); - } else { - Overlays.editOverlay(lodTextID, { text: warningText }); - } - }); - Controller.keyPressEvent.connect(keyPressEvent); Controller.mousePressEvent.connect(mousePressEvent); Controller.keyReleaseEvent.connect(keyReleaseEvent); diff --git a/scripts/system/snapshot.js b/scripts/system/snapshot.js index ae8ef52a15..658d1c3ced 100644 --- a/scripts/system/snapshot.js +++ b/scripts/system/snapshot.js @@ -411,8 +411,6 @@ function snapshotUploaded(isError, reply) { } else { print('Ignoring snapshotUploaded() callback for stale ' + (isGif ? 'GIF' : 'Still' ) + ' snapshot. Stale story ID:', storyID); } - } else { - print(reply); } isUploadingPrintableStill = false; } diff --git a/tools/auto-tester/src/Test.cpp b/tools/auto-tester/src/Test.cpp index 347cfd90dc..5d8b9115b8 100644 --- a/tools/auto-tester/src/Test.cpp +++ b/tools/auto-tester/src/Test.cpp @@ -24,15 +24,11 @@ extern AutoTester* autoTester; #include Test::Test() { - QString regex(EXPECTED_IMAGE_PREFIX + QString("\\\\d").repeated(NUM_DIGITS) + ".png"); - - expectedImageFilenameFormat = QRegularExpression(regex); - mismatchWindow.setModal(true); } bool Test::createTestResultsFolderPath(QString directory) { - QDateTime now = QDateTime::currentDateTime(); + QDateTime now = QDateTime::currentDateTime(); testResultsFolderPath = directory + "/" + TEST_RESULTS_FOLDER + "--" + now.toString(DATETIME_FORMAT); QDir testResultsFolder(testResultsFolderPath); @@ -76,7 +72,7 @@ bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar) QImage expectedImage(expectedImagesFullFilenames[i]); if (resultImage.width() != expectedImage.width() || resultImage.height() != expectedImage.height()) { - messageBox.critical(0, "Internal error #1", "Images are not the same size"); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Images are not the same size"); exit(-1); } @@ -84,7 +80,7 @@ bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar) try { similarityIndex = imageComparer.compareImages(resultImage, expectedImage); } catch (...) { - messageBox.critical(0, "Internal error #2", "Image not in expected format"); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Image not in expected format"); exit(-1); } @@ -131,20 +127,20 @@ bool Test::compareImageLists(bool isInteractiveMode, QProgressBar* progressBar) void Test::appendTestResultsToFile(QString testResultsFolderPath, TestFailure testFailure, QPixmap comparisonImage) { if (!QDir().exists(testResultsFolderPath)) { - messageBox.critical(0, "Internal error #3", "Folder " + testResultsFolderPath + " not found"); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Folder " + testResultsFolderPath + " not found"); exit(-1); } QString failureFolderPath { testResultsFolderPath + "/" + "Failure_" + QString::number(index) }; if (!QDir().mkdir(failureFolderPath)) { - messageBox.critical(0, "Internal error #4", "Failed to create folder " + failureFolderPath); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create folder " + failureFolderPath); exit(-1); } ++index; QFile descriptionFile(failureFolderPath + "/" + TEST_RESULTS_FILENAME); if (!descriptionFile.open(QIODevice::ReadWrite)) { - messageBox.critical(0, "Internal error #5", "Failed to create file " + TEST_RESULTS_FILENAME); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create file " + TEST_RESULTS_FILENAME); exit(-1); } @@ -164,14 +160,14 @@ void Test::appendTestResultsToFile(QString testResultsFolderPath, TestFailure te sourceFile = testFailure._pathname + testFailure._expectedImageFilename; destinationFile = failureFolderPath + "/" + "Expected Image.jpg"; if (!QFile::copy(sourceFile, destinationFile)) { - messageBox.critical(0, "Internal error #6", "Failed to copy " + sourceFile + " to " + destinationFile); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to copy " + sourceFile + " to " + destinationFile); exit(-1); } sourceFile = testFailure._pathname + testFailure._actualImageFilename; destinationFile = failureFolderPath + "/" + "Actual Image.jpg"; if (!QFile::copy(sourceFile, destinationFile)) { - messageBox.critical(0, "Internal error #7", "Failed to copy " + sourceFile + " to " + destinationFile); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to copy " + sourceFile + " to " + destinationFile); exit(-1); } @@ -209,11 +205,6 @@ void Test::startTestsEvaluation() { QStringList sortedTestResultsFilenames = createListOfAll_imagesInDirectory("png", pathToTestResultsDirectory); QStringList expectedImagesURLs; - const QString URLPrefix("https://raw.githubusercontent.com"); - const QString githubUser("NissimHadar"); - const QString testsRepo("hifi_tests"); - const QString branch("addRecursionToAutotester"); - resultImagesFullFilenames.clear(); expectedImagesFilenames.clear(); expectedImagesFullFilenames.clear(); @@ -226,16 +217,16 @@ void Test::startTestsEvaluation() { QString expectedImagePartialSourceDirectory = getExpectedImagePartialSourceDirectory(currentFilename); // Images are stored on GitHub as ExpectedImage_ddddd.png - // Extract the digits at the end of the filename (exluding the file extension) + // Extract the digits at the end of the filename (excluding the file extension) QString expectedImageFilenameTail = currentFilename.left(currentFilename.length() - 4).right(NUM_DIGITS); QString expectedImageStoredFilename = EXPECTED_IMAGE_PREFIX + expectedImageFilenameTail + ".png"; - QString imageURLString(URLPrefix + "/" + githubUser + "/" + testsRepo + "/" + branch + "/" + - expectedImagePartialSourceDirectory + "/" + expectedImageStoredFilename); + QString imageURLString("https://github.com/" + githubUser + "/hifi_tests/blob/" + gitHubBranch + "/" + + expectedImagePartialSourceDirectory + "/" + expectedImageStoredFilename + "?raw=true"); expectedImagesURLs << imageURLString; - // The image retrieved from Github needs a unique name + // The image retrieved from GitHub needs a unique name QString expectedImageFilename = currentFilename.replace("/", "_").replace(".", "_EI."); expectedImagesFilenames << expectedImageFilename; @@ -273,25 +264,31 @@ bool Test::isAValidDirectory(QString pathname) { return true; } -void Test::importTest(QTextStream& textStream, const QString& testPathname) { - // `testPathname` includes the full path to the test. We need the portion below (and including) `tests` - QStringList filenameParts = testPathname.split('/'); +QString Test::extractPathFromTestsDown(QString fullPath) { + // `fullPath` includes the full path to the test. We need the portion below (and including) `tests` + QStringList pathParts = fullPath.split('/'); int i{ 0 }; - while (i < filenameParts.length() && filenameParts[i] != "tests") { + while (i < pathParts.length() && pathParts[i] != "tests") { ++i; } - if (i == filenameParts.length()) { - messageBox.critical(0, "Internal error #10", "Bad testPathname"); + if (i == pathParts.length()) { + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Bad testPathname"); exit(-1); } - QString filename; - for (int j = i; j < filenameParts.length(); ++j) { - filename += "/" + filenameParts[j]; + QString partialPath; + for (int j = i; j < pathParts.length(); ++j) { + partialPath += "/" + pathParts[j]; } - textStream << "Script.include(\"" << "https://raw.githubusercontent.com/" << user << "/hifi_tests/" << branch << filename + "\");" << endl; + return partialPath; +} + +void Test::importTest(QTextStream& textStream, const QString& testPathname) { + QString partialPath = extractPathFromTestsDown(testPathname); + textStream << "Script.include(\"" << "https://github.com/" << githubUser + << "/hifi_tests/blob/" << gitHubBranch << partialPath + "?raw=true\");" << endl; } // Creates a single script in a user-selected folder. @@ -353,7 +350,7 @@ void Test::createRecursiveScript(QString topLevelDirectory, bool interactiveMode QFile allTestsFilename(topLevelDirectory + "/" + recursiveTestsFilename); if (!allTestsFilename.open(QIODevice::WriteOnly | QIODevice::Text)) { messageBox.critical(0, - "Internal Error #8", + "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create \"" + recursiveTestsFilename + "\" in directory \"" + topLevelDirectory + "\"" ); @@ -363,7 +360,9 @@ void Test::createRecursiveScript(QString topLevelDirectory, bool interactiveMode QTextStream textStream(&allTestsFilename); textStream << "// This is an automatically generated file, created by auto-tester" << endl << endl; - textStream << "var autoTester = Script.require(\"https://raw.githubusercontent.com/" + user + "/hifi_tests/" + branch + "/tests/utils/autoTester.js\");" << endl; + textStream << "var autoTester = Script.require(\"https://github.com/" + githubUser + "/hifi_tests/blob/" + + gitHubBranch + "/tests/utils/autoTester.js?raw=true\");" << endl; + textStream << "autoTester.enableRecursive();" << endl << endl; QVector testPathnames; @@ -454,6 +453,203 @@ void Test::createTest() { messageBox.information(0, "Success", "Test images have been created"); } +ExtractedText Test::getTestScriptLines(QString testFileName) { + ExtractedText relevantTextFromTest; + + QFile inputFile(testFileName); + inputFile.open(QIODevice::ReadOnly); + if (!inputFile.isOpen()) { + messageBox.critical(0, + "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), + "Failed to open \"" + testFileName + ); + } + + QTextStream stream(&inputFile); + QString line = stream.readLine(); + + // Name of test is the string in the following line: + // autoTester.perform("Apply Material Entities to Avatars", Script.resolvePath("."), function(testType) {... + const QString ws("\\h*"); //white-space character + const QString functionPerformName(ws + "autoTester" + ws + "\\." + ws + "perform"); + const QString quotedString("\\\".+\\\""); + const QString ownPath("Script" + ws + "\\." + ws + "resolvePath" + ws + "\\(" + ws + "\\\"\\.\\\"" + ws + "\\)"); + const QString functionParameter("function" + ws + "\\(testType" + ws + "\\)"); + QString regexTestTitle(ws + functionPerformName + "\\(" + quotedString + "\\," + ws + ownPath + "\\," + ws + functionParameter + ws + "{" + ".*"); + QRegularExpression lineContainingTitle = QRegularExpression(regexTestTitle); + + // Assert platform checks that test is running on the correct OS + const QString functionAssertPlatform(ws + "autoTester" + ws + "\\." + ws + "assertPlatform"); + const QString regexAssertPlatform(ws + functionAssertPlatform + ws + "\\(" + ws + quotedString + ".*"); + const QRegularExpression lineAssertPlatform = QRegularExpression(regexAssertPlatform); + + // Assert display checks that test is running on the correct display + const QString functionAssertDisplay(ws + "autoTester" + ws + "\\." + ws + "assertDisplay"); + const QString regexAssertDisplay(ws + functionAssertDisplay + ws + "\\(" + ws + quotedString + ".*"); + const QRegularExpression lineAssertDisplay = QRegularExpression(regexAssertDisplay); + + // Assert CPU checks that test is running on the correct type of CPU + const QString functionAssertCPU(ws + "autoTester" + ws + "\\." + ws + "assertCPU"); + const QString regexAssertCPU(ws + functionAssertCPU + ws + "\\(" + ws + quotedString + ".*"); + const QRegularExpression lineAssertCPU = QRegularExpression(regexAssertCPU); + + // Assert GPU checks that test is running on the correct type of GPU + const QString functionAssertGPU(ws + "autoTester" + ws + "\\." + ws + "assertGPU"); + const QString regexAssertGPU(ws + functionAssertGPU + ws + "\\(" + ws + quotedString + ".*"); + const QRegularExpression lineAssertGPU = QRegularExpression(regexAssertGPU); + + // Each step is either of the following forms: + // autoTester.addStepSnapshot("Take snapshot"... + // autoTester.addStep("Clean up after test"... + const QString functionAddStepSnapshotName(ws + "autoTester" + ws + "\\." + ws + "addStepSnapshot"); + const QString regexStepSnapshot(ws + functionAddStepSnapshotName + ws + "\\(" + ws + quotedString + ".*"); + const QRegularExpression lineStepSnapshot = QRegularExpression(regexStepSnapshot); + + const QString functionAddStepName(ws + "autoTester" + ws + "\\." + ws + "addStep"); + const QString regexStep(ws + functionAddStepName + ws + "\\(" + ws + quotedString + ws + "\\)" + ".*"); + const QRegularExpression lineStep = QRegularExpression(regexStep); + + while (!line.isNull()) { + line = stream.readLine(); + if (lineContainingTitle.match(line).hasMatch()) { + QStringList tokens = line.split('"'); + relevantTextFromTest.title = tokens[1]; + } else if (lineAssertPlatform.match(line).hasMatch()) { + QStringList platforms = line.split('"'); + relevantTextFromTest.platform = platforms[1]; + } else if (lineAssertDisplay.match(line).hasMatch()) { + QStringList displays = line.split('"'); + relevantTextFromTest.display = displays[1]; + } else if (lineAssertCPU.match(line).hasMatch()) { + QStringList cpus = line.split('"'); + relevantTextFromTest.cpu = cpus[1]; + } else if (lineAssertGPU.match(line).hasMatch()) { + QStringList gpus = line.split('"'); + relevantTextFromTest.gpu = gpus[1]; + } else if (lineStepSnapshot.match(line).hasMatch()) { + QStringList tokens = line.split('"'); + QString nameOfStep = tokens[1]; + + Step *step = new Step(); + step->text = nameOfStep; + step->takeSnapshot = true; + relevantTextFromTest.stepList.emplace_back(step); + } else if (lineStep.match(line).hasMatch()) { + QStringList tokens = line.split('"'); + QString nameOfStep = tokens[1]; + + Step *step = new Step(); + step->text = nameOfStep; + step->takeSnapshot = false; + relevantTextFromTest.stepList.emplace_back(step); + } + } + + inputFile.close(); + + return relevantTextFromTest; +} + +// Create an MD file for a user-selected test. +// The folder selected must contain a script named "test.js", the file produced is named "test.md" +void Test::createMDFile() { + // Folder selection + QString testDirectory = QFileDialog::getExistingDirectory(nullptr, "Please select folder containing the test", ".", QFileDialog::ShowDirsOnly); + if (testDirectory == "") { + return; + } + + // Verify folder contains test.js file + QString testFileName(testDirectory + "/" + TEST_FILENAME); + QFileInfo testFileInfo(testFileName); + if (!testFileInfo.exists()) { + messageBox.critical(0, "Error", "Could not find file: " + TEST_FILENAME); + return; + } + + ExtractedText testScriptLines = getTestScriptLines(testFileName); + + QString mdFilename(testDirectory + "/" + "test.md"); + QFile mdFile(mdFilename); + if (!mdFile.open(QIODevice::WriteOnly)) { + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Failed to create file " + mdFilename); + exit(-1); + } + + QTextStream stream(&mdFile); + + //Test title + QString testName = testScriptLines.title; + stream << "# " << testName << "\n"; + + // Find the relevant part of the path to the test (i.e. from "tests" down + QString partialPath = extractPathFromTestsDown(testDirectory); + + stream << "## Run this script URL: [Manual](./test.js?raw=true) [Auto](./testAuto.js?raw=true)(from menu/Edit/Open and Run scripts from URL...)." << "\n\n"; + + stream << "## Preconditions" << "\n"; + stream << "- In an empty region of a domain with editing rights." << "\n\n"; + + // Platform + QStringList platforms = testScriptLines.platform.split(" ");; + stream << "## Platforms\n"; + stream << "Run the test on each of the following platforms\n"; + for (int i = 0; i < platforms.size(); ++i) { + // Note that the platforms parameter may include extra spaces, these appear as empty strings in the list + if (platforms[i] != QString()) { + stream << " - " << platforms[i] << "\n"; + } + } + + // Display + QStringList displays = testScriptLines.display.split(" "); + stream << "## Displays\n"; + stream << "Run the test on each of the following displays\n"; + for (int i = 0; i < displays.size(); ++i) { + // Note that the displays parameter may include extra spaces, these appear as empty strings in the list + if (displays[i] != QString()) { + stream << " - " << displays[i] << "\n"; + } + } + + // CPU + QStringList cpus = testScriptLines.cpu.split(" "); + stream << "## Processors\n"; + stream << "Run the test on each of the following processors\n"; + for (int i = 0; i < cpus.size(); ++i) { + // Note that the cpus parameter may include extra spaces, these appear as empty strings in the list + if (cpus[i] != QString()) { + stream << " - " << cpus[i] << "\n"; + } + } + + // GPU + QStringList gpus = testScriptLines.gpu.split(" "); + stream << "## Graphics Cards\n"; + stream << "Run the test on graphics cards from each of the following vendors\n"; + for (int i = 0; i < gpus.size(); ++i) { + // Note that the gpus parameter may include extra spaces, these appear as empty strings in the list + if (gpus[i] != QString()) { + stream << " - " << gpus[i] << "\n"; + } + } + + stream << "## Steps\n"; + stream << "Press space bar to advance step by step\n\n"; + + int snapShotIndex { 0 }; + for (size_t i = 0; i < testScriptLines.stepList.size(); ++i) { + stream << "### Step " << QString::number(i) << "\n"; + stream << "- " << testScriptLines.stepList[i + 1]->text << "\n"; + if (testScriptLines.stepList[i]->takeSnapshot) { + stream << "- ![](./ExpectedImage_" << QString::number(snapShotIndex).rightJustified(5, '0') << ".png)\n"; + ++snapShotIndex; + } + } + + mdFile.close(); +} + void Test::copyJPGtoPNG(QString sourceJPGFullFilename, QString destinationPNGFullFilename) { QFile::remove(destinationPNGFullFilename); @@ -526,7 +722,7 @@ QString Test::getExpectedImagePartialSourceDirectory(QString filename) { } if (i < 0) { - messageBox.critical(0, "Internal error #9", "Bad filename"); + messageBox.critical(0, "Internal error: " + QString(__FILE__) + ":" + QString::number(__LINE__), "Bad filename"); exit(-1); } diff --git a/tools/auto-tester/src/Test.h b/tools/auto-tester/src/Test.h index cd5075002a..3d04b00df9 100644 --- a/tools/auto-tester/src/Test.h +++ b/tools/auto-tester/src/Test.h @@ -19,6 +19,24 @@ #include "ImageComparer.h" #include "ui/MismatchWindow.h" +class Step { +public: + QString text; + bool takeSnapshot; +}; + +using StepList = std::vector; + +class ExtractedText { +public: + QString title; + QString platform; + QString display; + QString cpu; + QString gpu; + StepList stepList; +}; + class Test { public: Test(); @@ -31,7 +49,7 @@ public: void createRecursiveScript(QString topLevelDirectory, bool interactiveMode); void createTest(); - void deleteOldSnapshots(); + void createMDFile(); bool compareImageLists(bool isInteractiveMode, QProgressBar* progressBar); @@ -47,7 +65,7 @@ public: void zipAndDeleteTestResultsFolder(); bool isAValidDirectory(QString pathname); - + QString extractPathFromTestsDown(QString fullPath); QString getExpectedImageDestinationDirectory(QString filename); QString getExpectedImagePartialSourceDirectory(QString filename); @@ -62,8 +80,6 @@ private: QDir imageDirectory; - QRegularExpression expectedImageFilenameFormat; - MismatchWindow mismatchWindow; ImageComparer imageComparer; @@ -81,9 +97,11 @@ private: QStringList resultImagesFullFilenames; // Used for accessing GitHub - const QString user { "NissimHadar" }; - const QString branch { "addRecursionToAutotester" }; + const QString githubUser{ "highfidelity" }; + const QString gitHubBranch { "master" }; const QString DATETIME_FORMAT { "yyyy-MM-dd_hh-mm-ss" }; + + ExtractedText getTestScriptLines(QString testFileName); }; #endif // hifi_test_h \ No newline at end of file diff --git a/tools/auto-tester/src/ui/AutoTester.cpp b/tools/auto-tester/src/ui/AutoTester.cpp index a5e13331dd..9153365184 100644 --- a/tools/auto-tester/src/ui/AutoTester.cpp +++ b/tools/auto-tester/src/ui/AutoTester.cpp @@ -33,7 +33,11 @@ void AutoTester::on_createRecursiveScriptsRecursivelyButton_clicked() { } void AutoTester::on_createTestButton_clicked() { - test->createTest(); + test->createTest(); +} + +void AutoTester::on_createMDFileButton_clicked() { + test->createMDFile(); } void AutoTester::on_closeButton_clicked() { diff --git a/tools/auto-tester/src/ui/AutoTester.h b/tools/auto-tester/src/ui/AutoTester.h index 938e7ca2d2..82ff3780e3 100644 --- a/tools/auto-tester/src/ui/AutoTester.h +++ b/tools/auto-tester/src/ui/AutoTester.h @@ -29,8 +29,9 @@ private slots: void on_evaluateTestsButton_clicked(); void on_createRecursiveScriptButton_clicked(); void on_createRecursiveScriptsRecursivelyButton_clicked(); - void on_createTestButton_clicked(); - void on_closeButton_clicked(); + void on_createTestButton_clicked(); + void on_createMDFileButton_clicked(); + void on_closeButton_clicked(); void saveImage(int index); diff --git a/tools/auto-tester/src/ui/AutoTester.ui b/tools/auto-tester/src/ui/AutoTester.ui index 55c3897e58..600de283ad 100644 --- a/tools/auto-tester/src/ui/AutoTester.ui +++ b/tools/auto-tester/src/ui/AutoTester.ui @@ -7,7 +7,7 @@ 0 0 607 - 395 + 514 @@ -18,7 +18,7 @@ 20 - 300 + 420 220 40 @@ -44,7 +44,7 @@ 20 - 135 + 255 220 40 @@ -70,7 +70,7 @@ 23 - 100 + 220 131 20 @@ -86,7 +86,7 @@ 20 - 190 + 310 255 23 @@ -108,6 +108,19 @@ Create Recursive Scripts Recursively + + + + 20 + 90 + 220 + 40 + + + + Create MD file + + diff --git a/tools/bake-tools/bake.py b/tools/bake-tools/bake.py new file mode 100644 index 0000000000..0c8d5e1048 --- /dev/null +++ b/tools/bake-tools/bake.py @@ -0,0 +1,91 @@ +import os, json, sys, shutil, subprocess, shlex, time +EXE = os.environ['HIFI_OVEN'] + +def listFiles(directory, extension): + items = os.listdir(directory) + fileList = [] + for f in items: + if f.endswith('.' + extension): + fileList.append(f) + return fileList + +def camelCaseString(string): + string = string.replace('-', ' ') + return ''.join(x for x in string.title() if not x.isspace()) + +def groupFiles(originalDirectory, newDirectory, files): + for file in files: + newPath = os.sep.join([newDirectory, file]) + originalPath = os.sep.join([originalDirectory, file]) + shutil.move(originalPath, newPath) + +def groupKTXFiles(directory, filePath): + baseFile = os.path.basename(filePath) + filename = os.path.splitext(baseFile)[0] + camelCaseFileName = camelCaseString(filename) + path = os.sep.join([directory, camelCaseFileName]) + files = listFiles(directory, 'ktx') + if len(files) > 0: + createDirectory(path) + groupFiles(directory, path, files) + + newFilePath = os.sep.join([path, baseFile+'.baked.fbx']) + originalFilePath = os.sep.join([directory, baseFile+'.baked.fbx']) + originalFilePath.strip() + shutil.move(originalFilePath, newFilePath) + +def bakeFile(filePath, outputDirectory): + createDirectory(outputDirectory) + cmd = EXE + ' -i ' + filePath + ' -o ' + outputDirectory + ' -t fbx' + args = shlex.split(cmd) + process = subprocess.Popen(cmd, stdout=False, stderr=False) + process.wait() + bakedFile = os.path.splitext(filePath)[0] + groupKTXFiles(outputDirectory, bakedFile) + +def bakeFilesInDirectory(directory, outputDirectory): + for root, subFolders, filenames in os.walk(directory): + for filename in filenames: + if filename.endswith('.fbx'): + filePath = os.sep.join([root, filename]) + absFilePath = os.path.abspath(filePath) + outputFolder = os.path.join(outputDirectory, os.path.relpath(root)) + print "Baking file: " + filename + bakeFile(absFilePath, outputFolder) + else: + filePath = os.sep.join([root, filename]) + absFilePath = os.path.abspath(filePath) + outputFolder = os.path.join(outputDirectory, os.path.relpath(root)) + newFilePath = os.sep.join([outputFolder, filename]) + createDirectory(outputFolder) + print "moving file: " + filename + " to: " + outputFolder + shutil.copy(absFilePath, newFilePath) + +def createDirectory(directory): + if not os.path.exists(directory): + os.makedirs(directory) + +def checkIfExeExists(): + if not os.path.isfile(EXE) and os.access(EXE, os.X_OK): + print 'HIFI_OVEN evironment variable is not set' + sys.exit() + +def handleOptions(): + option = sys.argv[1] + if option == '--help' or option == '-h': + print 'Usage: bake.py INPUT_DIRECTORY[directory to bake] OUTPUT_DIRECTORY[directory to place backed files]' + print 'Note: Output directory will be created if directory does not exist' + sys.exit() + +def main(): + argsLength = len(sys.argv) + if argsLength == 3: + checkIfExeExists() + rootDirectory = sys.argv[1] + outputDirectory = os.path.abspath(sys.argv[2]) + createDirectory(outputDirectory) + bakeFilesInDirectory(rootDirectory, outputDirectory) + elif argsLength == 2: + handleOptions() + +main() diff --git a/tools/bake-tools/convertToRelativePaths.py b/tools/bake-tools/convertToRelativePaths.py new file mode 100644 index 0000000000..27a7b7ac02 --- /dev/null +++ b/tools/bake-tools/convertToRelativePaths.py @@ -0,0 +1,82 @@ +import json, os, sys, gzip + +prefix = 'file:///~/' +MAP = {} +def createAssetMapping(assetDirectory): + baseDirectory = os.path.basename(os.path.normpath(assetDirectory)) + for root, subfolder, filenames in os.walk(assetDirectory): + for filename in filenames: + if not filename.endswith('.ktx'): + substring = os.path.commonprefix([assetDirectory, root]) + newPath = root.replace(substring, ''); + filePath = os.sep.join([newPath, filename]) + if filePath[0] == '\\': + filePath = filePath[1:] + finalPath = prefix + baseDirectory + '/' + filePath + finalPath = finalPath.replace('\\', '/') + file = os.path.splitext(filename)[0] + file = os.path.splitext(file)[0] + MAP[file] = finalPath + +def hasURL(prop): + if "URL" in prop: + return True + return False + + +def handleURL(url): + newUrl = url + if "atp:" in url: + baseFilename = os.path.basename(url) + filename = os.path.splitext(baseFilename)[0] + newUrl = MAP[filename] + print newUrl + return newUrl + +def handleOptions(): + option = sys.argv[1] + if option == '--help' or option == '-h': + print 'Usage: convertToRelativePaths.py INPUT[json file you want to update the urls] INPUT[directory that the baked files are located in]' + sys.exit() + +def main(): + argsLength = len(sys.argv) + if argsLength == 3: + jsonFile = sys.argv[1] + gzipFile = jsonFile + '.gz' + assetDirectory = sys.argv[2] + createAssetMapping(assetDirectory) + f = open(jsonFile) + data = json.load(f) + f.close() + for entity in data['Entities']: + for prop in entity: + value = entity[prop] + if hasURL(prop): + value = handleURL(value) + if prop == "script": + value = handleURL(value) + if prop == "textures": + try: + tmp = json.loads(value) + for index in tmp: + tmp[index] = handleURL(tmp[index]) + value = json.dumps(tmp) + except: + value = handleURL(value) + + if prop == "serverScripts": + value = handleURL(value) + + entity[prop] = value + + + jsonString = json.dumps(data) + jsonBytes= jsonString.encode('utf-8') + with gzip.GzipFile(gzipFile, 'w') as fout: # 4. gzip + fout.write(jsonBytes) + + elif argsLength == 2: + handleOptions() + +main()