Merge pull request #13179 from dback2/cloneables

Server-Managed Cloneables + Cloneable Properties
This commit is contained in:
John Conklin II 2018-05-23 12:40:27 -07:00 committed by GitHub
commit 3e665fb616
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 588 additions and 176 deletions

View file

@ -44,6 +44,7 @@ EntityServer::EntityServer(ReceivedMessage& message) :
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListenerForTypes({ PacketType::EntityAdd,
PacketType::EntityClone,
PacketType::EntityEdit,
PacketType::EntityErase,
PacketType::EntityPhysics,

View file

@ -154,3 +154,11 @@ void EntityEditPacketSender::queueEraseEntityMessage(const EntityItemID& entityI
queueOctreeEditMessage(PacketType::EntityErase, bufferOut);
}
}
void EntityEditPacketSender::queueCloneEntityMessage(const EntityItemID& entityIDToClone, const EntityItemID& newEntityID) {
QByteArray bufferOut(NLPacket::maxPayloadSize(PacketType::EntityClone), 0);
if (EntityItemProperties::encodeCloneEntityMessage(entityIDToClone, newEntityID, bufferOut)) {
queueOctreeEditMessage(PacketType::EntityClone, bufferOut);
}
}

View file

@ -38,6 +38,7 @@ public:
void queueEraseEntityMessage(const EntityItemID& entityItemID);
void queueCloneEntityMessage(const EntityItemID& entityIDToClone, const EntityItemID& newEntityID);
// My server type is the model server
virtual char getMyNodeType() const override { return NodeType::EntityServer; }

View file

@ -124,6 +124,13 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param
requestedProperties += PROP_LAST_EDITED_BY;
requestedProperties += PROP_CLONEABLE;
requestedProperties += PROP_CLONE_LIFETIME;
requestedProperties += PROP_CLONE_LIMIT;
requestedProperties += PROP_CLONE_DYNAMIC;
requestedProperties += PROP_CLONE_AVATAR_ENTITY;
requestedProperties += PROP_CLONE_ORIGIN_ID;
return requestedProperties;
}
@ -288,6 +295,13 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet
APPEND_ENTITY_PROPERTY(PROP_QUERY_AA_CUBE, getQueryAACube());
APPEND_ENTITY_PROPERTY(PROP_LAST_EDITED_BY, getLastEditedBy());
APPEND_ENTITY_PROPERTY(PROP_CLONEABLE, getCloneable());
APPEND_ENTITY_PROPERTY(PROP_CLONE_LIFETIME, getCloneLifetime());
APPEND_ENTITY_PROPERTY(PROP_CLONE_LIMIT, getCloneLimit());
APPEND_ENTITY_PROPERTY(PROP_CLONE_DYNAMIC, getCloneDynamic());
APPEND_ENTITY_PROPERTY(PROP_CLONE_AVATAR_ENTITY, getCloneAvatarEntity());
APPEND_ENTITY_PROPERTY(PROP_CLONE_ORIGIN_ID, getCloneOriginID());
appendSubclassData(packetData, params, entityTreeElementExtraEncodeData,
requestedProperties,
propertyFlags,
@ -848,6 +862,13 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
READ_ENTITY_PROPERTY(PROP_LAST_EDITED_BY, QUuid, setLastEditedBy);
READ_ENTITY_PROPERTY(PROP_CLONEABLE, bool, setCloneable);
READ_ENTITY_PROPERTY(PROP_CLONE_LIFETIME, float, setCloneLifetime);
READ_ENTITY_PROPERTY(PROP_CLONE_LIMIT, float, setCloneLimit);
READ_ENTITY_PROPERTY(PROP_CLONE_DYNAMIC, bool, setCloneDynamic);
READ_ENTITY_PROPERTY(PROP_CLONE_AVATAR_ENTITY, bool, setCloneAvatarEntity);
READ_ENTITY_PROPERTY(PROP_CLONE_ORIGIN_ID, QUuid, setCloneOriginID);
bytesRead += readEntitySubclassDataFromBuffer(dataAt, (bytesLeftToRead - bytesRead), args,
propertyFlags, overwriteLocalData, somethingChanged);
@ -1275,6 +1296,13 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper
COPY_ENTITY_PROPERTY_TO_PROPERTIES(lastEditedBy, getLastEditedBy);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneable, getCloneable);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneLifetime, getCloneLifetime);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneLimit, getCloneLimit);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneDynamic, getCloneDynamic);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneAvatarEntity, getCloneAvatarEntity);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(cloneOriginID, getCloneOriginID);
properties._defaultSettings = false;
return properties;
@ -1382,6 +1410,13 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) {
SET_ENTITY_PROPERTY_FROM_PROPERTIES(lastEditedBy, setLastEditedBy);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneable, setCloneable);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneLifetime, setCloneLifetime);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneLimit, setCloneLimit);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneDynamic, setCloneDynamic);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneAvatarEntity, setCloneAvatarEntity);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(cloneOriginID, setCloneOriginID);
if (updateQueryAACube()) {
somethingChanged = true;
}
@ -2975,3 +3010,118 @@ std::unordered_map<std::string, graphics::MultiMaterial> 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::getCloneLifetime() const {
float result;
withReadLock([&] {
result = _cloneLifetime;
});
return result;
}
void EntityItem::setCloneLifetime(float value) {
withWriteLock([&] {
_cloneLifetime = value;
});
}
float EntityItem::getCloneLimit() const {
float result;
withReadLock([&] {
result = _cloneLimit;
});
return result;
}
void EntityItem::setCloneLimit(float value) {
withWriteLock([&] {
_cloneLimit = value;
});
}
bool EntityItem::getCloneDynamic() const {
bool result;
withReadLock([&] {
result = _cloneDynamic;
});
return result;
}
void EntityItem::setCloneDynamic(bool value) {
withWriteLock([&] {
_cloneDynamic = value;
});
}
bool EntityItem::getCloneAvatarEntity() const {
bool result;
withReadLock([&] {
result = _cloneAvatarEntity;
});
return result;
}
void EntityItem::setCloneAvatarEntity(bool value) {
withWriteLock([&] {
_cloneAvatarEntity = value;
});
}
const QUuid EntityItem::getCloneOriginID() const {
QUuid result;
withReadLock([&] {
result = _cloneOriginID;
});
return result;
}
void EntityItem::setCloneOriginID(const QUuid& value) {
withWriteLock([&] {
_cloneOriginID = value;
});
}
void EntityItem::addCloneID(const QUuid& cloneID) {
withWriteLock([&] {
if (!_cloneIDs.contains(cloneID)) {
_cloneIDs.append(cloneID);
}
});
}
void EntityItem::removeCloneID(const QUuid& cloneID) {
withWriteLock([&] {
int index = _cloneIDs.indexOf(cloneID);
if (index >= 0) {
_cloneIDs.removeAt(index);
}
});
}
const QVector<QUuid> EntityItem::getCloneIDs() const {
QVector<QUuid> result;
withReadLock([&] {
result = _cloneIDs;
});
return result;
}
void EntityItem::setCloneIDs(const QVector<QUuid>& cloneIDs) {
withWriteLock([&] {
_cloneIDs = cloneIDs;
});
}

View file

@ -341,6 +341,19 @@ public:
quint32 getStaticCertificateVersion() const;
void setStaticCertificateVersion(const quint32&);
bool getCloneable() const;
void setCloneable(bool value);
float getCloneLifetime() const;
void setCloneLifetime(float value);
float getCloneLimit() const;
void setCloneLimit(float value);
bool getCloneDynamic() const;
void setCloneDynamic(bool value);
bool getCloneAvatarEntity() const;
void setCloneAvatarEntity(bool value);
const QUuid getCloneOriginID() const;
void setCloneOriginID(const QUuid& value);
// TODO: get rid of users of getRadius()...
float getRadius() const;
@ -494,6 +507,11 @@ public:
void setSimulationOwnershipExpiry(uint64_t expiry) { _simulationOwnershipExpiry = expiry; }
uint64_t getSimulationOwnershipExpiry() const { return _simulationOwnershipExpiry; }
void addCloneID(const QUuid& cloneID);
void removeCloneID(const QUuid& cloneID);
const QVector<QUuid> getCloneIDs() const;
void setCloneIDs(const QVector<QUuid>& cloneIDs);
signals:
void requestRenderUpdate();
@ -648,6 +666,14 @@ protected:
bool _cauterized { false }; // if true, don't draw because it would obscure 1st-person camera
bool _cloneable { ENTITY_ITEM_DEFAULT_CLONEABLE };
float _cloneLifetime { ENTITY_ITEM_DEFAULT_CLONE_LIFETIME };
float _cloneLimit { ENTITY_ITEM_DEFAULT_CLONE_LIMIT };
bool _cloneDynamic { ENTITY_ITEM_DEFAULT_CLONE_DYNAMIC };
bool _cloneAvatarEntity { ENTITY_ITEM_DEFAULT_CLONE_AVATAR_ENTITY };
QUuid _cloneOriginID;
QVector<QUuid> _cloneIDs;
private:
std::unordered_map<std::string, graphics::MultiMaterial> _materials;
std::mutex _materialsLock;

View file

@ -436,6 +436,13 @@ 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_CLONE_LIFETIME, cloneLifetime);
CHECK_PROPERTY_CHANGE(PROP_CLONE_LIMIT, cloneLimit);
CHECK_PROPERTY_CHANGE(PROP_CLONE_DYNAMIC, cloneDynamic);
CHECK_PROPERTY_CHANGE(PROP_CLONE_AVATAR_ENTITY, cloneAvatarEntity);
CHECK_PROPERTY_CHANGE(PROP_CLONE_ORIGIN_ID, cloneOriginID);
changedProperties += _animation.getChangedProperties();
changedProperties += _keyLight.getChangedProperties();
changedProperties += _ambientLight.getChangedProperties();
@ -1430,6 +1437,13 @@ 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_CLONE_LIFETIME, cloneLifetime);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONE_LIMIT, cloneLimit);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONE_DYNAMIC, cloneDynamic);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONE_AVATAR_ENTITY, cloneAvatarEntity);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLONE_ORIGIN_ID, cloneOriginID);
// Rendering info
if (!skipDefaults && !strictSemantics) {
QScriptValue renderInfo = engine->newObject();
@ -1642,6 +1656,13 @@ 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(cloneLifetime, float, setCloneLifetime);
COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneLimit, float, setCloneLimit);
COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneDynamic, bool, setCloneDynamic);
COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneAvatarEntity, bool, setCloneAvatarEntity);
COPY_PROPERTY_FROM_QSCRIPTVALUE(cloneOriginID, QUuid, setCloneOriginID);
_lastEdited = usecTimestampNow();
}
@ -1793,6 +1814,13 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
COPY_PROPERTY_IF_CHANGED(dpi);
COPY_PROPERTY_IF_CHANGED(cloneable);
COPY_PROPERTY_IF_CHANGED(cloneLifetime);
COPY_PROPERTY_IF_CHANGED(cloneLimit);
COPY_PROPERTY_IF_CHANGED(cloneDynamic);
COPY_PROPERTY_IF_CHANGED(cloneAvatarEntity);
COPY_PROPERTY_IF_CHANGED(cloneOriginID);
_lastEdited = usecTimestampNow();
}
@ -2017,6 +2045,13 @@ 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_CLONE_LIFETIME, CloneLifetime, cloneLifetime, float);
ADD_PROPERTY_TO_MAP(PROP_CLONE_LIMIT, CloneLimit, cloneLimit, float);
ADD_PROPERTY_TO_MAP(PROP_CLONE_DYNAMIC, CloneDynamic, cloneDynamic, bool);
ADD_PROPERTY_TO_MAP(PROP_CLONE_AVATAR_ENTITY, CloneAvatarEntity, cloneAvatarEntity, bool);
ADD_PROPERTY_TO_MAP(PROP_CLONE_ORIGIN_ID, CloneOriginID, cloneOriginID, QUuid);
// FIXME - these are not yet handled
//ADD_PROPERTY_TO_MAP(PROP_CREATED, Created, created, quint64);
@ -2331,6 +2366,12 @@ 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_CLONE_LIFETIME, properties.getCloneLifetime());
APPEND_ENTITY_PROPERTY(PROP_CLONE_LIMIT, properties.getCloneLimit());
APPEND_ENTITY_PROPERTY(PROP_CLONE_DYNAMIC, properties.getCloneDynamic());
APPEND_ENTITY_PROPERTY(PROP_CLONE_AVATAR_ENTITY, properties.getCloneAvatarEntity());
}
if (propertyCount > 0) {
@ -2701,6 +2742,12 @@ 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_CLONE_LIFETIME, float, setCloneLifetime);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONE_LIMIT, float, setCloneLimit);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONE_DYNAMIC, bool, setCloneDynamic);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CLONE_AVATAR_ENTITY, bool, setCloneAvatarEntity);
return valid;
}
@ -2780,6 +2827,52 @@ 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)(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 +3034,13 @@ void EntityItemProperties::markAllChanged() {
_dpiChanged = true;
_relayParentJointsChanged = true;
_cloneableChanged = true;
_cloneLifetimeChanged = true;
_cloneLimitChanged = true;
_cloneDynamicChanged = true;
_cloneAvatarEntityChanged = true;
_cloneOriginIDChanged = true;
}
// The minimum bounding box for the entity.
@ -3373,6 +3473,25 @@ QList<QString> EntityItemProperties::listChangedProperties() {
out += "isUVModeStretch";
}
if (cloneableChanged()) {
out += "cloneable";
}
if (cloneLifetimeChanged()) {
out += "cloneLifetime";
}
if (cloneLimitChanged()) {
out += "cloneLimit";
}
if (cloneDynamicChanged()) {
out += "cloneDynamic";
}
if (cloneAvatarEntityChanged()) {
out += "cloneAvatarEntity";
}
if (cloneOriginIDChanged()) {
out += "cloneOriginID";
}
getAnimation().listChangedProperties(out);
getKeyLight().listChangedProperties(out);
getAmbientLight().listChangedProperties(out);
@ -3536,3 +3655,18 @@ bool EntityItemProperties::verifyStaticCertificateProperties() {
// I.e., if we can verify that the certificateID was produced by High Fidelity signing the static certificate hash.
return verifySignature(EntityItem::_marketplacePublicKey, getStaticCertificateHash(), QByteArray::fromBase64(getCertificateID().toUtf8()));
}
void EntityItemProperties::convertToCloneProperties(const EntityItemID& entityIDToClone) {
setName(getName() + "-clone-" + entityIDToClone.toString());
setLocked(false);
setLifetime(getCloneLifetime());
setDynamic(getCloneDynamic());
setClientOnly(getCloneAvatarEntity());
setCreated(usecTimestampNow());
setLastEdited(usecTimestampNow());
setCloneable(ENTITY_ITEM_DEFAULT_CLONEABLE);
setCloneLifetime(ENTITY_ITEM_DEFAULT_CLONE_LIFETIME);
setCloneLimit(ENTITY_ITEM_DEFAULT_CLONE_LIMIT);
setCloneDynamic(ENTITY_ITEM_DEFAULT_CLONE_DYNAMIC);
setCloneAvatarEntity(ENTITY_ITEM_DEFAULT_CLONE_AVATAR_ENTITY);
}

View file

@ -272,6 +272,13 @@ 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_DEFAULT_CLONEABLE);
DEFINE_PROPERTY(PROP_CLONE_LIFETIME, CloneLifetime, cloneLifetime, float, ENTITY_ITEM_DEFAULT_CLONE_LIFETIME);
DEFINE_PROPERTY(PROP_CLONE_LIMIT, CloneLimit, cloneLimit, float, ENTITY_ITEM_DEFAULT_CLONE_LIMIT);
DEFINE_PROPERTY(PROP_CLONE_DYNAMIC, CloneDynamic, cloneDynamic, bool, ENTITY_ITEM_DEFAULT_CLONE_DYNAMIC);
DEFINE_PROPERTY(PROP_CLONE_AVATAR_ENTITY, CloneAvatarEntity, cloneAvatarEntity, bool, ENTITY_ITEM_DEFAULT_CLONE_AVATAR_ENTITY);
DEFINE_PROPERTY_REF(PROP_CLONE_ORIGIN_ID, CloneOriginID, cloneOriginID, QUuid, ENTITY_ITEM_DEFAULT_CLONE_ORIGIN_ID);
static QString getComponentModeString(uint32_t mode);
static QString getComponentModeAsString(uint32_t mode);
@ -294,6 +301,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);
@ -369,6 +378,8 @@ public:
bool verifyStaticCertificateProperties();
static bool verifySignature(const QString& key, const QByteArray& text, const QByteArray& signature);
void convertToCloneProperties(const EntityItemID& entityIDToClone);
protected:
QString getCollisionMaskAsString() const;
void setCollisionMaskFromString(const QString& maskString);
@ -515,6 +526,13 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) {
DEBUG_PROPERTY_IF_CHANGED(debug, properties, AmbientLightMode, ambientLightMode, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, SkyboxMode, skyboxMode, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, Cloneable, cloneable, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, CloneLifetime, cloneLifetime, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, CloneLimit, cloneLimit, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, CloneDynamic, cloneDynamic, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, CloneAvatarEntity, cloneAvatarEntity, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, CloneOriginID, cloneOriginID, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, VoxelVolumeSize, voxelVolumeSize, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, VoxelData, voxelData, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, VoxelSurfaceStyle, voxelSurfaceStyle, "");

View file

@ -97,4 +97,11 @@ const QUuid ENTITY_ITEM_DEFAULT_LAST_EDITED_BY = QUuid();
const bool ENTITY_ITEM_DEFAULT_RELAY_PARENT_JOINTS = false;
const bool ENTITY_ITEM_DEFAULT_CLONEABLE = false;
const float ENTITY_ITEM_DEFAULT_CLONE_LIFETIME = 300.0f;
const int ENTITY_ITEM_DEFAULT_CLONE_LIMIT = 0;
const bool ENTITY_ITEM_DEFAULT_CLONE_DYNAMIC = false;
const bool ENTITY_ITEM_DEFAULT_CLONE_AVATAR_ENTITY = false;
const QUuid ENTITY_ITEM_DEFAULT_CLONE_ORIGIN_ID = QUuid();
#endif // hifi_EntityItemPropertiesDefaults_h

View file

@ -204,6 +204,13 @@ enum EntityPropertyList {
PROP_CERTIFICATE_ID,
PROP_STATIC_CERTIFICATE_VERSION,
PROP_CLONEABLE,
PROP_CLONE_LIFETIME,
PROP_CLONE_LIMIT,
PROP_CLONE_DYNAMIC,
PROP_CLONE_AVATAR_ENTITY,
PROP_CLONE_ORIGIN_ID,
PROP_HAZE_MODE,
PROP_KEYLIGHT_COLOR,

View file

@ -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,37 @@ QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties
}
}
bool EntityScriptingInterface::addLocalEntityCopy(EntityItemProperties& properties, EntityItemID& id, bool isClone) {
bool success = true;
id = EntityItemID(QUuid::createUuid());
if (_entityTree) {
_entityTree->withWriteLock([&] {
EntityItemPointer entity = _entityTree->addEntity(id, properties, isClone);
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 +327,28 @@ QUuid EntityScriptingInterface::addModelEntity(const QString& name, const QStrin
return addEntity(properties);
}
QUuid EntityScriptingInterface::cloneEntity(QUuid entityIDToClone) {
EntityItemID newEntityID;
EntityItemProperties properties = getEntityProperties(entityIDToClone);
bool cloneAvatarEntity = properties.getCloneAvatarEntity();
properties.convertToCloneProperties(entityIDToClone);
if (cloneAvatarEntity) {
return addEntity(properties, true);
} else {
// setLastEdited timestamp to 0 to ensure this entity gets updated with the properties
// from the server-created entity, don't change this unless you know what you are doing
properties.setLastEdited(0);
bool success = addLocalEntityCopy(properties, newEntityID, true);
if (success) {
getEntityPacketSender()->queueCloneEntityMessage(entityIDToClone, newEntityID);
return newEntityID;
} else {
return QUuid();
}
}
}
EntityItemProperties EntityScriptingInterface::getEntityProperties(QUuid identity) {
EntityPropertyFlags noSpecificProperties;
return getEntityProperties(identity, noSpecificProperties);

View file

@ -224,6 +224,16 @@ 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);
/**jsdoc
* Request a clone of an entity. Only entities that have been marked as 'cloneable' will be able to be cloned using this method.
* A cloned entity has most of the properties of the orignal entity, and can be requested from clients that do not have rez permissions.
* The client requests a clone from the entity server, which returns back the entityID of a valid clone if the operation was allowed.
* @function Entities.cloneEntity
* @param {Uuid} entityIDToClone - the ID of the entity to clone
* @returns {Entities.EntityID} The ID of the newly created clone
*/
Q_INVOKABLE QUuid cloneEntity(QUuid entityIDToClone);
/**jsdoc
* Get the properties of an entity.
* @function Entities.getEntityProperties
@ -1875,6 +1885,7 @@ private:
bool polyVoxWorker(QUuid entityID, std::function<bool(PolyVoxEntityItem&)> actor);
bool setPoints(QUuid entityID, std::function<bool(LineEntityItem&)> actor);
void queueEntityMessage(PacketType packetType, EntityItemID entityID, const EntityItemProperties& properties);
bool addLocalEntityCopy(EntityItemProperties& propertiesWithSimID, EntityItemID& id, bool isClone = false);
EntityItemPointer checkForTreeEntityAndTypeMatch(const QUuid& entityID,
EntityTypes::EntityType entityType = EntityTypes::Unknown);

View file

@ -65,6 +65,7 @@ void EntitySimulation::prepareEntityForDelete(EntityItemPointer entity) {
removeEntityInternal(entity);
if (entity->getElement()) {
_deadEntities.insert(entity);
_entityTree->cleanupCloneIDs(entity->getEntityItemID());
}
}
}

View file

@ -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:
@ -492,7 +493,7 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti
return true;
}
EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) {
EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties, bool isClone) {
EntityItemPointer result = NULL;
EntityItemProperties props = properties;
@ -504,7 +505,7 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti
if (!properties.getClientOnly() && getIsClient() &&
!nodeList->getThisNodeCanRez() && !nodeList->getThisNodeCanRezTmp() &&
!nodeList->getThisNodeCanRezCertified() && !nodeList->getThisNodeCanRezTmpCertified() && !_serverlessDomain) {
!nodeList->getThisNodeCanRezCertified() && !nodeList->getThisNodeCanRezTmpCertified() && !_serverlessDomain && !isClone) {
return nullptr;
}
@ -592,6 +593,7 @@ void EntityTree::deleteEntity(const EntityItemID& entityID, bool force, bool ign
return;
}
cleanupCloneIDs(entityID);
unhookChildAvatar(entityID);
emit deletingEntity(entityID);
emit deletingEntityPointer(existingEntity.get());
@ -625,6 +627,28 @@ void EntityTree::unhookChildAvatar(const EntityItemID entityID) {
});
}
void EntityTree::cleanupCloneIDs(const EntityItemID& entityID) {
EntityItemPointer entity = findEntityByEntityItemID(entityID);
if (entity) {
// remove clone ID from it's clone origin's clone ID list if clone origin exists
const QUuid& cloneOriginID = entity->getCloneOriginID();
if (!cloneOriginID.isNull()) {
EntityItemPointer cloneOrigin = findEntityByID(cloneOriginID);
if (cloneOrigin) {
cloneOrigin->removeCloneID(entityID);
}
}
// clear the clone origin ID on any clones that this entity had
const QVector<QUuid>& cloneIDs = entity->getCloneIDs();
foreach(const QUuid& cloneChildID, cloneIDs) {
EntityItemPointer cloneChild = findEntityByEntityItemID(cloneChildID);
if (cloneChild) {
cloneChild->setCloneOriginID(QUuid());
}
}
}
}
void EntityTree::deleteEntities(QSet<EntityItemID> entityIDs, bool force, bool ignoreWarnings) {
// NOTE: callers must lock the tree before using this method
DeleteEntityOperator theOperator(getThisPointer());
@ -653,6 +677,7 @@ void EntityTree::deleteEntities(QSet<EntityItemID> entityIDs, bool force, bool i
}
// tell our delete operator about this entityID
cleanupCloneIDs(entityID);
unhookChildAvatar(entityID);
theOperator.addEntityIDToDeleteList(entityID);
emit deletingEntity(entityID);
@ -1392,6 +1417,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,6 +1426,9 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
break;
}
case PacketType::EntityClone:
isClone = true; // fall through to next case
// FALLTHRU
case PacketType::EntityAdd:
isAdd = true; // fall through to next case
// FALLTHRU
@ -1422,8 +1451,22 @@ 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<const char*>(editData), maxLength);
validEditPacket = EntityItemProperties::decodeCloneEntityMessage(buffer, processedBytes, entityIDToClone, entityItemID);
if (validEditPacket) {
entityToClone = findEntityByEntityItemID(entityIDToClone);
if (entityToClone) {
properties = entityToClone->getProperties();
}
}
} else {
validEditPacket = EntityItemProperties::decodeEntityEditPacket(editData, maxLength, processedBytes, entityItemID, properties);
}
endDecode = usecTimestampNow();
EntityItemPointer existingEntity;
@ -1491,24 +1534,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 +1611,32 @@ 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.getCloneLimit();
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 && cloneLimit != 0) {
failedAdd = true;
qCDebug(entities) << "User attempted to clone entity ID:" << entityIDToClone << " which reached it's cloneable limit.";
} else {
if (isClone) {
properties.convertToCloneProperties(entityIDToClone);
}
// this is a new entity... assign a new entityID
properties.setCreated(properties.getLastEdited());
properties.setLastEditedBy(senderNode->getUUID());
@ -1597,10 +1657,15 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
}
}
if (newEntity && isClone) {
entityToClone->addCloneID(newEntity->getEntityItemID());
newEntity->setCloneOriginID(entityIDToClone);
}
if (newEntity) {
newEntity->markAsChangedOnServer();
notifyNewlyCreatedEntity(*newEntity, senderNode);
startLogging = usecTimestampNow();
if (wantEditLogging()) {
qCDebug(entities) << "User [" << senderNode->getUUID() << "] added entity. ID:"
@ -1927,6 +1992,7 @@ int EntityTree::processEraseMessage(ReceivedMessage& message, const SharedNodePo
if (shouldEraseEntity(entityID, sourceNode)) {
entityItemIDsToDelete << entityItemID;
cleanupCloneIDs(entityItemID);
}
}
deleteEntities(entityItemIDsToDelete, true, true);
@ -1976,6 +2042,7 @@ int EntityTree::processEraseMessageDetails(const QByteArray& dataByteArray, cons
if (shouldEraseEntity(entityID, sourceNode)) {
entityItemIDsToDelete << entityItemID;
cleanupCloneIDs(entityItemID);
}
}
@ -2322,6 +2389,8 @@ bool EntityTree::readFromMap(QVariantMap& map) {
return false;
}
QMap<QUuid, QVector<QUuid>> cloneIDs;
bool success = true;
foreach (QVariant entityVariant, entitiesQList) {
// QVariantMap --> QScriptValue --> EntityItemProperties --> Entity
@ -2409,11 +2478,43 @@ bool EntityTree::readFromMap(QVariantMap& map) {
}
}
// Convert old cloneable entities so they use cloneableData instead of userData
if (contentVersion < (int)EntityVersion::CloneableData) {
QJsonObject userData = QJsonDocument::fromJson(properties.getUserData().toUtf8()).object();
QJsonObject grabbableKey = userData["grabbableKey"].toObject();
QJsonValue cloneable = grabbableKey["cloneable"];
if (cloneable.isBool() && cloneable.toBool()) {
QJsonValue cloneLifetime = grabbableKey["cloneLifetime"];
QJsonValue cloneLimit = grabbableKey["cloneLimit"];
QJsonValue cloneDynamic = grabbableKey["cloneDynamic"];
QJsonValue cloneAvatarEntity = grabbableKey["cloneAvatarEntity"];
// This is cloneable, we need to convert the properties
properties.setCloneable(true);
properties.setCloneLifetime(cloneLifetime.toInt());
properties.setCloneLimit(cloneLimit.toInt());
properties.setCloneDynamic(cloneDynamic.toBool());
properties.setCloneAvatarEntity(cloneAvatarEntity.toBool());
}
}
EntityItemPointer entity = addEntity(entityItemID, properties);
if (!entity) {
qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType();
success = false;
}
const QUuid& cloneOriginID = entity->getCloneOriginID();
if (!cloneOriginID.isNull()) {
cloneIDs[cloneOriginID].push_back(entity->getEntityItemID());
}
}
for (const auto& entityID : cloneIDs.keys()) {
auto entity = findEntityByID(entityID);
if (entity) {
entity->setCloneIDs(cloneIDs.value(entityID));
}
}
return success;

View file

@ -110,13 +110,14 @@ public:
// The newer API...
void postAddEntity(EntityItemPointer entityItem);
EntityItemPointer addEntity(const EntityItemID& entityID, const EntityItemProperties& properties);
EntityItemPointer addEntity(const EntityItemID& entityID, const EntityItemProperties& properties, bool isClone = false);
// use this method if you only know the entityID
bool updateEntity(const EntityItemID& entityID, const EntityItemProperties& properties, const SharedNodePointer& senderNode = SharedNodePointer(nullptr));
// check if the avatar is a child of this entity, If so set the avatar parentID to null
void unhookChildAvatar(const EntityItemID entityID);
void cleanupCloneIDs(const EntityItemID& entityID);
void deleteEntity(const EntityItemID& entityID, bool force = false, bool ignoreWarnings = true);
void deleteEntities(QSet<EntityItemID> entityIDs, bool force = false, bool ignoreWarnings = true);

View file

@ -29,10 +29,11 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::DomainList:
return static_cast<PacketVersion>(DomainListVersion::GetMachineFingerprintFromUUIDSupport);
case PacketType::EntityAdd:
case PacketType::EntityClone:
case PacketType::EntityEdit:
case PacketType::EntityData:
case PacketType::EntityPhysics:
return static_cast<PacketVersion>(EntityVersion::MaterialData);
return static_cast<PacketVersion>(EntityVersion::CloneableData);
case PacketType::EntityQuery:
return static_cast<PacketVersion>(EntityQueryPacketVersion::ConicalFrustums);
case PacketType::AvatarIdentity:

View file

@ -131,6 +131,8 @@ public:
OctreeDataFileReply,
OctreeDataPersist,
EntityClone,
NUM_PACKET_TYPE
};
@ -232,7 +234,8 @@ enum class EntityVersion : PacketVersion {
SoftEntities,
MaterialEntities,
ShadowControl,
MaterialData
MaterialData,
CloneableData
};
enum class EntityScriptCallMethodVersion : PacketVersion {

View file

@ -413,8 +413,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa
this.cloneHotspot = function(props, controllerData) {
if (entityIsCloneable(props)) {
var worldEntityProps = controllerData.nearbyEntityProperties[this.hand];
var cloneID = cloneEntity(props, worldEntityProps);
var cloneID = cloneEntity(props);
return cloneID;
}

View file

@ -235,8 +235,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js");
// switch to grabbing
var targetCloneable = entityIsCloneable(targetProps);
if (targetCloneable) {
var worldEntityProps = controllerData.nearbyEntityProperties[this.hand];
var cloneID = cloneEntity(targetProps, worldEntityProps);
var cloneID = cloneEntity(targetProps);
var cloneProps = Entities.getEntityProperties(cloneID);
this.targetEntityID = cloneID;
this.startNearGrabAction(controllerData, cloneProps);

View file

@ -35,6 +35,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js");
this.lastUnexpectedChildrenCheckTime = 0;
this.robbed = false;
this.highlightedEntity = null;
this.cloneAllowed = true;
this.parameters = makeDispatcherModuleParameters(
500,
@ -272,6 +273,7 @@ Script.include("/~/system/libraries/cloneEntityUtils.js");
controllerData.secondaryValues[this.hand] < TRIGGER_OFF_VALUE) {
this.checkForUnexpectedChildren(controllerData);
this.robbed = false;
this.cloneAllowed = true;
return makeRunningValues(false, [], []);
}
@ -335,13 +337,16 @@ Script.include("/~/system/libraries/cloneEntityUtils.js");
var targetCloneable = entityIsCloneable(targetProps);
if (targetCloneable) {
var worldEntityProps = controllerData.nearbyEntityProperties[this.hand];
var cloneID = cloneEntity(targetProps, worldEntityProps);
var cloneProps = Entities.getEntityProperties(cloneID);
this.grabbing = true;
this.targetEntityID = cloneID;
this.startNearParentingGrabEntity(controllerData, cloneProps);
if (this.cloneAllowed) {
var cloneID = cloneEntity(targetProps);
if (cloneID !== null) {
var cloneProps = Entities.getEntityProperties(cloneID);
this.grabbing = true;
this.targetEntityID = cloneID;
this.startNearParentingGrabEntity(controllerData, cloneProps);
this.cloneAllowed = false; // prevent another clone call until inputs released
}
}
} else if (targetProps) {
this.grabbing = true;
this.startNearParentingGrabEntity(controllerData, targetProps);

View file

@ -1040,12 +1040,13 @@ function loaded() {
elWantsTrigger.checked = false;
elIgnoreIK.checked = true;
elCloneable.checked = false;
elCloneableDynamic.checked = false;
elCloneable.checked = properties.cloneable;
elCloneableDynamic.checked = properties.cloneDynamic;
elCloneableAvatarEntity.checked = properties.cloneAvatarEntity;
elCloneableGroup.style.display = elCloneable.checked ? "block": "none";
elCloneableLimit.value = 0;
elCloneableLifetime.value = 300;
elCloneableLimit.value = properties.cloneLimit;
elCloneableLifetime.value = properties.cloneLifetime;
var grabbablesSet = false;
var parsedUserData = {};
try {
@ -1069,27 +1070,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 +1440,12 @@ 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('cloneDynamic'));
elCloneableAvatarEntity.addEventListener('change', createEmitCheckedPropertyUpdateFunction('cloneAvatarEntity'));
elCloneableLifetime.addEventListener('change', createEmitNumberPropertyUpdateFunction('cloneLifetime'));
elCloneableLimit.addEventListener('change', createEmitNumberPropertyUpdateFunction('cloneLimit'));
elWantsTrigger.addEventListener('change', function() {
userDataChanger("grabbableKey", "wantsTrigger", elWantsTrigger, elUserData, false);

View file

@ -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.cloneDynamic;
}
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;
cloneEntity = function(props) {
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;
}
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;
return null;
};

View file

@ -128,7 +128,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