diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 91c2dba70e..27b7d0d302 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -1124,15 +1124,11 @@ void AvatarMixer::handleOctreePacket(QSharedPointer message, Sh } case PacketType::EntityData: - if (senderNode->getCanRezAvatarEntities()) { - _entityViewer.processDatagram(*message, senderNode); - } + _entityViewer.processDatagram(*message, senderNode); break; case PacketType::EntityErase: - if (senderNode->getCanRezAvatarEntities()) { - _entityViewer.processEraseMessage(*message, senderNode); - } + _entityViewer.processEraseMessage(*message, senderNode); break; default: diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index f86dc7f766..ac4ad11ecc 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -269,7 +269,14 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message, // the avatar mixer uses the negative value of the sent version instanceVersionRef = -packetTraitVersion; } else { - _avatar->processTraitInstance(traitType, instanceID, message.read(traitSize)); + // Don't accept avatar entity data for distribution unless sender has rez permissions on the domain. + // The sender shouldn't be sending avatar entity data, however this provides a back-up. + if (sendingNode.getCanRezAvatarEntities()) { + _avatar->processTraitInstance(traitType, instanceID, message.read(traitSize)); + } else { + message.read(traitSize); + } + instanceVersionRef = packetTraitVersion; } @@ -290,6 +297,27 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message, } } +void AvatarMixerClientData::emulateDeleteEntitiesTraitsMessage(const QList& avatarEntityIDs) { + // Emulates processSetTraitsMessage() actions on behalf of an avatar whose canRezAvatarEntities permission has been removed. + // The source avatar should be removing its avatar entities. However, this provides a back-up. + + for (const auto& entityID : avatarEntityIDs) { + auto traitType = AvatarTraits::AvatarEntity; + auto& instanceVersionRef = _lastReceivedTraitVersions.getInstanceValueRef(traitType, entityID); + + _avatar->processDeletedTraitInstance(traitType, entityID); + // Mixer doesn't need deleted IDs. + _avatar->getAndClearRecentlyRemovedIDs(); + + // to track a deleted instance but keep version information + // the avatar mixer uses the negative value of the sent version + // Because there is no originating message from an avatar we enlarge the magnitude by 1. + instanceVersionRef = -instanceVersionRef - 1; + } + + _lastReceivedTraitsChange = std::chrono::steady_clock::now(); +} + void AvatarMixerClientData::processBulkAvatarTraitsAckMessage(ReceivedMessage& message) { // Avatar Traits flow control marks each outgoing avatar traits packet with a // sequence number. The mixer caches the traits sent in the traits packet. diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index 98c8d7e15b..83a2ff384a 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -132,6 +132,7 @@ public: int processPackets(const SlaveSharedData& slaveSharedData); // returns number of packets processed void processSetTraitsMessage(ReceivedMessage& message, const SlaveSharedData& slaveSharedData, Node& sendingNode); + void emulateDeleteEntitiesTraitsMessage(const QList& avatarEntityIDs); void processBulkAvatarTraitsAckMessage(ReceivedMessage& message); void checkSkeletonURLAgainstWhitelist(const SlaveSharedData& slaveSharedData, Node& sendingNode, AvatarTraits::TraitVersion traitVersion); diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index 522f0bf163..9a3ef3d0b5 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -432,6 +432,17 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) } } + // The source avatar should be removing its avatar entities. However, provide a back-up. + if (sendAvatar) { + if (!sourceAvatarNode->getCanRezAvatarEntities()) { + auto sourceAvatarNodeData = reinterpret_cast(sourceAvatarNode->getLinkedData()); + auto avatarEntityIDs = sourceAvatarNodeData->getAvatar().getAvatarEntityIDs(); + if (avatarEntityIDs.count() > 0) { + sourceAvatarNodeData->emulateDeleteEntitiesTraitsMessage(avatarEntityIDs); + } + } + } + if (sendAvatar) { AvatarDataSequenceNumber lastSeqToReceiver = destinationNodeData->getLastBroadcastSequenceNumber(sourceAvatarNode->getLocalID()); AvatarDataSequenceNumber lastSeqFromSender = sourceAvatarNodeData->getLastReceivedSequenceNumber(); diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 598b4870fb..6d4dce8322 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 2.4, + "version": 2.5, "settings": [ { "name": "metaverse", @@ -338,7 +338,7 @@ "name": "standard_permissions", "type": "table", "label": "Domain-Wide User Permissions", - "help": "Indicate which types of users can have which domain-wide permissions.", + "help": "Indicate which types of users can have which domain-wide permissions.", "caption": "Standard Permissions", "can_add_new_rows": false, "groups": [ @@ -347,8 +347,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -365,7 +365,7 @@ }, { "name": "id_can_rez_avatar_entities", - "label": "Rez Attachments", + "label": "Rez Avatar Entities", "type": "checkbox", "editable": true, "default": false @@ -492,8 +492,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -535,7 +535,7 @@ }, { "name": "id_can_rez_avatar_entities", - "label": "Rez Attachments", + "label": "Rez Avatar Entities", "type": "checkbox", "editable": true, "default": false @@ -628,8 +628,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -668,7 +668,7 @@ }, { "name": "id_can_rez_avatar_entities", - "label": "Rez Attachments", + "label": "Rez Avatar Entities", "type": "checkbox", "editable": true, "default": false @@ -756,8 +756,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -774,7 +774,7 @@ }, { "name": "id_can_rez_avatar_entities", - "label": "Rez Attachments", + "label": "Rez Avatar Entities", "type": "checkbox", "editable": true, "default": false @@ -862,8 +862,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -880,7 +880,7 @@ }, { "name": "id_can_rez_avatar_entities", - "label": "Rez Attachments", + "label": "Rez Avatar Entities", "type": "checkbox", "editable": true, "default": false @@ -968,8 +968,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -986,7 +986,7 @@ }, { "name": "id_can_rez_avatar_entities", - "label": "Rez Attachments", + "label": "Rez Avatar Entities", "type": "checkbox", "editable": true, "default": false @@ -1074,8 +1074,8 @@ "span": 1 }, { - "label": "Permissions ?", - "span": 11 + "label": "Permissions ?", + "span": 12 } ], "columns": [ @@ -1092,7 +1092,7 @@ }, { "name": "id_can_rez_avatar_entities", - "label": "Rez Attachments", + "label": "Rez Avatar Entities", "type": "checkbox", "editable": true, "default": false diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 42a0ffa64e..653e536195 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -363,9 +363,6 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena foreach (auto permissionsSet, permissionsSets) { for (auto entry : permissionsSet) { const auto& userKey = entry.first; - - permissionsSet[userKey]->set(NodePermissions::Permission::canRezAvatarEntities); - if (onlyEditorsAreRezzers) { if (permissionsSet[userKey]->can(NodePermissions::Permission::canAdjustLocks)) { permissionsSet[userKey]->set(NodePermissions::Permission::canRezPermanentEntities); @@ -530,6 +527,28 @@ void DomainServerSettingsManager::setupConfigMap(const QString& userConfigFilena *newAdminRoles = adminRoles; } + if (oldVersion < 2.5) { + // Default values for new canRezAvatarEntities permission. + unpackPermissions(); + std::list> permissionsSets{ + _standardAgentPermissions.get(), + _agentPermissions.get(), + _ipPermissions.get(), + _macPermissions.get(), + _machineFingerprintPermissions.get(), + _groupPermissions.get(), + _groupForbiddens.get() + }; + foreach (auto permissionsSet, permissionsSets) { + for (auto entry : permissionsSet) { + const auto& userKey = entry.first; + if (permissionsSet[userKey]->can(NodePermissions::Permission::canConnectToDomain)) { + permissionsSet[userKey]->set(NodePermissions::Permission::canRezAvatarEntities); + } + } + } + packPermissions(); + } // write the current description version to our settings *versionVariant = _descriptionVersion; @@ -1451,6 +1470,8 @@ QJsonObject DomainServerSettingsManager::settingsResponseObjectForType(const QSt SettingsBackupFlag settingsBackupFlag) { QJsonObject responseObject; + responseObject["version"] = _descriptionVersion; // Domain settings version number. + if (!typeValue.isEmpty() || authentication == Authenticated) { // convert the string type value to a QJsonValue QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt()); diff --git a/interface/resources/qml/hifi/AvatarApp.qml b/interface/resources/qml/hifi/AvatarApp.qml index cfc1121af9..2c8d62a598 100644 --- a/interface/resources/qml/hifi/AvatarApp.qml +++ b/interface/resources/qml/hifi/AvatarApp.qml @@ -518,7 +518,12 @@ Rectangle { glyphText: "\ue02e" onClicked: { - adjustWearables.open(currentAvatar); + if (!AddressManager.isConnected || Entities.canRezAvatarEntities()) { + adjustWearables.open(currentAvatar); + } else { + Window.alert("You cannot use wearables on this domain.") + } + } } @@ -529,7 +534,11 @@ Rectangle { glyphText: wearablesFrozen ? hifi.glyphs.lock : hifi.glyphs.unlock; onClicked: { - emitSendToScript({'method' : 'toggleWearablesFrozen'}); + if (!AddressManager.isConnected || Entities.canRezAvatarEntities()) { + emitSendToScript({'method' : 'toggleWearablesFrozen'}); + } else { + Window.alert("You cannot use wearables on this domain.") + } } } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 06ce6c3d6c..dd1240058a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1306,6 +1306,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _entityServerConnectionTimer.setSingleShot(true); connect(&_entityServerConnectionTimer, &QTimer::timeout, this, &Application::setFailedToConnectToEntityServer); + connect(&domainHandler, &DomainHandler::confirmConnectWithoutAvatarEntities, + this, &Application::confirmConnectWithoutAvatarEntities); + connect(&domainHandler, &DomainHandler::connectedToDomain, this, [this]() { if (!isServerlessMode()) { _entityServerConnectionTimer.setInterval(ENTITY_SERVER_ADDED_TIMEOUT); @@ -9172,6 +9175,32 @@ void Application::setShowBulletConstraintLimits(bool value) { _physicsEngine->setShowBulletConstraintLimits(value); } +void Application::confirmConnectWithoutAvatarEntities() { + + if (_confirmConnectWithoutAvatarEntitiesDialog) { + // Dialog is already displayed. + return; + } + + if (!getMyAvatar()->hasAvatarEntities()) { + // No avatar entities so continue with login. + DependencyManager::get()->getDomainHandler().setCanConnectWithoutAvatarEntities(true); + return; + } + + QString continueMessage = "Your wearables will not display on this domain. Continue?"; + _confirmConnectWithoutAvatarEntitiesDialog = OffscreenUi::asyncQuestion("Continue Without Wearables", continueMessage, + QMessageBox::Yes | QMessageBox::No); + if (_confirmConnectWithoutAvatarEntitiesDialog->getDialogItem()) { + QObject::connect(_confirmConnectWithoutAvatarEntitiesDialog, &ModalDialogListener::response, this, [=](QVariant answer) { + QObject::disconnect(_confirmConnectWithoutAvatarEntitiesDialog, &ModalDialogListener::response, this, nullptr); + _confirmConnectWithoutAvatarEntitiesDialog = nullptr; + bool shouldConnect = (static_cast(answer.toInt()) == QMessageBox::Yes); + DependencyManager::get()->getDomainHandler().setCanConnectWithoutAvatarEntities(shouldConnect); + }); + } +} + void Application::createLoginDialog() { const glm::vec3 LOGIN_DIMENSIONS { 0.89f, 0.5f, 0.01f }; const auto OFFSET = glm::vec2(0.7f, -0.1f); diff --git a/interface/src/Application.h b/interface/src/Application.h index 5cb5fdd5c0..55e412ee9f 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -50,6 +50,7 @@ #include #include #include +#include #include "avatar/MyAvatar.h" #include "FancyCamera.h" @@ -325,6 +326,8 @@ public: int getOtherAvatarsReplicaCount() { return DependencyManager::get()->getReplicaCount(); } void setOtherAvatarsReplicaCount(int count) { DependencyManager::get()->setReplicaCount(count); } + void confirmConnectWithoutAvatarEntities(); + bool getLoginDialogPoppedUp() const { return _loginDialogPoppedUp; } void createLoginDialog(); void updateLoginDialogPosition(); @@ -723,6 +726,8 @@ private: bool _loginDialogPoppedUp{ false }; bool _desktopRootItemCreated{ false }; + ModalDialogListener* _confirmConnectWithoutAvatarEntitiesDialog { nullptr }; + bool _developerMenuVisible{ false }; QString _previousAvatarSkeletonModel; float _previousAvatarTargetScale; diff --git a/interface/src/ConnectionMonitor.cpp b/interface/src/ConnectionMonitor.cpp index 070015f05b..cd5235e42b 100644 --- a/interface/src/ConnectionMonitor.cpp +++ b/interface/src/ConnectionMonitor.cpp @@ -35,6 +35,7 @@ void ConnectionMonitor::init() { connect(&domainHandler, &DomainHandler::connectedToDomain, this, &ConnectionMonitor::stopTimer); connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &ConnectionMonitor::stopTimer); connect(&domainHandler, &DomainHandler::redirectToErrorDomainURL, this, &ConnectionMonitor::stopTimer); + connect(&domainHandler, &DomainHandler::confirmConnectWithoutAvatarEntities, this, &ConnectionMonitor::stopTimer); connect(this, &ConnectionMonitor::setRedirectErrorState, &domainHandler, &DomainHandler::setRedirectErrorState); auto accountManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::loginComplete, this, &ConnectionMonitor::startTimer); diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 0f66f3bb41..46f2ad3be8 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -278,6 +278,9 @@ MyAvatar::MyAvatar(QThread* thread) : // when we leave a domain we lift whatever restrictions that domain may have placed on our scale connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, &MyAvatar::leaveDomain); + auto nodeList = DependencyManager::get(); + connect(nodeList.data(), &NodeList::canRezAvatarEntitiesChanged, this, &MyAvatar::handleCanRezAvatarEntitiesChanged); + _bodySensorMatrix = deriveBodyFromHMDSensor(); using namespace recording; @@ -1533,7 +1536,19 @@ void MyAvatar::storeAvatarEntityDataPayload(const QUuid& entityID, const QByteAr void MyAvatar::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) { // NOTE: the requiresRemovalFromTree argument is unused + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring clearAvatarEntity() because don't have canRezAvatarEntities permission on domain"; + return; + } + AvatarData::clearAvatarEntity(entityID); + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + // Don't delete potentially non-rezzed avatar entities from cache, otherwise they're removed from settings. + return; + } + _avatarEntitiesLock.withWriteLock([&] { _cachedAvatarEntityBlobsToDelete.push_back(entityID); }); @@ -1564,6 +1579,29 @@ void MyAvatar::sanitizeAvatarEntityProperties(EntityItemProperties& properties) properties.markAllChanged(); } +void MyAvatar::addAvatarEntitiesToTree() { + AvatarEntityMap::const_iterator constItr = _cachedAvatarEntityBlobs.begin(); + while (constItr != _cachedAvatarEntityBlobs.end()) { + QUuid id = constItr.key(); + _entitiesToAdd.push_back(id); // worked once: hat shown. then unshown when permissions removed but then entity was deleted somewhere along the line! + ++constItr; + } +} + +bool MyAvatar::hasAvatarEntities() const { + return _cachedAvatarEntityBlobs.count() > 0; +} + +void MyAvatar::handleCanRezAvatarEntitiesChanged(bool canRezAvatarEntities) { + if (canRezAvatarEntities) { + // Start displaying avatar entities. + addAvatarEntitiesToTree(); + } else { + // Stop displaying avatar entities. + removeAvatarEntitiesFromTree(); + } +} + void MyAvatar::handleChangedAvatarEntityData() { // NOTE: this is a per-frame update if (getID().isNull() || @@ -1583,6 +1621,8 @@ void MyAvatar::handleChangedAvatarEntityData() { return; } + bool canRezAvatarEntites = DependencyManager::get()->getThisNodeCanRezAvatarEntities(); + // We collect changes to AvatarEntities and then handle them all in one spot per frame: handleChangedAvatarEntityData(). // Basically this is a "transaction pattern" with an extra complication: these changes can come from two // "directions" and the "authoritative source" of each direction is different, so we maintain two distinct sets @@ -1669,12 +1709,15 @@ void MyAvatar::handleChangedAvatarEntityData() { continue; } sanitizeAvatarEntityProperties(properties); - entityTree->withWriteLock([&] { - EntityItemPointer entity = entityTree->addEntity(id, properties); - if (entity) { - packetSender->queueEditAvatarEntityMessage(entityTree, id); - } - }); + if (canRezAvatarEntites) { + entityTree->withWriteLock([&] { + EntityItemPointer entity = entityTree->addEntity(id, properties); + if (entity) { + packetSender->queueEditAvatarEntityMessage(entityTree, id); + } + }); + } + } // CHANGE real entities @@ -1692,7 +1735,7 @@ void MyAvatar::handleChangedAvatarEntityData() { skip = true; } }); - if (!skip) { + if (!skip && canRezAvatarEntites) { sanitizeAvatarEntityProperties(properties); entityTree->withWriteLock([&] { if (entityTree->updateEntity(id, properties)) { @@ -1834,6 +1877,11 @@ AvatarEntityMap MyAvatar::getAvatarEntityData() const { return data; } + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring getAvatarEntityData() because don't have canRezAvatarEntities permission on domain"; + return data; + } + QList avatarEntityIDs; _avatarEntitiesLock.withReadLock([&] { avatarEntityIDs = _packedAvatarEntityData.keys(); @@ -1879,6 +1927,12 @@ void MyAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { // avatarEntityData is expected to be a map of QByteArrays that represent EntityItemProperties objects from JavaScript, // aka: unfortunately-formatted-binary-blobs because we store them in non-human-readable format in Settings. // + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring setAvatarEntityData() because don't have canRezAvatarEntities permission on domain"; + return; + } + if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) { // the data is suspect qCDebug(interfaceapp) << "discard suspect AvatarEntityData with size =" << avatarEntityData.size(); @@ -1939,6 +1993,12 @@ void MyAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) { void MyAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) { // NOTE: this is an invokable Script call + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring updateAvatarEntity() because don't have canRezAvatarEntities permission on domain"; + return; + } + bool changed = false; _avatarEntitiesLock.withWriteLock([&] { auto data = QJsonDocument::fromBinaryData(entityData); @@ -2565,6 +2625,13 @@ QVariantList MyAvatar::getAvatarEntitiesVariant() { QVariantList avatarEntitiesData; auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; + + if (entityTree && !DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) + << "Ignoring getAvatarEntitiesVariant() because don't have canRezAvatarEntities permission on domain"; + return avatarEntitiesData; + } + if (entityTree) { QList avatarEntityIDs; _avatarEntitiesLock.withReadLock([&] { @@ -2897,6 +2964,11 @@ void MyAvatar::attach(const QString& modelURL, const QString& jointName, ); return; } + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring attach() because don't have canRezAvatarEntities permission on domain"; + return; + } + AttachmentData data; data.modelURL = modelURL; data.jointName = jointName; @@ -2918,6 +2990,11 @@ void MyAvatar::detachOne(const QString& modelURL, const QString& jointName) { ); return; } + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring detachOne() because don't have canRezAvatarEntities permission on domain"; + return; + } + QUuid entityID; if (findAvatarEntity(modelURL, jointName, entityID)) { DependencyManager::get()->deleteEntity(entityID); @@ -2933,6 +3010,11 @@ void MyAvatar::detachAll(const QString& modelURL, const QString& jointName) { ); return; } + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring detachAll() because don't have canRezAvatarEntities permission on domain"; + return; + } + QUuid entityID; while (findAvatarEntity(modelURL, jointName, entityID)) { DependencyManager::get()->deleteEntity(entityID); @@ -2946,6 +3028,11 @@ void MyAvatar::setAttachmentData(const QVector& attachmentData) Q_ARG(const QVector&, attachmentData)); return; } + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring setAttachmentData() because don't have canRezAvatarEntities permission on domain"; + return; + } + std::vector newEntitiesProperties; for (auto& data : attachmentData) { QUuid entityID; @@ -2968,6 +3055,12 @@ void MyAvatar::setAttachmentData(const QVector& attachmentData) QVector MyAvatar::getAttachmentData() const { QVector attachmentData; + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) << "Ignoring getAttachmentData() because don't have canRezAvatarEntities permission on domain"; + return attachmentData; + } + QList avatarEntityIDs; _avatarEntitiesLock.withReadLock([&] { avatarEntityIDs = _packedAvatarEntityData.keys(); @@ -2982,6 +3075,13 @@ QVector MyAvatar::getAttachmentData() const { QVariantList MyAvatar::getAttachmentsVariant() const { QVariantList result; + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) + << "Ignoring getAttachmentsVariant() because don't have canRezAvatarEntities permission on domain"; + return result; + } + for (const auto& attachment : getAttachmentData()) { result.append(attachment.toVariant()); } @@ -2994,6 +3094,13 @@ void MyAvatar::setAttachmentsVariant(const QVariantList& variant) { Q_ARG(const QVariantList&, variant)); return; } + + if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) { + qCDebug(interfaceapp) + << "Ignoring setAttachmentsVariant() because don't have canRezAvatarEntities permission on domain"; + return; + } + QVector newAttachments; newAttachments.reserve(variant.size()); for (const auto& attachmentVar : variant) { @@ -4065,7 +4172,8 @@ float MyAvatar::getGravity() { void MyAvatar::setSessionUUID(const QUuid& sessionUUID) { QUuid oldSessionID = getSessionUUID(); Avatar::setSessionUUID(sessionUUID); - bool sendPackets = !DependencyManager::get()->getSessionUUID().isNull(); + bool sendPackets = !DependencyManager::get()->getSessionUUID().isNull() + && DependencyManager::get()->getThisNodeCanRezAvatarEntities(); if (!sendPackets) { return; } diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 3d278cf983..ca5ed3b73c 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1454,6 +1454,7 @@ public: void removeWornAvatarEntity(const EntityItemID& entityID); void clearWornAvatarEntities(); + bool hasAvatarEntities() const; /**jsdoc * Checks whether your avatar is flying. @@ -1939,6 +1940,8 @@ public: void avatarEntityDataToJson(QJsonObject& root) const override; + void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override; + /**jsdoc * @comment Uses the base class's JSDoc. */ @@ -2277,12 +2280,6 @@ public slots: */ bool getEnableMeshVisible() const override; - /**jsdoc - * @function MyAvatar.storeAvatarEntityDataPayload - * @deprecated This function is deprecated and will be removed. - */ - void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override; - /**jsdoc * @comment Uses the base class's JSDoc. */ @@ -2656,6 +2653,7 @@ private slots: protected: void handleChangedAvatarEntityData(); + void handleCanRezAvatarEntitiesChanged(bool canRezAvatarEntities); virtual void beParentOfChild(SpatiallyNestablePointer newChild) const override; virtual void forgetChild(SpatiallyNestablePointer newChild) const override; virtual void recalculateChildCauterization() const override; @@ -2710,6 +2708,7 @@ private: void attachmentDataToEntityProperties(const AttachmentData& data, EntityItemProperties& properties); AttachmentData entityPropertiesToAttachmentData(const EntityItemProperties& properties) const; bool findAvatarEntity(const QString& modelURL, const QString& jointName, QUuid& entityID); + void addAvatarEntitiesToTree(); bool cameraInsideHead(const glm::vec3& cameraPosition) const; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index adb7222ee3..234cc67c72 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -3046,6 +3046,24 @@ void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFr } } +void AvatarData::clearAvatarEntities() { + QList avatarEntityIDs; + _avatarEntitiesLock.withReadLock([&] { + avatarEntityIDs = _packedAvatarEntityData.keys(); + }); + for (const auto& entityID : avatarEntityIDs) { + clearAvatarEntity(entityID); + } +} + +QList AvatarData::getAvatarEntityIDs() const { + QList avatarEntityIDs; + _avatarEntitiesLock.withReadLock([&] { + avatarEntityIDs = _packedAvatarEntityData.keys(); + }); + return avatarEntityIDs; +} + AvatarEntityMap AvatarData::getAvatarEntityData() const { // overridden where needed // NOTE: the return value is expected to be a map of unfortunately-formatted-binary-blobs diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 2e25c9559c..fffe8460f1 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -1186,6 +1186,10 @@ public: * @deprecated This function is deprecated and will be removed. */ Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true); + + void clearAvatarEntities(); + + QList getAvatarEntityIDs() const; /**jsdoc * Enables blend shapes set using {@link Avatar.setBlendshape} or {@link MyAvatar.setBlendshape} to be transmitted to other diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index a5bffecb14..d87bab97cd 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -487,6 +487,15 @@ QUuid EntityScriptingInterface::addEntityInternal(const EntityItemProperties& pr _activityTracking.addedEntityCount++; + auto nodeList = DependencyManager::get(); + + if (entityHostType == entity::HostType::AVATAR && !nodeList->getThisNodeCanRezAvatarEntities()) { + qCDebug(entities) << "Ignoring addEntity() because don't have canRezAvatarEntities permission on domain"; + // Only need to intercept methods that may add an avatar entity because avatar entities are removed from the tree when + // the user doesn't have canRezAvatarEntities permission. + return QUuid(); + } + EntityItemProperties propertiesWithSimID = properties; propertiesWithSimID.setEntityHostType(entityHostType); if (entityHostType == entity::HostType::AVATAR) { @@ -499,7 +508,6 @@ QUuid EntityScriptingInterface::addEntityInternal(const EntityItemProperties& pr } // the created time will be set in EntityTree::addEntity by recordCreationTime() - auto nodeList = DependencyManager::get(); auto sessionID = nodeList->getSessionUUID(); propertiesWithSimID.setLastEditedBy(sessionID); diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp index 6a47d74864..5a1d8fb4a0 100644 --- a/libraries/networking/src/DomainHandler.cpp +++ b/libraries/networking/src/DomainHandler.cpp @@ -126,6 +126,8 @@ void DomainHandler::hardReset(QString reason) { emit resetting(); softReset(reason); + _haveAskedConnectWithoutAvatarEntities = false; + _canConnectWithoutAvatarEntities = false; _isInErrorState = false; emit redirectErrorStateChanged(_isInErrorState); @@ -364,10 +366,14 @@ void DomainHandler::setIsConnected(bool isConnected) { _lastDomainConnectionError = -1; emit connectedToDomain(_domainURL); + // FIXME: Reinstate the requestDomainSettings() call here in version 2021.2.0 instead of having it in + // NodeList::processDomainServerList(). + /* if (_domainURL.scheme() == URL_SCHEME_HIFI && !_domainURL.host().isEmpty()) { // we've connected to new domain - time to ask it for global settings requestDomainSettings(); } + */ } else { emit disconnectedFromDomain(); @@ -375,6 +381,24 @@ void DomainHandler::setIsConnected(bool isConnected) { } } +void DomainHandler::setCanConnectWithoutAvatarEntities(bool canConnect) { + _canConnectWithoutAvatarEntities = canConnect; + _haveAskedConnectWithoutAvatarEntities = true; +} + +bool DomainHandler::canConnectWithoutAvatarEntities() { + if (!_canConnectWithoutAvatarEntities && !_haveAskedConnectWithoutAvatarEntities) { + if (_isConnected) { + // Already connected so don't ask. (Permission removed from user while in the domain.) + setCanConnectWithoutAvatarEntities(true); + } else { + // Ask whether to connect to the domain. + emit confirmConnectWithoutAvatarEntities(); + } + } + return _canConnectWithoutAvatarEntities; +} + void DomainHandler::connectedToServerless(std::map namedPaths) { _namedPaths = namedPaths; setIsConnected(true); diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h index 56d32d8609..923b5913e7 100644 --- a/libraries/networking/src/DomainHandler.h +++ b/libraries/networking/src/DomainHandler.h @@ -98,6 +98,7 @@ public: Node::LocalID getLocalID() const { return _localID; } void setLocalID(Node::LocalID localID) { _localID = localID; } + QString getScheme() const { return _domainURL.scheme(); } QString getHostname() const { return _domainURL.host(); } QUrl getErrorDomainURL(){ return _errorDomainURL; } @@ -133,6 +134,9 @@ public: bool isConnected() const { return _isConnected; } void setIsConnected(bool isConnected); + void setCanConnectWithoutAvatarEntities(bool canConnect); + bool canConnectWithoutAvatarEntities(); + bool isServerless() const { return _domainURL.scheme() != URL_SCHEME_HIFI; } bool getInterstitialModeEnabled() const; void setInterstitialModeEnabled(bool enableInterstitialMode); @@ -252,6 +256,7 @@ signals: void completedSocketDiscovery(); void resetting(); + void confirmConnectWithoutAvatarEntities(); void connectedToDomain(QUrl domainURL); void disconnectedFromDomain(); @@ -287,6 +292,8 @@ private: HifiSockAddr _iceServerSockAddr; NetworkPeer _icePeer; bool _isConnected { false }; + bool _haveAskedConnectWithoutAvatarEntities { false }; + bool _canConnectWithoutAvatarEntities { false }; bool _isInErrorState { false }; QJsonObject _settingsObject; QString _pendingPath; diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index a523a7ff36..05e71d0225 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -94,6 +94,12 @@ NodeList::NodeList(char newOwnerType, int socketListenPort, int dtlsListenPort) // send a ping punch immediately connect(&_domainHandler, &DomainHandler::icePeerSocketsReceived, this, &NodeList::pingPunchForDomainServer); + // FIXME: Can remove this temporary work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.) + // Adjust our canRezAvatarEntities permissions on older domains that do not have this setting. + // DomainServerList and DomainSettings packets can come in either order so need to adjust with both occurrences. + auto nodeList = DependencyManager::get(); + connect(&_domainHandler, &DomainHandler::settingsReceived, this, &NodeList::adjustCanRezAvatarEntitiesPerSettings); + auto accountManager = DependencyManager::get(); // assume that we may need to send a new DS check in anytime a new keypair is generated @@ -726,6 +732,11 @@ void NodeList::processDomainServerList(QSharedPointer message) // pull the permissions/right/privileges for this node out of the stream NodePermissions newPermissions; packetStream >> newPermissions; + // FIXME: Can remove this temporary work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.) + // Adjust our canRezAvatarEntities permissions on older domains that do not have this setting. + // DomainServerList and DomainSettings packets can come in either order so need to adjust with both occurrences. + bool adjustedPermissions = adjustCanRezAvatarEntitiesPermissions(_domainHandler.getSettingsObject(), newPermissions, false); + // Is packet authentication enabled? bool isAuthenticated; packetStream >> isAuthenticated; @@ -781,7 +792,7 @@ void NodeList::processDomainServerList(QSharedPointer message) DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList); if (_domainHandler.isConnected() && _domainHandler.getUUID() != domainUUID) { - // Recieved packet from different domain. + // Received packet from different domain. qWarning() << "IGNORING DomainList packet from" << domainUUID << "while connected to" << _domainHandler.getUUID() << ": sent " << pingLagTime << " msec ago."; qWarning(networking) << "DomainList request lag (interface->ds): " << domainServerRequestLag << "msec"; @@ -809,6 +820,23 @@ void NodeList::processDomainServerList(QSharedPointer message) setSessionLocalID(newLocalID); setSessionUUID(newUUID); + // FIXME: Remove this call to requestDomainSettings() and reinstate the one in DomainHandler::setIsConnected(), in version + // 2021.2.0. (New protocol version implies a domain server upgrade.) + if (!_domainHandler.isConnected() + && _domainHandler.getScheme() == URL_SCHEME_HIFI && !_domainHandler.getHostname().isEmpty()) { + // We're about to connect but we need the domain settings (in particular, the node permissions) in order to adjust the + // canRezAvatarEntities permission above before using the permissions in determining whether or not to connect without + // avatar entities rezzing below. + _domainHandler.requestDomainSettings(); + } + + // Don't continue login to the domain if have avatar entities and don't have permissions to rez them, unless user has OKed + // continuing login. + if (!newPermissions.can(NodePermissions::Permission::canRezAvatarEntities) + && (!adjustedPermissions || !_domainHandler.canConnectWithoutAvatarEntities())) { + return; + } + // if this was the first domain-server list from this domain, we've now connected if (!_domainHandler.isConnected()) { _domainHandler.setLocalID(domainLocalID); @@ -1368,3 +1396,35 @@ void NodeList::setRequestsDomainListData(bool isRequesting) { void NodeList::startThread() { moveToNewNamedThread(this, "NodeList Thread", QThread::TimeCriticalPriority); } + + +// FIXME: Can remove this work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.) +bool NodeList::adjustCanRezAvatarEntitiesPermissions(const QJsonObject& domainSettingsObject, + NodePermissions& permissions, bool notify) { + + if (domainSettingsObject.isEmpty()) { + // Don't have enough information to adjust yet. + return false; // Failed to adjust. + } + + const double CANREZAVATARENTITIES_INTRODUCED_VERSION = 2.5; + auto version = domainSettingsObject.value("version"); + if (version.isUndefined() || version.isDouble() && version.toDouble() < CANREZAVATARENTITIES_INTRODUCED_VERSION) { + // On domains without the canRezAvatarEntities permission available, set it to the same as canConnectToDomain. + if (permissions.can(NodePermissions::Permission::canConnectToDomain)) { + if (!permissions.can(NodePermissions::Permission::canRezAvatarEntities)) { + permissions.set(NodePermissions::Permission::canRezAvatarEntities); + if (notify) { + emit canRezAvatarEntitiesChanged(permissions.can(NodePermissions::Permission::canRezAvatarEntities)); + } + } + } + } + + return true; // Successfully adjusted. +} + +// FIXME: Can remove this work-around in version 2021.2.0. (New protocol version implies a domain server upgrade.) +void NodeList::adjustCanRezAvatarEntitiesPerSettings(const QJsonObject& domainSettingsObject) { + adjustCanRezAvatarEntitiesPermissions(domainSettingsObject, _permissions, true); +} diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 4954c53c84..e850d2c07a 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -123,6 +123,11 @@ public slots: void processUsernameFromIDReply(QSharedPointer message); + // FIXME: Can remove these work-arounds in version 2021.2.0. (New protocol version implies a domain server upgrade.) + bool adjustCanRezAvatarEntitiesPermissions(const QJsonObject& domainSettingsObject, NodePermissions& permissions, + bool notify); + void adjustCanRezAvatarEntitiesPerSettings(const QJsonObject& domainSettingsObject); + #if (PR_BUILD || DEV_BUILD) void toggleSendNewerDSConnectVersion(bool shouldSendNewerVersion) { _shouldSendNewerVersion = shouldSendNewerVersion; } #endif diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp index e89b7b4dfd..a218bf7dd4 100644 --- a/libraries/networking/src/NodePermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -68,7 +68,7 @@ NodePermissions::NodePermissions(QMap perms) { permissions |= perms["id_can_kick"].toBool() ? Permission::canKick : Permission::none; permissions |= perms["id_can_replace_content"].toBool() ? Permission::canReplaceDomainContent : Permission::none; permissions |= perms["id_can_get_and_set_private_user_data"].toBool() ? Permission::canGetAndSetPrivateUserData : Permission::none; - permissions |= perms["id_can_rez_attachments"].toBool() ? Permission::canRezAvatarEntities : Permission::none; + permissions |= perms["id_can_rez_avatar_entities"].toBool() ? Permission::canRezAvatarEntities : Permission::none; } QVariant NodePermissions::toVariant(QHash groupRanks) { @@ -173,6 +173,9 @@ QDebug operator<<(QDebug debug, const NodePermissions& perms) { if (perms.can(NodePermissions::Permission::canGetAndSetPrivateUserData)) { debug << " get-and-set-private-user-data"; } + if (perms.can(NodePermissions::Permission::canRezAvatarEntities)) { + debug << " rez-avatar-entities"; + } debug.nospace() << "]"; return debug.nospace(); }