diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 33e1034128..9816cebf43 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -253,10 +253,29 @@ void AvatarMixer::start() { int lockWait, nodeTransform, functor; + // Set our query each frame { _entityViewer.queryOctree(); } + // Dirty the hero status if there's been an entity change. + { + if (_dirtyHeroStatus) { + _dirtyHeroStatus = false; + nodeList->nestedEach([](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + std::for_each(cbegin, cend, [](const SharedNodePointer& node) { + if (node->getType() == NodeType::Agent) { + NodeData* nodeData = node->getLinkedData(); + if (nodeData) { + auto& avatar = static_cast(nodeData)->getAvatar(); + avatar.setNeedsHeroCheck(); + } + } + }); + }); + } + } + // Allow nodes to process any pending/queued packets across our worker threads { auto start = usecTimestampNow(); @@ -827,7 +846,7 @@ void AvatarMixer::sendStatsPacket() { QJsonObject avatarsObject; auto nodeList = DependencyManager::get(); - // add stats for each listerner + // add stats for each listener nodeList->eachNode([&](const SharedNodePointer& node) { QJsonObject avatarStats; @@ -851,6 +870,12 @@ void AvatarMixer::sendStatsPacket() { avatarStats["delta_full_vs_avatar_data_kbps"] = (double)outboundAvatarDataKbps - avatarStats[OUTBOUND_AVATAR_DATA_STATS_KEY].toDouble(); } + + if (node->getType() != NodeType::Agent) { // Nodes that aren't avatars + const QString displayName + { node->getType() == NodeType::EntityScriptServer ? "ENTITY SCRIPT SERVER" : "ENTITY SERVER" }; + avatarStats["display_name"] = displayName; + } } avatarsObject[uuidStringWithoutCurlyBraces(node->getUUID())] = avatarStats; @@ -973,19 +998,30 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { { const QString CONNECTION_RATE = "connection_rate"; auto nodeList = DependencyManager::get(); - auto defaultConnectionRate = nodeList->getMaxConnectionRate(); - int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toInt((int)defaultConnectionRate); - nodeList->setMaxConnectionRate(connectionRate); + bool success; + int connectionRate = avatarMixerGroupObject[CONNECTION_RATE].toString().toInt(&success); + if (success) { + nodeList->setMaxConnectionRate(connectionRate); + } + } + + { // Fraction of downstream bandwidth reserved for 'hero' avatars: + static const QString PRIORITY_FRACTION_KEY = "priority_fraction"; + if (avatarMixerGroupObject.contains(PRIORITY_FRACTION_KEY)) { + float priorityFraction = float(avatarMixerGroupObject[PRIORITY_FRACTION_KEY].toDouble()); + _slavePool.setPriorityReservedFraction(std::min(std::max(0.0f, priorityFraction), 1.0f)); + qCDebug(avatars) << "Avatar mixer reserving" << priorityFraction << "of bandwidth for priority avatars"; + } } const QString AVATARS_SETTINGS_KEY = "avatars"; static const QString MIN_HEIGHT_OPTION = "min_avatar_height"; - float settingMinHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); + float settingMinHeight = avatarMixerGroupObject[MIN_HEIGHT_OPTION].toDouble(MIN_AVATAR_HEIGHT); _domainMinimumHeight = glm::clamp(settingMinHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); static const QString MAX_HEIGHT_OPTION = "max_avatar_height"; - float settingMaxHeight = domainSettings[AVATARS_SETTINGS_KEY].toObject()[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); + float settingMaxHeight = avatarMixerGroupObject[MAX_HEIGHT_OPTION].toDouble(MAX_AVATAR_HEIGHT); _domainMaximumHeight = glm::clamp(settingMaxHeight, MIN_AVATAR_HEIGHT, MAX_AVATAR_HEIGHT); // make sure that the domain owner didn't flip min and max @@ -997,11 +1033,11 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { << "and a maximum avatar height of" << _domainMaximumHeight; static const QString AVATAR_WHITELIST_OPTION = "avatar_whitelist"; - _slaveSharedData.skeletonURLWhitelist = domainSettings[AVATARS_SETTINGS_KEY].toObject()[AVATAR_WHITELIST_OPTION] + _slaveSharedData.skeletonURLWhitelist = avatarMixerGroupObject[AVATAR_WHITELIST_OPTION] .toString().split(',', QString::KeepEmptyParts); static const QString REPLACEMENT_AVATAR_OPTION = "replacement_avatar"; - _slaveSharedData.skeletonReplacementURL = domainSettings[AVATARS_SETTINGS_KEY].toObject()[REPLACEMENT_AVATAR_OPTION] + _slaveSharedData.skeletonReplacementURL = avatarMixerGroupObject[REPLACEMENT_AVATAR_OPTION] .toString(); if (_slaveSharedData.skeletonURLWhitelist.count() == 1 && _slaveSharedData.skeletonURLWhitelist[0].isEmpty()) { @@ -1018,9 +1054,12 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { void AvatarMixer::setupEntityQuery() { _entityViewer.init(); + EntityTreePointer entityTree = _entityViewer.getTree(); DependencyManager::registerInheritance(); - DependencyManager::set(_entityViewer.getTree()); - _slaveSharedData.entityTree = _entityViewer.getTree(); + DependencyManager::set(entityTree); + + connect(entityTree.get(), &EntityTree::addingEntityPointer, this, &AvatarMixer::entityAdded); + connect(entityTree.get(), &EntityTree::deletingEntityPointer, this, &AvatarMixer::entityChange); // ES query: {"avatarPriority": true, "type": "Zone"} QJsonObject priorityZoneQuery; @@ -1028,6 +1067,7 @@ void AvatarMixer::setupEntityQuery() { priorityZoneQuery["type"] = "Zone"; _entityViewer.getOctreeQuery().setJSONParameters(priorityZoneQuery); + _slaveSharedData.entityTree = entityTree; } void AvatarMixer::handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode) { @@ -1064,6 +1104,25 @@ void AvatarMixer::handleOctreePacket(QSharedPointer message, Sh } } +void AvatarMixer::entityAdded(EntityItem* entity) { + if (entity->getType() == EntityTypes::Zone) { + _dirtyHeroStatus = true; + entity->registerChangeHandler([this](const EntityItemID& entityItemID) { + entityChange(); + }); + } +} + +void AvatarMixer::entityRemoved(EntityItem * entity) { + if (entity->getType() == EntityTypes::Zone) { + _dirtyHeroStatus = true; + } +} + +void AvatarMixer::entityChange() { + _dirtyHeroStatus = true; +} + void AvatarMixer::aboutToFinish() { DependencyManager::destroy(); DependencyManager::destroy(); diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 9393ea6c56..10dff5e8a4 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -34,8 +34,8 @@ public: static bool shouldReplicateTo(const Node& from, const Node& to) { return to.getType() == NodeType::DownstreamAvatarMixer && - to.getPublicSocket() != from.getPublicSocket() && - to.getLocalSocket() != from.getLocalSocket(); + to.getPublicSocket() != from.getPublicSocket() && + to.getLocalSocket() != from.getLocalSocket(); } public slots: @@ -46,6 +46,11 @@ public slots: void sendStatsPacket() override; + // Avatar zone possibly changed + void entityAdded(EntityItem* entity); + void entityRemoved(EntityItem* entity); + void entityChange(); + private slots: void queueIncomingPacket(QSharedPointer message, SharedNodePointer node); void handleAdjustAvatarSorting(QSharedPointer message, SharedNodePointer senderNode); @@ -80,6 +85,7 @@ private: // Attach to entity tree for avatar-priority zone info. EntityTreeHeadlessViewer _entityViewer; + bool _dirtyHeroStatus { true }; // Dirty the needs-hero-update // FIXME - new throttling - use these values somehow float _trailingMixRatio { 0.0f }; diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index dfbeca96ce..1b86e0dff2 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -129,7 +129,7 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared incrementNumOutOfOrderSends(); } _lastReceivedSequenceNumber = sequenceNumber; - glm::vec3 oldPosition = getPosition(); + glm::vec3 oldPosition = _avatar->getClientGlobalPosition(); bool oldHasPriority = _avatar->getHasPriority(); // compute the offset to the data payload @@ -140,23 +140,13 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message, const SlaveShared // Regardless of what the client says, restore the priority as we know it without triggering any update. _avatar->setHasPriorityWithoutTimestampReset(oldHasPriority); - auto newPosition = getPosition(); - if (newPosition != oldPosition) { -//#define AVATAR_HERO_TEST_HACK -#ifdef AVATAR_HERO_TEST_HACK - { - const static QString heroKey { "HERO" }; - _avatar->setPriorityAvatar(_avatar->getDisplayName().contains(heroKey)); - } -#else + auto newPosition = _avatar->getClientGlobalPosition(); + if (newPosition != oldPosition || _avatar->getNeedsHeroCheck()) { EntityTree& entityTree = *slaveSharedData.entityTree; - FindPriorityZone findPriorityZone { newPosition, false } ; + FindPriorityZone findPriorityZone { newPosition } ; entityTree.recurseTreeWithOperation(&FindPriorityZone::operation, &findPriorityZone); _avatar->setHasPriority(findPriorityZone.isInPriorityZone); - //if (findPriorityZone.isInPriorityZone) { - // qCWarning(avatars) << "Avatar" << _avatar->getSessionDisplayName() << "in hero zone"; - //} -#endif + _avatar->setNeedsHeroCheck(false); } return true; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp index fdbdf21607..32c944f5b8 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -43,12 +43,14 @@ void AvatarMixerSlave::configure(ConstIter begin, ConstIter end) { void AvatarMixerSlave::configureBroadcast(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, - float maxKbpsPerNode, float throttlingRatio) { + float maxKbpsPerNode, float throttlingRatio, + float priorityReservedFraction) { _begin = begin; _end = end; _lastFrameTimestamp = lastFrameTimestamp; _maxKbpsPerNode = maxKbpsPerNode; _throttlingRatio = throttlingRatio; + _avatarHeroFraction = priorityReservedFraction; } void AvatarMixerSlave::harvestStats(AvatarMixerSlaveStats& stats) { @@ -310,7 +312,6 @@ namespace { } // Close anonymous namespace. void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) { - const float AVATAR_HERO_FRACTION { 0.4f }; const Node* destinationNode = node.data(); auto nodeList = DependencyManager::get(); @@ -345,7 +346,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) // max number of avatarBytes per frame (13 900, typical) const int maxAvatarBytesPerFrame = int(_maxKbpsPerNode * BYTES_PER_KILOBIT / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND); - const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * AVATAR_HERO_FRACTION); // 5555, typical + const int maxHeroBytesPerFrame = int(maxAvatarBytesPerFrame * _avatarHeroFraction); // 5555, typical // keep track of the number of other avatars held back in this frame int numAvatarsHeldBack = 0; @@ -471,8 +472,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) SortableAvatar(avatarNodeData, sourceAvatarNode, lastEncodeTime)); } - // If Avatar A's PAL WAS open but is no longer open, AND - // Avatar A is ignoring Avatar B OR Avatar B is ignoring Avatar A... + // If Node A's PAL WAS open but is no longer open, AND + // Node A is ignoring Avatar B OR Node B is ignoring Avatar A... // // This is a bit heavy-handed still - there are cases where a kill packet // will be sent when it doesn't need to be (but where it _should_ be OK to send). @@ -541,7 +542,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) const MixerAvatar* sourceAvatar = sourceNodeData->getConstAvatarData(); // Typically all out-of-view avatars but such avatars' priorities will rise with time: - bool isLowerPriority = currentVariant != kHero && sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD; // XXX: hero handling? + bool isLowerPriority = sortedAvatar.getPriority() <= OUT_OF_VIEW_THRESHOLD; if (isLowerPriority) { detail = PALIsOpen ? AvatarData::PALMinimum : AvatarData::MinimumData; @@ -550,8 +551,8 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node) detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO ? AvatarData::SendAllData : AvatarData::CullSmallData; destinationNodeData->incrementAvatarInView(); - // If the time that the mixer sent AVATAR DATA about Avatar B to Avatar A is BEFORE OR EQUAL TO - // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Avatar A. + // If the time that the mixer sent AVATAR DATA about Avatar B to Node A is BEFORE OR EQUAL TO + // the time that Avatar B flagged an IDENTITY DATA change, send IDENTITY DATA about Avatar B to Node A. if (sourceAvatar->hasProcessedFirstIdentity() && destinationNodeData->getLastBroadcastTime(sourceNode->getLocalID()) <= sourceNodeData->getIdentityChangeTimestamp()) { identityBytesSent += sendIdentityPacket(*identityPacketList, sourceNodeData, *destinationNode); diff --git a/assignment-client/src/avatars/AvatarMixerSlave.h b/assignment-client/src/avatars/AvatarMixerSlave.h index 8c5ad6b181..f14e50e11f 100644 --- a/assignment-client/src/avatars/AvatarMixerSlave.h +++ b/assignment-client/src/avatars/AvatarMixerSlave.h @@ -110,7 +110,8 @@ public: void configure(ConstIter begin, ConstIter end); void configureBroadcast(ConstIter begin, ConstIter end, p_high_resolution_clock::time_point lastFrameTimestamp, - float maxKbpsPerNode, float throttlingRatio); + float maxKbpsPerNode, float throttlingRatio, + float priorityReservedFraction); void processIncomingPackets(const SharedNodePointer& node); void broadcastAvatarData(const SharedNodePointer& node); @@ -140,6 +141,7 @@ private: p_high_resolution_clock::time_point _lastFrameTimestamp; float _maxKbpsPerNode { 0.0f }; float _throttlingRatio { 0.0f }; + float _avatarHeroFraction { 0.4f }; AvatarMixerSlaveStats _stats; SlaveSharedData* _sharedData; diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp index 013d914cbe..357b347a94 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -76,7 +76,8 @@ void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end, float maxKbpsPerNode, float throttlingRatio) { _function = &AvatarMixerSlave::broadcastAvatarData; _configure = [=](AvatarMixerSlave& slave) { - slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio); + slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio, + _priorityReservedFraction); }; run(begin, end); } diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.h b/assignment-client/src/avatars/AvatarMixerSlavePool.h index 71a9ace0d3..b05abde2a3 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.h +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.h @@ -73,7 +73,10 @@ public: void each(std::function functor); void setNumThreads(int numThreads); - int numThreads() { return _numThreads; } + int numThreads() const { return _numThreads; } + + void setPriorityReservedFraction(float fraction) { _priorityReservedFraction = fraction; } + float getPriorityReservedFraction() const { return _priorityReservedFraction; } private: void run(ConstIter begin, ConstIter end); @@ -91,7 +94,11 @@ private: ConditionVariable _poolCondition; void (AvatarMixerSlave::*_function)(const SharedNodePointer& node); std::function _configure; + + // Set from Domain Settings: + float _priorityReservedFraction { 0.4f }; int _numThreads { 0 }; + int _numStarted { 0 }; // guarded by _mutex int _numFinished { 0 }; // guarded by _mutex int _numStopped { 0 }; // guarded by _mutex diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h index 3e80704495..01e5e91b44 100644 --- a/assignment-client/src/avatars/MixerAvatar.h +++ b/assignment-client/src/avatars/MixerAvatar.h @@ -19,8 +19,12 @@ class MixerAvatar : public AvatarData { public: + bool getNeedsHeroCheck() const { return _needsHeroCheck; } + void setNeedsHeroCheck(bool needsHeroCheck = true) + { _needsHeroCheck = needsHeroCheck; } private: + bool _needsHeroCheck { false }; }; using MixerAvatarSharedPointer = std::shared_ptr; diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index fbe5675bd8..e5df411099 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -74,6 +74,7 @@ * avatar. Read-only. * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only. * * @example Create a scriptable avatar. * (function () { diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 140c7d6c17..352106dcf7 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1310,6 +1310,15 @@ "placeholder": "50", "default": "50", "advanced": true + }, + { + "name": "priority_fraction", + "type": "double", + "label": "Hero Bandwidth", + "help": "Fraction of downstream bandwidth reserved for avatars in 'Hero' zones", + "placeholder": "0.40", + "default": "0.40", + "advanced": true } ] }, diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 3f60b9cada..0859c20153 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -295,6 +295,7 @@ class MyAvatar : public Avatar { * @comment Avatar.setAbsoluteJointTranslationInObjectFrame as setAbsoluteJointTranslationInObjectFrame - Don't borrow because implementation is different. * @borrows Avatar.getTargetScale as getTargetScale * @borrows Avatar.resetLastSent as resetLastSent + * @borrows Avatar.hasPriority as hasPriority */ // FIXME: `glm::vec3 position` is not accessible from QML, so this exposes position in a QML-native type Q_PROPERTY(QVector3D qmlPosition READ getQmlPosition) diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 9c923580f9..a2b0b808ba 100755 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1143,10 +1143,11 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) { // we store the hand state as well as other items in a shared bitset. The hand state is an octal, but is split // into two sections to maintain backward compatibility. The bits are ordered as such (0-7 left to right). // AA 6/1/18 added three more flags bits 8,9, and 10 for procedural audio, blink, and eye saccade enabled - // +---+-----+-----+--+--+--+--+-----+ - // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|xxxxx| - // +---+-----+-----+--+--+--+--+-----+ + // +---+-----+-----+--+--+--+--+--+----+ + // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|He|xxxx| + // +---+-----+-----+--+--+--+--+--+----+ // Hand state - H0,H1,H2 is found in the 3rd, 4th, and 8th bits + // Hero-avatar status (He) - 12th bit auto newHandState = getSemiNibbleAt(bitItems, HAND_STATE_START_BIT) + (oneAtBit16(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index dd6f0a9efd..caa1f9f892 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -479,6 +479,7 @@ class AvatarData : public QObject, public SpatiallyNestable { * avatar. Read-only. * @property {number} sensorToWorldScale - The scale that transforms dimensions in the user's real world to the avatar's * size in the virtual world. Read-only. + * @property {boolean} hasPriority - is the avatar in a Hero zone? Read-only. */ Q_PROPERTY(glm::vec3 position READ getWorldPosition WRITE setPositionViaScript) Q_PROPERTY(float scale READ getDomainLimitedScale WRITE setTargetScale) @@ -518,6 +519,8 @@ class AvatarData : public QObject, public SpatiallyNestable { Q_PROPERTY(float sensorToWorldScale READ getSensorToWorldScale) + Q_PROPERTY(bool hasPriority READ getHasPriority) + public: virtual QString getName() const override { return QString("Avatar:") + _displayName; } diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp index a716a40ad8..18717c8ca3 100644 --- a/libraries/avatars/src/ScriptAvatarData.cpp +++ b/libraries/avatars/src/ScriptAvatarData.cpp @@ -343,6 +343,14 @@ glm::mat4 ScriptAvatarData::getControllerRightHandMatrix() const { // END // +bool ScriptAvatarData::getHasPriority() const { + if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { + return sharedAvatarData->getHasPriority(); + } else { + return false; + } +} + glm::quat ScriptAvatarData::getAbsoluteJointRotationInObjectFrame(int index) const { if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { return sharedAvatarData->getAbsoluteJointRotationInObjectFrame(index); diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index 91bac61728..01f7ff360a 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -68,6 +68,8 @@ class ScriptAvatarData : public QObject { Q_PROPERTY(glm::mat4 controllerLeftHandMatrix READ getControllerLeftHandMatrix) Q_PROPERTY(glm::mat4 controllerRightHandMatrix READ getControllerRightHandMatrix) + Q_PROPERTY(bool hasPriority READ getHasPriority) + public: ScriptAvatarData(AvatarSharedPointer avatarData); @@ -133,6 +135,8 @@ public: glm::mat4 getControllerLeftHandMatrix() const; glm::mat4 getControllerRightHandMatrix() const; + bool getHasPriority() const; + signals: void displayNameChanged(); void sessionDisplayNameChanged();