Merge pull request #7840 from sethalves/avatar-entities-3

Avatar entities
This commit is contained in:
Brad Hefta-Gaub 2016-05-21 07:57:11 -07:00
commit 5b98b062f8
28 changed files with 779 additions and 134 deletions

View file

@ -762,6 +762,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
// Tell our entity edit sender about our known jurisdictions
_entityEditSender.setServerJurisdictions(&_entityServerJurisdictions);
_entityEditSender.setMyAvatar(getMyAvatar());
// For now we're going to set the PPS for outbound packets to be super high, this is
// probably not the right long term solution. But for now, we're going to do this to
@ -1050,6 +1051,10 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
// Make sure we don't time out during slow operations at startup
updateHeartbeat();
OctreeEditPacketSender* packetSender = entityScriptingInterface->getPacketSender();
EntityEditPacketSender* entityPacketSender = static_cast<EntityEditPacketSender*>(packetSender);
entityPacketSender->setMyAvatar(getMyAvatar());
connect(this, &Application::applicationStateChanged, this, &Application::activeChanged);
qCDebug(interfaceapp, "Startup time: %4.2f seconds.", (double)startupTimer.elapsed() / 1000.0);
@ -4194,6 +4199,7 @@ void Application::clearDomainOctreeDetails() {
qCDebug(interfaceapp) << "Clearing domain octree details...";
resetPhysicsReadyInformation();
getMyAvatar()->setAvatarEntityDataChanged(true); // to recreate worn entities
// reset our node to stats and node to jurisdiction maps... since these must be changing...
_entityServerJurisdictions.withWriteLock([&] {

View file

@ -31,6 +31,7 @@
#include <SharedUtil.h>
#include <TextRenderer3D.h>
#include <TextureCache.h>
#include <VariantMapToScriptValue.h>
#include <DebugDraw.h>
#include "Application.h"
@ -102,6 +103,18 @@ Avatar::Avatar(RigPointer rig) :
Avatar::~Avatar() {
assert(isDead()); // mark dead before calling the dtor
EntityTreeRenderer* treeRenderer = qApp->getEntities();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
if (entityTree) {
entityTree->withWriteLock([&] {
AvatarEntityMap avatarEntities = getAvatarEntityData();
for (auto entityID : avatarEntities.keys()) {
entityTree->deleteEntity(entityID, true, true);
}
});
}
if (_motionState) {
delete _motionState;
_motionState = nullptr;
@ -157,6 +170,90 @@ void Avatar::animateScaleChanges(float deltaTime) {
}
}
void Avatar::updateAvatarEntities() {
// - if queueEditEntityMessage sees clientOnly flag it does _myAvatar->updateAvatarEntity()
// - updateAvatarEntity saves the bytes and sets _avatarEntityDataLocallyEdited
// - MyAvatar::update notices _avatarEntityDataLocallyEdited and calls sendIdentityPacket
// - sendIdentityPacket sends the entity bytes to the server which relays them to other interfaces
// - AvatarHashMap::processAvatarIdentityPacket on other interfaces call avatar->setAvatarEntityData()
// - setAvatarEntityData saves the bytes and sets _avatarEntityDataChanged = true
// - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are...
if (!_avatarEntityDataChanged) {
return;
}
if (getID() == QUuid()) {
return; // wait until MyAvatar gets an ID before doing this.
}
EntityTreeRenderer* treeRenderer = qApp->getEntities();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
if (!entityTree) {
return;
}
bool success = true;
QScriptEngine scriptEngine;
entityTree->withWriteLock([&] {
AvatarEntityMap avatarEntities = getAvatarEntityData();
for (auto entityID : avatarEntities.keys()) {
// see EntityEditPacketSender::queueEditEntityMessage for the other end of this. unpack properties
// and either add or update the entity.
QByteArray jsonByteArray = avatarEntities.value(entityID);
QJsonDocument jsonProperties = QJsonDocument::fromBinaryData(jsonByteArray);
if (!jsonProperties.isObject()) {
qCDebug(interfaceapp) << "got bad avatarEntity json" << QString(jsonByteArray.toHex());
continue;
}
QVariant variantProperties = jsonProperties.toVariant();
QVariantMap asMap = variantProperties.toMap();
QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine);
EntityItemProperties properties;
EntityItemPropertiesFromScriptValueHonorReadOnly(scriptProperties, properties);
properties.setClientOnly(true);
properties.setOwningAvatarID(getID());
// there's no entity-server to tell us we're the simulation owner, so always set the
// simulationOwner to the owningAvatarID and a high priority.
properties.setSimulationOwner(getID(), AVATAR_ENTITY_SIMULATION_PRIORITY);
if (properties.getParentID() == AVATAR_SELF_ID) {
properties.setParentID(getID());
}
EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID));
if (entity) {
if (entityTree->updateEntity(entityID, properties)) {
entity->updateLastEditedFromRemote();
} else {
success = false;
}
} else {
entity = entityTree->addEntity(entityID, properties);
if (!entity) {
success = false;
}
}
}
AvatarEntityIDs recentlyDettachedAvatarEntities = getAndClearRecentlyDetachedIDs();
foreach (auto entityID, recentlyDettachedAvatarEntities) {
if (!_avatarEntityData.contains(entityID)) {
entityTree->deleteEntity(entityID, true, true);
}
}
});
if (success) {
setAvatarEntityDataChanged(false);
}
}
void Avatar::simulate(float deltaTime) {
PerformanceTimer perfTimer("simulate");
@ -229,6 +326,7 @@ void Avatar::simulate(float deltaTime) {
simulateAttachments(deltaTime);
updatePalms();
updateAvatarEntities();
}
bool Avatar::isLookingAtMe(AvatarSharedPointer avatar) const {
@ -1088,7 +1186,7 @@ void Avatar::setParentID(const QUuid& parentID) {
if (success) {
setTransform(beforeChangeTransform, success);
if (!success) {
qDebug() << "Avatar::setParentID failed to reset avatar's location.";
qCDebug(interfaceapp) << "Avatar::setParentID failed to reset avatar's location.";
}
}
}
@ -1103,7 +1201,7 @@ void Avatar::setParentJointIndex(quint16 parentJointIndex) {
if (success) {
setTransform(beforeChangeTransform, success);
if (!success) {
qDebug() << "Avatar::setParentJointIndex failed to reset avatar's location.";
qCDebug(interfaceapp) << "Avatar::setParentJointIndex failed to reset avatar's location.";
}
}
}

View file

@ -64,6 +64,7 @@ public:
typedef std::shared_ptr<render::Item::PayloadInterface> PayloadPointer;
void init();
void updateAvatarEntities();
void simulate(float deltaTime);
virtual void simulateAttachments(float deltaTime);

View file

@ -309,6 +309,10 @@ void MyAvatar::update(float deltaTime) {
head->setAudioLoudness(audio->getLastInputLoudness());
head->setAudioAverageLoudness(audio->getAudioAverageInputLoudness());
if (_avatarEntityDataLocallyEdited) {
sendIdentityPacket();
}
simulate(deltaTime);
currentEnergy += energyChargeRate;
@ -448,7 +452,8 @@ void MyAvatar::simulate(float deltaTime) {
EntityItemProperties properties = entity->getProperties();
properties.setQueryAACubeDirty();
properties.setLastEdited(now);
packetSender->queueEditEntityMessage(PacketType::EntityEdit, entity->getID(), properties);
packetSender->queueEditEntityMessage(PacketType::EntityEdit, entityTree, entity->getID(), properties);
entity->setLastBroadcast(usecTimestampNow());
}
}
@ -464,6 +469,8 @@ void MyAvatar::simulate(float deltaTime) {
_characterController.setEnabled(true);
}
}
updateAvatarEntities();
}
// thread-safe
@ -711,6 +718,16 @@ void MyAvatar::saveData() {
}
settings.endArray();
settings.beginWriteArray("avatarEntityData");
int avatarEntityIndex = 0;
for (auto entityID : _avatarEntityData.keys()) {
settings.setArrayIndex(avatarEntityIndex);
settings.setValue("id", entityID);
settings.setValue("properties", _avatarEntityData.value(entityID));
avatarEntityIndex++;
}
settings.endArray();
settings.setValue("displayName", _displayName);
settings.setValue("collisionSoundURL", _collisionSoundURL);
settings.setValue("useSnapTurn", _useSnapTurn);
@ -822,6 +839,17 @@ void MyAvatar::loadData() {
settings.endArray();
setAttachmentData(attachmentData);
int avatarEntityCount = settings.beginReadArray("avatarEntityData");
for (int i = 0; i < avatarEntityCount; i++) {
settings.setArrayIndex(i);
QUuid entityID = settings.value("id").toUuid();
// QUuid entityID = QUuid::createUuid(); // generate a new ID
QByteArray properties = settings.value("properties").toByteArray();
updateAvatarEntity(entityID, properties);
}
settings.endArray();
setAvatarEntityDataChanged(true);
setDisplayName(settings.value("displayName").toString());
setCollisionSoundURL(settings.value("collisionSoundURL", DEFAULT_AVATAR_COLLISION_SOUND_URL).toString());
setSnapTurn(settings.value("useSnapTurn", _useSnapTurn).toBool());

View file

@ -955,14 +955,16 @@ void AvatarData::clearJointsData() {
}
bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray& data) {
// this is used by the avatar-mixer
QDataStream packetStream(data);
QUuid avatarUUID;
QUrl unusedModelURL; // legacy faceModel support
QUrl skeletonModelURL;
QVector<AttachmentData> attachmentData;
AvatarEntityMap avatarEntityData;
QString displayName;
packetStream >> avatarUUID >> unusedModelURL >> skeletonModelURL >> attachmentData >> displayName;
packetStream >> avatarUUID >> unusedModelURL >> skeletonModelURL >> attachmentData >> displayName >> avatarEntityData;
bool hasIdentityChanged = false;
@ -982,6 +984,11 @@ bool AvatarData::hasIdentityChangedAfterParsing(const QByteArray& data) {
hasIdentityChanged = true;
}
if (avatarEntityData != _avatarEntityData) {
setAvatarEntityData(avatarEntityData);
hasIdentityChanged = true;
}
return hasIdentityChanged;
}
@ -993,7 +1000,7 @@ QByteArray AvatarData::identityByteArray() {
QUrl unusedModelURL; // legacy faceModel support
identityStream << QUuid() << unusedModelURL << urlToSend << _attachmentData << _displayName;
identityStream << QUuid() << unusedModelURL << urlToSend << _attachmentData << _displayName << _avatarEntityData;
return identityData;
}
@ -1167,6 +1174,8 @@ void AvatarData::sendIdentityPacket() {
[&](const SharedNodePointer& node) {
nodeList->sendPacketList(std::move(packetList), *node);
});
_avatarEntityDataLocallyEdited = false;
}
void AvatarData::updateJointMappings() {
@ -1339,6 +1348,7 @@ static const QString JSON_AVATAR_HEAD_MODEL = QStringLiteral("headModel");
static const QString JSON_AVATAR_BODY_MODEL = QStringLiteral("bodyModel");
static const QString JSON_AVATAR_DISPLAY_NAME = QStringLiteral("displayName");
static const QString JSON_AVATAR_ATTACHEMENTS = QStringLiteral("attachments");
static const QString JSON_AVATAR_ENTITIES = QStringLiteral("attachedEntities");
static const QString JSON_AVATAR_SCALE = QStringLiteral("scale");
QJsonValue toJsonValue(const JointData& joint) {
@ -1377,6 +1387,17 @@ QJsonObject AvatarData::toJson() const {
root[JSON_AVATAR_ATTACHEMENTS] = attachmentsJson;
}
if (!_avatarEntityData.empty()) {
QJsonArray avatarEntityJson;
for (auto entityID : _avatarEntityData.keys()) {
QVariantMap entityData;
entityData.insert("id", entityID);
entityData.insert("properties", _avatarEntityData.value(entityID));
avatarEntityJson.push_back(QVariant(entityData).toJsonObject());
}
root[JSON_AVATAR_ENTITIES] = avatarEntityJson;
}
auto recordingBasis = getRecordingBasis();
bool success;
Transform avatarTransform = getTransform(success);
@ -1476,6 +1497,13 @@ void AvatarData::fromJson(const QJsonObject& json) {
setAttachmentData(attachments);
}
// if (json.contains(JSON_AVATAR_ENTITIES) && json[JSON_AVATAR_ENTITIES].isArray()) {
// QJsonArray attachmentsJson = json[JSON_AVATAR_ATTACHEMENTS].toArray();
// for (auto attachmentJson : attachmentsJson) {
// // TODO -- something
// }
// }
// Joint rotations are relative to the avatar, so they require no basis correction
if (json.contains(JSON_AVATAR_JOINT_ARRAY)) {
QVector<JointData> jointArray;
@ -1628,9 +1656,69 @@ void AvatarData::setAttachmentsVariant(const QVariantList& variant) {
QVector<AttachmentData> newAttachments;
newAttachments.reserve(variant.size());
for (const auto& attachmentVar : variant) {
AttachmentData attachment;
AttachmentData attachment;
attachment.fromVariant(attachmentVar);
newAttachments.append(attachment);
}
setAttachmentData(newAttachments);
}
void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "updateAvatarEntity", Q_ARG(const QUuid&, entityID), Q_ARG(QByteArray, entityData));
return;
}
_avatarEntityData.insert(entityID, entityData);
_avatarEntityDataLocallyEdited = true;
}
void AvatarData::clearAvatarEntity(const QUuid& entityID) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "clearAvatarEntity", Q_ARG(const QUuid&, entityID));
return;
}
_avatarEntityData.remove(entityID);
_avatarEntityDataLocallyEdited = true;
}
AvatarEntityMap AvatarData::getAvatarEntityData() const {
if (QThread::currentThread() != thread()) {
AvatarEntityMap result;
QMetaObject::invokeMethod(const_cast<AvatarData*>(this), "getAvatarEntityData", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(AvatarEntityMap, result));
return result;
}
return _avatarEntityData;
}
void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "setAvatarEntityData", Q_ARG(const AvatarEntityMap&, avatarEntityData));
return;
}
if (_avatarEntityData != avatarEntityData) {
// keep track of entities that were attached to this avatar but no longer are
AvatarEntityIDs previousAvatarEntityIDs = QSet<QUuid>::fromList(_avatarEntityData.keys());
_avatarEntityData = avatarEntityData;
setAvatarEntityDataChanged(true);
foreach (auto entityID, previousAvatarEntityIDs) {
if (!_avatarEntityData.contains(entityID)) {
_avatarEntityDetached.insert(entityID);
}
}
}
}
AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() {
if (QThread::currentThread() != thread()) {
AvatarEntityIDs result;
QMetaObject::invokeMethod(const_cast<AvatarData*>(this), "getRecentlyDetachedIDs", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(AvatarEntityIDs, result));
return result;
}
AvatarEntityIDs result = _avatarEntityDetached;
_avatarEntityDetached.clear();
return result;
}

View file

@ -61,6 +61,8 @@ typedef unsigned long long quint64;
using AvatarSharedPointer = std::shared_ptr<AvatarData>;
using AvatarWeakPointer = std::weak_ptr<AvatarData>;
using AvatarHash = QHash<QUuid, AvatarSharedPointer>;
using AvatarEntityMap = QMap<QUuid, QByteArray>;
using AvatarEntityIDs = QSet<QUuid>;
using AvatarDataSequenceNumber = uint16_t;
@ -134,6 +136,10 @@ class AttachmentData;
class Transform;
using TransformPointer = std::shared_ptr<Transform>;
// When writing out avatarEntities to a QByteArray, if the parentID is the ID of MyAvatar, use this ID instead. This allows
// the value to be reset when the sessionID changes.
const QUuid AVATAR_SELF_ID = QUuid("{00000000-0000-0000-0000-000000000001}");
class AvatarData : public QObject, public SpatiallyNestable {
Q_OBJECT
@ -272,6 +278,9 @@ public:
Q_INVOKABLE QVariantList getAttachmentsVariant() const;
Q_INVOKABLE void setAttachmentsVariant(const QVariantList& variant);
Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData);
Q_INVOKABLE void clearAvatarEntity(const QUuid& entityID);
void setForceFaceTrackerConnected(bool connected) { _forceFaceTrackerConnected = connected; }
// key state
@ -323,6 +332,11 @@ public:
glm::vec3 getClientGlobalPosition() { return _globalPosition; }
Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const;
Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData);
void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; }
AvatarEntityIDs getAndClearRecentlyDetachedIDs();
public slots:
void sendAvatarDataPacket();
void sendIdentityPacket();
@ -390,6 +404,11 @@ protected:
// updates about one avatar to another.
glm::vec3 _globalPosition;
AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar
AvatarEntityMap _avatarEntityData;
bool _avatarEntityDataLocallyEdited { false };
bool _avatarEntityDataChanged { false };
private:
friend void avatarStateFromFrame(const QByteArray& frameData, AvatarData* _avatar);
static QUrl _defaultFullAvatarModelUrl;

View file

@ -107,21 +107,23 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer<ReceivedMessage> mess
}
void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
// this is used by clients
// setup a data stream to parse the packet
QDataStream identityStream(message->getMessage());
QUuid sessionUUID;
while (!identityStream.atEnd()) {
QUrl faceMeshURL, skeletonURL;
QVector<AttachmentData> attachmentData;
AvatarEntityMap avatarEntityData;
QString displayName;
identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> attachmentData >> displayName;
identityStream >> sessionUUID >> faceMeshURL >> skeletonURL >> attachmentData >> displayName >> avatarEntityData;
// mesh URL for a UUID, find avatar in our list
auto avatar = newOrExistingAvatar(sessionUUID, sendingNode);
if (avatar->getSkeletonModelURL().isEmpty() || (avatar->getSkeletonModelURL() != skeletonURL)) {
avatar->setSkeletonModelURL(skeletonURL); // Will expand "" to default and so will not continuously fire
}
@ -130,6 +132,8 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
avatar->setAttachmentData(attachmentData);
}
avatar->setAvatarEntityData(avatarEntityData);
if (avatar->getDisplayName() != displayName) {
avatar->setDisplayName(displayName);
}

View file

@ -47,6 +47,9 @@ namespace render {
}
void makeEntityItemStatusGetters(EntityItemPointer entity, render::Item::Status::Getters& statusGetters) {
auto nodeList = DependencyManager::get<NodeList>();
const QUuid& myNodeID = nodeList->getSessionUUID();
statusGetters.push_back([entity] () -> render::Item::Status::Value {
quint64 delta = usecTimestampNow() - entity->getLastEditedFromRemote();
const float WAIT_THRESHOLD_INV = 1.0f / (0.2f * USECS_PER_SECOND);
@ -81,9 +84,7 @@ void makeEntityItemStatusGetters(EntityItemPointer entity, render::Item::Status:
(unsigned char)RenderItemStatusIcon::ACTIVE_IN_BULLET);
});
statusGetters.push_back([entity] () -> render::Item::Status::Value {
auto nodeList = DependencyManager::get<NodeList>();
const QUuid& myNodeID = nodeList->getSessionUUID();
statusGetters.push_back([entity, myNodeID] () -> render::Item::Status::Value {
bool weOwnSimulation = entity->getSimulationOwner().matchesValidID(myNodeID);
bool otherOwnSimulation = !weOwnSimulation && !entity->getSimulationOwner().isNull();
@ -106,4 +107,18 @@ void makeEntityItemStatusGetters(EntityItemPointer entity, render::Item::Status:
return render::Item::Status::Value(0.0f, render::Item::Status::Value::GREEN,
(unsigned char)RenderItemStatusIcon::HAS_ACTIONS);
});
statusGetters.push_back([entity, myNodeID] () -> render::Item::Status::Value {
if (entity->getClientOnly()) {
if (entity->getOwningAvatarID() == myNodeID) {
return render::Item::Status::Value(1.0f, render::Item::Status::Value::GREEN,
(unsigned char)RenderItemStatusIcon::CLIENT_ONLY);
} else {
return render::Item::Status::Value(1.0f, render::Item::Status::Value::RED,
(unsigned char)RenderItemStatusIcon::CLIENT_ONLY);
}
}
return render::Item::Status::Value(0.0f, render::Item::Status::Value::GREEN,
(unsigned char)RenderItemStatusIcon::CLIENT_ONLY);
});
}

View file

@ -26,6 +26,7 @@ enum class RenderItemStatusIcon {
SIMULATION_OWNER = 3,
HAS_ACTIONS = 4,
OTHER_SIMULATION_OWNER = 5,
CLIENT_ONLY = 6,
NONE = 255
};

View file

@ -981,7 +981,7 @@ void RenderablePolyVoxEntityItem::compressVolumeDataAndSendEditPacket() {
PhysicalEntitySimulationPointer peSimulation = std::static_pointer_cast<PhysicalEntitySimulation>(simulation);
EntityEditPacketSender* packetSender = peSimulation ? peSimulation->getPacketSender() : nullptr;
if (packetSender) {
packetSender->queueEditEntityMessage(PacketType::EntityEdit, entity->getID(), properties);
packetSender->queueEditEntityMessage(PacketType::EntityEdit, tree, entity->getID(), properties);
}
});
});

View file

@ -10,6 +10,7 @@
//
#include <assert.h>
#include <QJsonDocument>
#include <PerfStat.h>
#include <OctalCode.h>
#include <udt/PacketHeaders.h>
@ -35,18 +36,76 @@ void EntityEditPacketSender::adjustEditPacketForClockSkew(PacketType type, QByte
}
}
void EntityEditPacketSender::queueEditEntityMessage(PacketType type, EntityItemID modelID,
const EntityItemProperties& properties) {
void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type,
EntityTreePointer entityTree,
EntityItemID entityItemID,
const EntityItemProperties& properties) {
if (!_shouldSend) {
return; // bail early
}
if (properties.getOwningAvatarID() != _myAvatar->getID()) {
return; // don't send updates for someone else's avatarEntity
}
assert(properties.getClientOnly());
// this is an avatar-based entity. update our avatar-data rather than sending to the entity-server
assert(_myAvatar);
if (!entityTree) {
qDebug() << "EntityEditPacketSender::queueEditEntityMessage null entityTree.";
return;
}
EntityItemPointer entity = entityTree->findEntityByEntityItemID(entityItemID);
if (!entity) {
qDebug() << "EntityEditPacketSender::queueEditEntityMessage can't find entity.";
return;
}
// the properties that get serialized into the avatar identity packet should be the entire set
// rather than just the ones being edited.
entity->setProperties(properties);
EntityItemProperties entityProperties = entity->getProperties();
QScriptValue scriptProperties = EntityItemNonDefaultPropertiesToScriptValue(&_scriptEngine, entityProperties);
QVariant variantProperties = scriptProperties.toVariant();
QJsonDocument jsonProperties = QJsonDocument::fromVariant(variantProperties);
// the ID of the parent/avatar changes from session to session. use a special UUID to indicate the avatar
QJsonObject jsonObject = jsonProperties.object();
if (QUuid(jsonObject["parentID"].toString()) == _myAvatar->getID()) {
jsonObject["parentID"] = AVATAR_SELF_ID.toString();
}
jsonProperties = QJsonDocument(jsonObject);
QByteArray binaryProperties = jsonProperties.toBinaryData();
_myAvatar->updateAvatarEntity(entityItemID, binaryProperties);
entity->setLastBroadcast(usecTimestampNow());
return;
}
void EntityEditPacketSender::queueEditEntityMessage(PacketType type,
EntityTreePointer entityTree,
EntityItemID entityItemID,
const EntityItemProperties& properties) {
if (!_shouldSend) {
return; // bail early
}
if (properties.getClientOnly()) {
queueEditAvatarEntityMessage(type, entityTree, entityItemID, properties);
return;
}
QByteArray bufferOut(NLPacket::maxPayloadSize(type), 0);
if (EntityItemProperties::encodeEntityEditPacket(type, modelID, properties, bufferOut)) {
if (EntityItemProperties::encodeEntityEditPacket(type, entityItemID, properties, bufferOut)) {
#ifdef WANT_DEBUG
qCDebug(entities) << "calling queueOctreeEditMessage()...";
qCDebug(entities) << " id:" << modelID;
qCDebug(entities) << " id:" << entityItemID;
qCDebug(entities) << " properties:" << properties;
#endif
queueOctreeEditMessage(type, bufferOut);
@ -58,6 +117,10 @@ void EntityEditPacketSender::queueEraseEntityMessage(const EntityItemID& entityI
return; // bail early
}
// in case this was a clientOnly entity:
assert(_myAvatar);
_myAvatar->clearAvatarEntity(entityItemID);
QByteArray bufferOut(NLPacket::maxPayloadSize(PacketType::EntityErase), 0);
if (EntityItemProperties::encodeEraseEntityMessage(entityItemID, bufferOut)) {

View file

@ -15,6 +15,7 @@
#include <OctreeEditPacketSender.h>
#include "EntityItem.h"
#include "AvatarData.h"
/// Utility for processing, packing, queueing and sending of outbound edit voxel messages.
class EntityEditPacketSender : public OctreeEditPacketSender {
@ -22,11 +23,21 @@ class EntityEditPacketSender : public OctreeEditPacketSender {
public:
EntityEditPacketSender();
void setMyAvatar(AvatarData* myAvatar) { _myAvatar = myAvatar; }
AvatarData* getMyAvatar() { return _myAvatar; }
void clearAvatarEntity(QUuid entityID) { assert(_myAvatar); _myAvatar->clearAvatarEntity(entityID); }
void queueEditAvatarEntityMessage(PacketType type, EntityTreePointer entityTree,
EntityItemID entityItemID, const EntityItemProperties& properties);
/// Queues an array of several voxel edit messages. Will potentially send a pending multi-command packet. Determines
/// which voxel-server node or nodes the packet should be sent to. Can be called even before voxel servers are known, in
/// which case up to MaxPendingMessages will be buffered and processed when voxel servers are known.
/// NOTE: EntityItemProperties assumes that all distances are in meter units
void queueEditEntityMessage(PacketType type, EntityItemID modelID, const EntityItemProperties& properties);
void queueEditEntityMessage(PacketType type, EntityTreePointer entityTree,
EntityItemID entityItemID, const EntityItemProperties& properties);
void queueEraseEntityMessage(const EntityItemID& entityItemID);
@ -40,5 +51,7 @@ public slots:
private:
bool _shouldProcessNack = true;
AvatarData* _myAvatar { nullptr };
QScriptEngine _scriptEngine;
};
#endif // hifi_EntityEditPacketSender_h

View file

@ -137,6 +137,9 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param
requestedProperties += PROP_PARENT_JOINT_INDEX;
requestedProperties += PROP_QUERY_AA_CUBE;
requestedProperties += PROP_CLIENT_ONLY;
requestedProperties += PROP_OWNING_AVATAR_ID;
return requestedProperties;
}
@ -1093,6 +1096,8 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper
properties._id = getID();
properties._idSet = true;
properties._created = _created;
properties.setClientOnly(_clientOnly);
properties.setOwningAvatarID(_owningAvatarID);
properties._type = getType();
@ -1133,6 +1138,9 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper
COPY_ENTITY_PROPERTY_TO_PROPERTIES(localPosition, getLocalPosition);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(localRotation, getLocalOrientation);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(clientOnly, getClientOnly);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(owningAvatarID, getOwningAvatarID);
properties._defaultSettings = false;
return properties;
@ -1222,6 +1230,9 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) {
SET_ENTITY_PROPERTY_FROM_PROPERTIES(parentJointIndex, setParentJointIndex);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(queryAACube, setQueryAACube);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(clientOnly, setClientOnly);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(owningAvatarID, setOwningAvatarID);
AACube saveQueryAACube = _queryAACube;
checkAndAdjustQueryAACube();
if (saveQueryAACube != _queryAACube) {

View file

@ -372,6 +372,7 @@ public:
glm::vec3 entityToWorld(const glm::vec3& point) const;
quint64 getLastEditedFromRemote() const { return _lastEditedFromRemote; }
void updateLastEditedFromRemote() { _lastEditedFromRemote = usecTimestampNow(); }
void getAllTerseUpdateProperties(EntityItemProperties& properties) const;
@ -422,12 +423,19 @@ public:
/// entity to definitively state if the preload signal should be sent.
///
/// We only want to preload if:
/// there is some script, and either the script value or the scriptTimestamp
/// there is some script, and either the script value or the scriptTimestamp
/// value have changed since our last preload
bool shouldPreloadScript() const { return !_script.isEmpty() &&
bool shouldPreloadScript() const { return !_script.isEmpty() &&
((_loadedScript != _script) || (_loadedScriptTimestamp != _scriptTimestamp)); }
void scriptHasPreloaded() { _loadedScript = _script; _loadedScriptTimestamp = _scriptTimestamp; }
bool getClientOnly() const { return _clientOnly; }
void setClientOnly(bool clientOnly) { _clientOnly = clientOnly; }
// if this entity is client-only, which avatar is it associated with?
QUuid getOwningAvatarID() const { return _owningAvatarID; }
void setOwningAvatarID(const QUuid& owningAvatarID) { _owningAvatarID = owningAvatarID; }
protected:
void setSimulated(bool simulated) { _simulated = simulated; }
@ -539,6 +547,9 @@ protected:
mutable QHash<QUuid, quint64> _previouslyDeletedActions;
QUuid _sourceUUID; /// the server node UUID we came from
bool _clientOnly { false };
QUuid _owningAvatarID;
};
#endif // hifi_EntityItem_h

View file

@ -307,9 +307,13 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
CHECK_PROPERTY_CHANGE(PROP_QUERY_AA_CUBE, queryAACube);
CHECK_PROPERTY_CHANGE(PROP_LOCAL_POSITION, localPosition);
CHECK_PROPERTY_CHANGE(PROP_LOCAL_ROTATION, localRotation);
CHECK_PROPERTY_CHANGE(PROP_FLYING_ALLOWED, flyingAllowed);
CHECK_PROPERTY_CHANGE(PROP_GHOSTING_ALLOWED, ghostingAllowed);
CHECK_PROPERTY_CHANGE(PROP_CLIENT_ONLY, clientOnly);
CHECK_PROPERTY_CHANGE(PROP_OWNING_AVATAR_ID, owningAvatarID);
changedProperties += _animation.getChangedProperties();
changedProperties += _keyLight.getChangedProperties();
changedProperties += _skybox.getChangedProperties();
@ -544,6 +548,12 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_POSITION, localPosition);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCAL_ROTATION, localRotation);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CLIENT_ONLY, clientOnly);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_OWNING_AVATAR_ID, owningAvatarID);
properties.setProperty("clientOnly", convertScriptValue(engine, getClientOnly()));
properties.setProperty("owningAvatarID", convertScriptValue(engine, getOwningAvatarID()));
// FIXME - I don't think these properties are supported any more
//COPY_PROPERTY_TO_QSCRIPTVALUE(localRenderAlpha);
@ -683,6 +693,9 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
COPY_PROPERTY_FROM_QSCRIPTVALUE(flyingAllowed, bool, setFlyingAllowed);
COPY_PROPERTY_FROM_QSCRIPTVALUE(ghostingAllowed, bool, setGhostingAllowed);
COPY_PROPERTY_FROM_QSCRIPTVALUE(clientOnly, bool, setClientOnly);
COPY_PROPERTY_FROM_QSCRIPTVALUE(owningAvatarID, QUuid, setOwningAvatarID);
_lastEdited = usecTimestampNow();
}
@ -1579,6 +1592,9 @@ void EntityItemProperties::markAllChanged() {
_flyingAllowedChanged = true;
_ghostingAllowedChanged = true;
_clientOnlyChanged = true;
_owningAvatarIDChanged = true;
}
// The minimum bounding box for the entity.
@ -1899,6 +1915,12 @@ QList<QString> EntityItemProperties::listChangedProperties() {
if (queryAACubeChanged()) {
out += "queryAACube";
}
if (clientOnlyChanged()) {
out += "clientOnly";
}
if (owningAvatarIDChanged()) {
out += "owningAvatarID";
}
if (flyingAllowedChanged()) {
out += "flyingAllowed";

View file

@ -208,6 +208,9 @@ public:
DEFINE_PROPERTY(PROP_FLYING_ALLOWED, FlyingAllowed, flyingAllowed, bool, ZoneEntityItem::DEFAULT_FLYING_ALLOWED);
DEFINE_PROPERTY(PROP_GHOSTING_ALLOWED, GhostingAllowed, ghostingAllowed, bool, ZoneEntityItem::DEFAULT_GHOSTING_ALLOWED);
DEFINE_PROPERTY(PROP_CLIENT_ONLY, ClientOnly, clientOnly, bool, false);
DEFINE_PROPERTY_REF(PROP_OWNING_AVATAR_ID, OwningAvatarID, owningAvatarID, QUuid, UNKNOWN_ENTITY_ID);
static QString getBackgroundModeString(BackgroundMode mode);
@ -422,6 +425,9 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) {
DEBUG_PROPERTY_IF_CHANGED(debug, properties, FlyingAllowed, flyingAllowed, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, GhostingAllowed, ghostingAllowed, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, ClientOnly, clientOnly, "");
DEBUG_PROPERTY_IF_CHANGED(debug, properties, OwningAvatarID, owningAvatarID, "");
properties.getAnimation().debugDump();
properties.getSkybox().debugDump();
properties.getStage().debugDump();

View file

@ -172,6 +172,9 @@ enum EntityPropertyList {
PROP_FLYING_ALLOWED, // can avatars in a zone fly?
PROP_GHOSTING_ALLOWED, // can avatars in a zone turn off physics?
PROP_CLIENT_ONLY, // doesn't go over wire
PROP_OWNING_AVATAR_ID, // doesn't go over wire
////////////////////////////////////////////////////////////////////////////////////////////////////
// ATTENTION: add new properties to end of list just ABOVE this line
PROP_AFTER_LAST_ITEM,

View file

@ -36,7 +36,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership
void EntityScriptingInterface::queueEntityMessage(PacketType packetType,
EntityItemID entityID, const EntityItemProperties& properties) {
getEntityPacketSender()->queueEditEntityMessage(packetType, entityID, properties);
getEntityPacketSender()->queueEditEntityMessage(packetType, _entityTree, entityID, properties);
}
bool EntityScriptingInterface::canAdjustLocks() {
@ -123,10 +123,17 @@ EntityItemProperties convertLocationFromScriptSemantics(const EntityItemProperti
}
QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties) {
QUuid EntityScriptingInterface::addEntity(const EntityItemProperties& properties, bool clientOnly) {
EntityItemProperties propertiesWithSimID = convertLocationFromScriptSemantics(properties);
propertiesWithSimID.setDimensionsInitialized(properties.dimensionsChanged());
if (clientOnly) {
auto nodeList = DependencyManager::get<NodeList>();
const QUuid myNodeID = nodeList->getSessionUUID();
propertiesWithSimID.setClientOnly(clientOnly);
propertiesWithSimID.setOwningAvatarID(myNodeID);
}
auto dimensions = propertiesWithSimID.getDimensions();
float volume = dimensions.x * dimensions.y * dimensions.z;
auto density = propertiesWithSimID.getDensity();
@ -272,13 +279,21 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
bool updatedEntity = false;
_entityTree->withWriteLock([&] {
EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
if (!entity) {
return;
}
auto nodeList = DependencyManager::get<NodeList>();
if (entity->getClientOnly() && entity->getOwningAvatarID() != nodeList->getSessionUUID()) {
// don't edit other avatar's avatarEntities
return;
}
if (scriptSideProperties.parentRelatedPropertyChanged()) {
// All of parentID, parentJointIndex, position, rotation are needed to make sense of any of them.
// If any of these changed, pull any missing properties from the entity.
EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
if (!entity) {
return;
}
//existing entity, retrieve old velocity for check down below
oldVelocity = entity->getVelocity().length();
@ -296,6 +311,8 @@ QUuid EntityScriptingInterface::editEntity(QUuid id, const EntityItemProperties&
}
}
properties = convertLocationFromScriptSemantics(properties);
properties.setClientOnly(entity->getClientOnly());
properties.setOwningAvatarID(entity->getOwningAvatarID());
float cost = calculateCost(density * volume, oldVelocity, newVelocity);
cost *= costMultiplier;
@ -384,6 +401,14 @@ void EntityScriptingInterface::deleteEntity(QUuid id) {
EntityItemPointer entity = _entityTree->findEntityByEntityItemID(entityID);
if (entity) {
auto nodeList = DependencyManager::get<NodeList>();
const QUuid myNodeID = nodeList->getSessionUUID();
if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) {
// don't delete other avatar's avatarEntities
shouldDelete = false;
return;
}
auto dimensions = entity->getDimensions();
float volume = dimensions.x * dimensions.y * dimensions.z;
auto density = entity->getDensity();
@ -771,6 +796,11 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID,
return false;
}
auto nodeList = DependencyManager::get<NodeList>();
const QUuid myNodeID = nodeList->getSessionUUID();
EntityItemProperties properties;
EntityItemPointer entity;
bool doTransmit = false;
_entityTree->withWriteLock([&] {
@ -786,15 +816,20 @@ bool EntityScriptingInterface::actionWorker(const QUuid& entityID,
return;
}
if (entity->getClientOnly() && entity->getOwningAvatarID() != myNodeID) {
return;
}
doTransmit = actor(simulation, entity);
if (doTransmit) {
properties.setClientOnly(entity->getClientOnly());
properties.setOwningAvatarID(entity->getOwningAvatarID());
_entityTree->entityChanged(entity);
}
});
// transmit the change
if (doTransmit) {
EntityItemProperties properties;
_entityTree->withReadLock([&] {
properties = entity->getProperties();
});

View file

@ -82,7 +82,7 @@ public slots:
Q_INVOKABLE bool canRez();
/// adds a model with the specific properties
Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties);
Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties, bool clientOnly = false);
/// temporary method until addEntity can be used from QJSEngine
Q_INVOKABLE QUuid addModelEntity(const QString& name, const QString& modelUrl, const glm::vec3& position);

View file

@ -311,7 +311,9 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI
EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) {
EntityItemPointer result = NULL;
if (getIsClient()) {
bool clientOnly = properties.getClientOnly();
if (!clientOnly && getIsClient()) {
// if our Node isn't allowed to create entities in this domain, don't try.
auto nodeList = DependencyManager::get<NodeList>();
if (nodeList && !nodeList->getThisNodeCanRez()) {
@ -1382,8 +1384,11 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra
properties.markAllChanged(); // so the entire property set is considered new, since we're making a new entity
EntityTreeElementPointer entityTreeElement = std::static_pointer_cast<EntityTreeElement>(element);
EntityTreePointer tree = entityTreeElement->getTree();
// queue the packet to send to the server
args->packetSender->queueEditEntityMessage(PacketType::EntityAdd, newID, properties);
args->packetSender->queueEditEntityMessage(PacketType::EntityAdd, tree, newID, properties);
// also update the local tree instantly (note: this is not our tree, but an alternate tree)
if (args->otherTree) {

View file

@ -27,6 +27,7 @@ const quint8 RECRUIT_SIMULATION_PRIORITY = VOLUNTEER_SIMULATION_PRIORITY + 1;
// When poking objects with scripts an observer will bid at SCRIPT_EDIT priority.
const quint8 SCRIPT_GRAB_SIMULATION_PRIORITY = 0x80;
const quint8 SCRIPT_POKE_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY - 1;
const quint8 AVATAR_ENTITY_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY + 1;
// PERSONAL priority (needs a better name) is the level at which a simulation observer owns its own avatar
// which really just means: things that collide with it will be bid at a priority level one lower

View file

@ -50,7 +50,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
return VERSION_ENTITIES_NO_FLY_ZONES;
case PacketType::AvatarData:
case PacketType::BulkAvatarData:
return static_cast<PacketVersion>(AvatarMixerPacketVersion::SoftAttachmentSupport);
return static_cast<PacketVersion>(AvatarMixerPacketVersion::AvatarEntities);
case PacketType::ICEServerHeartbeat:
return 18; // ICE Server Heartbeat signing
case PacketType::AssetGetInfo:

View file

@ -175,7 +175,8 @@ const PacketVersion VERSION_ENTITIES_NO_FLY_ZONES = 58;
enum class AvatarMixerPacketVersion : PacketVersion {
TranslationSupport = 17,
SoftAttachmentSupport
SoftAttachmentSupport,
AvatarEntities
};
#endif // hifi_PacketHeaders_h

View file

@ -404,6 +404,11 @@ bool EntityMotionState::shouldSendUpdate(uint32_t simulationStep) {
assert(_body);
assert(entityTreeIsLocked());
if (_entity->getClientOnly() && _entity->getOwningAvatarID() != Physics::getSessionUUID()) {
// don't send updates for someone else's avatarEntities
return false;
}
if (_entity->actionDataNeedsTransmit()) {
return true;
}
@ -547,8 +552,14 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_
qCDebug(physics) << "EntityMotionState::sendUpdate()... calling queueEditEntityMessage()...";
#endif
entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, id, properties);
_entity->setLastBroadcast(usecTimestampNow());
EntityTreeElementPointer element = _entity->getElement();
EntityTreePointer tree = element ? element->getTree() : nullptr;
properties.setClientOnly(_entity->getClientOnly());
properties.setOwningAvatarID(_entity->getOwningAvatarID());
entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, tree, id, properties);
_entity->setLastBroadcast(now);
// if we've moved an entity with children, check/update the queryAACube of all descendents and tell the server
// if they've changed.
@ -559,8 +570,13 @@ void EntityMotionState::sendUpdate(OctreeEditPacketSender* packetSender, uint32_
EntityItemProperties newQueryCubeProperties;
newQueryCubeProperties.setQueryAACube(descendant->getQueryAACube());
newQueryCubeProperties.setLastEdited(properties.getLastEdited());
entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, descendant->getID(), newQueryCubeProperties);
entityDescendant->setLastBroadcast(usecTimestampNow());
newQueryCubeProperties.setClientOnly(entityDescendant->getClientOnly());
newQueryCubeProperties.setOwningAvatarID(entityDescendant->getOwningAvatarID());
entityPacketSender->queueEditEntityMessage(PacketType::EntityEdit, tree,
descendant->getID(), newQueryCubeProperties);
entityDescendant->setLastBroadcast(now);
}
}
});

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64mm"
height="64mm"
viewBox="0 0 226.77165 226.77165"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="lock.svg">
<defs
id="defs4">
<linearGradient
id="linearGradient5587"
osb:paint="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop5589" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5587"
id="linearGradient5591"
x1="63.214287"
y1="970.93364"
x2="165.35715"
y2="970.93364"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="-342.3833"
inkscape:cy="122.87157"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2782"
inkscape:window-height="1764"
inkscape:window-x="98"
inkscape:window-y="36"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-825.59055)">
<rect
style="fill:url(#linearGradient5591);fill-opacity:1;stroke:#000000;stroke-linejoin:round;stroke-opacity:1;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4138"
width="97.14286"
height="82.85714"
x="65.714287"
y="929.50507" />
<path
style="fill:none;fill-rule:evenodd;stroke:#a7abdf;stroke-width:9.00350285;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 78.319686,927.59356 c -2.221784,0.87707 -0.135759,-31.35492 36.848474,-30.3048 34.29837,0.97388 35.8249,30.70886 35.8249,30.70886"
id="path4148"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64mm"
height="64mm"
viewBox="0 0 226.77165 226.77165"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="unlock.svg">
<defs
id="defs4">
<linearGradient
id="linearGradient4135"
osb:paint="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop4137" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4135"
id="linearGradient4139"
x1="63.214287"
y1="970.93364"
x2="165.35715"
y2="970.93364"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="-280.07997"
inkscape:cy="122.87157"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="2782"
inkscape:window-height="1764"
inkscape:window-x="98"
inkscape:window-y="36"
inkscape:window-maximized="1" />
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-825.59055)">
<rect
style="fill:url(#linearGradient4139);fill-opacity:1;stroke:#000000;stroke-linejoin:round;stroke-opacity:1;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4138"
width="97.14286"
height="82.85714"
x="65.714287"
y="929.50507" />
<path
style="fill:none;fill-rule:evenodd;stroke:#abaac5;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 142.93659,923.06268 c -2.19267,1.09633 -0.13398,-39.19338 36.36549,-37.88073 33.84882,1.21733 35.35534,38.3858 35.35534,38.3858"
id="path4148"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -22,42 +22,28 @@ var MINIMUM_DROP_DISTANCE_FROM_JOINT = 0.8;
var ATTACHED_ENTITY_SEARCH_DISTANCE = 10.0;
var ATTACHED_ENTITIES_SETTINGS_KEY = "ATTACHED_ENTITIES";
var DRESSING_ROOM_DISTANCE = 2.0;
var SHOW_TOOL_BAR = false;
var SHOW_TOOL_BAR = true;
// tool bar
if (SHOW_TOOL_BAR) {
var BUTTON_SIZE = 32;
var PADDING = 3;
var BUTTON_SIZE = 64;
var PADDING = 6;
Script.include(["libraries/toolBars.js"]);
var toolBar = new ToolBar(0, 0, ToolBar.VERTICAL, "highfidelity.attachedEntities.toolbar", function(screenSize) {
return {
x: (BUTTON_SIZE + PADDING),
y: (screenSize.y / 2 - BUTTON_SIZE * 2 + PADDING)
};
});
var saveButton = toolBar.addOverlay("image", {
var toolBar = new ToolBar(0, 0, ToolBar.HORIZONTAL, "highfidelity.attachedEntities.toolbar");
var lockButton = toolBar.addTool({
width: BUTTON_SIZE,
height: BUTTON_SIZE,
imageURL: ".../save.png",
imageURL: Script.resolvePath("assets/images/lock.svg"),
color: {
red: 255,
green: 255,
blue: 255
},
alpha: 1
});
var loadButton = toolBar.addOverlay("image", {
width: BUTTON_SIZE,
height: BUTTON_SIZE,
imageURL: ".../load.png",
color: {
red: 255,
green: 255,
blue: 255
},
alpha: 1
});
alpha: 1,
visible: true
}, false);
}
@ -67,10 +53,8 @@ function mousePressEvent(event) {
y: event.y
});
if (clickedOverlay == saveButton) {
manager.saveAttachedEntities();
} else if (clickedOverlay == loadButton) {
manager.loadAttachedEntities();
if (lockButton === toolBar.clicked(clickedOverlay)) {
manager.toggleLocked();
}
}
@ -92,6 +76,8 @@ Script.scriptEnding.connect(scriptEnding);
function AttachedEntitiesManager() {
var clothingLocked = true;
this.subscribeToMessages = function() {
Messages.subscribe('Hifi-Object-Manipulation');
Messages.messageReceived.connect(this.handleWearableMessages);
@ -128,26 +114,14 @@ function AttachedEntitiesManager() {
}
}
this.avatarIsInDressingRoom = function() {
// return true if MyAvatar is near the dressing room
var possibleDressingRoom = Entities.findEntities(MyAvatar.position, DRESSING_ROOM_DISTANCE);
for (i = 0; i < possibleDressingRoom.length; i++) {
var entityID = possibleDressingRoom[i];
var props = Entities.getEntityProperties(entityID);
if (props.name == 'Hifi-Dressing-Room-Base') {
return true;
}
}
return false;
}
this.handleEntityRelease = function(grabbedEntity, releasedFromJoint) {
// if this is still equipped, just rewrite the position information.
var grabData = getEntityCustomData('grabKey', grabbedEntity, {});
if ("refCount" in grabData && grabData.refCount > 0) {
manager.updateRelativeOffsets(grabbedEntity);
return;
}
// if ("refCount" in grabData && grabData.refCount > 0) {
// // for adjusting things in your other hand
// manager.updateRelativeOffsets(grabbedEntity);
// return;
// }
var allowedJoints = getEntityCustomData('wearable', grabbedEntity, DEFAULT_WEARABLE_DATA).joints;
@ -179,21 +153,27 @@ function AttachedEntitiesManager() {
}
if (bestJointIndex != -1) {
var wearProps = {
parentID: MyAvatar.sessionUUID,
parentJointIndex: bestJointIndex
};
var wearProps = Entities.getEntityProperties(grabbedEntity);
wearProps.parentID = MyAvatar.sessionUUID;
wearProps.parentJointIndex = bestJointIndex;
var updatePresets = false;
if (bestJointOffset && bestJointOffset.constructor === Array) {
if (this.avatarIsInDressingRoom() || bestJointOffset.length < 2) {
this.updateRelativeOffsets(grabbedEntity);
if (!clothingLocked || bestJointOffset.length < 2) {
// we're unlocked or this thing didn't have a preset position, so update it
updatePresets = true;
} else {
// don't snap the entity to the preferred position if the avatar is in the dressing room.
// don't snap the entity to the preferred position if unlocked
wearProps.localPosition = bestJointOffset[0];
wearProps.localRotation = bestJointOffset[1];
}
}
Entities.editEntity(grabbedEntity, wearProps);
Entities.deleteEntity(grabbedEntity);
grabbedEntity = Entities.addEntity(wearProps, true);
if (updatePresets) {
this.updateRelativeOffsets(grabbedEntity);
}
} else if (props.parentID != NULL_UUID) {
// drop the entity and set it to have no parent (not on the avatar), unless it's being equipped in a hand.
if (props.parentID === MyAvatar.sessionUUID &&
@ -201,7 +181,26 @@ function AttachedEntitiesManager() {
props.parentJointIndex == MyAvatar.getJointIndex("LeftHand"))) {
// this is equipped on a hand -- don't clear the parent.
} else {
Entities.editEntity(grabbedEntity, { parentID: NULL_UUID });
var wearProps = Entities.getEntityProperties(grabbedEntity);
wearProps.parentID = NULL_UUID;
wearProps.parentJointIndex = -1;
delete wearProps.id;
delete wearProps.created;
delete wearProps.age;
delete wearProps.ageAsText;
delete wearProps.naturalDimensions;
delete wearProps.naturalPosition;
delete wearProps.actionData;
delete wearProps.sittingPoints;
delete wearProps.boundingBox;
delete wearProps.clientOnly;
delete wearProps.owningAvatarID;
delete wearProps.localPosition;
delete wearProps.localRotation;
Entities.deleteEntity(grabbedEntity);
Entities.addEntity(wearProps);
}
}
}
@ -221,21 +220,32 @@ function AttachedEntitiesManager() {
return false;
}
this.saveAttachedEntities = function() {
print("--- saving attached entities ---");
saveData = [];
var nearbyEntities = Entities.findEntities(MyAvatar.position, ATTACHED_ENTITY_SEARCH_DISTANCE);
for (i = 0; i < nearbyEntities.length; i++) {
var entityID = nearbyEntities[i];
if (this.updateRelativeOffsets(entityID)) {
var props = Entities.getEntityProperties(entityID); // refresh, because updateRelativeOffsets changed them
this.scrubProperties(props);
saveData.push(props);
}
this.toggleLocked = function() {
print("toggleLocked");
if (clothingLocked) {
clothingLocked = false;
toolBar.setImageURL(Script.resolvePath("assets/images/unlock.svg"), lockButton);
} else {
clothingLocked = true;
toolBar.setImageURL(Script.resolvePath("assets/images/lock.svg"), lockButton);
}
Settings.setValue(ATTACHED_ENTITIES_SETTINGS_KEY, JSON.stringify(saveData));
}
// this.saveAttachedEntities = function() {
// print("--- saving attached entities ---");
// saveData = [];
// var nearbyEntities = Entities.findEntities(MyAvatar.position, ATTACHED_ENTITY_SEARCH_DISTANCE);
// for (i = 0; i < nearbyEntities.length; i++) {
// var entityID = nearbyEntities[i];
// if (this.updateRelativeOffsets(entityID)) {
// var props = Entities.getEntityProperties(entityID); // refresh, because updateRelativeOffsets changed them
// this.scrubProperties(props);
// saveData.push(props);
// }
// }
// Settings.setValue(ATTACHED_ENTITIES_SETTINGS_KEY, JSON.stringify(saveData));
// }
this.scrubProperties = function(props) {
var toScrub = ["queryAACube", "position", "rotation",
"created", "ageAsText", "naturalDimensions",
@ -258,37 +268,37 @@ function AttachedEntitiesManager() {
}
}
this.loadAttachedEntities = function(grabbedEntity) {
print("--- loading attached entities ---");
jsonAttachmentData = Settings.getValue(ATTACHED_ENTITIES_SETTINGS_KEY);
var loadData = [];
try {
loadData = JSON.parse(jsonAttachmentData);
} catch (e) {
print('error parsing saved attachment data');
return;
}
// this.loadAttachedEntities = function(grabbedEntity) {
// print("--- loading attached entities ---");
// jsonAttachmentData = Settings.getValue(ATTACHED_ENTITIES_SETTINGS_KEY);
// var loadData = [];
// try {
// loadData = JSON.parse(jsonAttachmentData);
// } catch (e) {
// print('error parsing saved attachment data');
// return;
// }
for (i = 0; i < loadData.length; i ++) {
var savedProps = loadData[ i ];
var currentProps = Entities.getEntityProperties(savedProps.id);
if (currentProps.id == savedProps.id &&
// TODO -- also check that parentJointIndex matches?
currentProps.parentID == MyAvatar.sessionUUID) {
// entity is already in-world. TODO -- patch it up?
continue;
}
this.scrubProperties(savedProps);
delete savedProps["id"];
savedProps.parentID = MyAvatar.sessionUUID; // this will change between sessions
var loadedEntityID = Entities.addEntity(savedProps);
// for (i = 0; i < loadData.length; i ++) {
// var savedProps = loadData[ i ];
// var currentProps = Entities.getEntityProperties(savedProps.id);
// if (currentProps.id == savedProps.id &&
// // TODO -- also check that parentJointIndex matches?
// currentProps.parentID == MyAvatar.sessionUUID) {
// // entity is already in-world. TODO -- patch it up?
// continue;
// }
// this.scrubProperties(savedProps);
// delete savedProps["id"];
// savedProps.parentID = MyAvatar.sessionUUID; // this will change between sessions
// var loadedEntityID = Entities.addEntity(savedProps, true);
Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
action: 'loaded',
grabbedEntity: loadedEntityID
}));
}
}
// Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({
// action: 'loaded',
// grabbedEntity: loadedEntityID
// }));
// }
// }
}
var manager = new AttachedEntitiesManager();

View file

@ -56,6 +56,10 @@ Overlay2D = function(properties, overlay) { // overlay is an optional variable
properties.alpha = alpha;
Overlays.editOverlay(overlay, { alpha: alpha });
}
this.setImageURL = function(imageURL) {
properties.imageURL = imageURL;
Overlays.editOverlay(overlay, { imageURL: imageURL });
}
this.show = function(doShow) {
properties.visible = doShow;
Overlays.editOverlay(overlay, { visible: doShow });
@ -254,7 +258,7 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit
}
this.save();
}
this.setAlpha = function(alpha, tool) {
if(typeof(tool) === 'undefined') {
for(var tool in this.tools) {
@ -268,7 +272,11 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit
this.tools[tool].setAlpha(alpha);
}
}
this.setImageURL = function(imageURL, tool) {
this.tools[tool].setImageURL(imageURL);
}
this.setBack = function(color, alpha) {
if (color == null) {
Overlays.editOverlay(this.back, { visible: false });
@ -478,4 +486,4 @@ ToolBar = function(x, y, direction, optionalPersistenceKey, optionalInitialPosit
}
ToolBar.SPACING = 6;
ToolBar.VERTICAL = 0;
ToolBar.HORIZONTAL = 1;
ToolBar.HORIZONTAL = 1;