From 3241c182714c2f2bf221d1f910dab38049644406 Mon Sep 17 00:00:00 2001 From: David Back Date: Wed, 9 May 2018 18:10:40 -0700 Subject: [PATCH] first pass cloneables WIP --- .../src/entities/EntityServer.cpp | 1 + .../entities/src/EntityEditPacketSender.cpp | 14 ++- .../entities/src/EntityEditPacketSender.h | 1 + libraries/entities/src/EntityItem.cpp | 98 ++++++++++++++++++ libraries/entities/src/EntityItem.h | 23 +++++ .../entities/src/EntityItemProperties.cpp | 97 ++++++++++++++++++ libraries/entities/src/EntityItemProperties.h | 7 ++ .../src/EntityItemPropertiesDefaults.h | 5 + libraries/entities/src/EntityPropertyFlags.h | 5 + .../entities/src/EntityScriptingInterface.cpp | 73 +++++++++----- .../entities/src/EntityScriptingInterface.h | 3 + libraries/entities/src/EntityTree.cpp | 99 +++++++++++++++---- libraries/entities/src/EntityTree.h | 1 + libraries/gpu/src/gpu/Buffer.cpp | 2 +- .../networking/src/udt/PacketHeaders.cpp | 3 +- libraries/networking/src/udt/PacketHeaders.h | 4 +- scripts/system/html/js/entityProperties.js | 75 ++------------ scripts/system/libraries/cloneEntityUtils.js | 60 +++-------- .../libraries/controllerDispatcherUtils.js | 3 +- 19 files changed, 410 insertions(+), 164 deletions(-) diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp index c108dad6cf..3ca8c1ecd1 100644 --- a/assignment-client/src/entities/EntityServer.cpp +++ b/assignment-client/src/entities/EntityServer.cpp @@ -44,6 +44,7 @@ EntityServer::EntityServer(ReceivedMessage& message) : auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); packetReceiver.registerListenerForTypes({ PacketType::EntityAdd, + PacketType::EntityClone, PacketType::EntityEdit, PacketType::EntityErase, PacketType::EntityPhysics, diff --git a/libraries/entities/src/EntityEditPacketSender.cpp b/libraries/entities/src/EntityEditPacketSender.cpp index d89dd4f9d0..0ae4f7ac2b 100644 --- a/libraries/entities/src/EntityEditPacketSender.cpp +++ b/libraries/entities/src/EntityEditPacketSender.cpp @@ -34,7 +34,7 @@ void EntityEditPacketSender::processEntityEditNackPacket(QSharedPointer EntityItem::getMaterial } return toReturn; } + +bool EntityItem::getCloneable() const { + bool result; + withReadLock([&] { + result = _cloneable; + }); + return result; +} + +void EntityItem::setCloneable(bool value) { + withWriteLock([&] { + _cloneable = value; + }); +} + +float EntityItem::getCloneableLifetime() const { + float result; + withReadLock([&] { + result = _cloneableLifetime; + }); + return result; +} + +void EntityItem::setCloneableLifetime(float value) { + withWriteLock([&] { + _cloneableLifetime = value; + }); +} + +float EntityItem::getCloneableLimit() const { + float result; + withReadLock([&] { + result = _cloneableLimit; + }); + return result; +} + +void EntityItem::setCloneableLimit(float value) { + withWriteLock([&] { + _cloneableLimit = value; + }); +} + +bool EntityItem::getCloneableDynamic() const { + bool result; + withReadLock([&] { + result = _cloneableDynamic; + }); + return result; +} + +void EntityItem::setCloneableDynamic(const bool value) { + withWriteLock([&] { + _cloneableDynamic = value; + }); +} + +bool EntityItem::addCloneID(const QUuid& cloneID) { + if (!_cloneIDs.contains(cloneID)) { + _cloneIDs.append(cloneID); + return true; + } + return false; +} + +bool EntityItem::removeCloneID(const QUuid& cloneID) { + int index = _cloneIDs.indexOf(cloneID); + if (index > 0) { + _cloneIDs.removeAt(index); + return true; + } + return false; +} \ No newline at end of file diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index a88250a133..95cc5f96e1 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -341,6 +341,15 @@ public: quint32 getStaticCertificateVersion() const; void setStaticCertificateVersion(const quint32&); + bool getCloneable() const; + void setCloneable(bool value); + float getCloneableLifetime() const; + void setCloneableLifetime(float value); + float getCloneableLimit() const; + void setCloneableLimit(float value); + bool getCloneableDynamic() const; + void setCloneableDynamic(const bool value); + // TODO: get rid of users of getRadius()... float getRadius() const; @@ -494,6 +503,12 @@ public: void setSimulationOwnershipExpiry(uint64_t expiry) { _simulationOwnershipExpiry = expiry; } uint64_t getSimulationOwnershipExpiry() const { return _simulationOwnershipExpiry; } + bool addCloneID(const QUuid& cloneID); + bool removeCloneID(const QUuid& cloneID); + const QList& getCloneIDs() const { return _cloneIDs; } + void setCloneParent(const QUuid& cloneParentID) { _cloneParentID = cloneParentID; } + const QUuid& getCloneParent() const { return _cloneParentID; } + signals: void requestRenderUpdate(); @@ -648,6 +663,14 @@ protected: bool _cauterized { false }; // if true, don't draw because it would obscure 1st-person camera + bool _cloneable; + float _cloneableLifetime; + float _cloneableLimit; + bool _cloneableDynamic; + + QList _cloneIDs; + QUuid _cloneParentID; + private: std::unordered_map _materials; std::mutex _materialsLock; diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index 4d7c114176..93c2eb885e 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -436,6 +436,11 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_DPI, dpi); CHECK_PROPERTY_CHANGE(PROP_RELAY_PARENT_JOINTS, relayParentJoints); + CHECK_PROPERTY_CHANGE(PROP_CLONEABLE, cloneable); + CHECK_PROPERTY_CHANGE(PROP_CLONEABLE_LIFETIME, cloneableLifetime); + CHECK_PROPERTY_CHANGE(PROP_CLONEABLE_LIMIT, cloneableLimit); + CHECK_PROPERTY_CHANGE(PROP_CLONEABLE_DYNAMIC, cloneableDynamic); + changedProperties += _animation.getChangedProperties(); changedProperties += _keyLight.getChangedProperties(); changedProperties += _ambientLight.getChangedProperties(); @@ -1430,6 +1435,11 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLIENT_ONLY, clientOnly); // Gettable but not settable except at entity creation COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_OWNING_AVATAR_ID, owningAvatarID); // Gettable but not settable + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONEABLE, cloneable); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONEABLE_LIFETIME, cloneableLifetime); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONEABLE_LIMIT, cloneableLimit); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONEABLE_DYNAMIC, cloneableDynamic); + // Rendering info if (!skipDefaults && !strictSemantics) { QScriptValue renderInfo = engine->newObject(); @@ -1642,6 +1652,11 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(dpi, uint16_t, setDPI); + COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneable, bool, setCloneable); + COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneableLifetime, float, setCloneableLifetime); + COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneableLimit, float, setCloneableLimit); + COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneableDynamic, bool, setCloneableDynamic); + _lastEdited = usecTimestampNow(); } @@ -2017,6 +2032,11 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_DPI, DPI, dpi, uint16_t); + ADD_PROPERTY_TO_MAP(PROP_CLONEABLE, Cloneable, cloneable, bool); + ADD_PROPERTY_TO_MAP(PROP_CLONEABLE_LIFETIME, CloneableLifetime, cloneableLifetime, float); + ADD_PROPERTY_TO_MAP(PROP_CLONEABLE_LIMIT, CloneableLimit, cloneableLimit, float); + ADD_PROPERTY_TO_MAP(PROP_CLONEABLE_DYNAMIC, CloneableDynamic, cloneableDynamic, bool); + // FIXME - these are not yet handled //ADD_PROPERTY_TO_MAP(PROP_CREATED, Created, created, quint64); @@ -2331,6 +2351,11 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy APPEND_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, properties.getEntityInstanceNumber()); APPEND_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, properties.getCertificateID()); APPEND_ENTITY_PROPERTY(PROP_STATIC_CERTIFICATE_VERSION, properties.getStaticCertificateVersion()); + + APPEND_ENTITY_PROPERTY(PROP_CLONEABLE, properties.getCloneable()); + APPEND_ENTITY_PROPERTY(PROP_CLONEABLE_LIFETIME, properties.getCloneableLifetime()); + APPEND_ENTITY_PROPERTY(PROP_CLONEABLE_LIMIT, properties.getCloneableLimit()); + APPEND_ENTITY_PROPERTY(PROP_CLONEABLE_DYNAMIC, properties.getCloneableDynamic()); } if (propertyCount > 0) { @@ -2701,6 +2726,11 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CERTIFICATE_ID, QString, setCertificateID); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_STATIC_CERTIFICATE_VERSION, quint32, setStaticCertificateVersion); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONEABLE, bool, setCloneable); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONEABLE_LIFETIME, float, setCloneableLifetime); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONEABLE_LIMIT, float, setCloneableLimit); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONEABLE_DYNAMIC, bool, setCloneableDynamic); + return valid; } @@ -2780,6 +2810,54 @@ bool EntityItemProperties::encodeEraseEntityMessage(const EntityItemID& entityIt return true; } +bool EntityItemProperties::encodeCloneEntityMessage(const EntityItemID& entityIDToClone, const EntityItemID& newEntityID, QByteArray& buffer) { + + char* copyAt = buffer.data(); + int outputLength = 0; + + if (buffer.size() < (int)(sizeof(NUM_BYTES_RFC4122_UUID) * 2)) { + qCDebug(entities) << "ERROR - encodeCloneEntityMessage() called with buffer that is too small!"; + return false; + } + + memcpy(copyAt, entityIDToClone.toRfc4122().constData(), NUM_BYTES_RFC4122_UUID); + copyAt += NUM_BYTES_RFC4122_UUID; + outputLength += NUM_BYTES_RFC4122_UUID; + + memcpy(copyAt, newEntityID.toRfc4122().constData(), NUM_BYTES_RFC4122_UUID); + copyAt += NUM_BYTES_RFC4122_UUID; + outputLength += NUM_BYTES_RFC4122_UUID; + + buffer.resize(outputLength); + + return true; +} + +bool EntityItemProperties::decodeCloneEntityMessage(const QByteArray& buffer, int& processedBytes, EntityItemID& entityIDToClone, EntityItemID& newEntityID) { + + const unsigned char* packetData = (const unsigned char*)buffer.constData(); + const unsigned char* dataAt = packetData; + size_t packetLength = buffer.size(); + processedBytes = 0; + + if (NUM_BYTES_RFC4122_UUID * 2 > packetLength) { + qCDebug(entities) << "EntityItemProperties::processEraseMessageDetails().... bailing because not enough bytes in buffer"; + return false; // bail to prevent buffer overflow + } + + QByteArray encodedID = buffer.mid((int)processedBytes, NUM_BYTES_RFC4122_UUID); + entityIDToClone = QUuid::fromRfc4122(encodedID); + dataAt += encodedID.size(); + processedBytes += encodedID.size(); + + encodedID = buffer.mid((int)processedBytes, NUM_BYTES_RFC4122_UUID); + newEntityID = QUuid::fromRfc4122(encodedID); + dataAt += encodedID.size(); + processedBytes += encodedID.size(); + + return true; +} + void EntityItemProperties::markAllChanged() { _lastEditedByChanged = true; _simulationOwnerChanged = true; @@ -2941,6 +3019,11 @@ void EntityItemProperties::markAllChanged() { _dpiChanged = true; _relayParentJointsChanged = true; + + _cloneableChanged = true; + _cloneableLifetimeChanged = true; + _cloneableLimitChanged = true; + _cloneableDynamicChanged = true; } // The minimum bounding box for the entity. @@ -3373,6 +3456,20 @@ QList EntityItemProperties::listChangedProperties() { out += "isUVModeStretch"; } + if (cloneableChanged()) { + out += "cloneable"; + } + if (cloneableLifetimeChanged()) { + out += "cloneableLifetime"; + } + if (cloneableLimitChanged()) { + out += "cloneableLimit"; + } + if (cloneableDynamicChanged()) { + out += "cloneableDynamic"; + } + + getAnimation().listChangedProperties(out); getKeyLight().listChangedProperties(out); getAmbientLight().listChangedProperties(out); diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index 39ea2e0bdd..3ae2186cab 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -272,6 +272,11 @@ public: DEFINE_PROPERTY_REF(PROP_SERVER_SCRIPTS, ServerScripts, serverScripts, QString, ENTITY_ITEM_DEFAULT_SERVER_SCRIPTS); DEFINE_PROPERTY(PROP_RELAY_PARENT_JOINTS, RelayParentJoints, relayParentJoints, bool, ENTITY_ITEM_DEFAULT_RELAY_PARENT_JOINTS); + DEFINE_PROPERTY(PROP_CLONEABLE, Cloneable, cloneable, bool, ENTITY_ITEM_CLONEABLE); + DEFINE_PROPERTY(PROP_CLONEABLE_LIFETIME, CloneableLifetime, cloneableLifetime, float, ENTITY_ITEM_CLONEABLE_LIFETIME); + DEFINE_PROPERTY(PROP_CLONEABLE_LIMIT, CloneableLimit, cloneableLimit, float, ENTITY_ITEM_CLONEABLE_LIMIT); + DEFINE_PROPERTY(PROP_CLONEABLE_DYNAMIC, CloneableDynamic, cloneableDynamic, bool, ENTITY_ITEM_CLONEABLE_DYNAMIC); + static QString getComponentModeString(uint32_t mode); static QString getComponentModeAsString(uint32_t mode); @@ -294,6 +299,8 @@ public: QByteArray& buffer, EntityPropertyFlags requestedProperties, EntityPropertyFlags& didntFitProperties); static bool encodeEraseEntityMessage(const EntityItemID& entityItemID, QByteArray& buffer); + static bool encodeCloneEntityMessage(const EntityItemID& entityIDToClone, const EntityItemID& newEntityID, QByteArray& buffer); + static bool decodeCloneEntityMessage(const QByteArray& buffer, int& processedBytes, EntityItemID& entityIDToClone, EntityItemID& newEntityID); static bool decodeEntityEditPacket(const unsigned char* data, int bytesToRead, int& processedBytes, EntityItemID& entityID, EntityItemProperties& properties); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index d2ddd687dd..6e46453bda 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -97,4 +97,9 @@ const QUuid ENTITY_ITEM_DEFAULT_LAST_EDITED_BY = QUuid(); const bool ENTITY_ITEM_DEFAULT_RELAY_PARENT_JOINTS = false; +const bool ENTITY_ITEM_CLONEABLE = false; +const float ENTITY_ITEM_CLONEABLE_LIFETIME = 300.0f; +const int ENTITY_ITEM_CLONEABLE_LIMIT = 0; +const bool ENTITY_ITEM_CLONEABLE_DYNAMIC = false; + #endif // hifi_EntityItemPropertiesDefaults_h diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index 99a5f287ea..f698739e01 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -242,6 +242,11 @@ enum EntityPropertyList { PROP_MATERIAL_MAPPING_ROT, PROP_MATERIAL_DATA, + PROP_CLONEABLE, + PROP_CLONEABLE_LIFETIME, + PROP_CLONEABLE_LIMIT, + PROP_CLONEABLE_DYNAMIC, + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 7c16214a78..132fec2c51 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -258,33 +258,9 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties propertiesWithSimID = convertPropertiesFromScriptSemantics(propertiesWithSimID, scalesWithParent); propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged()); - EntityItemID id = EntityItemID(QUuid::createUuid()); - + EntityItemID id; // If we have a local entity tree set, then also update it. - bool success = true; - if (_entityTree) { - _entityTree->withWriteLock([&] { - EntityItemPointer entity = _entityTree->addEntity(id, propertiesWithSimID); - if (entity) { - if (propertiesWithSimID.queryAACubeRelatedPropertyChanged()) { - // due to parenting, the server may not know where something is in world-space, so include the bounding cube. - bool success; - AACube queryAACube = entity->getQueryAACube(success); - if (success) { - propertiesWithSimID.setQueryAACube(queryAACube); - } - } - - entity->setLastBroadcast(usecTimestampNow()); - // since we're creating this object we will immediately volunteer to own its simulation - entity->flagForOwnershipBid(VOLUNTEER_SIMULATION_PRIORITY); - propertiesWithSimID.setLastEdited(entity->getLastEdited()); - } else { - qCDebug(entities) << "script failed to add new Entity to local Octree"; - success = false; - } - }); - } + bool success = addLocalEntityCopy(propertiesWithSimID, id); // queue the packet if (success) { @@ -295,6 +271,39 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties } } +bool EntityScriptingInterface::addLocalEntityCopy(EntityItemProperties& properties, EntityItemID& id) { + bool success = true; + + id = EntityItemID(QUuid::createUuid()); + + if (_entityTree) { + _entityTree->withWriteLock([&] { + EntityItemPointer entity = _entityTree->addEntity(id, properties); + if (entity) { + if (properties.queryAACubeRelatedPropertyChanged()) { + // due to parenting, the server may not know where something is in world-space, so include the bounding cube. + bool success; + AACube queryAACube = entity->getQueryAACube(success); + if (success) { + properties.setQueryAACube(queryAACube); + } + } + + entity->setLastBroadcast(usecTimestampNow()); + // since we're creating this object we will immediately volunteer to own its simulation + entity->flagForOwnershipBid(VOLUNTEER_SIMULATION_PRIORITY); + properties.setLastEdited(entity->getLastEdited()); + } + else { + qCDebug(entities) << "script failed to add new Entity to local Octree"; + success = false; + } + }); + } + + return success; +} + QUuid EntityScriptingInterface::addModelEntity(const QString& name, const QString& modelUrl, const QString& textures, const QString& shapeType, bool dynamic, bool collisionless, const glm::vec3& position, const glm::vec3& gravity) { @@ -320,6 +329,18 @@ QUuid EntityScriptingInterface::addModelEntity(const QString& name, const QStrin return addEntity(properties); } +QUuid EntityScriptingInterface::cloneEntity(QUuid entityIDToClone) { + EntityItemID newEntityID; + EntityItemProperties properties = getEntityProperties(entityIDToClone); + if (addLocalEntityCopy(properties, newEntityID)) { + qCDebug(entities) << "DBACK POOPY cloneEntity addLocalEntityCopy" << newEntityID; + getEntityPacketSender()->queueCloneEntityMessage(entityIDToClone, newEntityID); + return newEntityID; + } else { + return QUuid(); + } +} + EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity) { EntityPropertyFlags noSpecificProperties; return getEntityProperties(identity, noSpecificProperties); diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index 8adb5138f2..6935c9e8c4 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -224,6 +224,8 @@ public slots: Q_INVOKABLE QUuid addModelEntity(const QString& name, const QString& modelUrl, const QString& textures, const QString& shapeType, bool dynamic, bool collisionless, const glm::vec3& position, const glm::vec3& gravity); + Q_INVOKABLE QUuid cloneEntity(QUuid entityIDToClone); + /**jsdoc * Get the properties of an entity. * @function Entities.getEntityProperties @@ -1875,6 +1877,7 @@ private: bool polyVoxWorker(QUuid entityID, std::function actor); bool setPoints(QUuid entityID, std::function actor); void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties); + bool addLocalEntityCopy(EntityItemProperties& propertiesWithSimID, EntityItemID& id); EntityItemPointer checkForTreeEntityAndTypeMatch(const QUuid& entityID, EntityTypes::EntityType entityType = EntityTypes::Unknown); diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index 3149527216..1e8055ff46 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -228,6 +228,7 @@ bool EntityTree::handlesEditPacketType(PacketType packetType) const { // we handle these types of "edit" packets switch (packetType) { case PacketType::EntityAdd: + case PacketType::EntityClone: case PacketType::EntityEdit: case PacketType::EntityErase: case PacketType::EntityPhysics: @@ -592,6 +593,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign return; } + removeCloneIDFromCloneParent(entityID); unhookChildAvatar(entityID); emit deletingEntity(entityID); emit deletingEntityPointer(existingEntity.get()); @@ -625,6 +627,19 @@ void EntityTree::unhookChildAvatar(const EntityItemID entityID) { }); } +void EntityTree::removeCloneIDFromCloneParent(const EntityItemID& entityID) { + EntityItemPointer entity = findEntityByEntityItemID(entityID); + if (entity) { + const QUuid& cloneParentID = entity->getCloneParent(); + if (!cloneParentID.isNull()) { + EntityItemPointer cloneParent = findEntityByID(cloneParentID); + if (cloneParent) { + cloneParent->removeCloneID(entityID); + } + } + } +} + void EntityTree::deleteEntities(QSet entityIDs, bool force, bool ignoreWarnings) { // NOTE: callers must lock the tree before using this method DeleteEntityOperator theOperator(getThisPointer()); @@ -653,6 +668,7 @@ void EntityTree::deleteEntities(QSet entityIDs, bool force, bool i } // tell our delete operator about this entityID + removeCloneIDFromCloneParent(entityID); unhookChildAvatar(entityID); theOperator.addEntityIDToDeleteList(entityID); emit deletingEntity(entityID); @@ -1392,6 +1408,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c int processedBytes = 0; bool isAdd = false; + bool isClone = false; // we handle these types of "edit" packets switch (message.getType()) { case PacketType::EntityErase: { @@ -1400,8 +1417,10 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c break; } + case PacketType::EntityClone: + isClone = true; // fall through to next case case PacketType::EntityAdd: - isAdd = true; // fall through to next case + isAdd = true; // fall through to next case // FALLTHRU case PacketType::EntityPhysics: case PacketType::EntityEdit: { @@ -1422,8 +1441,21 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c EntityItemProperties properties; startDecode = usecTimestampNow(); - bool validEditPacket = EntityItemProperties::decodeEntityEditPacket(editData, maxLength, processedBytes, - entityItemID, properties); + bool validEditPacket = false; + EntityItemID entityIDToClone; + EntityItemPointer entityToClone; + if (isClone) { + QByteArray buffer = QByteArray::fromRawData(reinterpret_cast(editData), maxLength); + validEditPacket = EntityItemProperties::decodeCloneEntityMessage(buffer, processedBytes, entityIDToClone, entityItemID); + entityToClone = findEntityByEntityItemID(entityIDToClone); + if (entityToClone) { + properties = entityToClone->getProperties(); + } + } + else { + validEditPacket = EntityItemProperties::decodeEntityEditPacket(editData, maxLength, processedBytes, entityItemID, properties); + } + endDecode = usecTimestampNow(); EntityItemPointer existingEntity; @@ -1491,24 +1523,26 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } - if ((isAdd || properties.lifetimeChanged()) && - ((!senderNode->getCanRez() && senderNode->getCanRezTmp()) || - (!senderNode->getCanRezCertified() && senderNode->getCanRezTmpCertified()))) { - // this node is only allowed to rez temporary entities. if need be, cap the lifetime. - if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || - properties.getLifetime() > _maxTmpEntityLifetime) { - properties.setLifetime(_maxTmpEntityLifetime); + if (!isClone) { + if ((isAdd || properties.lifetimeChanged()) && + ((!senderNode->getCanRez() && senderNode->getCanRezTmp()) || + (!senderNode->getCanRezCertified() && senderNode->getCanRezTmpCertified()))) { + // this node is only allowed to rez temporary entities. if need be, cap the lifetime. + if (properties.getLifetime() == ENTITY_ITEM_IMMORTAL_LIFETIME || + properties.getLifetime() > _maxTmpEntityLifetime) { + properties.setLifetime(_maxTmpEntityLifetime); + bumpTimestamp(properties); + } + } + + if (isAdd && properties.getLocked() && !senderNode->isAllowedEditor()) { + // if a node can't change locks, don't allow it to create an already-locked entity -- automatically + // clear the locked property and allow the unlocked entity to be created. + properties.setLocked(false); bumpTimestamp(properties); } } - if (isAdd && properties.getLocked() && !senderNode->isAllowedEditor()) { - // if a node can't change locks, don't allow it to create an already-locked entity -- automatically - // clear the locked property and allow the unlocked entity to be created. - properties.setLocked(false); - bumpTimestamp(properties); - } - // If we got a valid edit packet, then it could be a new entity or it could be an update to // an existing entity... handle appropriately if (validEditPacket) { @@ -1566,17 +1600,39 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c } else if (isAdd) { bool failedAdd = !allowed; bool isCertified = !properties.getCertificateID().isEmpty(); + bool isCloneable = properties.getCloneable(); + int cloneLimit = properties.getCloneableLimit(); if (!allowed) { qCDebug(entities) << "Filtered entity add. ID:" << entityItemID; - } else if (!isCertified && !senderNode->getCanRez() && !senderNode->getCanRezTmp()) { + } else if (!isClone && !isCertified && !senderNode->getCanRez() && !senderNode->getCanRezTmp()) { failedAdd = true; qCDebug(entities) << "User without 'uncertified rez rights' [" << senderNode->getUUID() << "] attempted to add an uncertified entity with ID:" << entityItemID; - } else if (isCertified && !senderNode->getCanRezCertified() && !senderNode->getCanRezTmpCertified()) { + } else if (!isClone && isCertified && !senderNode->getCanRezCertified() && !senderNode->getCanRezTmpCertified()) { failedAdd = true; qCDebug(entities) << "User without 'certified rez rights' [" << senderNode->getUUID() << "] attempted to add a certified entity with ID:" << entityItemID; + } else if (isClone && isCertified) { + failedAdd = true; + qCDebug(entities) << "User attempted to clone certified entity from entity ID:" << entityIDToClone; + } else if (isClone && !isCloneable) { + failedAdd = true; + qCDebug(entities) << "User attempted to clone non-cloneable entity from entity ID:" << entityIDToClone; + } else if (isClone && entityToClone && entityToClone->getCloneIDs().size() >= cloneLimit) { + failedAdd = true; + qCDebug(entities) << "User attempted to clone entity ID:" << entityIDToClone << " which reached it's cloneable limit."; } else { + if (isClone) { + properties.setName(properties.getName() + "-clone-" + entityIDToClone.toString()); + properties.setLocked(false); + properties.setLifetime(properties.getCloneableLifetime()); + properties.setDynamic(properties.getCloneableDynamic()); + properties.setCloneable(ENTITY_ITEM_CLONEABLE); + properties.setCloneableLifetime(ENTITY_ITEM_CLONEABLE_LIFETIME); + properties.setCloneableLimit(ENTITY_ITEM_CLONEABLE_LIMIT); + properties.setCloneableDynamic(ENTITY_ITEM_CLONEABLE_DYNAMIC); + } + // this is a new entity... assign a new entityID properties.setCreated(properties.getLastEdited()); properties.setLastEditedBy(senderNode->getUUID()); @@ -1600,6 +1656,11 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c if (newEntity) { newEntity->markAsChangedOnServer(); notifyNewlyCreatedEntity(*newEntity, senderNode); + if (isClone) { + entityToClone->addCloneID(newEntity->getEntityItemID()); + newEntity->setCloneParent(entityIDToClone); + qCDebug(entities) << "DBACK POOPY addedEntity clone " << newEntity->getEntityItemID(); + } startLogging = usecTimestampNow(); if (wantEditLogging()) { diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h index ee9fb10554..0d7cc54df7 100644 --- a/libraries/entities/src/EntityTree.h +++ b/libraries/entities/src/EntityTree.h @@ -117,6 +117,7 @@ public: // check if the avatar is a child of this entity, If so set the avatar parentID to null void unhookChildAvatar(const EntityItemID entityID); + void removeCloneIDFromCloneParent(const EntityItemID& entityID); void deleteEntity(const EntityItemID& entityID, bool force = false, bool ignoreWarnings = true); void deleteEntities(QSet entityIDs, bool force = false, bool ignoreWarnings = true); diff --git a/libraries/gpu/src/gpu/Buffer.cpp b/libraries/gpu/src/gpu/Buffer.cpp index ebb768e597..fe3570e7f7 100644 --- a/libraries/gpu/src/gpu/Buffer.cpp +++ b/libraries/gpu/src/gpu/Buffer.cpp @@ -99,7 +99,7 @@ Buffer::Update::Update(const Buffer& parent) : buffer(parent) { void Buffer::Update::apply() const { // Make sure we're loaded in order buffer._applyUpdateCount++; - assert(buffer._applyUpdateCount == updateNumber); + //assert(buffer._applyUpdateCount == updateNumber); const auto pageSize = buffer._pages._pageSize; buffer._renderSysmem.resize(size); diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 70880833bf..e84f3ffe7a 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -29,10 +29,11 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::DomainList: return static_cast(DomainListVersion::GetMachineFingerprintFromUUIDSupport); case PacketType::EntityAdd: + case PacketType::EntityClone: case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return static_cast(EntityVersion::MaterialData); + return static_cast(EntityVersion::CloneableData); case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::ConicalFrustums); case PacketType::AvatarIdentity: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 7d374f3625..5cb2d49a53 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -75,6 +75,7 @@ public: EntityData, EntityQuery, EntityAdd, + EntityClone, EntityErase, EntityEdit, DomainServerConnectionToken, @@ -232,7 +233,8 @@ enum class EntityVersion : PacketVersion { SoftEntities, MaterialEntities, ShadowControl, - MaterialData + MaterialData, + CloneableData }; enum class EntityScriptCallMethodVersion : PacketVersion { diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index 4b6329db44..f0bcdad708 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -1040,12 +1040,12 @@ function loaded() { elWantsTrigger.checked = false; elIgnoreIK.checked = true; - elCloneable.checked = false; - elCloneableDynamic.checked = false; + elCloneable.checked = properties.cloneable; + elCloneableDynamic.checked = properties.cloneableDynamic; elCloneableGroup.style.display = elCloneable.checked ? "block": "none"; - elCloneableLimit.value = 0; - elCloneableLifetime.value = 300; - + elCloneableLimit.value = properties.cloneableLimit; + elCloneableLifetime.value = properties.cloneableLifetime; + var grabbablesSet = false; var parsedUserData = {}; try { @@ -1069,27 +1069,6 @@ function loaded() { } else { elIgnoreIK.checked = true; } - if ("cloneable" in grabbableData) { - elCloneable.checked = grabbableData.cloneable; - elCloneableGroup.style.display = elCloneable.checked ? "block" : "none"; - elCloneableDynamic.checked = - grabbableData.cloneDynamic ? grabbableData.cloneDynamic : properties.dynamic; - if (elCloneable.checked) { - if ("cloneLifetime" in grabbableData) { - elCloneableLifetime.value = - grabbableData.cloneLifetime ? grabbableData.cloneLifetime : 300; - } - if ("cloneLimit" in grabbableData) { - elCloneableLimit.value = grabbableData.cloneLimit ? grabbableData.cloneLimit : 0; - } - if ("cloneAvatarEntity" in grabbableData) { - elCloneableAvatarEntity.checked = - grabbableData.cloneAvatarEntity ? grabbableData.cloneAvatarEntity : false; - } - } - } else { - elCloneable.checked = false; - } } } catch (e) { // TODO: What should go here? @@ -1460,45 +1439,11 @@ function loaded() { } userDataChanger("grabbableKey", "grabbable", elGrabbable, elUserData, true); }); - elCloneableDynamic.addEventListener('change', function(event) { - userDataChanger("grabbableKey", "cloneDynamic", event.target, elUserData, -1); - }); - - elCloneableAvatarEntity.addEventListener('change', function(event) { - userDataChanger("grabbableKey", "cloneAvatarEntity", event.target, elUserData, -1); - }); - - elCloneable.addEventListener('change', function (event) { - var checked = event.target.checked; - if (checked) { - multiDataUpdater("grabbableKey", { - cloneLifetime: elCloneableLifetime, - cloneLimit: elCloneableLimit, - cloneDynamic: elCloneableDynamic, - cloneAvatarEntity: elCloneableAvatarEntity, - cloneable: event.target, - grabbable: null - }, elUserData, {}); - elCloneableGroup.style.display = "block"; - updateProperty('dynamic', false); - } else { - multiDataUpdater("grabbableKey", { - cloneLifetime: null, - cloneLimit: null, - cloneDynamic: null, - cloneAvatarEntity: null, - cloneable: false - }, elUserData, {}); - elCloneableGroup.style.display = "none"; - } - }); - - var numberListener = function (event) { - userDataChanger("grabbableKey", - event.target.getAttribute("data-user-data-type"), parseInt(event.target.value), elUserData, false); - }; - elCloneableLifetime.addEventListener('change', numberListener); - elCloneableLimit.addEventListener('change', numberListener); + + elCloneable.addEventListener('change', createEmitCheckedPropertyUpdateFunction('cloneable')); + elCloneableDynamic.addEventListener('change', createEmitCheckedPropertyUpdateFunction('cloneableDynamic')); + elCloneableLifetime.addEventListener('change', createEmitNumberPropertyUpdateFunction('cloneableLifetime')); + elCloneableLimit.addEventListener('change', createEmitNumberPropertyUpdateFunction('cloneableLimit')); elWantsTrigger.addEventListener('change', function() { userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false); diff --git a/scripts/system/libraries/cloneEntityUtils.js b/scripts/system/libraries/cloneEntityUtils.js index 63b161eb80..bd33d32342 100644 --- a/scripts/system/libraries/cloneEntityUtils.js +++ b/scripts/system/libraries/cloneEntityUtils.js @@ -33,8 +33,7 @@ if (typeof Object.assign !== 'function') { entityIsCloneable = function(props) { if (props) { - var grabbableData = getGrabbableData(props); - return grabbableData.cloneable; + return props.cloneable; } return false; }; @@ -42,56 +41,19 @@ entityIsCloneable = function(props) { propsAreCloneDynamic = function(props) { var cloneable = entityIsCloneable(props); if (cloneable) { - var grabInfo = getGrabbableData(props); - if (grabInfo.cloneDynamic) { - return true; - } + return props.cloneableDynamic; } return false; }; - cloneEntity = function(props, worldEntityProps) { - // we need all the properties, for this - var cloneableProps = Entities.getEntityProperties(props.id); - - var count = 0; - worldEntityProps.forEach(function(itemWE) { - if (itemWE.name.indexOf('-clone-' + cloneableProps.id) !== -1) { - count++; - } - }); - - var grabInfo = getGrabbableData(cloneableProps); - var limit = grabInfo.cloneLimit ? grabInfo.cloneLimit : 0; - if (count >= limit && limit !== 0) { - return null; - } - - cloneableProps.name = cloneableProps.name + '-clone-' + cloneableProps.id; - var lifetime = grabInfo.cloneLifetime ? grabInfo.cloneLifetime : 300; - var dynamic = grabInfo.cloneDynamic ? grabInfo.cloneDynamic : false; - var triggerable = grabInfo.triggerable ? grabInfo.triggerable : false; - var avatarEntity = grabInfo.cloneAvatarEntity ? grabInfo.cloneAvatarEntity : false; - var cUserData = Object.assign({}, JSON.parse(cloneableProps.userData)); - var cProperties = Object.assign({}, cloneableProps); - - - delete cUserData.grabbableKey.cloneLifetime; - delete cUserData.grabbableKey.cloneable; - delete cUserData.grabbableKey.cloneDynamic; - delete cUserData.grabbableKey.cloneLimit; - delete cUserData.grabbableKey.cloneAvatarEntity; - delete cProperties.id; - - - cProperties.dynamic = dynamic; - cProperties.locked = false; - cUserData.grabbableKey.triggerable = triggerable; - cUserData.grabbableKey.grabbable = true; - cProperties.lifetime = lifetime; - cProperties.userData = JSON.stringify(cUserData); - - var cloneID = Entities.addEntity(cProperties, avatarEntity); - return cloneID; + var entityToClone = props.id; + var certificateID = Entities.getEntityProperties(entityToClone, ['certificateID']).certificateID; + // ensure entity is cloneable and does not have a certificate ID, whereas cloneable limits + // will now be handled by the server where the entity add will fail if limit reached + if (entityIsCloneable(props) && (certificateID === undefined || certificateID.length === 0)) { + var cloneID = Entities.cloneEntity(entityToClone); + return cloneID; + } + return null; }; diff --git a/scripts/system/libraries/controllerDispatcherUtils.js b/scripts/system/libraries/controllerDispatcherUtils.js index 71dc5e4273..f3f0e8dd01 100644 --- a/scripts/system/libraries/controllerDispatcherUtils.js +++ b/scripts/system/libraries/controllerDispatcherUtils.js @@ -125,7 +125,8 @@ DISPATCHER_PROPERTIES = [ "dimensions", "userData", "type", - "href" + "href", + "cloneable" ]; // priority -- a lower priority means the module will be asked sooner than one with a higher priority in a given update step