Merge branch 'master' into multiSphereAvatar04
# Resolved Conflicts: # interface/src/avatar/MyAvatar.h # interface/src/avatar/OtherAvatar.cpp
|
@ -504,7 +504,7 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre
|
||||||
float distance = glm::max(glm::length(relativePosition), EPSILON);
|
float distance = glm::max(glm::length(relativePosition), EPSILON);
|
||||||
float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition);
|
float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition);
|
||||||
|
|
||||||
float gain = 1.0f;
|
float gain = masterListenerGain;
|
||||||
if (!isSoloing) {
|
if (!isSoloing) {
|
||||||
gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho);
|
gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho);
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
|
||||||
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
|
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
|
||||||
packetReceiver.registerListener(PacketType::AvatarIdentityRequest, this, "handleAvatarIdentityRequestPacket");
|
packetReceiver.registerListener(PacketType::AvatarIdentityRequest, this, "handleAvatarIdentityRequestPacket");
|
||||||
packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket");
|
packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket");
|
||||||
|
packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket");
|
||||||
|
|
||||||
packetReceiver.registerListenerForTypes({
|
packetReceiver.registerListenerForTypes({
|
||||||
PacketType::ReplicatedAvatarIdentity,
|
PacketType::ReplicatedAvatarIdentity,
|
||||||
|
@ -767,6 +768,9 @@ void AvatarMixer::sendStatsPacket() {
|
||||||
|
|
||||||
float averageOverBudgetAvatars = averageNodes ? aggregateStats.overBudgetAvatars / averageNodes : 0.0f;
|
float averageOverBudgetAvatars = averageNodes ? aggregateStats.overBudgetAvatars / averageNodes : 0.0f;
|
||||||
slavesAggregatObject["sent_3_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
|
slavesAggregatObject["sent_3_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
|
||||||
|
slavesAggregatObject["sent_4_averageDataBytes"] = TIGHT_LOOP_STAT(aggregateStats.numDataBytesSent);
|
||||||
|
slavesAggregatObject["sent_5_averageTraitsBytes"] = TIGHT_LOOP_STAT(aggregateStats.numTraitsBytesSent);
|
||||||
|
slavesAggregatObject["sent_6_averageIdentityBytes"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityBytesSent);
|
||||||
|
|
||||||
slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime);
|
slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime);
|
||||||
slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
|
slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
|
||||||
|
@ -775,7 +779,7 @@ void AvatarMixer::sendStatsPacket() {
|
||||||
slavesAggregatObject["timing_5_packetSending"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.packetSendingElapsedTime);
|
slavesAggregatObject["timing_5_packetSending"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.packetSendingElapsedTime);
|
||||||
slavesAggregatObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.jobElapsedTime);
|
slavesAggregatObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.jobElapsedTime);
|
||||||
|
|
||||||
statsObject["slaves_aggregate"] = slavesAggregatObject;
|
statsObject["slaves_aggregate (per frame)"] = slavesAggregatObject;
|
||||||
|
|
||||||
_handleViewFrustumPacketElapsedTime = 0;
|
_handleViewFrustumPacketElapsedTime = 0;
|
||||||
_handleAvatarIdentityPacketElapsedTime = 0;
|
_handleAvatarIdentityPacketElapsedTime = 0;
|
||||||
|
@ -800,7 +804,8 @@ void AvatarMixer::sendStatsPacket() {
|
||||||
// add the key to ask the domain-server for a username replacement, if it has it
|
// add the key to ask the domain-server for a username replacement, if it has it
|
||||||
avatarStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidStringWithoutCurlyBraces(node->getUUID());
|
avatarStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidStringWithoutCurlyBraces(node->getUUID());
|
||||||
|
|
||||||
avatarStats[NODE_OUTBOUND_KBPS_STAT_KEY] = node->getOutboundKbps();
|
float outboundAvatarDataKbps = node->getOutboundKbps();
|
||||||
|
avatarStats[NODE_OUTBOUND_KBPS_STAT_KEY] = outboundAvatarDataKbps;
|
||||||
avatarStats[NODE_INBOUND_KBPS_STAT_KEY] = node->getInboundKbps();
|
avatarStats[NODE_INBOUND_KBPS_STAT_KEY] = node->getInboundKbps();
|
||||||
|
|
||||||
AvatarMixerClientData* clientData = static_cast<AvatarMixerClientData*>(node->getLinkedData());
|
AvatarMixerClientData* clientData = static_cast<AvatarMixerClientData*>(node->getLinkedData());
|
||||||
|
@ -811,7 +816,7 @@ void AvatarMixer::sendStatsPacket() {
|
||||||
|
|
||||||
// add the diff between the full outbound bandwidth and the measured bandwidth for AvatarData send only
|
// add the diff between the full outbound bandwidth and the measured bandwidth for AvatarData send only
|
||||||
avatarStats["delta_full_vs_avatar_data_kbps"] =
|
avatarStats["delta_full_vs_avatar_data_kbps"] =
|
||||||
avatarStats[NODE_OUTBOUND_KBPS_STAT_KEY].toDouble() - avatarStats[OUTBOUND_AVATAR_DATA_STATS_KEY].toDouble();
|
(double)outboundAvatarDataKbps - avatarStats[OUTBOUND_AVATAR_DATA_STATS_KEY].toDouble();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,8 @@
|
||||||
|
|
||||||
#include "AvatarMixerSlave.h"
|
#include "AvatarMixerSlave.h"
|
||||||
|
|
||||||
AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) :
|
AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) :
|
||||||
NodeData(nodeID, nodeLocalID)
|
NodeData(nodeID, nodeLocalID) {
|
||||||
{
|
|
||||||
// in case somebody calls getSessionUUID on the AvatarData instance, make sure it has the right ID
|
// in case somebody calls getSessionUUID on the AvatarData instance, make sure it has the right ID
|
||||||
_avatar->setID(nodeID);
|
_avatar->setID(nodeID);
|
||||||
}
|
}
|
||||||
|
@ -68,6 +67,9 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
|
||||||
case PacketType::SetAvatarTraits:
|
case PacketType::SetAvatarTraits:
|
||||||
processSetTraitsMessage(*packet, slaveSharedData, *node);
|
processSetTraitsMessage(*packet, slaveSharedData, *node);
|
||||||
break;
|
break;
|
||||||
|
case PacketType::BulkAvatarTraitsAck:
|
||||||
|
processBulkAvatarTraitsAckMessage(*packet);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Q_UNREACHABLE();
|
Q_UNREACHABLE();
|
||||||
}
|
}
|
||||||
|
@ -79,12 +81,11 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
|
||||||
}
|
}
|
||||||
|
|
||||||
int AvatarMixerClientData::parseData(ReceivedMessage& message) {
|
int AvatarMixerClientData::parseData(ReceivedMessage& message) {
|
||||||
|
|
||||||
// pull the sequence number from the data first
|
// pull the sequence number from the data first
|
||||||
uint16_t sequenceNumber;
|
uint16_t sequenceNumber;
|
||||||
|
|
||||||
message.readPrimitive(&sequenceNumber);
|
message.readPrimitive(&sequenceNumber);
|
||||||
|
|
||||||
if (sequenceNumber < _lastReceivedSequenceNumber && _lastReceivedSequenceNumber != UINT16_MAX) {
|
if (sequenceNumber < _lastReceivedSequenceNumber && _lastReceivedSequenceNumber != UINT16_MAX) {
|
||||||
incrementNumOutOfOrderSends();
|
incrementNumOutOfOrderSends();
|
||||||
}
|
}
|
||||||
|
@ -95,7 +96,8 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
||||||
const SlaveSharedData& slaveSharedData, Node& sendingNode) {
|
const SlaveSharedData& slaveSharedData,
|
||||||
|
Node& sendingNode) {
|
||||||
// pull the trait version from the message
|
// pull the trait version from the message
|
||||||
AvatarTraits::TraitVersion packetTraitVersion;
|
AvatarTraits::TraitVersion packetTraitVersion;
|
||||||
message.readPrimitive(&packetTraitVersion);
|
message.readPrimitive(&packetTraitVersion);
|
||||||
|
@ -134,7 +136,7 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
||||||
AvatarTraits::TraitInstanceID instanceID = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
AvatarTraits::TraitInstanceID instanceID = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID));
|
||||||
|
|
||||||
if (message.getBytesLeftToRead() == 0) {
|
if (message.getBytesLeftToRead() == 0) {
|
||||||
qWarning () << "Received an instanced trait with no size from" << message.getSenderSockAddr();
|
qWarning() << "Received an instanced trait with no size from" << message.getSenderSockAddr();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +144,8 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
||||||
message.readPrimitive(&traitSize);
|
message.readPrimitive(&traitSize);
|
||||||
|
|
||||||
if (traitSize < -1 || traitSize > message.getBytesLeftToRead()) {
|
if (traitSize < -1 || traitSize > message.getBytesLeftToRead()) {
|
||||||
qWarning() << "Refusing to process instanced trait of size" << traitSize << "from" << message.getSenderSockAddr();
|
qWarning() << "Refusing to process instanced trait of size" << traitSize << "from"
|
||||||
|
<< message.getSenderSockAddr();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +157,7 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
||||||
if (traitSize == AvatarTraits::DELETED_TRAIT_SIZE) {
|
if (traitSize == AvatarTraits::DELETED_TRAIT_SIZE) {
|
||||||
_avatar->processDeletedTraitInstance(traitType, instanceID);
|
_avatar->processDeletedTraitInstance(traitType, instanceID);
|
||||||
// Mixer doesn't need deleted IDs.
|
// Mixer doesn't need deleted IDs.
|
||||||
_avatar->getAndClearRecentlyDetachedIDs();
|
_avatar->getAndClearRecentlyRemovedIDs();
|
||||||
|
|
||||||
// to track a deleted instance but keep version information
|
// to track a deleted instance but keep version information
|
||||||
// the avatar mixer uses the negative value of the sent version
|
// the avatar mixer uses the negative value of the sent version
|
||||||
|
@ -169,7 +172,8 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
||||||
message.seek(message.getPosition() + traitSize);
|
message.seek(message.getPosition() + traitSize);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "Refusing to process traits packet with instanced trait of unprocessable type from" << message.getSenderSockAddr();
|
qWarning() << "Refusing to process traits packet with instanced trait of unprocessable type from"
|
||||||
|
<< message.getSenderSockAddr();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,7 +184,63 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedData &slaveSharedData, Node& sendingNode,
|
void AvatarMixerClientData::processBulkAvatarTraitsAckMessage(ReceivedMessage& message) {
|
||||||
|
// Avatar Traits flow control marks each outgoing avatar traits packet with a
|
||||||
|
// sequence number. The mixer caches the traits sent in the traits packet.
|
||||||
|
// Until an ack with the sequence number comes back, all updates to _traits
|
||||||
|
// in that packet_ are ignored. Updates to traits not in that packet will
|
||||||
|
// be sent.
|
||||||
|
|
||||||
|
// Look up the avatar/trait data associated with this ack and update the 'last ack' list
|
||||||
|
// with it.
|
||||||
|
AvatarTraits::TraitMessageSequence seq;
|
||||||
|
message.readPrimitive(&seq);
|
||||||
|
auto sentAvatarTraitVersions = _perNodePendingTraitVersions.find(seq);
|
||||||
|
if (sentAvatarTraitVersions != _perNodePendingTraitVersions.end()) {
|
||||||
|
for (auto& perNodeTraitVersions : sentAvatarTraitVersions->second) {
|
||||||
|
auto& nodeId = perNodeTraitVersions.first;
|
||||||
|
auto& traitVersions = perNodeTraitVersions.second;
|
||||||
|
// For each trait that was sent in the traits packet,
|
||||||
|
// update the 'acked' trait version. Traits not
|
||||||
|
// sent in the traits packet keep their version.
|
||||||
|
|
||||||
|
// process simple traits
|
||||||
|
auto simpleReceivedIt = traitVersions.simpleCBegin();
|
||||||
|
while (simpleReceivedIt != traitVersions.simpleCEnd()) {
|
||||||
|
if (*simpleReceivedIt != AvatarTraits::DEFAULT_TRAIT_VERSION) {
|
||||||
|
auto traitType = static_cast<AvatarTraits::TraitType>(std::distance(traitVersions.simpleCBegin(), simpleReceivedIt));
|
||||||
|
_perNodeAckedTraitVersions[nodeId][traitType] = *simpleReceivedIt;
|
||||||
|
}
|
||||||
|
simpleReceivedIt++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// process instanced traits
|
||||||
|
auto instancedSentIt = traitVersions.instancedCBegin();
|
||||||
|
while (instancedSentIt != traitVersions.instancedCEnd()) {
|
||||||
|
auto traitType = instancedSentIt->traitType;
|
||||||
|
|
||||||
|
for (auto& sentInstance : instancedSentIt->instances) {
|
||||||
|
auto instanceID = sentInstance.id;
|
||||||
|
const auto sentVersion = sentInstance.value;
|
||||||
|
_perNodeAckedTraitVersions[nodeId].instanceInsert(traitType, instanceID, sentVersion);
|
||||||
|
}
|
||||||
|
instancedSentIt++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_perNodePendingTraitVersions.erase(sentAvatarTraitVersions);
|
||||||
|
} else {
|
||||||
|
// This can happen either the BulkAvatarTraits was sent with no simple traits,
|
||||||
|
// or if the avatar mixer restarts while there are pending
|
||||||
|
// BulkAvatarTraits messages in-flight.
|
||||||
|
if (seq > getTraitsMessageSequence()) {
|
||||||
|
qWarning() << "Received BulkAvatarTraitsAck with future seq (potential avatar mixer restart) " << seq << " from "
|
||||||
|
<< message.getSenderSockAddr();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AvatarMixerClientData::checkSkeletonURLAgainstWhitelist(const SlaveSharedData& slaveSharedData,
|
||||||
|
Node& sendingNode,
|
||||||
AvatarTraits::TraitVersion traitVersion) {
|
AvatarTraits::TraitVersion traitVersion) {
|
||||||
const auto& whitelist = slaveSharedData.skeletonURLWhitelist;
|
const auto& whitelist = slaveSharedData.skeletonURLWhitelist;
|
||||||
|
|
||||||
|
@ -282,14 +342,18 @@ void AvatarMixerClientData::removeFromRadiusIgnoringSet(const QUuid& other) {
|
||||||
|
|
||||||
void AvatarMixerClientData::resetSentTraitData(Node::LocalID nodeLocalID) {
|
void AvatarMixerClientData::resetSentTraitData(Node::LocalID nodeLocalID) {
|
||||||
_lastSentTraitsTimestamps[nodeLocalID] = TraitsCheckTimestamp();
|
_lastSentTraitsTimestamps[nodeLocalID] = TraitsCheckTimestamp();
|
||||||
_sentTraitVersions[nodeLocalID].reset();
|
_perNodeSentTraitVersions[nodeLocalID].reset();
|
||||||
|
_perNodeAckedTraitVersions[nodeLocalID].reset();
|
||||||
|
for (auto && pendingTraitVersions : _perNodePendingTraitVersions) {
|
||||||
|
pendingTraitVersions.second[nodeLocalID].reset();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AvatarMixerClientData::readViewFrustumPacket(const QByteArray& message) {
|
void AvatarMixerClientData::readViewFrustumPacket(const QByteArray& message) {
|
||||||
_currentViewFrustums.clear();
|
_currentViewFrustums.clear();
|
||||||
|
|
||||||
auto sourceBuffer = reinterpret_cast<const unsigned char*>(message.constData());
|
auto sourceBuffer = reinterpret_cast<const unsigned char*>(message.constData());
|
||||||
|
|
||||||
uint8_t numFrustums = 0;
|
uint8_t numFrustums = 0;
|
||||||
memcpy(&numFrustums, sourceBuffer, sizeof(numFrustums));
|
memcpy(&numFrustums, sourceBuffer, sizeof(numFrustums));
|
||||||
sourceBuffer += sizeof(numFrustums);
|
sourceBuffer += sizeof(numFrustums);
|
||||||
|
@ -317,7 +381,8 @@ void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const {
|
||||||
jsonObject["total_num_out_of_order_sends"] = _numOutOfOrderSends;
|
jsonObject["total_num_out_of_order_sends"] = _numOutOfOrderSends;
|
||||||
|
|
||||||
jsonObject[OUTBOUND_AVATAR_DATA_STATS_KEY] = getOutboundAvatarDataKbps();
|
jsonObject[OUTBOUND_AVATAR_DATA_STATS_KEY] = getOutboundAvatarDataKbps();
|
||||||
jsonObject[INBOUND_AVATAR_DATA_STATS_KEY] = _avatar->getAverageBytesReceivedPerSecond() / (float) BYTES_PER_KILOBIT;
|
jsonObject[OUTBOUND_AVATAR_TRAITS_STATS_KEY] = getOutboundAvatarTraitsKbps();
|
||||||
|
jsonObject[INBOUND_AVATAR_DATA_STATS_KEY] = _avatar->getAverageBytesReceivedPerSecond() / (float)BYTES_PER_KILOBIT;
|
||||||
|
|
||||||
jsonObject["av_data_receive_rate"] = _avatar->getReceiveRate();
|
jsonObject["av_data_receive_rate"] = _avatar->getReceiveRate();
|
||||||
jsonObject["recent_other_av_in_view"] = _recentOtherAvatarsInView;
|
jsonObject["recent_other_av_in_view"] = _recentOtherAvatarsInView;
|
||||||
|
@ -338,5 +403,5 @@ void AvatarMixerClientData::cleanupKilledNode(const QUuid&, Node::LocalID nodeLo
|
||||||
removeLastBroadcastSequenceNumber(nodeLocalID);
|
removeLastBroadcastSequenceNumber(nodeLocalID);
|
||||||
removeLastBroadcastTime(nodeLocalID);
|
removeLastBroadcastTime(nodeLocalID);
|
||||||
_lastSentTraitsTimestamps.erase(nodeLocalID);
|
_lastSentTraitsTimestamps.erase(nodeLocalID);
|
||||||
_sentTraitVersions.erase(nodeLocalID);
|
_perNodeSentTraitVersions.erase(nodeLocalID);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
#include <shared/ConicalViewFrustum.h>
|
#include <shared/ConicalViewFrustum.h>
|
||||||
|
|
||||||
const QString OUTBOUND_AVATAR_DATA_STATS_KEY = "outbound_av_data_kbps";
|
const QString OUTBOUND_AVATAR_DATA_STATS_KEY = "outbound_av_data_kbps";
|
||||||
|
const QString OUTBOUND_AVATAR_TRAITS_STATS_KEY = "outbound_av_traits_kbps";
|
||||||
const QString INBOUND_AVATAR_DATA_STATS_KEY = "inbound_av_data_kbps";
|
const QString INBOUND_AVATAR_DATA_STATS_KEY = "inbound_av_data_kbps";
|
||||||
|
|
||||||
struct SlaveSharedData;
|
struct SlaveSharedData;
|
||||||
|
@ -42,6 +43,7 @@ public:
|
||||||
AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID);
|
AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID);
|
||||||
virtual ~AvatarMixerClientData() {}
|
virtual ~AvatarMixerClientData() {}
|
||||||
using HRCTime = p_high_resolution_clock::time_point;
|
using HRCTime = p_high_resolution_clock::time_point;
|
||||||
|
using PerNodeTraitVersions = std::unordered_map<Node::LocalID, AvatarTraits::TraitVersions>;
|
||||||
|
|
||||||
int parseData(ReceivedMessage& message) override;
|
int parseData(ReceivedMessage& message) override;
|
||||||
AvatarData& getAvatar() { return *_avatar; }
|
AvatarData& getAvatar() { return *_avatar; }
|
||||||
|
@ -85,10 +87,15 @@ public:
|
||||||
void incrementNumFramesSinceFRDAdjustment() { ++_numFramesSinceAdjustment; }
|
void incrementNumFramesSinceFRDAdjustment() { ++_numFramesSinceAdjustment; }
|
||||||
void resetNumFramesSinceFRDAdjustment() { _numFramesSinceAdjustment = 0; }
|
void resetNumFramesSinceFRDAdjustment() { _numFramesSinceAdjustment = 0; }
|
||||||
|
|
||||||
void recordSentAvatarData(int numBytes) { _avgOtherAvatarDataRate.updateAverage((float) numBytes); }
|
void recordSentAvatarData(int numDataBytes, int numTraitsBytes = 0) {
|
||||||
|
_avgOtherAvatarDataRate.updateAverage(numDataBytes);
|
||||||
|
_avgOtherAvatarTraitsRate.updateAverage(numTraitsBytes);
|
||||||
|
}
|
||||||
|
|
||||||
float getOutboundAvatarDataKbps() const
|
float getOutboundAvatarDataKbps() const
|
||||||
{ return _avgOtherAvatarDataRate.getAverageSampleValuePerSecond() / (float) BYTES_PER_KILOBIT; }
|
{ return _avgOtherAvatarDataRate.getAverageSampleValuePerSecond() / (float) BYTES_PER_KILOBIT; }
|
||||||
|
float getOutboundAvatarTraitsKbps() const
|
||||||
|
{ return _avgOtherAvatarTraitsRate.getAverageSampleValuePerSecond() / BYTES_PER_KILOBIT; }
|
||||||
|
|
||||||
void loadJSONStats(QJsonObject& jsonObject) const;
|
void loadJSONStats(QJsonObject& jsonObject) const;
|
||||||
|
|
||||||
|
@ -124,6 +131,7 @@ public:
|
||||||
int processPackets(const SlaveSharedData& slaveSharedData); // returns number of packets processed
|
int processPackets(const SlaveSharedData& slaveSharedData); // returns number of packets processed
|
||||||
|
|
||||||
void processSetTraitsMessage(ReceivedMessage& message, const SlaveSharedData& slaveSharedData, Node& sendingNode);
|
void processSetTraitsMessage(ReceivedMessage& message, const SlaveSharedData& slaveSharedData, Node& sendingNode);
|
||||||
|
void processBulkAvatarTraitsAckMessage(ReceivedMessage& message);
|
||||||
void checkSkeletonURLAgainstWhitelist(const SlaveSharedData& slaveSharedData, Node& sendingNode,
|
void checkSkeletonURLAgainstWhitelist(const SlaveSharedData& slaveSharedData, Node& sendingNode,
|
||||||
AvatarTraits::TraitVersion traitVersion);
|
AvatarTraits::TraitVersion traitVersion);
|
||||||
|
|
||||||
|
@ -138,7 +146,14 @@ public:
|
||||||
void setLastOtherAvatarTraitsSendPoint(Node::LocalID otherAvatar, TraitsCheckTimestamp sendPoint)
|
void setLastOtherAvatarTraitsSendPoint(Node::LocalID otherAvatar, TraitsCheckTimestamp sendPoint)
|
||||||
{ _lastSentTraitsTimestamps[otherAvatar] = sendPoint; }
|
{ _lastSentTraitsTimestamps[otherAvatar] = sendPoint; }
|
||||||
|
|
||||||
AvatarTraits::TraitVersions& getLastSentTraitVersions(Node::LocalID otherAvatar) { return _sentTraitVersions[otherAvatar]; }
|
AvatarTraits::TraitMessageSequence getTraitsMessageSequence() const { return _currentTraitsMessageSequence; }
|
||||||
|
AvatarTraits::TraitMessageSequence nextTraitsMessageSequence() { return ++_currentTraitsMessageSequence; }
|
||||||
|
AvatarTraits::TraitVersions& getPendingTraitVersions(AvatarTraits::TraitMessageSequence seq, Node::LocalID otherId) {
|
||||||
|
return _perNodePendingTraitVersions[seq][otherId];
|
||||||
|
}
|
||||||
|
|
||||||
|
AvatarTraits::TraitVersions& getLastSentTraitVersions(Node::LocalID otherAvatar) { return _perNodeSentTraitVersions[otherAvatar]; }
|
||||||
|
AvatarTraits::TraitVersions& getLastAckedTraitVersions(Node::LocalID otherAvatar) { return _perNodeAckedTraitVersions[otherAvatar]; }
|
||||||
|
|
||||||
void resetSentTraitData(Node::LocalID nodeID);
|
void resetSentTraitData(Node::LocalID nodeID);
|
||||||
|
|
||||||
|
@ -171,6 +186,7 @@ private:
|
||||||
int _numOutOfOrderSends = 0;
|
int _numOutOfOrderSends = 0;
|
||||||
|
|
||||||
SimpleMovingAverage _avgOtherAvatarDataRate;
|
SimpleMovingAverage _avgOtherAvatarDataRate;
|
||||||
|
SimpleMovingAverage _avgOtherAvatarTraitsRate;
|
||||||
std::vector<QUuid> _radiusIgnoredOthers;
|
std::vector<QUuid> _radiusIgnoredOthers;
|
||||||
ConicalViewFrustums _currentViewFrustums;
|
ConicalViewFrustums _currentViewFrustums;
|
||||||
|
|
||||||
|
@ -183,8 +199,27 @@ private:
|
||||||
AvatarTraits::TraitVersions _lastReceivedTraitVersions;
|
AvatarTraits::TraitVersions _lastReceivedTraitVersions;
|
||||||
TraitsCheckTimestamp _lastReceivedTraitsChange;
|
TraitsCheckTimestamp _lastReceivedTraitsChange;
|
||||||
|
|
||||||
|
AvatarTraits::TraitMessageSequence _currentTraitsMessageSequence{ 0 };
|
||||||
|
|
||||||
|
// Cache of trait versions sent in a given packet (indexed by sequence number)
|
||||||
|
// When an ack is received, the sequence number in the ack is used to look up
|
||||||
|
// the sent trait versions and they are copied to _perNodeAckedTraitVersions.
|
||||||
|
// We remember the data in _perNodePendingTraitVersions instead of requiring
|
||||||
|
// the client to return all of the versions for each trait it received in a given packet,
|
||||||
|
// reducing the size of the ack packet.
|
||||||
|
std::unordered_map<AvatarTraits::TraitMessageSequence, PerNodeTraitVersions> _perNodePendingTraitVersions;
|
||||||
|
|
||||||
|
// Versions of traits that have been acked, which will be compared to incoming
|
||||||
|
// trait updates. Incoming updates going to a given node will be ignored if
|
||||||
|
// the ack for the previous packet (containing those versions) has not been
|
||||||
|
// received.
|
||||||
|
PerNodeTraitVersions _perNodeAckedTraitVersions;
|
||||||
|
|
||||||
std::unordered_map<Node::LocalID, TraitsCheckTimestamp> _lastSentTraitsTimestamps;
|
std::unordered_map<Node::LocalID, TraitsCheckTimestamp> _lastSentTraitsTimestamps;
|
||||||
std::unordered_map<Node::LocalID, AvatarTraits::TraitVersions> _sentTraitVersions;
|
|
||||||
|
// cache of traits sent to a node which are compared to incoming traits to
|
||||||
|
// prevent sending traits that have already been sent.
|
||||||
|
PerNodeTraitVersions _perNodeSentTraitVersions;
|
||||||
|
|
||||||
std::atomic_bool _isIgnoreRadiusEnabled { false };
|
std::atomic_bool _isIgnoreRadiusEnabled { false };
|
||||||
};
|
};
|
||||||
|
|
|
@ -73,52 +73,82 @@ int AvatarMixerSlave::sendIdentityPacket(NLPacketList& packetList, const AvatarM
|
||||||
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray();
|
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray();
|
||||||
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
|
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
|
||||||
packetList.write(individualData);
|
packetList.write(individualData);
|
||||||
_stats.numIdentityPackets++;
|
_stats.numIdentityPacketsSent++;
|
||||||
|
_stats.numIdentityBytesSent += individualData.size();
|
||||||
return individualData.size();
|
return individualData.size();
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
qint64 AvatarMixerSlave::addTraitsNodeHeader(AvatarMixerClientData* listeningNodeData,
|
||||||
|
const AvatarMixerClientData* sendingNodeData,
|
||||||
|
NLPacketList& traitsPacketList,
|
||||||
|
qint64 bytesWritten) {
|
||||||
|
if (bytesWritten == 0) {
|
||||||
|
if (traitsPacketList.getNumPackets() == 0) {
|
||||||
|
// This is the beginning of the traits packet, write out the sequence number.
|
||||||
|
bytesWritten += traitsPacketList.writePrimitive(listeningNodeData->nextTraitsMessageSequence());
|
||||||
|
}
|
||||||
|
// This is the beginning of the traits for a node, write out the node id
|
||||||
|
bytesWritten += traitsPacketList.write(sendingNodeData->getNodeID().toRfc4122());
|
||||||
|
}
|
||||||
|
return bytesWritten;
|
||||||
|
}
|
||||||
|
|
||||||
qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData,
|
qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData,
|
||||||
const AvatarMixerClientData* sendingNodeData,
|
const AvatarMixerClientData* sendingNodeData,
|
||||||
NLPacketList& traitsPacketList) {
|
NLPacketList& traitsPacketList) {
|
||||||
|
|
||||||
auto otherNodeLocalID = sendingNodeData->getNodeLocalID();
|
// Avatar Traits flow control marks each outgoing avatar traits packet with a
|
||||||
|
// sequence number. The mixer caches the traits sent in the traits packet.
|
||||||
|
// Until an ack with the sequence number comes back, all updates to _traits
|
||||||
|
// in that packet_ are ignored. Updates to traits not in that packet will
|
||||||
|
// be sent.
|
||||||
|
|
||||||
|
auto sendingNodeLocalID = sendingNodeData->getNodeLocalID();
|
||||||
|
|
||||||
// Perform a simple check with two server clock time points
|
// Perform a simple check with two server clock time points
|
||||||
// to see if there is any new traits data for this avatar that we need to send
|
// to see if there is any new traits data for this avatar that we need to send
|
||||||
auto timeOfLastTraitsSent = listeningNodeData->getLastOtherAvatarTraitsSendPoint(otherNodeLocalID);
|
auto timeOfLastTraitsSent = listeningNodeData->getLastOtherAvatarTraitsSendPoint(sendingNodeLocalID);
|
||||||
auto timeOfLastTraitsChange = sendingNodeData->getLastReceivedTraitsChange();
|
auto timeOfLastTraitsChange = sendingNodeData->getLastReceivedTraitsChange();
|
||||||
|
bool allTraitsUpdated = true;
|
||||||
|
|
||||||
qint64 bytesWritten = 0;
|
qint64 bytesWritten = 0;
|
||||||
|
|
||||||
if (timeOfLastTraitsChange > timeOfLastTraitsSent) {
|
if (timeOfLastTraitsChange > timeOfLastTraitsSent) {
|
||||||
// there is definitely new traits data to send
|
// there is definitely new traits data to send
|
||||||
|
|
||||||
// add the avatar ID to mark the beginning of traits for this avatar
|
|
||||||
bytesWritten += traitsPacketList.write(sendingNodeData->getNodeID().toRfc4122());
|
|
||||||
|
|
||||||
auto sendingAvatar = sendingNodeData->getAvatarSharedPointer();
|
auto sendingAvatar = sendingNodeData->getAvatarSharedPointer();
|
||||||
|
|
||||||
// compare trait versions so we can see what exactly needs to go out
|
// compare trait versions so we can see what exactly needs to go out
|
||||||
auto& lastSentVersions = listeningNodeData->getLastSentTraitVersions(otherNodeLocalID);
|
auto& lastSentVersions = listeningNodeData->getLastSentTraitVersions(sendingNodeLocalID);
|
||||||
|
auto& lastAckedVersions = listeningNodeData->getLastAckedTraitVersions(sendingNodeLocalID);
|
||||||
const auto& lastReceivedVersions = sendingNodeData->getLastReceivedTraitVersions();
|
const auto& lastReceivedVersions = sendingNodeData->getLastReceivedTraitVersions();
|
||||||
|
|
||||||
auto simpleReceivedIt = lastReceivedVersions.simpleCBegin();
|
auto simpleReceivedIt = lastReceivedVersions.simpleCBegin();
|
||||||
while (simpleReceivedIt != lastReceivedVersions.simpleCEnd()) {
|
while (simpleReceivedIt != lastReceivedVersions.simpleCEnd()) {
|
||||||
auto traitType = static_cast<AvatarTraits::TraitType>(std::distance(lastReceivedVersions.simpleCBegin(),
|
auto traitType = static_cast<AvatarTraits::TraitType>(std::distance(lastReceivedVersions.simpleCBegin(),
|
||||||
simpleReceivedIt));
|
simpleReceivedIt));
|
||||||
|
|
||||||
auto lastReceivedVersion = *simpleReceivedIt;
|
auto lastReceivedVersion = *simpleReceivedIt;
|
||||||
auto& lastSentVersionRef = lastSentVersions[traitType];
|
auto& lastSentVersionRef = lastSentVersions[traitType];
|
||||||
|
auto& lastAckedVersionRef = lastAckedVersions[traitType];
|
||||||
|
|
||||||
if (lastReceivedVersions[traitType] > lastSentVersionRef) {
|
// hold sending more traits until we've been acked that the last one we sent was received
|
||||||
// there is an update to this trait, add it to the traits packet
|
if (lastSentVersionRef == lastAckedVersionRef) {
|
||||||
bytesWritten += sendingAvatar->packTrait(traitType, traitsPacketList, lastReceivedVersion);
|
if (lastReceivedVersion > lastSentVersionRef) {
|
||||||
|
bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten);
|
||||||
// update the last sent version
|
// there is an update to this trait, add it to the traits packet
|
||||||
lastSentVersionRef = lastReceivedVersion;
|
bytesWritten += sendingAvatar->packTrait(traitType, traitsPacketList, lastReceivedVersion);
|
||||||
|
// update the last sent version
|
||||||
|
lastSentVersionRef = lastReceivedVersion;
|
||||||
|
// Remember which versions we sent in this particular packet
|
||||||
|
// so we can verify when it's acked.
|
||||||
|
auto& pendingTraitVersions = listeningNodeData->getPendingTraitVersions(listeningNodeData->getTraitsMessageSequence(), sendingNodeLocalID);
|
||||||
|
pendingTraitVersions[traitType] = lastReceivedVersion;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
allTraitsUpdated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
++simpleReceivedIt;
|
++simpleReceivedIt;
|
||||||
|
@ -131,6 +161,7 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
|
||||||
|
|
||||||
// get or create the sent trait versions for this trait type
|
// get or create the sent trait versions for this trait type
|
||||||
auto& sentIDValuePairs = lastSentVersions.getInstanceIDValuePairs(traitType);
|
auto& sentIDValuePairs = lastSentVersions.getInstanceIDValuePairs(traitType);
|
||||||
|
auto& ackIDValuePairs = lastAckedVersions.getInstanceIDValuePairs(traitType);
|
||||||
|
|
||||||
// enumerate each received instance
|
// enumerate each received instance
|
||||||
for (auto& receivedInstance : instancedReceivedIt->instances) {
|
for (auto& receivedInstance : instancedReceivedIt->instances) {
|
||||||
|
@ -148,8 +179,19 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
|
||||||
{
|
{
|
||||||
return sentInstance.id == instanceID;
|
return sentInstance.id == instanceID;
|
||||||
});
|
});
|
||||||
|
// look for existing acked version for this instance
|
||||||
|
auto ackedInstanceIt = std::find_if(ackIDValuePairs.begin(), ackIDValuePairs.end(),
|
||||||
|
[instanceID](auto& ackInstance) { return ackInstance.id == instanceID; });
|
||||||
|
|
||||||
|
// if we have a sent version, then we must have an acked instance of the same trait with the same
|
||||||
|
// version to go on, otherwise we drop the received trait
|
||||||
|
if (sentInstanceIt != sentIDValuePairs.end() &&
|
||||||
|
(ackedInstanceIt == ackIDValuePairs.end() || sentInstanceIt->value != ackedInstanceIt->value)) {
|
||||||
|
allTraitsUpdated = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!isDeleted && (sentInstanceIt == sentIDValuePairs.end() || receivedVersion > sentInstanceIt->value)) {
|
if (!isDeleted && (sentInstanceIt == sentIDValuePairs.end() || receivedVersion > sentInstanceIt->value)) {
|
||||||
|
bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten);
|
||||||
|
|
||||||
// this instance version exists and has never been sent or is newer so we need to send it
|
// this instance version exists and has never been sent or is newer so we need to send it
|
||||||
bytesWritten += sendingAvatar->packTraitInstance(traitType, instanceID, traitsPacketList, receivedVersion);
|
bytesWritten += sendingAvatar->packTraitInstance(traitType, instanceID, traitsPacketList, receivedVersion);
|
||||||
|
@ -159,25 +201,40 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
|
||||||
} else {
|
} else {
|
||||||
sentIDValuePairs.emplace_back(instanceID, receivedVersion);
|
sentIDValuePairs.emplace_back(instanceID, receivedVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto& pendingTraitVersions =
|
||||||
|
listeningNodeData->getPendingTraitVersions(listeningNodeData->getTraitsMessageSequence(),
|
||||||
|
sendingNodeLocalID);
|
||||||
|
pendingTraitVersions.instanceInsert(traitType, instanceID, receivedVersion);
|
||||||
|
|
||||||
} else if (isDeleted && sentInstanceIt != sentIDValuePairs.end() && absoluteReceivedVersion > sentInstanceIt->value) {
|
} else if (isDeleted && sentInstanceIt != sentIDValuePairs.end() && absoluteReceivedVersion > sentInstanceIt->value) {
|
||||||
|
bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten);
|
||||||
|
|
||||||
// this instance version was deleted and we haven't sent the delete to this client yet
|
// this instance version was deleted and we haven't sent the delete to this client yet
|
||||||
bytesWritten += AvatarTraits::packInstancedTraitDelete(traitType, instanceID, traitsPacketList, absoluteReceivedVersion);
|
bytesWritten += AvatarTraits::packInstancedTraitDelete(traitType, instanceID, traitsPacketList, absoluteReceivedVersion);
|
||||||
|
|
||||||
// update the last sent version for this trait instance to the absolute value of the deleted version
|
// update the last sent version for this trait instance to the absolute value of the deleted version
|
||||||
sentInstanceIt->value = absoluteReceivedVersion;
|
sentInstanceIt->value = absoluteReceivedVersion;
|
||||||
|
|
||||||
|
auto& pendingTraitVersions =
|
||||||
|
listeningNodeData->getPendingTraitVersions(listeningNodeData->getTraitsMessageSequence(),
|
||||||
|
sendingNodeLocalID);
|
||||||
|
pendingTraitVersions.instanceInsert(traitType, instanceID, absoluteReceivedVersion);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
++instancedReceivedIt;
|
++instancedReceivedIt;
|
||||||
}
|
}
|
||||||
|
if (bytesWritten) {
|
||||||
// write a null trait type to mark the end of trait data for this avatar
|
// write a null trait type to mark the end of trait data for this avatar
|
||||||
bytesWritten += traitsPacketList.writePrimitive(AvatarTraits::NullTrait);
|
bytesWritten += traitsPacketList.writePrimitive(AvatarTraits::NullTrait);
|
||||||
|
// since we send all traits for this other avatar, update the time of last traits sent
|
||||||
// since we send all traits for this other avatar, update the time of last traits sent
|
// to match the time of last traits change
|
||||||
// to match the time of last traits change
|
if (allTraitsUpdated) {
|
||||||
listeningNodeData->setLastOtherAvatarTraitsSendPoint(otherNodeLocalID, timeOfLastTraitsChange);
|
listeningNodeData->setLastOtherAvatarTraitsSendPoint(sendingNodeLocalID, timeOfLastTraitsChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -191,7 +248,8 @@ int AvatarMixerSlave::sendReplicatedIdentityPacket(const Node& agentNode, const
|
||||||
auto identityPacket = NLPacketList::create(PacketType::ReplicatedAvatarIdentity, QByteArray(), true, true);
|
auto identityPacket = NLPacketList::create(PacketType::ReplicatedAvatarIdentity, QByteArray(), true, true);
|
||||||
identityPacket->write(individualData);
|
identityPacket->write(individualData);
|
||||||
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPacket), destinationNode);
|
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPacket), destinationNode);
|
||||||
_stats.numIdentityPackets++;
|
_stats.numIdentityPacketsSent++;
|
||||||
|
_stats.numIdentityBytesSent += individualData.size();
|
||||||
return individualData.size();
|
return individualData.size();
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -419,6 +477,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
|
||||||
|
|
||||||
int remainingAvatars = (int)sortedAvatars.size();
|
int remainingAvatars = (int)sortedAvatars.size();
|
||||||
auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true);
|
auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true);
|
||||||
|
|
||||||
auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData);
|
auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData);
|
||||||
const int avatarPacketCapacity = avatarPacket->getPayloadCapacity();
|
const int avatarPacketCapacity = avatarPacket->getPayloadCapacity();
|
||||||
int avatarSpaceAvailable = avatarPacketCapacity;
|
int avatarSpaceAvailable = avatarPacketCapacity;
|
||||||
|
@ -539,17 +598,16 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
|
||||||
++numPacketsSent;
|
++numPacketsSent;
|
||||||
}
|
}
|
||||||
|
|
||||||
_stats.numPacketsSent += numPacketsSent;
|
_stats.numDataPacketsSent += numPacketsSent;
|
||||||
_stats.numBytesSent += numAvatarDataBytes;
|
_stats.numDataBytesSent += numAvatarDataBytes;
|
||||||
|
|
||||||
// record the bytes sent for other avatar data in the AvatarMixerClientData
|
|
||||||
nodeData->recordSentAvatarData(numAvatarDataBytes);
|
|
||||||
|
|
||||||
// close the current traits packet list
|
// close the current traits packet list
|
||||||
traitsPacketList->closeCurrentPacket();
|
traitsPacketList->closeCurrentPacket();
|
||||||
|
|
||||||
if (traitsPacketList->getNumPackets() >= 1) {
|
if (traitsPacketList->getNumPackets() >= 1) {
|
||||||
// send the traits packet list
|
// send the traits packet list
|
||||||
|
_stats.numTraitsBytesSent += traitBytesSent;
|
||||||
|
_stats.numTraitsPacketsSent += (int) traitsPacketList->getNumPackets();
|
||||||
nodeList->sendPacketList(std::move(traitsPacketList), *destinationNode);
|
nodeList->sendPacketList(std::move(traitsPacketList), *destinationNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -559,6 +617,10 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
|
||||||
nodeList->sendPacketList(std::move(identityPacketList), *destinationNode);
|
nodeList->sendPacketList(std::move(identityPacketList), *destinationNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// record the bytes sent for other avatar data in the AvatarMixerClientData
|
||||||
|
nodeData->recordSentAvatarData(numAvatarDataBytes, traitBytesSent);
|
||||||
|
|
||||||
|
|
||||||
// record the number of avatars held back this frame
|
// record the number of avatars held back this frame
|
||||||
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
|
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
|
||||||
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
|
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
|
||||||
|
@ -685,8 +747,8 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin
|
||||||
// close the current packet so that we're always sending something
|
// close the current packet so that we're always sending something
|
||||||
avatarPacketList->closeCurrentPacket(true);
|
avatarPacketList->closeCurrentPacket(true);
|
||||||
|
|
||||||
_stats.numPacketsSent += (int)avatarPacketList->getNumPackets();
|
_stats.numDataPacketsSent += (int)avatarPacketList->getNumPackets();
|
||||||
_stats.numBytesSent += numAvatarDataBytes;
|
_stats.numDataBytesSent += numAvatarDataBytes;
|
||||||
|
|
||||||
// send the replicated bulk avatar data
|
// send the replicated bulk avatar data
|
||||||
auto nodeList = DependencyManager::get<NodeList>();
|
auto nodeList = DependencyManager::get<NodeList>();
|
||||||
|
|
|
@ -24,9 +24,12 @@ public:
|
||||||
|
|
||||||
int nodesBroadcastedTo { 0 };
|
int nodesBroadcastedTo { 0 };
|
||||||
int downstreamMixersBroadcastedTo { 0 };
|
int downstreamMixersBroadcastedTo { 0 };
|
||||||
int numPacketsSent { 0 };
|
int numDataBytesSent { 0 };
|
||||||
int numBytesSent { 0 };
|
int numTraitsBytesSent { 0 };
|
||||||
int numIdentityPackets { 0 };
|
int numIdentityBytesSent { 0 };
|
||||||
|
int numDataPacketsSent { 0 };
|
||||||
|
int numTraitsPacketsSent { 0 };
|
||||||
|
int numIdentityPacketsSent { 0 };
|
||||||
int numOthersIncluded { 0 };
|
int numOthersIncluded { 0 };
|
||||||
int overBudgetAvatars { 0 };
|
int overBudgetAvatars { 0 };
|
||||||
|
|
||||||
|
@ -45,9 +48,13 @@ public:
|
||||||
// sending job stats
|
// sending job stats
|
||||||
nodesBroadcastedTo = 0;
|
nodesBroadcastedTo = 0;
|
||||||
downstreamMixersBroadcastedTo = 0;
|
downstreamMixersBroadcastedTo = 0;
|
||||||
numPacketsSent = 0;
|
|
||||||
numBytesSent = 0;
|
numDataBytesSent = 0;
|
||||||
numIdentityPackets = 0;
|
numTraitsBytesSent = 0;
|
||||||
|
numIdentityBytesSent = 0;
|
||||||
|
numDataPacketsSent = 0;
|
||||||
|
numTraitsPacketsSent = 0;
|
||||||
|
numIdentityPacketsSent = 0;
|
||||||
numOthersIncluded = 0;
|
numOthersIncluded = 0;
|
||||||
overBudgetAvatars = 0;
|
overBudgetAvatars = 0;
|
||||||
|
|
||||||
|
@ -65,9 +72,12 @@ public:
|
||||||
|
|
||||||
nodesBroadcastedTo += rhs.nodesBroadcastedTo;
|
nodesBroadcastedTo += rhs.nodesBroadcastedTo;
|
||||||
downstreamMixersBroadcastedTo += rhs.downstreamMixersBroadcastedTo;
|
downstreamMixersBroadcastedTo += rhs.downstreamMixersBroadcastedTo;
|
||||||
numPacketsSent += rhs.numPacketsSent;
|
numDataBytesSent += rhs.numDataBytesSent;
|
||||||
numBytesSent += rhs.numBytesSent;
|
numTraitsBytesSent += rhs.numTraitsBytesSent;
|
||||||
numIdentityPackets += rhs.numIdentityPackets;
|
numIdentityBytesSent += rhs.numIdentityBytesSent;
|
||||||
|
numDataPacketsSent += rhs.numDataPacketsSent;
|
||||||
|
numTraitsPacketsSent += rhs.numTraitsPacketsSent;
|
||||||
|
numIdentityPacketsSent += rhs.numIdentityPacketsSent;
|
||||||
numOthersIncluded += rhs.numOthersIncluded;
|
numOthersIncluded += rhs.numOthersIncluded;
|
||||||
overBudgetAvatars += rhs.overBudgetAvatars;
|
overBudgetAvatars += rhs.overBudgetAvatars;
|
||||||
|
|
||||||
|
@ -104,6 +114,11 @@ private:
|
||||||
int sendIdentityPacket(NLPacketList& packet, const AvatarMixerClientData* nodeData, const Node& destinationNode);
|
int sendIdentityPacket(NLPacketList& packet, const AvatarMixerClientData* nodeData, const Node& destinationNode);
|
||||||
int sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode);
|
int sendReplicatedIdentityPacket(const Node& agentNode, const AvatarMixerClientData* nodeData, const Node& destinationNode);
|
||||||
|
|
||||||
|
qint64 addTraitsNodeHeader(AvatarMixerClientData* listeningNodeData,
|
||||||
|
const AvatarMixerClientData* sendingNodeData,
|
||||||
|
NLPacketList& traitsPacketList,
|
||||||
|
qint64 bytesWritten);
|
||||||
|
|
||||||
qint64 addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData,
|
qint64 addChangedTraitsToBulkPacket(AvatarMixerClientData* listeningNodeData,
|
||||||
const AvatarMixerClientData* sendingNodeData,
|
const AvatarMixerClientData* sendingNodeData,
|
||||||
NLPacketList& traitsPacketList);
|
NLPacketList& traitsPacketList);
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
#include <GLMHelpers.h>
|
#include <GLMHelpers.h>
|
||||||
#include <ResourceRequestObserver.h>
|
#include <ResourceRequestObserver.h>
|
||||||
#include <AvatarLogging.h>
|
#include <AvatarLogging.h>
|
||||||
|
#include <EntityItem.h>
|
||||||
|
#include <EntityItemProperties.h>
|
||||||
|
|
||||||
|
|
||||||
ScriptableAvatar::ScriptableAvatar() {
|
ScriptableAvatar::ScriptableAvatar() {
|
||||||
|
@ -249,3 +251,157 @@ void ScriptableAvatar::setHasProceduralEyeFaceMovement(bool hasProceduralEyeFace
|
||||||
void ScriptableAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) {
|
void ScriptableAvatar::setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement) {
|
||||||
_headData->setHasAudioEnabledFaceMovement(hasAudioEnabledFaceMovement);
|
_headData->setHasAudioEnabledFaceMovement(hasAudioEnabledFaceMovement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AvatarEntityMap ScriptableAvatar::getAvatarEntityData() const {
|
||||||
|
// DANGER: Now that we store the AvatarEntityData in packed format this call is potentially Very Expensive!
|
||||||
|
// Avoid calling this method if possible.
|
||||||
|
AvatarEntityMap data;
|
||||||
|
QUuid sessionID = getID();
|
||||||
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
|
for (const auto& itr : _entities) {
|
||||||
|
QUuid id = itr.first;
|
||||||
|
EntityItemPointer entity = itr.second;
|
||||||
|
EntityItemProperties properties = entity->getProperties();
|
||||||
|
QByteArray blob;
|
||||||
|
EntityItemProperties::propertiesToBlob(_scriptEngine, sessionID, properties, blob);
|
||||||
|
data[id] = blob;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptableAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
|
||||||
|
// Note: this is an invokable Script call
|
||||||
|
// avatarEntityData is expected to be a map of QByteArrays that represent EntityItemProperties objects from JavaScript
|
||||||
|
//
|
||||||
|
if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) {
|
||||||
|
// the data is suspect
|
||||||
|
qCDebug(avatars) << "discard suspect avatarEntityData with size =" << avatarEntityData.size();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert binary data to EntityItemProperties
|
||||||
|
// NOTE: this operation is NOT efficient
|
||||||
|
std::map<QUuid, EntityItemProperties> newProperties;
|
||||||
|
AvatarEntityMap::const_iterator dataItr = avatarEntityData.begin();
|
||||||
|
while (dataItr != avatarEntityData.end()) {
|
||||||
|
EntityItemProperties properties;
|
||||||
|
const QByteArray& blob = dataItr.value();
|
||||||
|
if (!blob.isNull() && EntityItemProperties::blobToProperties(_scriptEngine, blob, properties)) {
|
||||||
|
newProperties[dataItr.key()] = properties;
|
||||||
|
}
|
||||||
|
++dataItr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete existing entities not found in avatarEntityData
|
||||||
|
std::vector<QUuid> idsToClear;
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
std::map<QUuid, EntityItemPointer>::iterator entityItr = _entities.begin();
|
||||||
|
while (entityItr != _entities.end()) {
|
||||||
|
QUuid id = entityItr->first;
|
||||||
|
std::map<QUuid, EntityItemProperties>::const_iterator propertiesItr = newProperties.find(id);
|
||||||
|
if (propertiesItr == newProperties.end()) {
|
||||||
|
idsToClear.push_back(id);
|
||||||
|
entityItr = _entities.erase(entityItr);
|
||||||
|
} else {
|
||||||
|
++entityItr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// add or update entities
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
std::map<QUuid, EntityItemProperties>::const_iterator propertiesItr = newProperties.begin();
|
||||||
|
while (propertiesItr != newProperties.end()) {
|
||||||
|
QUuid id = propertiesItr->first;
|
||||||
|
const EntityItemProperties& properties = propertiesItr->second;
|
||||||
|
std::map<QUuid, EntityItemPointer>::iterator entityItr = _entities.find(id);
|
||||||
|
EntityItemPointer entity;
|
||||||
|
if (entityItr != _entities.end()) {
|
||||||
|
entity = entityItr->second;
|
||||||
|
entity->setProperties(properties);
|
||||||
|
} else {
|
||||||
|
entity = EntityTypes::constructEntityItem(id, properties);
|
||||||
|
}
|
||||||
|
if (entity) {
|
||||||
|
// build outgoing payload
|
||||||
|
OctreePacketData packetData(false, AvatarTraits::MAXIMUM_TRAIT_SIZE);
|
||||||
|
EncodeBitstreamParams params;
|
||||||
|
EntityTreeElementExtraEncodeDataPointer extra { nullptr };
|
||||||
|
OctreeElement::AppendState appendState = entity->appendEntityData(&packetData, params, extra);
|
||||||
|
|
||||||
|
if (appendState == OctreeElement::COMPLETED) {
|
||||||
|
_entities[id] = entity;
|
||||||
|
QByteArray tempArray((const char*)packetData.getUncompressedData(), packetData.getUncompressedSize());
|
||||||
|
storeAvatarEntityDataPayload(id, tempArray);
|
||||||
|
} else {
|
||||||
|
// payload doesn't fit
|
||||||
|
entityItr = _entities.find(id);
|
||||||
|
if (entityItr != _entities.end()) {
|
||||||
|
_entities.erase(entityItr);
|
||||||
|
idsToClear.push_back(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++propertiesItr;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// clear deleted traits
|
||||||
|
for (const auto& id : idsToClear) {
|
||||||
|
clearAvatarEntity(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScriptableAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) {
|
||||||
|
if (entityData.isNull()) {
|
||||||
|
// interpret this as a DELETE
|
||||||
|
std::map<QUuid, EntityItemPointer>::iterator itr = _entities.find(entityID);
|
||||||
|
if (itr != _entities.end()) {
|
||||||
|
_entities.erase(itr);
|
||||||
|
clearAvatarEntity(entityID);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityItemPointer entity;
|
||||||
|
EntityItemProperties properties;
|
||||||
|
if (!EntityItemProperties::blobToProperties(_scriptEngine, entityData, properties)) {
|
||||||
|
// entityData is corrupt
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<QUuid, EntityItemPointer>::iterator itr = _entities.find(entityID);
|
||||||
|
if (itr == _entities.end()) {
|
||||||
|
// this is an ADD
|
||||||
|
entity = EntityTypes::constructEntityItem(entityID, properties);
|
||||||
|
if (entity) {
|
||||||
|
OctreePacketData packetData(false, AvatarTraits::MAXIMUM_TRAIT_SIZE);
|
||||||
|
EncodeBitstreamParams params;
|
||||||
|
EntityTreeElementExtraEncodeDataPointer extra { nullptr };
|
||||||
|
OctreeElement::AppendState appendState = entity->appendEntityData(&packetData, params, extra);
|
||||||
|
|
||||||
|
if (appendState == OctreeElement::COMPLETED) {
|
||||||
|
_entities[entityID] = entity;
|
||||||
|
QByteArray tempArray((const char*)packetData.getUncompressedData(), packetData.getUncompressedSize());
|
||||||
|
storeAvatarEntityDataPayload(entityID, tempArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this is an UPDATE
|
||||||
|
entity = itr->second;
|
||||||
|
bool somethingChanged = entity->setProperties(properties);
|
||||||
|
if (somethingChanged) {
|
||||||
|
OctreePacketData packetData(false, AvatarTraits::MAXIMUM_TRAIT_SIZE);
|
||||||
|
EncodeBitstreamParams params;
|
||||||
|
EntityTreeElementExtraEncodeDataPointer extra { nullptr };
|
||||||
|
OctreeElement::AppendState appendState = entity->appendEntityData(&packetData, params, extra);
|
||||||
|
|
||||||
|
if (appendState == OctreeElement::COMPLETED) {
|
||||||
|
QByteArray tempArray((const char*)packetData.getUncompressedData(), packetData.getUncompressedSize());
|
||||||
|
storeAvatarEntityDataPayload(entityID, tempArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
#include <AnimSkeleton.h>
|
#include <AnimSkeleton.h>
|
||||||
#include <AvatarData.h>
|
#include <AvatarData.h>
|
||||||
#include <ScriptEngine.h>
|
#include <ScriptEngine.h>
|
||||||
|
#include <EntityItem.h>
|
||||||
|
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* The <code>Avatar</code> API is used to manipulate scriptable avatars on the domain. This API is a subset of the
|
* The <code>Avatar</code> API is used to manipulate scriptable avatars on the domain. This API is a subset of the
|
||||||
|
@ -185,6 +186,26 @@ public:
|
||||||
void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement);
|
void setHasAudioEnabledFaceMovement(bool hasAudioEnabledFaceMovement);
|
||||||
bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); }
|
bool getHasAudioEnabledFaceMovement() const override { return _headData->getHasAudioEnabledFaceMovement(); }
|
||||||
|
|
||||||
|
/**jsdoc
|
||||||
|
* Potentially Very Expensive. Do not use.
|
||||||
|
* @function Avatar.getAvatarEntityData
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const override;
|
||||||
|
|
||||||
|
/**jsdoc
|
||||||
|
* @function MyAvatar.setAvatarEntityData
|
||||||
|
* @param {object} avatarEntityData
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override;
|
||||||
|
|
||||||
|
/**jsdoc
|
||||||
|
* @function MyAvatar.updateAvatarEntity
|
||||||
|
* @param {Uuid} entityID
|
||||||
|
* @param {string} entityData
|
||||||
|
*/
|
||||||
|
Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void update(float deltatime);
|
void update(float deltatime);
|
||||||
|
|
||||||
|
@ -202,6 +223,8 @@ private:
|
||||||
QHash<QString, int> _fstJointIndices; ///< 1-based, since zero is returned for missing keys
|
QHash<QString, int> _fstJointIndices; ///< 1-based, since zero is returned for missing keys
|
||||||
QStringList _fstJointNames; ///< in order of depth-first traversal
|
QStringList _fstJointNames; ///< in order of depth-first traversal
|
||||||
QUrl _skeletonFBXURL;
|
QUrl _skeletonFBXURL;
|
||||||
|
mutable QScriptEngine _scriptEngine;
|
||||||
|
std::map<QUuid, EntityItemPointer> _entities;
|
||||||
|
|
||||||
/// Loads the joint indices, names from the FST file (if any)
|
/// Loads the joint indices, names from the FST file (if any)
|
||||||
void updateJointMappings();
|
void updateJointMappings();
|
||||||
|
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 39 KiB |
|
@ -10,7 +10,7 @@ import "avatarapp"
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
width: 480
|
width: 480
|
||||||
height: 706
|
height: 706
|
||||||
|
|
||||||
property bool keyboardEnabled: true
|
property bool keyboardEnabled: true
|
||||||
property bool keyboardRaised: false
|
property bool keyboardRaised: false
|
||||||
|
@ -416,7 +416,7 @@ Rectangle {
|
||||||
width: 21.2
|
width: 21.2
|
||||||
height: 19.3
|
height: 19.3
|
||||||
source: isAvatarInFavorites ? '../../images/FavoriteIconActive.svg' : '../../images/FavoriteIconInActive.svg'
|
source: isAvatarInFavorites ? '../../images/FavoriteIconActive.svg' : '../../images/FavoriteIconInActive.svg'
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextStyle5
|
// TextStyle5
|
||||||
|
@ -425,7 +425,7 @@ Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: isAvatarInFavorites ? avatarName : "Add to Favorites"
|
text: isAvatarInFavorites ? avatarName : "Add to Favorites"
|
||||||
elide: Qt.ElideRight
|
elide: Qt.ElideRight
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -157,7 +157,7 @@ Rectangle {
|
||||||
visible = false;
|
visible = false;
|
||||||
adjustWearablesClosed(status, avatarName);
|
adjustWearablesClosed(status, avatarName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
HifiConstants { id: hifi }
|
HifiConstants { id: hifi }
|
||||||
|
|
||||||
|
@ -230,7 +230,7 @@ Rectangle {
|
||||||
lineHeightMode: Text.FixedHeight
|
lineHeightMode: Text.FixedHeight
|
||||||
lineHeight: 18;
|
lineHeight: 18;
|
||||||
text: "Wearable"
|
text: "Wearable"
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
spacing: 10
|
spacing: 10
|
||||||
|
@ -241,7 +241,7 @@ Rectangle {
|
||||||
lineHeight: 18;
|
lineHeight: 18;
|
||||||
text: "<a href='#'>Get more</a>"
|
text: "<a href='#'>Get more</a>"
|
||||||
linkColor: hifi.colors.blueHighlight
|
linkColor: hifi.colors.blueHighlight
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
onLinkActivated: {
|
onLinkActivated: {
|
||||||
popup.showGetWearables(function() {
|
popup.showGetWearables(function() {
|
||||||
emitSendToScript({'method' : 'navigate', 'url' : 'hifi://AvatarIsland/11.5848,-8.10862,-2.80195'})
|
emitSendToScript({'method' : 'navigate', 'url' : 'hifi://AvatarIsland/11.5848,-8.10862,-2.80195'})
|
||||||
|
|
|
@ -104,11 +104,11 @@ Rectangle {
|
||||||
size: 17;
|
size: 17;
|
||||||
text: "Avatar Scale"
|
text: "Avatar Scale"
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
@ -118,7 +118,7 @@ Rectangle {
|
||||||
text: 'T'
|
text: 'T'
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
HifiControlsUit.Slider {
|
HifiControlsUit.Slider {
|
||||||
|
@ -136,7 +136,7 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
||||||
// TextStyle9
|
// TextStyle9
|
||||||
|
@ -165,7 +165,7 @@ Rectangle {
|
||||||
text: 'T'
|
text: 'T'
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
Layout.alignment: Qt.AlignVCenter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,8 +316,7 @@ Rectangle {
|
||||||
InputTextStyle4 {
|
InputTextStyle4 {
|
||||||
id: avatarAnimationUrlInputText
|
id: avatarAnimationUrlInputText
|
||||||
font.pixelSize: 17
|
font.pixelSize: 17
|
||||||
anchors.left: parent.left
|
Layout.fillWidth: true
|
||||||
anchors.right: parent.right
|
|
||||||
placeholderText: 'user\\file\\dir'
|
placeholderText: 'user\\file\\dir'
|
||||||
|
|
||||||
onFocusChanged: {
|
onFocusChanged: {
|
||||||
|
@ -346,8 +345,7 @@ Rectangle {
|
||||||
InputTextStyle4 {
|
InputTextStyle4 {
|
||||||
id: avatarCollisionSoundUrlInputText
|
id: avatarCollisionSoundUrlInputText
|
||||||
font.pixelSize: 17
|
font.pixelSize: 17
|
||||||
anchors.left: parent.left
|
Layout.fillWidth: true
|
||||||
anchors.right: parent.right
|
|
||||||
placeholderText: 'https://hifi-public.s3.amazonaws.com/sounds/Collisions-'
|
placeholderText: 'https://hifi-public.s3.amazonaws.com/sounds/Collisions-'
|
||||||
|
|
||||||
onFocusChanged: {
|
onFocusChanged: {
|
||||||
|
|
|
@ -725,6 +725,8 @@ const QString TEST_RESULTS_LOCATION_COMMAND{ "--testResultsLocation" };
|
||||||
bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
|
bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
|
||||||
const char** constArgv = const_cast<const char**>(argv);
|
const char** constArgv = const_cast<const char**>(argv);
|
||||||
|
|
||||||
|
qInstallMessageHandler(messageHandler);
|
||||||
|
|
||||||
// HRS: I could not figure out how to move these any earlier in startup, so when using this option, be sure to also supply
|
// HRS: I could not figure out how to move these any earlier in startup, so when using this option, be sure to also supply
|
||||||
// --allowMultipleInstances
|
// --allowMultipleInstances
|
||||||
auto reportAndQuit = [&](const char* commandSwitch, std::function<void(FILE* fp)> report) {
|
auto reportAndQuit = [&](const char* commandSwitch, std::function<void(FILE* fp)> report) {
|
||||||
|
@ -975,6 +977,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
||||||
QApplication(argc, argv),
|
QApplication(argc, argv),
|
||||||
_window(new MainWindow(desktop())),
|
_window(new MainWindow(desktop())),
|
||||||
_sessionRunTimer(startupTimer),
|
_sessionRunTimer(startupTimer),
|
||||||
|
_logger(new FileLogger(this)),
|
||||||
_previousSessionCrashed(setupEssentials(argc, argv, runningMarkerExisted)),
|
_previousSessionCrashed(setupEssentials(argc, argv, runningMarkerExisted)),
|
||||||
_entitySimulation(new PhysicalEntitySimulation()),
|
_entitySimulation(new PhysicalEntitySimulation()),
|
||||||
_physicsEngine(new PhysicsEngine(Vectors::ZERO)),
|
_physicsEngine(new PhysicsEngine(Vectors::ZERO)),
|
||||||
|
@ -1064,9 +1067,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
||||||
installNativeEventFilter(&MyNativeEventFilter::getInstance());
|
installNativeEventFilter(&MyNativeEventFilter::getInstance());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
_logger = new FileLogger(this);
|
|
||||||
qInstallMessageHandler(messageHandler);
|
|
||||||
|
|
||||||
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf");
|
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf");
|
||||||
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/fontawesome-webfont.ttf");
|
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/fontawesome-webfont.ttf");
|
||||||
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/hifi-glyphs.ttf");
|
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/hifi-glyphs.ttf");
|
||||||
|
@ -2061,7 +2061,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
|
||||||
properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1;
|
properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1;
|
||||||
properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1;
|
properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1;
|
||||||
properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1;
|
properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1;
|
||||||
properties["atp_in_kbps"] = messagesMixerNode ? assetServerNode->getInboundKbps() : 0.0f;
|
properties["atp_in_kbps"] = assetServerNode ? assetServerNode->getInboundKbps() : 0.0f;
|
||||||
|
|
||||||
auto loadingRequests = ResourceCache::getLoadingRequests();
|
auto loadingRequests = ResourceCache::getLoadingRequests();
|
||||||
|
|
||||||
|
@ -2460,6 +2460,9 @@ void Application::updateHeartbeat() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::onAboutToQuit() {
|
void Application::onAboutToQuit() {
|
||||||
|
// quickly save AvatarEntityData before the EntityTree is dismantled
|
||||||
|
getMyAvatar()->saveAvatarEntityDataToSettings();
|
||||||
|
|
||||||
emit beforeAboutToQuit();
|
emit beforeAboutToQuit();
|
||||||
|
|
||||||
if (getLoginDialogPoppedUp() && _firstRun.get()) {
|
if (getLoginDialogPoppedUp() && _firstRun.get()) {
|
||||||
|
@ -6763,8 +6766,10 @@ void Application::updateWindowTitle() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::clearDomainOctreeDetails() {
|
void Application::clearDomainOctreeDetails() {
|
||||||
|
// before we delete all entities get MyAvatar's AvatarEntityData ready
|
||||||
|
getMyAvatar()->prepareAvatarEntityDataForReload();
|
||||||
|
|
||||||
// if we're about to quit, we really don't need to do any of these things...
|
// if we're about to quit, we really don't need to do the rest of these things...
|
||||||
if (_aboutToQuit) {
|
if (_aboutToQuit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -6792,8 +6797,6 @@ void Application::clearDomainOctreeDetails() {
|
||||||
ShaderCache::instance().clearUnusedResources();
|
ShaderCache::instance().clearUnusedResources();
|
||||||
DependencyManager::get<TextureCache>()->clearUnusedResources();
|
DependencyManager::get<TextureCache>()->clearUnusedResources();
|
||||||
DependencyManager::get<recording::ClipCache>()->clearUnusedResources();
|
DependencyManager::get<recording::ClipCache>()->clearUnusedResources();
|
||||||
|
|
||||||
getMyAvatar()->setAvatarEntityDataChanged(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::domainURLChanged(QUrl domainURL) {
|
void Application::domainURLChanged(QUrl domainURL) {
|
||||||
|
|
|
@ -594,6 +594,8 @@ private:
|
||||||
|
|
||||||
bool _aboutToQuit { false };
|
bool _aboutToQuit { false };
|
||||||
|
|
||||||
|
FileLogger* _logger { nullptr };
|
||||||
|
|
||||||
bool _previousSessionCrashed;
|
bool _previousSessionCrashed;
|
||||||
|
|
||||||
DisplayPluginPointer _displayPlugin;
|
DisplayPluginPointer _displayPlugin;
|
||||||
|
@ -674,8 +676,6 @@ private:
|
||||||
QPointer<EntityScriptServerLogDialog> _entityScriptServerLogDialog;
|
QPointer<EntityScriptServerLogDialog> _entityScriptServerLogDialog;
|
||||||
QDir _defaultScriptsLocation;
|
QDir _defaultScriptsLocation;
|
||||||
|
|
||||||
FileLogger* _logger;
|
|
||||||
|
|
||||||
TouchEvent _lastTouchEvent;
|
TouchEvent _lastTouchEvent;
|
||||||
|
|
||||||
quint64 _lastNackTime;
|
quint64 _lastNackTime;
|
||||||
|
|
|
@ -60,7 +60,6 @@ void addAvatarEntities(const QVariantList& avatarEntities) {
|
||||||
entityProperties.setParentID(myNodeID);
|
entityProperties.setParentID(myNodeID);
|
||||||
entityProperties.setEntityHostType(entity::HostType::AVATAR);
|
entityProperties.setEntityHostType(entity::HostType::AVATAR);
|
||||||
entityProperties.setOwningAvatarID(myNodeID);
|
entityProperties.setOwningAvatarID(myNodeID);
|
||||||
entityProperties.setSimulationOwner(myNodeID, AVATAR_ENTITY_SIMULATION_PRIORITY);
|
|
||||||
entityProperties.markAllChanged();
|
entityProperties.markAllChanged();
|
||||||
|
|
||||||
EntityItemID id = EntityItemID(QUuid::createUuid());
|
EntityItemID id = EntityItemID(QUuid::createUuid());
|
||||||
|
|
|
@ -50,6 +50,7 @@
|
||||||
#include <RecordingScriptingInterface.h>
|
#include <RecordingScriptingInterface.h>
|
||||||
#include <trackers/FaceTracker.h>
|
#include <trackers/FaceTracker.h>
|
||||||
#include <RenderableModelEntityItem.h>
|
#include <RenderableModelEntityItem.h>
|
||||||
|
#include <VariantMapToScriptValue.h>
|
||||||
|
|
||||||
#include "MyHead.h"
|
#include "MyHead.h"
|
||||||
#include "MySkeletonModel.h"
|
#include "MySkeletonModel.h"
|
||||||
|
@ -281,6 +282,8 @@ MyAvatar::MyAvatar(QThread* thread) :
|
||||||
|
|
||||||
MyAvatar::~MyAvatar() {
|
MyAvatar::~MyAvatar() {
|
||||||
_lookAtTargetAvatar.reset();
|
_lookAtTargetAvatar.reset();
|
||||||
|
delete _myScriptEngine;
|
||||||
|
_myScriptEngine = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyAvatar::setDominantHand(const QString& hand) {
|
void MyAvatar::setDominantHand(const QString& hand) {
|
||||||
|
@ -681,7 +684,7 @@ void MyAvatar::update(float deltaTime) {
|
||||||
|
|
||||||
_clientTraitsHandler->sendChangedTraitsToMixer();
|
_clientTraitsHandler->sendChangedTraitsToMixer();
|
||||||
|
|
||||||
simulate(deltaTime);
|
simulate(deltaTime, true);
|
||||||
|
|
||||||
currentEnergy += energyChargeRate;
|
currentEnergy += energyChargeRate;
|
||||||
currentEnergy -= getAccelerationEnergy();
|
currentEnergy -= getAccelerationEnergy();
|
||||||
|
@ -753,7 +756,7 @@ void MyAvatar::updateChildCauterization(SpatiallyNestablePointer object, bool ca
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyAvatar::simulate(float deltaTime) {
|
void MyAvatar::simulate(float deltaTime, bool inView) {
|
||||||
PerformanceTimer perfTimer("simulate");
|
PerformanceTimer perfTimer("simulate");
|
||||||
animateScaleChanges(deltaTime);
|
animateScaleChanges(deltaTime);
|
||||||
|
|
||||||
|
@ -897,7 +900,7 @@ void MyAvatar::simulate(float deltaTime) {
|
||||||
_characterController.setCollisionlessAllowed(collisionlessAllowed);
|
_characterController.setCollisionlessAllowed(collisionlessAllowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAvatarEntities();
|
handleChangedAvatarEntityData();
|
||||||
|
|
||||||
updateFadingStatus();
|
updateFadingStatus();
|
||||||
}
|
}
|
||||||
|
@ -1261,7 +1264,7 @@ void MyAvatar::saveAvatarUrl() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MyAvatar::resizeAvatarEntitySettingHandles(unsigned int avatarEntityIndex) {
|
void MyAvatar::resizeAvatarEntitySettingHandles(uint32_t maxIndex) {
|
||||||
// The original Settings interface saved avatar-entity array data like this:
|
// The original Settings interface saved avatar-entity array data like this:
|
||||||
// Avatar/avatarEntityData/size: 5
|
// Avatar/avatarEntityData/size: 5
|
||||||
// Avatar/avatarEntityData/1/id: ...
|
// Avatar/avatarEntityData/1/id: ...
|
||||||
|
@ -1271,14 +1274,15 @@ void MyAvatar::resizeAvatarEntitySettingHandles(unsigned int avatarEntityIndex)
|
||||||
// Avatar/avatarEntityData/5/properties: ...
|
// Avatar/avatarEntityData/5/properties: ...
|
||||||
//
|
//
|
||||||
// Create Setting::Handles to mimic this.
|
// Create Setting::Handles to mimic this.
|
||||||
|
uint32_t settingsIndex = (uint32_t)_avatarEntityIDSettings.size() + 1;
|
||||||
while (_avatarEntityIDSettings.size() <= avatarEntityIndex) {
|
while (settingsIndex <= maxIndex) {
|
||||||
Setting::Handle<QUuid> idHandle(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "avatarEntityData"
|
Setting::Handle<QUuid> idHandle(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "avatarEntityData"
|
||||||
<< QString::number(avatarEntityIndex + 1) << "id", QUuid());
|
<< QString::number(settingsIndex) << "id", QUuid());
|
||||||
_avatarEntityIDSettings.push_back(idHandle);
|
_avatarEntityIDSettings.push_back(idHandle);
|
||||||
Setting::Handle<QByteArray> dataHandle(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "avatarEntityData"
|
Setting::Handle<QByteArray> dataHandle(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "avatarEntityData"
|
||||||
<< QString::number(avatarEntityIndex + 1) << "properties", QByteArray());
|
<< QString::number(settingsIndex) << "properties", QByteArray());
|
||||||
_avatarEntityDataSettings.push_back(dataHandle);
|
_avatarEntityDataSettings.push_back(dataHandle);
|
||||||
|
settingsIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1308,26 +1312,54 @@ void MyAvatar::saveData() {
|
||||||
_flyingHMDSetting.set(getFlyingHMDPref());
|
_flyingHMDSetting.set(getFlyingHMDPref());
|
||||||
|
|
||||||
auto hmdInterface = DependencyManager::get<HMDScriptingInterface>();
|
auto hmdInterface = DependencyManager::get<HMDScriptingInterface>();
|
||||||
_avatarEntitiesLock.withReadLock([&] {
|
saveAvatarEntityDataToSettings();
|
||||||
QList<QUuid> avatarEntityIDs = _avatarEntityData.keys();
|
}
|
||||||
unsigned int avatarEntityCount = avatarEntityIDs.size();
|
|
||||||
unsigned int previousAvatarEntityCount = _avatarEntityCountSetting.get(0);
|
|
||||||
resizeAvatarEntitySettingHandles(std::max<unsigned int>(avatarEntityCount, previousAvatarEntityCount));
|
|
||||||
_avatarEntityCountSetting.set(avatarEntityCount);
|
|
||||||
|
|
||||||
unsigned int avatarEntityIndex = 0;
|
void MyAvatar::saveAvatarEntityDataToSettings() {
|
||||||
for (auto entityID : avatarEntityIDs) {
|
if (!_needToSaveAvatarEntitySettings) {
|
||||||
_avatarEntityIDSettings[avatarEntityIndex].set(entityID);
|
return;
|
||||||
_avatarEntityDataSettings[avatarEntityIndex].set(_avatarEntityData.value(entityID));
|
}
|
||||||
avatarEntityIndex++;
|
bool success = updateStaleAvatarEntityBlobs();
|
||||||
}
|
if (!success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_needToSaveAvatarEntitySettings = false;
|
||||||
|
|
||||||
// clean up any left-over (due to the list shrinking) slots
|
uint32_t numEntities = (uint32_t)_cachedAvatarEntityBlobs.size();
|
||||||
for (; avatarEntityIndex < previousAvatarEntityCount; avatarEntityIndex++) {
|
uint32_t prevNumEntities = _avatarEntityCountSetting.get(0);
|
||||||
_avatarEntityIDSettings[avatarEntityIndex].remove();
|
resizeAvatarEntitySettingHandles(std::max<uint32_t>(numEntities, prevNumEntities));
|
||||||
_avatarEntityDataSettings[avatarEntityIndex].remove();
|
|
||||||
|
// save new Settings
|
||||||
|
if (numEntities > 0) {
|
||||||
|
// save all unfortunately-formatted-binary-blobs to Settings
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
uint32_t i = 0;
|
||||||
|
AvatarEntityMap::const_iterator itr = _cachedAvatarEntityBlobs.begin();
|
||||||
|
while (itr != _cachedAvatarEntityBlobs.end()) {
|
||||||
|
_avatarEntityIDSettings[i].set(itr.key());
|
||||||
|
_avatarEntityDataSettings[i].set(itr.value());
|
||||||
|
++itr;
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
numEntities = i;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_avatarEntityCountSetting.set(numEntities);
|
||||||
|
|
||||||
|
// remove old Settings if any
|
||||||
|
if (numEntities < prevNumEntities) {
|
||||||
|
uint32_t numEntitiesToRemove = prevNumEntities - numEntities;
|
||||||
|
for (uint32_t i = 0; i < numEntitiesToRemove; ++i) {
|
||||||
|
if (_avatarEntityIDSettings.size() > numEntities) {
|
||||||
|
_avatarEntityIDSettings.back().remove();
|
||||||
|
_avatarEntityIDSettings.pop_back();
|
||||||
|
}
|
||||||
|
if (_avatarEntityDataSettings.size() > numEntities) {
|
||||||
|
_avatarEntityDataSettings.back().remove();
|
||||||
|
_avatarEntityDataSettings.pop_back();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float loadSetting(Settings& settings, const QString& name, float defaultValue) {
|
float loadSetting(Settings& settings, const QString& name, float defaultValue) {
|
||||||
|
@ -1424,7 +1456,410 @@ void MyAvatar::setEnableInverseKinematics(bool isEnabled) {
|
||||||
_skeletonModel->getRig().setEnableInverseKinematics(isEnabled);
|
_skeletonModel->getRig().setEnableInverseKinematics(isEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MyAvatar::storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) {
|
||||||
|
AvatarData::storeAvatarEntityDataPayload(entityID, payload);
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
_cachedAvatarEntityBlobsToAddOrUpdate.push_back(entityID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyAvatar::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) {
|
||||||
|
AvatarData::clearAvatarEntity(entityID, requiresRemovalFromTree);
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
_cachedAvatarEntityBlobsToDelete.push_back(entityID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyAvatar::sanitizeAvatarEntityProperties(EntityItemProperties& properties) const {
|
||||||
|
properties.setEntityHostType(entity::HostType::AVATAR);
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// When grabbing avatar entities, they are parented to the joint moving them, then when un-grabbed
|
||||||
|
// they go back to the default parent (null uuid). When un-gripped, others saw the entity disappear.
|
||||||
|
// The thinking here is the local position was noticed as changing, but not the parentID (since it is now
|
||||||
|
// back to the default), and the entity flew off somewhere. Marking all changed definitely fixes this,
|
||||||
|
// and seems safe (per Seth).
|
||||||
|
properties.markAllChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyAvatar::handleChangedAvatarEntityData() {
|
||||||
|
// NOTE: this is a per-frame update
|
||||||
|
if (getID().isNull() ||
|
||||||
|
getID() == AVATAR_SELF_ID ||
|
||||||
|
DependencyManager::get<NodeList>()->getSessionUUID() == QUuid()) {
|
||||||
|
// wait until MyAvatar and this Node gets an ID before doing this. Otherwise, various things go wrong:
|
||||||
|
// things get their parent fixed up from AVATAR_SELF_ID to a null uuid which means "no parent".
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_reloadAvatarEntityDataFromSettings) {
|
||||||
|
loadAvatarEntityDataFromSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||||
|
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||||
|
if (!entityTree) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We collect changes to AvatarEntities and then handle them all in one spot per frame: handleChangedAvatarEntityData().
|
||||||
|
// Basically this is a "transaction pattern" with an extra complication: these changes can come from two
|
||||||
|
// "directions" and the "authoritative source" of each direction is different, so we maintain two distinct sets
|
||||||
|
// of transaction lists:
|
||||||
|
//
|
||||||
|
// The _entitiesToDelete/Add/Update lists are for changes whose "authoritative sources" are already
|
||||||
|
// correctly stored in _cachedAvatarEntityBlobs. These come from loadAvatarEntityDataFromSettings() and
|
||||||
|
// setAvatarEntityData(). These changes need to be extracted from _cachedAvatarEntityBlobs and applied to
|
||||||
|
// real EntityItems.
|
||||||
|
//
|
||||||
|
// The _cachedAvatarEntityBlobsToDelete/Add/Update lists are for changes whose "authoritative sources" are
|
||||||
|
// already reflected in real EntityItems. These changes need to be propagated to _cachedAvatarEntityBlobs
|
||||||
|
// and eventually to Settings.
|
||||||
|
//
|
||||||
|
// The DELETEs also need to be propagated to the traits, which will eventually propagate to
|
||||||
|
// AvatarData::_packedAvatarEntityData via deeper logic.
|
||||||
|
|
||||||
|
// move the lists to minimize lock time
|
||||||
|
std::vector<QUuid> cachedBlobsToDelete;
|
||||||
|
std::vector<QUuid> cachedBlobsToUpdate;
|
||||||
|
std::vector<QUuid> entitiesToDelete;
|
||||||
|
std::vector<QUuid> entitiesToAdd;
|
||||||
|
std::vector<QUuid> entitiesToUpdate;
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
cachedBlobsToDelete = std::move(_cachedAvatarEntityBlobsToDelete);
|
||||||
|
cachedBlobsToUpdate = std::move(_cachedAvatarEntityBlobsToAddOrUpdate);
|
||||||
|
entitiesToDelete = std::move(_entitiesToDelete);
|
||||||
|
entitiesToAdd = std::move(_entitiesToAdd);
|
||||||
|
entitiesToUpdate = std::move(_entitiesToUpdate);
|
||||||
|
});
|
||||||
|
|
||||||
|
auto removeAllInstancesHelper = [] (const QUuid& id, std::vector<QUuid>& v) {
|
||||||
|
uint32_t i = 0;
|
||||||
|
while (i < v.size()) {
|
||||||
|
if (id == v[i]) {
|
||||||
|
v[i] = v.back();
|
||||||
|
v.pop_back();
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// remove delete-add and delete-update overlap
|
||||||
|
for (const auto& id : entitiesToDelete) {
|
||||||
|
removeAllInstancesHelper(id, cachedBlobsToUpdate);
|
||||||
|
removeAllInstancesHelper(id, entitiesToAdd);
|
||||||
|
removeAllInstancesHelper(id, entitiesToUpdate);
|
||||||
|
}
|
||||||
|
for (const auto& id : cachedBlobsToDelete) {
|
||||||
|
removeAllInstancesHelper(id, entitiesToUpdate);
|
||||||
|
removeAllInstancesHelper(id, cachedBlobsToUpdate);
|
||||||
|
}
|
||||||
|
for (const auto& id : entitiesToAdd) {
|
||||||
|
removeAllInstancesHelper(id, entitiesToUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE real entities
|
||||||
|
for (const auto& id : entitiesToDelete) {
|
||||||
|
entityTree->withWriteLock([&] {
|
||||||
|
entityTree->deleteEntity(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADD real entities
|
||||||
|
EntityEditPacketSender* packetSender = qApp->getEntityEditPacketSender();
|
||||||
|
for (const auto& id : entitiesToAdd) {
|
||||||
|
bool blobFailed = false;
|
||||||
|
EntityItemProperties properties;
|
||||||
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
|
AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.find(id);
|
||||||
|
if (itr == _cachedAvatarEntityBlobs.end()) {
|
||||||
|
blobFailed = true; // blob doesn't exist
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!EntityItemProperties::blobToProperties(*_myScriptEngine, itr.value(), properties)) {
|
||||||
|
blobFailed = true; // blob is corrupt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (blobFailed) {
|
||||||
|
// remove from _cachedAvatarEntityBlobUpdatesToSkip just in case:
|
||||||
|
// avoids a resource leak when blob updates to be skipped are never actually skipped
|
||||||
|
// when the blob fails to result in a real EntityItem
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
removeAllInstancesHelper(id, _cachedAvatarEntityBlobUpdatesToSkip);
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sanitizeAvatarEntityProperties(properties);
|
||||||
|
entityTree->withWriteLock([&] {
|
||||||
|
EntityItemPointer entity = entityTree->addEntity(id, properties);
|
||||||
|
if (entity) {
|
||||||
|
packetSender->queueEditEntityMessage(PacketType::EntityAdd, entityTree, id, properties);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHANGE real entities
|
||||||
|
for (const auto& id : entitiesToUpdate) {
|
||||||
|
EntityItemProperties properties;
|
||||||
|
bool skip = false;
|
||||||
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
|
AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.find(id);
|
||||||
|
if (itr == _cachedAvatarEntityBlobs.end()) {
|
||||||
|
skip = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!EntityItemProperties::blobToProperties(*_myScriptEngine, itr.value(), properties)) {
|
||||||
|
skip = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sanitizeAvatarEntityProperties(properties);
|
||||||
|
entityTree->withWriteLock([&] {
|
||||||
|
entityTree->updateEntity(id, properties);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE cached blobs
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
for (const auto& id : cachedBlobsToDelete) {
|
||||||
|
AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.find(id);
|
||||||
|
// remove blob and remember to remove from settings
|
||||||
|
if (itr != _cachedAvatarEntityBlobs.end()) {
|
||||||
|
_cachedAvatarEntityBlobs.erase(itr);
|
||||||
|
_needToSaveAvatarEntitySettings = true;
|
||||||
|
}
|
||||||
|
// also remove from list of stale blobs to avoid failed entity lookup later
|
||||||
|
std::set<QUuid>::iterator blobItr = _staleCachedAvatarEntityBlobs.find(id);
|
||||||
|
if (blobItr != _staleCachedAvatarEntityBlobs.end()) {
|
||||||
|
_staleCachedAvatarEntityBlobs.erase(blobItr);
|
||||||
|
}
|
||||||
|
// also remove from _cachedAvatarEntityBlobUpdatesToSkip just in case:
|
||||||
|
// avoids a resource leak when things are deleted before they could be skipped
|
||||||
|
removeAllInstancesHelper(id, _cachedAvatarEntityBlobUpdatesToSkip);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ADD/UPDATE cached blobs
|
||||||
|
for (const auto& id : cachedBlobsToUpdate) {
|
||||||
|
// computing the blobs is expensive and we want to avoid it when possible
|
||||||
|
// so we add these ids to _staleCachedAvatarEntityBlobs for later
|
||||||
|
// and only build the blobs when absolutely necessary
|
||||||
|
bool skip = false;
|
||||||
|
uint32_t i = 0;
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
while (i < _cachedAvatarEntityBlobUpdatesToSkip.size()) {
|
||||||
|
if (id == _cachedAvatarEntityBlobUpdatesToSkip[i]) {
|
||||||
|
_cachedAvatarEntityBlobUpdatesToSkip[i] = _cachedAvatarEntityBlobUpdatesToSkip.back();
|
||||||
|
_cachedAvatarEntityBlobUpdatesToSkip.pop_back();
|
||||||
|
skip = true;
|
||||||
|
break; // assume no duplicates
|
||||||
|
} else {
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!skip) {
|
||||||
|
_staleCachedAvatarEntityBlobs.insert(id);
|
||||||
|
_needToSaveAvatarEntitySettings = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE traits
|
||||||
|
// (no need to worry about the ADDs and UPDATEs: each will be handled when the interface
|
||||||
|
// tries to send a real update packet (via AvatarData::storeAvatarEntityDataPayload()))
|
||||||
|
if (_clientTraitsHandler) {
|
||||||
|
// we have a client traits handler
|
||||||
|
// flag removed entities as deleted so that changes are sent next frame
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
for (const auto& id : entitiesToDelete) {
|
||||||
|
if (_packedAvatarEntityData.find(id) != _packedAvatarEntityData.end()) {
|
||||||
|
_clientTraitsHandler->markInstancedTraitDeleted(AvatarTraits::AvatarEntity, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& id : cachedBlobsToDelete) {
|
||||||
|
if (_packedAvatarEntityData.find(id) != _packedAvatarEntityData.end()) {
|
||||||
|
_clientTraitsHandler->markInstancedTraitDeleted(AvatarTraits::AvatarEntity, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MyAvatar::updateStaleAvatarEntityBlobs() const {
|
||||||
|
// call this right before you actually need to use the blobs
|
||||||
|
//
|
||||||
|
// Note: this method is const (and modifies mutable data members)
|
||||||
|
// so we can call it at the Last Minute inside other const methods
|
||||||
|
//
|
||||||
|
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||||
|
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||||
|
if (!entityTree) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<QUuid> staleBlobs = std::move(_staleCachedAvatarEntityBlobs);
|
||||||
|
int32_t numFound = 0;
|
||||||
|
for (const auto& id : staleBlobs) {
|
||||||
|
bool found = false;
|
||||||
|
EntityItemProperties properties;
|
||||||
|
entityTree->withReadLock([&] {
|
||||||
|
EntityItemPointer entity = entityTree->findEntityByID(id);
|
||||||
|
if (entity) {
|
||||||
|
properties = entity->getProperties();
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (found) {
|
||||||
|
++numFound;
|
||||||
|
QByteArray blob;
|
||||||
|
EntityItemProperties::propertiesToBlob(*_myScriptEngine, getID(), properties, blob);
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
_cachedAvatarEntityBlobs[id] = blob;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyAvatar::prepareAvatarEntityDataForReload() {
|
||||||
|
saveAvatarEntityDataToSettings();
|
||||||
|
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
_packedAvatarEntityData.clear();
|
||||||
|
_entitiesToDelete.clear();
|
||||||
|
_entitiesToAdd.clear();
|
||||||
|
_entitiesToUpdate.clear();
|
||||||
|
_cachedAvatarEntityBlobs.clear();
|
||||||
|
_cachedAvatarEntityBlobsToDelete.clear();
|
||||||
|
_cachedAvatarEntityBlobsToAddOrUpdate.clear();
|
||||||
|
_cachedAvatarEntityBlobUpdatesToSkip.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
_reloadAvatarEntityDataFromSettings = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AvatarEntityMap MyAvatar::getAvatarEntityData() const {
|
||||||
|
// NOTE: the return value is expected to be a map of unfortunately-formatted-binary-blobs
|
||||||
|
updateStaleAvatarEntityBlobs();
|
||||||
|
AvatarEntityMap result;
|
||||||
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
|
result = _cachedAvatarEntityBlobs;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyAvatar::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
|
||||||
|
// Note: this is an invokable Script call
|
||||||
|
// avatarEntityData is expected to be a map of QByteArrays that represent EntityItemProperties objects from JavaScript,
|
||||||
|
// aka: unfortunately-formatted-binary-blobs because we store them in non-human-readable format in Settings.
|
||||||
|
//
|
||||||
|
if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) {
|
||||||
|
// the data is suspect
|
||||||
|
qCDebug(interfaceapp) << "discard suspect AvatarEntityData with size =" << avatarEntityData.size();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this overwrites ALL AvatarEntityData so we clear pending operations
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
_packedAvatarEntityData.clear();
|
||||||
|
_entitiesToDelete.clear();
|
||||||
|
_entitiesToAdd.clear();
|
||||||
|
_entitiesToUpdate.clear();
|
||||||
|
});
|
||||||
|
_needToSaveAvatarEntitySettings = true;
|
||||||
|
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
// find new and updated IDs
|
||||||
|
AvatarEntityMap::const_iterator constItr = avatarEntityData.begin();
|
||||||
|
while (constItr != avatarEntityData.end()) {
|
||||||
|
QUuid id = constItr.key();
|
||||||
|
if (_cachedAvatarEntityBlobs.find(id) == _cachedAvatarEntityBlobs.end()) {
|
||||||
|
_entitiesToAdd.push_back(id);
|
||||||
|
} else {
|
||||||
|
_entitiesToUpdate.push_back(id);
|
||||||
|
}
|
||||||
|
++constItr;
|
||||||
|
}
|
||||||
|
// find and erase deleted IDs from _cachedAvatarEntityBlobs
|
||||||
|
std::vector<QUuid> deletedIDs;
|
||||||
|
AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.begin();
|
||||||
|
while (itr != _cachedAvatarEntityBlobs.end()) {
|
||||||
|
QUuid id = itr.key();
|
||||||
|
if (std::find(_entitiesToUpdate.begin(), _entitiesToUpdate.end(), id) == _entitiesToUpdate.end()) {
|
||||||
|
deletedIDs.push_back(id);
|
||||||
|
itr = _cachedAvatarEntityBlobs.erase(itr);
|
||||||
|
} else {
|
||||||
|
++itr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// copy new data
|
||||||
|
constItr = avatarEntityData.begin();
|
||||||
|
while (constItr != avatarEntityData.end()) {
|
||||||
|
_cachedAvatarEntityBlobs.insert(constItr.key(), constItr.value());
|
||||||
|
++constItr;
|
||||||
|
}
|
||||||
|
// erase deleted IDs from _packedAvatarEntityData
|
||||||
|
for (const auto& id : deletedIDs) {
|
||||||
|
itr = _packedAvatarEntityData.find(id);
|
||||||
|
if (itr != _packedAvatarEntityData.end()) {
|
||||||
|
_packedAvatarEntityData.erase(itr);
|
||||||
|
} else {
|
||||||
|
++itr;
|
||||||
|
}
|
||||||
|
_entitiesToDelete.push_back(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyAvatar::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) {
|
||||||
|
// NOTE: this is an invokable Script call
|
||||||
|
// TODO: we should handle the case where entityData is corrupt or invalid
|
||||||
|
// BEFORE we store into _cachedAvatarEntityBlobs
|
||||||
|
_needToSaveAvatarEntitySettings = true;
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
AvatarEntityMap::iterator itr = _cachedAvatarEntityBlobs.find(entityID);
|
||||||
|
if (itr != _cachedAvatarEntityBlobs.end()) {
|
||||||
|
_entitiesToUpdate.push_back(entityID);
|
||||||
|
itr.value() = entityData;
|
||||||
|
} else {
|
||||||
|
_entitiesToAdd.push_back(entityID);
|
||||||
|
_cachedAvatarEntityBlobs.insert(entityID, entityData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MyAvatar::avatarEntityDataToJson(QJsonObject& root) const {
|
||||||
|
updateStaleAvatarEntityBlobs();
|
||||||
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
|
if (!_cachedAvatarEntityBlobs.empty()) {
|
||||||
|
QJsonArray avatarEntityJson;
|
||||||
|
int entityCount = 0;
|
||||||
|
AvatarEntityMap::const_iterator itr = _cachedAvatarEntityBlobs.begin();
|
||||||
|
while (itr != _cachedAvatarEntityBlobs.end()) {
|
||||||
|
QVariantMap entityData;
|
||||||
|
QUuid id = _avatarEntityForRecording.size() == _cachedAvatarEntityBlobs.size() ? _avatarEntityForRecording.values()[entityCount++] : itr.key();
|
||||||
|
entityData.insert("id", id);
|
||||||
|
entityData.insert("properties", itr.value().toBase64());
|
||||||
|
avatarEntityJson.push_back(QVariant(entityData).toJsonObject());
|
||||||
|
++itr;
|
||||||
|
}
|
||||||
|
const QString JSON_AVATAR_ENTITIES = QStringLiteral("attachedEntities");
|
||||||
|
root[JSON_AVATAR_ENTITIES] = avatarEntityJson;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void MyAvatar::loadData() {
|
void MyAvatar::loadData() {
|
||||||
|
if (!_myScriptEngine) {
|
||||||
|
_myScriptEngine = new QScriptEngine();
|
||||||
|
}
|
||||||
getHead()->setBasePitch(_headPitchSetting.get());
|
getHead()->setBasePitch(_headPitchSetting.get());
|
||||||
|
|
||||||
_yawSpeed = _yawSpeedSetting.get(_yawSpeed);
|
_yawSpeed = _yawSpeedSetting.get(_yawSpeed);
|
||||||
|
@ -1436,14 +1871,7 @@ void MyAvatar::loadData() {
|
||||||
|
|
||||||
useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName);
|
useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName);
|
||||||
|
|
||||||
int avatarEntityCount = _avatarEntityCountSetting.get(0);
|
loadAvatarEntityDataFromSettings();
|
||||||
for (int i = 0; i < avatarEntityCount; i++) {
|
|
||||||
resizeAvatarEntitySettingHandles(i);
|
|
||||||
// QUuid entityID = QUuid::createUuid(); // generate a new ID
|
|
||||||
QUuid entityID = _avatarEntityIDSettings[i].get(QUuid());
|
|
||||||
QByteArray properties = _avatarEntityDataSettings[i].get();
|
|
||||||
updateAvatarEntity(entityID, properties);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flying preferences must be loaded before calling setFlyingEnabled()
|
// Flying preferences must be loaded before calling setFlyingEnabled()
|
||||||
Setting::Handle<bool> firstRunVal { Settings::firstRun, true };
|
Setting::Handle<bool> firstRunVal { Settings::firstRun, true };
|
||||||
|
@ -1465,6 +1893,38 @@ void MyAvatar::loadData() {
|
||||||
setEnableDebugDrawPosition(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawPosition));
|
setEnableDebugDrawPosition(Menu::getInstance()->isOptionChecked(MenuOption::AnimDebugDrawPosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MyAvatar::loadAvatarEntityDataFromSettings() {
|
||||||
|
// this overwrites ALL AvatarEntityData so we clear pending operations
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
_packedAvatarEntityData.clear();
|
||||||
|
_entitiesToDelete.clear();
|
||||||
|
_entitiesToAdd.clear();
|
||||||
|
_entitiesToUpdate.clear();
|
||||||
|
});
|
||||||
|
_reloadAvatarEntityDataFromSettings = false;
|
||||||
|
_needToSaveAvatarEntitySettings = false;
|
||||||
|
|
||||||
|
int numEntities = _avatarEntityCountSetting.get(0);
|
||||||
|
if (numEntities == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resizeAvatarEntitySettingHandles(numEntities);
|
||||||
|
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
_entitiesToAdd.reserve(numEntities);
|
||||||
|
// TODO: build map between old and new IDs so we can restitch parent-child relationships
|
||||||
|
for (int i = 0; i < numEntities; i++) {
|
||||||
|
QUuid id = QUuid::createUuid(); // generate a new ID
|
||||||
|
_cachedAvatarEntityBlobs[id] = _avatarEntityDataSettings[i].get();
|
||||||
|
_entitiesToAdd.push_back(id);
|
||||||
|
// this blob is the "authoritative source" for this AvatarEntity and we want to avoid overwriting it
|
||||||
|
// (the outgoing update packet will flag it for save-back into the blob)
|
||||||
|
// which is why we remember its id: to skip its save-back later
|
||||||
|
_cachedAvatarEntityBlobUpdatesToSkip.push_back(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void MyAvatar::saveAttachmentData(const AttachmentData& attachment) const {
|
void MyAvatar::saveAttachmentData(const AttachmentData& attachment) const {
|
||||||
Settings settings;
|
Settings settings;
|
||||||
settings.beginGroup("savedAttachmentData");
|
settings.beginGroup("savedAttachmentData");
|
||||||
|
@ -1916,8 +2376,11 @@ void MyAvatar::clearAvatarEntities() {
|
||||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||||
|
|
||||||
AvatarEntityMap avatarEntities = getAvatarEntityData();
|
QList<QUuid> avatarEntityIDs;
|
||||||
for (auto entityID : avatarEntities.keys()) {
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
|
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||||
|
});
|
||||||
|
for (const auto& entityID : avatarEntityIDs) {
|
||||||
entityTree->withWriteLock([&entityID, &entityTree] {
|
entityTree->withWriteLock([&entityID, &entityTree] {
|
||||||
// remove this entity first from the entity tree
|
// remove this entity first from the entity tree
|
||||||
entityTree->deleteEntity(entityID, true, true);
|
entityTree->deleteEntity(entityID, true, true);
|
||||||
|
@ -1932,10 +2395,12 @@ void MyAvatar::clearAvatarEntities() {
|
||||||
void MyAvatar::removeWearableAvatarEntities() {
|
void MyAvatar::removeWearableAvatarEntities() {
|
||||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||||
|
|
||||||
if (entityTree) {
|
if (entityTree) {
|
||||||
AvatarEntityMap avatarEntities = getAvatarEntityData();
|
QList<QUuid> avatarEntityIDs;
|
||||||
for (auto entityID : avatarEntities.keys()) {
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
|
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||||
|
});
|
||||||
|
for (const auto& entityID : avatarEntityIDs) {
|
||||||
auto entity = entityTree->findEntityByID(entityID);
|
auto entity = entityTree->findEntityByID(entityID);
|
||||||
if (entity && isWearableEntity(entity)) {
|
if (entity && isWearableEntity(entity)) {
|
||||||
entityTree->withWriteLock([&entityID, &entityTree] {
|
entityTree->withWriteLock([&entityID, &entityTree] {
|
||||||
|
@ -1952,13 +2417,16 @@ void MyAvatar::removeWearableAvatarEntities() {
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantList MyAvatar::getAvatarEntitiesVariant() {
|
QVariantList MyAvatar::getAvatarEntitiesVariant() {
|
||||||
|
// NOTE: this method is NOT efficient
|
||||||
QVariantList avatarEntitiesData;
|
QVariantList avatarEntitiesData;
|
||||||
QScriptEngine scriptEngine;
|
|
||||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||||
if (entityTree) {
|
if (entityTree) {
|
||||||
AvatarEntityMap avatarEntities = getAvatarEntityData();
|
QList<QUuid> avatarEntityIDs;
|
||||||
for (auto entityID : avatarEntities.keys()) {
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
|
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||||
|
});
|
||||||
|
for (const auto& entityID : avatarEntityIDs) {
|
||||||
auto entity = entityTree->findEntityByID(entityID);
|
auto entity = entityTree->findEntityByID(entityID);
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -1969,7 +2437,7 @@ QVariantList MyAvatar::getAvatarEntitiesVariant() {
|
||||||
desiredProperties += PROP_LOCAL_POSITION;
|
desiredProperties += PROP_LOCAL_POSITION;
|
||||||
desiredProperties += PROP_LOCAL_ROTATION;
|
desiredProperties += PROP_LOCAL_ROTATION;
|
||||||
EntityItemProperties entityProperties = entity->getProperties(desiredProperties);
|
EntityItemProperties entityProperties = entity->getProperties(desiredProperties);
|
||||||
QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(&scriptEngine, entityProperties);
|
QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(_myScriptEngine, entityProperties);
|
||||||
avatarEntityData["properties"] = scriptProperties.toVariant();
|
avatarEntityData["properties"] = scriptProperties.toVariant();
|
||||||
avatarEntitiesData.append(QVariant(avatarEntityData));
|
avatarEntitiesData.append(QVariant(avatarEntityData));
|
||||||
}
|
}
|
||||||
|
@ -2358,17 +2826,17 @@ void MyAvatar::setAttachmentData(const QVector<AttachmentData>& attachmentData)
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<AttachmentData> MyAvatar::getAttachmentData() const {
|
QVector<AttachmentData> MyAvatar::getAttachmentData() const {
|
||||||
QVector<AttachmentData> avatarData;
|
QVector<AttachmentData> attachmentData;
|
||||||
auto avatarEntities = getAvatarEntityData();
|
QList<QUuid> avatarEntityIDs;
|
||||||
AvatarEntityMap::const_iterator dataItr = avatarEntities.begin();
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
while (dataItr != avatarEntities.end()) {
|
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||||
QUuid entityID = dataItr.key();
|
});
|
||||||
|
for (const auto& entityID : avatarEntityIDs) {
|
||||||
auto properties = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
|
auto properties = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
|
||||||
AttachmentData data = entityPropertiesToAttachmentData(properties);
|
AttachmentData data = entityPropertiesToAttachmentData(properties);
|
||||||
avatarData.append(data);
|
attachmentData.append(data);
|
||||||
dataItr++;
|
|
||||||
}
|
}
|
||||||
return avatarData;
|
return attachmentData;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantList MyAvatar::getAttachmentsVariant() const {
|
QVariantList MyAvatar::getAttachmentsVariant() const {
|
||||||
|
@ -2397,16 +2865,16 @@ void MyAvatar::setAttachmentsVariant(const QVariantList& variant) {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MyAvatar::findAvatarEntity(const QString& modelURL, const QString& jointName, QUuid& entityID) {
|
bool MyAvatar::findAvatarEntity(const QString& modelURL, const QString& jointName, QUuid& entityID) {
|
||||||
auto avatarEntities = getAvatarEntityData();
|
QList<QUuid> avatarEntityIDs;
|
||||||
AvatarEntityMap::const_iterator dataItr = avatarEntities.begin();
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
while (dataItr != avatarEntities.end()) {
|
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||||
entityID = dataItr.key();
|
});
|
||||||
|
for (const auto& entityID : avatarEntityIDs) {
|
||||||
auto props = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
|
auto props = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
|
||||||
if (props.getModelURL() == modelURL &&
|
if (props.getModelURL() == modelURL &&
|
||||||
(jointName.isEmpty() || props.getParentJointIndex() == getJointIndex(jointName))) {
|
(jointName.isEmpty() || props.getParentJointIndex() == getJointIndex(jointName))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
dataItr++;
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -4832,6 +5300,6 @@ void MyAvatar::releaseGrab(const QUuid& grabID) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<MyAvatar> MyAvatar::getSharedMe() {
|
std::shared_ptr<MyAvatar> MyAvatar::getMyAvatarSharedPointer() {
|
||||||
return DependencyManager::get<AvatarManager>()->getMyAvatar();
|
return DependencyManager::get<AvatarManager>()->getMyAvatar();
|
||||||
}
|
}
|
||||||
|
|
|
@ -578,9 +578,11 @@ public:
|
||||||
float getHMDRollControlRate() const { return _hmdRollControlRate; }
|
float getHMDRollControlRate() const { return _hmdRollControlRate; }
|
||||||
|
|
||||||
// get/set avatar data
|
// get/set avatar data
|
||||||
void resizeAvatarEntitySettingHandles(unsigned int avatarEntityIndex);
|
void resizeAvatarEntitySettingHandles(uint32_t maxIndex);
|
||||||
void saveData();
|
void saveData();
|
||||||
|
void saveAvatarEntityDataToSettings();
|
||||||
void loadData();
|
void loadData();
|
||||||
|
void loadAvatarEntityDataFromSettings();
|
||||||
|
|
||||||
void saveAttachmentData(const AttachmentData& attachment) const;
|
void saveAttachmentData(const AttachmentData& attachment) const;
|
||||||
AttachmentData loadAttachmentData(const QUrl& modelURL, const QString& jointName = QString()) const;
|
AttachmentData loadAttachmentData(const QUrl& modelURL, const QString& jointName = QString()) const;
|
||||||
|
@ -1187,6 +1189,7 @@ public:
|
||||||
virtual void setAttachmentsVariant(const QVariantList& variant) override;
|
virtual void setAttachmentsVariant(const QVariantList& variant) override;
|
||||||
|
|
||||||
glm::vec3 getNextPosition() { return _goToPending ? _goToPosition : getWorldPosition(); }
|
glm::vec3 getNextPosition() { return _goToPending ? _goToPosition : getWorldPosition(); }
|
||||||
|
void prepareAvatarEntityDataForReload();
|
||||||
|
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* Create a new grab.
|
* Create a new grab.
|
||||||
|
@ -1207,7 +1210,12 @@ public:
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void releaseGrab(const QUuid& grabID);
|
Q_INVOKABLE void releaseGrab(const QUuid& grabID);
|
||||||
|
|
||||||
std::shared_ptr<MyAvatar> getSharedMe();
|
std::shared_ptr<MyAvatar> getMyAvatarSharedPointer();
|
||||||
|
|
||||||
|
AvatarEntityMap getAvatarEntityData() const override;
|
||||||
|
void setAvatarEntityData(const AvatarEntityMap& avatarEntityData) override;
|
||||||
|
void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) override;
|
||||||
|
void avatarEntityDataToJson(QJsonObject& root) const override;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
|
||||||
|
@ -1407,6 +1415,10 @@ public slots:
|
||||||
*/
|
*/
|
||||||
bool getEnableMeshVisible() const override;
|
bool getEnableMeshVisible() const override;
|
||||||
|
|
||||||
|
void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload) override;
|
||||||
|
void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true) override;
|
||||||
|
void sanitizeAvatarEntityProperties(EntityItemProperties& properties) const;
|
||||||
|
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* Set whether or not your avatar mesh is visible.
|
* Set whether or not your avatar mesh is visible.
|
||||||
* @function MyAvatar.setEnableMeshVisible
|
* @function MyAvatar.setEnableMeshVisible
|
||||||
|
@ -1606,23 +1618,24 @@ signals:
|
||||||
*/
|
*/
|
||||||
void disableHandTouchForIDChanged(const QUuid& entityID, bool disable);
|
void disableHandTouchForIDChanged(const QUuid& entityID, bool disable);
|
||||||
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void leaveDomain();
|
void leaveDomain();
|
||||||
void updateCollisionCapsuleCache();
|
void updateCollisionCapsuleCache();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void handleChangedAvatarEntityData();
|
||||||
virtual void beParentOfChild(SpatiallyNestablePointer newChild) const override;
|
virtual void beParentOfChild(SpatiallyNestablePointer newChild) const override;
|
||||||
virtual void forgetChild(SpatiallyNestablePointer newChild) const override;
|
virtual void forgetChild(SpatiallyNestablePointer newChild) const override;
|
||||||
virtual void recalculateChildCauterization() const override;
|
virtual void recalculateChildCauterization() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool updateStaleAvatarEntityBlobs() const;
|
||||||
|
|
||||||
bool requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& positionOut);
|
bool requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& positionOut);
|
||||||
|
|
||||||
virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) override;
|
virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) override;
|
||||||
|
|
||||||
void simulate(float deltaTime);
|
void simulate(float deltaTime, bool inView) override;
|
||||||
void updateFromTrackers(float deltaTime);
|
void updateFromTrackers(float deltaTime);
|
||||||
void saveAvatarUrl();
|
void saveAvatarUrl();
|
||||||
virtual void render(RenderArgs* renderArgs) override;
|
virtual void render(RenderArgs* renderArgs) override;
|
||||||
|
@ -1925,6 +1938,7 @@ private:
|
||||||
bool _haveReceivedHeightLimitsFromDomain { false };
|
bool _haveReceivedHeightLimitsFromDomain { false };
|
||||||
int _disableHandTouchCount { 0 };
|
int _disableHandTouchCount { 0 };
|
||||||
bool _skeletonModelLoaded { false };
|
bool _skeletonModelLoaded { false };
|
||||||
|
bool _reloadAvatarEntityDataFromSettings { true };
|
||||||
|
|
||||||
Setting::Handle<QString> _dominantHandSetting;
|
Setting::Handle<QString> _dominantHandSetting;
|
||||||
Setting::Handle<float> _headPitchSetting;
|
Setting::Handle<float> _headPitchSetting;
|
||||||
|
@ -1943,6 +1957,38 @@ private:
|
||||||
Setting::Handle<bool> _allowTeleportingSetting { "allowTeleporting", true };
|
Setting::Handle<bool> _allowTeleportingSetting { "allowTeleporting", true };
|
||||||
std::vector<Setting::Handle<QUuid>> _avatarEntityIDSettings;
|
std::vector<Setting::Handle<QUuid>> _avatarEntityIDSettings;
|
||||||
std::vector<Setting::Handle<QByteArray>> _avatarEntityDataSettings;
|
std::vector<Setting::Handle<QByteArray>> _avatarEntityDataSettings;
|
||||||
|
|
||||||
|
// AvatarEntities stuff:
|
||||||
|
// We cache the "map of unfortunately-formatted-binary-blobs" because they are expensive to compute
|
||||||
|
// Do not confuse these with AvatarData::_packedAvatarEntityData which are in wire-format.
|
||||||
|
mutable AvatarEntityMap _cachedAvatarEntityBlobs;
|
||||||
|
|
||||||
|
// We collect changes to AvatarEntities and then handle them all in one spot per frame: updateAvatarEntities().
|
||||||
|
// Basically this is a "transaction pattern" with an extra complication: these changes can come from two
|
||||||
|
// "directions" and the "authoritative source" of each direction is different, so maintain two distinct sets of
|
||||||
|
// transaction lists;
|
||||||
|
//
|
||||||
|
// The _entitiesToDelete/Add/Update lists are for changes whose "authoritative sources" are already
|
||||||
|
// correctly stored in _cachedAvatarEntityBlobs. These come from loadAvatarEntityDataFromSettings() and
|
||||||
|
// setAvatarEntityData(). These changes need to be extracted from _cachedAvatarEntityBlobs and applied to
|
||||||
|
// real EntityItems.
|
||||||
|
std::vector<QUuid> _entitiesToDelete;
|
||||||
|
std::vector<QUuid> _entitiesToAdd;
|
||||||
|
std::vector<QUuid> _entitiesToUpdate;
|
||||||
|
//
|
||||||
|
// The _cachedAvatarEntityBlobsToDelete/Add/Update lists are for changes whose "authoritative sources" are
|
||||||
|
// already reflected in real EntityItems. These changes need to be propagated to _cachedAvatarEntityBlobs
|
||||||
|
// and eventually to settings.
|
||||||
|
std::vector<QUuid> _cachedAvatarEntityBlobsToDelete;
|
||||||
|
std::vector<QUuid> _cachedAvatarEntityBlobsToAddOrUpdate;
|
||||||
|
std::vector<QUuid> _cachedAvatarEntityBlobUpdatesToSkip;
|
||||||
|
//
|
||||||
|
// Also these lists for tracking delayed changes to blobs and Settings
|
||||||
|
mutable std::set<QUuid> _staleCachedAvatarEntityBlobs;
|
||||||
|
//
|
||||||
|
// keep a ScriptEngine around so we don't have to instantiate on the fly (these are very slow to create/delete)
|
||||||
|
QScriptEngine* _myScriptEngine { nullptr };
|
||||||
|
bool _needToSaveAvatarEntitySettings { false };
|
||||||
};
|
};
|
||||||
|
|
||||||
QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode);
|
QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode);
|
||||||
|
|
|
@ -368,7 +368,7 @@ btCollisionShape* MyCharacterController::createDetailedCollisionShapeForJoint(in
|
||||||
DetailedMotionState* MyCharacterController::createDetailedMotionStateForJoint(int jointIndex) {
|
DetailedMotionState* MyCharacterController::createDetailedMotionStateForJoint(int jointIndex) {
|
||||||
auto shape = createDetailedCollisionShapeForJoint(jointIndex);
|
auto shape = createDetailedCollisionShapeForJoint(jointIndex);
|
||||||
if (shape) {
|
if (shape) {
|
||||||
DetailedMotionState* motionState = new DetailedMotionState(_avatar->getSharedMe(), shape, jointIndex);
|
DetailedMotionState* motionState = new DetailedMotionState(_avatar->getMyAvatarSharedPointer(), shape, jointIndex);
|
||||||
motionState->setMass(_avatar->computeMass());
|
motionState->setMass(_avatar->computeMass());
|
||||||
return motionState;
|
return motionState;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,19 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
#include "OtherAvatar.h"
|
#include "OtherAvatar.h"
|
||||||
#include "Application.h"
|
|
||||||
|
|
||||||
|
#include <glm/gtx/norm.hpp>
|
||||||
|
#include <glm/gtx/vector_angle.hpp>
|
||||||
|
|
||||||
|
#include <AvatarLogging.h>
|
||||||
|
|
||||||
|
#include "Application.h"
|
||||||
#include "AvatarMotionState.h"
|
#include "AvatarMotionState.h"
|
||||||
#include "DetailedMotionState.h"
|
#include "DetailedMotionState.h"
|
||||||
|
|
||||||
|
const float DISPLAYNAME_FADE_TIME = 0.5f;
|
||||||
|
const float DISPLAYNAME_FADE_FACTOR = pow(0.01f, 1.0f / DISPLAYNAME_FADE_TIME);
|
||||||
|
|
||||||
static glm::u8vec3 getLoadingOrbColor(Avatar::LoadingStatus loadingStatus) {
|
static glm::u8vec3 getLoadingOrbColor(Avatar::LoadingStatus loadingStatus) {
|
||||||
|
|
||||||
const glm::u8vec3 NO_MODEL_COLOR(0xe3, 0xe3, 0xe3);
|
const glm::u8vec3 NO_MODEL_COLOR(0xe3, 0xe3, 0xe3);
|
||||||
|
@ -261,7 +269,7 @@ void OtherAvatar::updateCollisionGroup(bool myAvatarCollide) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void OtherAvatar::createDetailedMotionStates(const std::shared_ptr<OtherAvatar>& avatar){
|
void OtherAvatar::createDetailedMotionStates(const std::shared_ptr<OtherAvatar>& avatar) {
|
||||||
auto& detailedMotionStates = getDetailedMotionStates();
|
auto& detailedMotionStates = getDetailedMotionStates();
|
||||||
if (_bodyLOD == BodyLOD::Sphere) {
|
if (_bodyLOD == BodyLOD::Sphere) {
|
||||||
auto dMotionState = createMotionState(avatar, -1);
|
auto dMotionState = createMotionState(avatar, -1);
|
||||||
|
@ -278,3 +286,292 @@ void OtherAvatar::createDetailedMotionStates(const std::shared_ptr<OtherAvatar>&
|
||||||
}
|
}
|
||||||
_needsReinsertion = false;
|
_needsReinsertion = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OtherAvatar::simulate(float deltaTime, bool inView) {
|
||||||
|
PROFILE_RANGE(simulation, "simulate");
|
||||||
|
|
||||||
|
_globalPosition = _transit.isActive() ? _transit.getCurrentPosition() : _serverPosition;
|
||||||
|
if (!hasParent()) {
|
||||||
|
setLocalPosition(_globalPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
_simulationRate.increment();
|
||||||
|
if (inView) {
|
||||||
|
_simulationInViewRate.increment();
|
||||||
|
}
|
||||||
|
|
||||||
|
PerformanceTimer perfTimer("simulate");
|
||||||
|
{
|
||||||
|
PROFILE_RANGE(simulation, "updateJoints");
|
||||||
|
if (inView) {
|
||||||
|
Head* head = getHead();
|
||||||
|
if (_hasNewJointData || _transit.isActive()) {
|
||||||
|
_skeletonModel->getRig().copyJointsFromJointData(_jointData);
|
||||||
|
glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset());
|
||||||
|
_skeletonModel->getRig().computeExternalPoses(rootTransform);
|
||||||
|
_jointDataSimulationRate.increment();
|
||||||
|
|
||||||
|
_skeletonModel->simulate(deltaTime, true);
|
||||||
|
|
||||||
|
locationChanged(); // joints changed, so if there are any children, update them.
|
||||||
|
_hasNewJointData = false;
|
||||||
|
|
||||||
|
glm::vec3 headPosition = getWorldPosition();
|
||||||
|
if (!_skeletonModel->getHeadPosition(headPosition)) {
|
||||||
|
headPosition = getWorldPosition();
|
||||||
|
}
|
||||||
|
head->setPosition(headPosition);
|
||||||
|
}
|
||||||
|
head->setScale(getModelScale());
|
||||||
|
head->simulate(deltaTime);
|
||||||
|
relayJointDataToChildren();
|
||||||
|
} else {
|
||||||
|
// a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.
|
||||||
|
_skeletonModel->simulate(deltaTime, false);
|
||||||
|
}
|
||||||
|
_skeletonModelSimulationRate.increment();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update animation for display name fade in/out
|
||||||
|
if ( _displayNameTargetAlpha != _displayNameAlpha) {
|
||||||
|
// the alpha function is
|
||||||
|
// Fade out => alpha(t) = factor ^ t => alpha(t+dt) = alpha(t) * factor^(dt)
|
||||||
|
// Fade in => alpha(t) = 1 - factor^t => alpha(t+dt) = 1-(1-alpha(t))*coef^(dt)
|
||||||
|
// factor^(dt) = coef
|
||||||
|
float coef = pow(DISPLAYNAME_FADE_FACTOR, deltaTime);
|
||||||
|
if (_displayNameTargetAlpha < _displayNameAlpha) {
|
||||||
|
// Fading out
|
||||||
|
_displayNameAlpha *= coef;
|
||||||
|
} else {
|
||||||
|
// Fading in
|
||||||
|
_displayNameAlpha = 1.0f - (1.0f - _displayNameAlpha) * coef;
|
||||||
|
}
|
||||||
|
_displayNameAlpha = glm::abs(_displayNameAlpha - _displayNameTargetAlpha) < 0.01f ? _displayNameTargetAlpha : _displayNameAlpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
PROFILE_RANGE(simulation, "misc");
|
||||||
|
measureMotionDerivatives(deltaTime);
|
||||||
|
simulateAttachments(deltaTime);
|
||||||
|
updatePalms();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
PROFILE_RANGE(simulation, "entities");
|
||||||
|
handleChangedAvatarEntityData();
|
||||||
|
updateAttachedAvatarEntities();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
PROFILE_RANGE(simulation, "grabs");
|
||||||
|
updateGrabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFadingStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtherAvatar::handleChangedAvatarEntityData() {
|
||||||
|
PerformanceTimer perfTimer("attachments");
|
||||||
|
|
||||||
|
// AVATAR ENTITY UPDATE FLOW
|
||||||
|
// - if queueEditEntityMessage() sees "AvatarEntity" HostType it calls _myAvatar->storeAvatarEntityDataPayload()
|
||||||
|
// - storeAvatarEntityDataPayload() saves the payload and flags the trait instance for the entity as updated,
|
||||||
|
// - ClientTraitsHandler::sendChangedTraitsToMixea() sends the entity bytes to the mixer which relays them to other interfaces
|
||||||
|
// - AvatarHashMap::processBulkAvatarTraits() on other interfaces calls avatar->processTraitInstance()
|
||||||
|
// - AvatarData::processTraitInstance() calls storeAvatarEntityDataPayload(), which sets _avatarEntityDataChanged = true
|
||||||
|
// - (My)Avatar::simulate() calls handleChangedAvatarEntityData() every frame which checks _avatarEntityDataChanged
|
||||||
|
// and here we are...
|
||||||
|
|
||||||
|
// AVATAR ENTITY DELETE FLOW
|
||||||
|
// - EntityScriptingInterface::deleteEntity() calls _myAvatar->clearAvatarEntity() for deleted avatar entities
|
||||||
|
// - clearAvatarEntity() removes the avatar entity and flags the trait instance for the entity as deleted
|
||||||
|
// - ClientTraitsHandler::sendChangedTraitsToMixer() sends a deletion to the mixer which relays to other interfaces
|
||||||
|
// - AvatarHashMap::processBulkAvatarTraits() on other interfaces calls avatar->processDeletedTraitInstace()
|
||||||
|
// - AvatarData::processDeletedTraitInstance() calls clearAvatarEntity()
|
||||||
|
// - AvatarData::clearAvatarEntity() sets _avatarEntityDataChanged = true and adds the ID to the detached list
|
||||||
|
// - (My)Avatar::simulate() calls handleChangedAvatarEntityData() every frame which checks _avatarEntityDataChanged
|
||||||
|
// and here we are...
|
||||||
|
|
||||||
|
if (!_avatarEntityDataChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||||
|
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||||
|
if (!entityTree) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PackedAvatarEntityMap packedAvatarEntityData;
|
||||||
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
|
packedAvatarEntityData = _packedAvatarEntityData;
|
||||||
|
});
|
||||||
|
entityTree->withWriteLock([&] {
|
||||||
|
AvatarEntityMap::const_iterator dataItr = packedAvatarEntityData.begin();
|
||||||
|
while (dataItr != packedAvatarEntityData.end()) {
|
||||||
|
// compute hash of data. TODO? cache this?
|
||||||
|
QByteArray data = dataItr.value();
|
||||||
|
uint32_t newHash = qHash(data);
|
||||||
|
|
||||||
|
// check to see if we recognize this hash and whether it was already successfully processed
|
||||||
|
QUuid entityID = dataItr.key();
|
||||||
|
MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID);
|
||||||
|
if (stateItr != _avatarEntityDataHashes.end()) {
|
||||||
|
if (stateItr.value().success) {
|
||||||
|
if (newHash == stateItr.value().hash) {
|
||||||
|
// data hasn't changed --> nothing to do
|
||||||
|
++dataItr;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// NOTE: if the data was unsuccessful in producing an entity in the past
|
||||||
|
// we will try again just in case something changed (unlikely).
|
||||||
|
// Unfortunately constantly trying to build the entity for this data costs
|
||||||
|
// CPU cycles that we'd rather not spend.
|
||||||
|
// TODO? put a maximum number of tries on this?
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// sanity check data
|
||||||
|
QUuid id;
|
||||||
|
EntityTypes::EntityType type;
|
||||||
|
EntityTypes::extractEntityTypeAndID((unsigned char*)(data.data()), data.size(), type, id);
|
||||||
|
if (id != entityID || !EntityTypes::typeIsValid(type)) {
|
||||||
|
// skip for corrupt
|
||||||
|
++dataItr;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// remember this hash for the future
|
||||||
|
stateItr = _avatarEntityDataHashes.insert(entityID, AvatarEntityDataHash(newHash));
|
||||||
|
}
|
||||||
|
++dataItr;
|
||||||
|
|
||||||
|
EntityItemProperties properties;
|
||||||
|
int32_t bytesLeftToRead = data.size();
|
||||||
|
unsigned char* dataAt = (unsigned char*)(data.data());
|
||||||
|
if (!properties.constructFromBuffer(dataAt, bytesLeftToRead)) {
|
||||||
|
// properties are corrupt
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
properties.setEntityHostType(entity::HostType::AVATAR);
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: if this avatar entity is not attached to us, strip its entity script completely...
|
||||||
|
auto attachedScript = properties.getScript();
|
||||||
|
if (!isMyAvatar() && !attachedScript.isEmpty()) {
|
||||||
|
QString noScript;
|
||||||
|
properties.setScript(noScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto specifiedHref = properties.getHref();
|
||||||
|
if (!isMyAvatar() && !specifiedHref.isEmpty()) {
|
||||||
|
qCDebug(avatars) << "removing entity href from avatar attached entity:" << entityID << "old href:" << specifiedHref;
|
||||||
|
QString noHref;
|
||||||
|
properties.setHref(noHref);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When grabbing avatar entities, they are parented to the joint moving them, then when un-grabbed
|
||||||
|
// they go back to the default parent (null uuid). When un-gripped, others saw the entity disappear.
|
||||||
|
// The thinking here is the local position was noticed as changing, but not the parentID (since it is now
|
||||||
|
// back to the default), and the entity flew off somewhere. Marking all changed definitely fixes this,
|
||||||
|
// and seems safe (per Seth).
|
||||||
|
properties.markAllChanged();
|
||||||
|
|
||||||
|
// try to build the entity
|
||||||
|
EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID));
|
||||||
|
bool success = true;
|
||||||
|
if (entity) {
|
||||||
|
QUuid oldParentID = entity->getParentID();
|
||||||
|
if (entityTree->updateEntity(entityID, properties)) {
|
||||||
|
entity->updateLastEditedFromRemote();
|
||||||
|
} else {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
if (oldParentID != entity->getParentID()) {
|
||||||
|
if (entity->getParentID() == getID()) {
|
||||||
|
onAddAttachedAvatarEntity(entityID);
|
||||||
|
} else if (oldParentID == getID()) {
|
||||||
|
onRemoveAttachedAvatarEntity(entityID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entity = entityTree->addEntity(entityID, properties);
|
||||||
|
if (!entity) {
|
||||||
|
success = false;
|
||||||
|
} else if (entity->getParentID() == getID()) {
|
||||||
|
onAddAttachedAvatarEntity(entityID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stateItr.value().success = success;
|
||||||
|
}
|
||||||
|
|
||||||
|
AvatarEntityIDs recentlyRemovedAvatarEntities = getAndClearRecentlyRemovedIDs();
|
||||||
|
if (!recentlyRemovedAvatarEntities.empty()) {
|
||||||
|
// only lock this thread when absolutely necessary
|
||||||
|
AvatarEntityMap packedAvatarEntityData;
|
||||||
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
|
packedAvatarEntityData = _packedAvatarEntityData;
|
||||||
|
});
|
||||||
|
foreach (auto entityID, recentlyRemovedAvatarEntities) {
|
||||||
|
if (!packedAvatarEntityData.contains(entityID)) {
|
||||||
|
entityTree->deleteEntity(entityID, true, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move this outside of tree lock
|
||||||
|
// remove stale data hashes
|
||||||
|
foreach (auto entityID, recentlyRemovedAvatarEntities) {
|
||||||
|
MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID);
|
||||||
|
if (stateItr != _avatarEntityDataHashes.end()) {
|
||||||
|
_avatarEntityDataHashes.erase(stateItr);
|
||||||
|
}
|
||||||
|
onRemoveAttachedAvatarEntity(entityID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (packedAvatarEntityData.size() != _avatarEntityForRecording.size()) {
|
||||||
|
createRecordingIDs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setAvatarEntityDataChanged(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtherAvatar::onAddAttachedAvatarEntity(const QUuid& id) {
|
||||||
|
for (uint32_t i = 0; i < _attachedAvatarEntities.size(); ++i) {
|
||||||
|
if (_attachedAvatarEntities[i] == id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_attachedAvatarEntities.push_back(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtherAvatar::onRemoveAttachedAvatarEntity(const QUuid& id) {
|
||||||
|
for (uint32_t i = 0; i < _attachedAvatarEntities.size(); ++i) {
|
||||||
|
if (_attachedAvatarEntities[i] == id) {
|
||||||
|
if (i != _attachedAvatarEntities.size() - 1) {
|
||||||
|
_attachedAvatarEntities[i] = _attachedAvatarEntities.back();
|
||||||
|
}
|
||||||
|
_attachedAvatarEntities.pop_back();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OtherAvatar::updateAttachedAvatarEntities() {
|
||||||
|
if (!_attachedAvatarEntities.empty()) {
|
||||||
|
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||||
|
if (!treeRenderer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const QUuid& id : _attachedAvatarEntities) {
|
||||||
|
treeRenderer->onEntityChanged(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#define hifi_OtherAvatar_h
|
#define hifi_OtherAvatar_h
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <avatars-renderer/Avatar.h>
|
#include <avatars-renderer/Avatar.h>
|
||||||
#include <workload/Space.h>
|
#include <workload/Space.h>
|
||||||
|
@ -62,9 +63,17 @@ public:
|
||||||
|
|
||||||
void updateCollisionGroup(bool myAvatarCollide);
|
void updateCollisionGroup(bool myAvatarCollide);
|
||||||
|
|
||||||
|
void simulate(float deltaTime, bool inView) override;
|
||||||
|
|
||||||
friend AvatarManager;
|
friend AvatarManager;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void handleChangedAvatarEntityData();
|
||||||
|
void updateAttachedAvatarEntities();
|
||||||
|
void onAddAttachedAvatarEntity(const QUuid& id);
|
||||||
|
void onRemoveAttachedAvatarEntity(const QUuid& id);
|
||||||
|
|
||||||
|
std::vector<QUuid> _attachedAvatarEntities;
|
||||||
std::shared_ptr<Sphere3DOverlay> _otherAvatarOrbMeshPlaceholder { nullptr };
|
std::shared_ptr<Sphere3DOverlay> _otherAvatarOrbMeshPlaceholder { nullptr };
|
||||||
OverlayID _otherAvatarOrbMeshPlaceholderID { UNKNOWN_OVERLAY_ID };
|
OverlayID _otherAvatarOrbMeshPlaceholderID { UNKNOWN_OVERLAY_ID };
|
||||||
AvatarMotionState* _motionState { nullptr };
|
AvatarMotionState* _motionState { nullptr };
|
||||||
|
|
|
@ -43,7 +43,7 @@ bool MenuScriptingInterface::menuExists(const QString& menu) {
|
||||||
if (QThread::currentThread() == qApp->thread()) {
|
if (QThread::currentThread() == qApp->thread()) {
|
||||||
return Menu::getInstance()->menuExists(menu);
|
return Menu::getInstance()->menuExists(menu);
|
||||||
}
|
}
|
||||||
bool result;
|
bool result { false };
|
||||||
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuExists",
|
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuExists",
|
||||||
Q_RETURN_ARG(bool, result),
|
Q_RETURN_ARG(bool, result),
|
||||||
Q_ARG(const QString&, menu));
|
Q_ARG(const QString&, menu));
|
||||||
|
@ -86,7 +86,7 @@ bool MenuScriptingInterface::menuItemExists(const QString& menu, const QString&
|
||||||
if (QThread::currentThread() == qApp->thread()) {
|
if (QThread::currentThread() == qApp->thread()) {
|
||||||
return Menu::getInstance()->menuItemExists(menu, menuitem);
|
return Menu::getInstance()->menuItemExists(menu, menuitem);
|
||||||
}
|
}
|
||||||
bool result;
|
bool result { false };
|
||||||
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuItemExists",
|
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuItemExists",
|
||||||
Q_RETURN_ARG(bool, result),
|
Q_RETURN_ARG(bool, result),
|
||||||
Q_ARG(const QString&, menu),
|
Q_ARG(const QString&, menu),
|
||||||
|
@ -98,7 +98,7 @@ bool MenuScriptingInterface::isOptionChecked(const QString& menuOption) {
|
||||||
if (QThread::currentThread() == qApp->thread()) {
|
if (QThread::currentThread() == qApp->thread()) {
|
||||||
return Menu::getInstance()->isOptionChecked(menuOption);
|
return Menu::getInstance()->isOptionChecked(menuOption);
|
||||||
}
|
}
|
||||||
bool result;
|
bool result { false };
|
||||||
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isOptionChecked",
|
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isOptionChecked",
|
||||||
Q_RETURN_ARG(bool, result),
|
Q_RETURN_ARG(bool, result),
|
||||||
Q_ARG(const QString&, menuOption));
|
Q_ARG(const QString&, menuOption));
|
||||||
|
@ -115,7 +115,7 @@ bool MenuScriptingInterface::isMenuEnabled(const QString& menuOption) {
|
||||||
if (QThread::currentThread() == qApp->thread()) {
|
if (QThread::currentThread() == qApp->thread()) {
|
||||||
return Menu::getInstance()->isOptionChecked(menuOption);
|
return Menu::getInstance()->isOptionChecked(menuOption);
|
||||||
}
|
}
|
||||||
bool result;
|
bool result { false };
|
||||||
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isMenuEnabled",
|
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isMenuEnabled",
|
||||||
Q_RETURN_ARG(bool, result),
|
Q_RETURN_ARG(bool, result),
|
||||||
Q_ARG(const QString&, menuOption));
|
Q_ARG(const QString&, menuOption));
|
||||||
|
|
|
@ -27,6 +27,9 @@ Base3DOverlay::Base3DOverlay() :
|
||||||
_drawInFront(false),
|
_drawInFront(false),
|
||||||
_drawHUDLayer(false)
|
_drawHUDLayer(false)
|
||||||
{
|
{
|
||||||
|
// HACK: queryAACube stuff not actually relevant for 3DOverlays, and by setting _queryAACubeSet true here
|
||||||
|
// we can avoid incorrect evaluation for sending updates for entities with 3DOverlays children.
|
||||||
|
_queryAACubeSet = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) :
|
Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) :
|
||||||
|
@ -41,6 +44,9 @@ Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) :
|
||||||
_isVisibleInSecondaryCamera(base3DOverlay->_isVisibleInSecondaryCamera)
|
_isVisibleInSecondaryCamera(base3DOverlay->_isVisibleInSecondaryCamera)
|
||||||
{
|
{
|
||||||
setTransform(base3DOverlay->getTransform());
|
setTransform(base3DOverlay->getTransform());
|
||||||
|
// HACK: queryAACube stuff not actually relevant for 3DOverlays, and by setting _queryAACubeSet true here
|
||||||
|
// we can avoid incorrect evaluation for sending updates for entities with 3DOverlays children.
|
||||||
|
_queryAACubeSet = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap convertOverlayLocationFromScriptSemantics(const QVariantMap& properties, bool scalesWithParent) {
|
QVariantMap convertOverlayLocationFromScriptSemantics(const QVariantMap& properties, bool scalesWithParent) {
|
||||||
|
@ -209,6 +215,7 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) {
|
||||||
transaction.updateItem(itemID);
|
transaction.updateItem(itemID);
|
||||||
scene->enqueueTransaction(transaction);
|
scene->enqueueTransaction(transaction);
|
||||||
}
|
}
|
||||||
|
_queryAACubeSet = true; // HACK: just in case some SpatiallyNestable code accidentally set it false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ public:
|
||||||
Base3DOverlay(const Base3DOverlay* base3DOverlay);
|
Base3DOverlay(const Base3DOverlay* base3DOverlay);
|
||||||
|
|
||||||
void setVisible(bool visible) override;
|
void setVisible(bool visible) override;
|
||||||
|
bool queryAACubeNeedsUpdate() const override { return false; } // HACK: queryAACube not relevant for Overlays
|
||||||
|
|
||||||
virtual OverlayID getOverlayID() const override { return OverlayID(getID().toString()); }
|
virtual OverlayID getOverlayID() const override { return OverlayID(getID().toString()); }
|
||||||
void setOverlayID(OverlayID overlayID) override { setID(overlayID); }
|
void setOverlayID(OverlayID overlayID) override { setID(overlayID); }
|
||||||
|
|
|
@ -109,9 +109,6 @@ void AnimClip::copyFromNetworkAnim() {
|
||||||
jointMap.reserve(animJointCount);
|
jointMap.reserve(animJointCount);
|
||||||
for (int i = 0; i < animJointCount; i++) {
|
for (int i = 0; i < animJointCount; i++) {
|
||||||
int skeletonJoint = _skeleton->nameToJointIndex(animSkeleton.getJointName(i));
|
int skeletonJoint = _skeleton->nameToJointIndex(animSkeleton.getJointName(i));
|
||||||
if (skeletonJoint == -1) {
|
|
||||||
qCWarning(animation) << "animation contains joint =" << animSkeleton.getJointName(i) << " which is not in the skeleton";
|
|
||||||
}
|
|
||||||
jointMap.push_back(skeletonJoint);
|
jointMap.push_back(skeletonJoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -425,7 +425,6 @@ int Rig::indexOfJoint(const QString& jointName) const {
|
||||||
|
|
||||||
// This is a content error, so we should issue a warning.
|
// This is a content error, so we should issue a warning.
|
||||||
if (result < 0 && _jointNameWarningCount < MAX_JOINT_NAME_WARNING_COUNT) {
|
if (result < 0 && _jointNameWarningCount < MAX_JOINT_NAME_WARNING_COUNT) {
|
||||||
qCWarning(animation) << "Rig: Missing joint" << jointName << "in avatar model";
|
|
||||||
_jointNameWarningCount++;
|
_jointNameWarningCount++;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -1333,8 +1333,12 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
|
||||||
|
|
||||||
} else if (injector->isStereo()) {
|
} else if (injector->isStereo()) {
|
||||||
|
|
||||||
|
// calculate distance, gain
|
||||||
|
glm::vec3 relativePosition = injector->getPosition() - _positionGetter();
|
||||||
|
float distance = glm::max(glm::length(relativePosition), EPSILON);
|
||||||
|
float gain = gainForSource(distance, injector->getVolume());
|
||||||
|
|
||||||
// stereo gets directly mixed into mixBuffer
|
// stereo gets directly mixed into mixBuffer
|
||||||
float gain = injector->getVolume();
|
|
||||||
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) {
|
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) {
|
||||||
mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain;
|
mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain;
|
||||||
}
|
}
|
||||||
|
|
|
@ -695,7 +695,7 @@ static void ifft_radix8_first(complex_t* x, complex_t* y, int n, int p) {
|
||||||
// n >= 4
|
// n >= 4
|
||||||
static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
||||||
|
|
||||||
size_t t = n/4;
|
int t = n/4;
|
||||||
assert(t >= 1);
|
assert(t >= 1);
|
||||||
|
|
||||||
// NOTE: x[n/2].re is packed into x[0].im
|
// NOTE: x[n/2].re is packed into x[0].im
|
||||||
|
@ -707,7 +707,7 @@ static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
||||||
complex_t* xp0 = &x[1];
|
complex_t* xp0 = &x[1];
|
||||||
complex_t* xp1 = &x[n/2 - 1];
|
complex_t* xp1 = &x[n/2 - 1];
|
||||||
|
|
||||||
for (size_t i = 0; i < t; i++) {
|
for (int i = 0; i < t; i++) {
|
||||||
|
|
||||||
float ar = xp0[i].re;
|
float ar = xp0[i].re;
|
||||||
float ai = xp0[i].im;
|
float ai = xp0[i].im;
|
||||||
|
@ -743,7 +743,7 @@ static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
||||||
// n >= 4
|
// n >= 4
|
||||||
static void rifft_pre(complex_t* x, const complex_t* w, int n) {
|
static void rifft_pre(complex_t* x, const complex_t* w, int n) {
|
||||||
|
|
||||||
size_t t = n/4;
|
int t = n/4;
|
||||||
assert(t >= 1);
|
assert(t >= 1);
|
||||||
|
|
||||||
// NOTE: x[n/2].re is packed into x[0].im
|
// NOTE: x[n/2].re is packed into x[0].im
|
||||||
|
@ -755,7 +755,7 @@ static void rifft_pre(complex_t* x, const complex_t* w, int n) {
|
||||||
complex_t* xp0 = &x[1];
|
complex_t* xp0 = &x[1];
|
||||||
complex_t* xp1 = &x[n/2 - 1];
|
complex_t* xp1 = &x[n/2 - 1];
|
||||||
|
|
||||||
for (size_t i = 0; i < t; i++) {
|
for (int i = 0; i < t; i++) {
|
||||||
|
|
||||||
float ar = xp0[i].re;
|
float ar = xp0[i].re;
|
||||||
float ai = xp0[i].im;
|
float ai = xp0[i].im;
|
||||||
|
|
|
@ -973,8 +973,8 @@ FORCEINLINE static void ifft_radix8_first(complex_t* x, complex_t* y, int n, int
|
||||||
// n >= 32
|
// n >= 32
|
||||||
static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
||||||
|
|
||||||
size_t t = n/4;
|
int t = n/4;
|
||||||
assert(n/4 >= 8); // SIMD8
|
assert(t >= 8); // SIMD8
|
||||||
|
|
||||||
// NOTE: x[n/2].re is packed into x[0].im
|
// NOTE: x[n/2].re is packed into x[0].im
|
||||||
float tr = x[0].re;
|
float tr = x[0].re;
|
||||||
|
@ -985,7 +985,7 @@ static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
||||||
complex_t* xp0 = &x[1];
|
complex_t* xp0 = &x[1];
|
||||||
complex_t* xp1 = &x[n/2 - 8];
|
complex_t* xp1 = &x[n/2 - 8];
|
||||||
|
|
||||||
for (size_t i = 0; i < t; i += 8) {
|
for (int i = 0; i < t; i += 8) {
|
||||||
|
|
||||||
__m256 z0 = _mm256_loadu_ps(&xp0[i+0].re);
|
__m256 z0 = _mm256_loadu_ps(&xp0[i+0].re);
|
||||||
__m256 z1 = _mm256_loadu_ps(&xp0[i+4].re);
|
__m256 z1 = _mm256_loadu_ps(&xp0[i+4].re);
|
||||||
|
@ -1033,8 +1033,8 @@ static void rfft_post(complex_t* x, const complex_t* w, int n) {
|
||||||
// n >= 32
|
// n >= 32
|
||||||
static void rifft_pre(complex_t* x, const complex_t* w, int n) {
|
static void rifft_pre(complex_t* x, const complex_t* w, int n) {
|
||||||
|
|
||||||
size_t t = n/4;
|
int t = n/4;
|
||||||
assert(n/4 >= 8); // SIMD8
|
assert(t >= 8); // SIMD8
|
||||||
|
|
||||||
// NOTE: x[n/2].re is packed into x[0].im
|
// NOTE: x[n/2].re is packed into x[0].im
|
||||||
float tr = x[0].re;
|
float tr = x[0].re;
|
||||||
|
@ -1045,7 +1045,7 @@ static void rifft_pre(complex_t* x, const complex_t* w, int n) {
|
||||||
complex_t* xp0 = &x[1];
|
complex_t* xp0 = &x[1];
|
||||||
complex_t* xp1 = &x[n/2 - 8];
|
complex_t* xp1 = &x[n/2 - 8];
|
||||||
|
|
||||||
for (size_t i = 0; i < t; i += 8) {
|
for (int i = 0; i < t; i += 8) {
|
||||||
|
|
||||||
__m256 z0 = _mm256_loadu_ps(&xp0[i+0].re);
|
__m256 z0 = _mm256_loadu_ps(&xp0[i+0].re);
|
||||||
__m256 z1 = _mm256_loadu_ps(&xp0[i+4].re);
|
__m256 z1 = _mm256_loadu_ps(&xp0[i+4].re);
|
||||||
|
|
|
@ -310,175 +310,16 @@ void Avatar::setAvatarEntityDataChanged(bool value) {
|
||||||
_avatarEntityDataHashes.clear();
|
_avatarEntityDataHashes.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Avatar::updateAvatarEntities() {
|
|
||||||
PerformanceTimer perfTimer("attachments");
|
|
||||||
|
|
||||||
// AVATAR ENTITY UPDATE FLOW
|
|
||||||
// - if queueEditEntityMessage sees avatarEntity flag it does _myAvatar->updateAvatarEntity()
|
|
||||||
// - updateAvatarEntity saves the bytes and flags the trait instance for the entity as updated
|
|
||||||
// - ClientTraitsHandler::sendChangedTraitsToMixer sends the entity bytes to the mixer which relays them to other interfaces
|
|
||||||
// - AvatarHashMap::processBulkAvatarTraits on other interfaces calls avatar->processTraitInstace
|
|
||||||
// - AvatarData::processTraitInstance calls updateAvatarEntity, which sets _avatarEntityDataChanged = true
|
|
||||||
// - (My)Avatar::simulate notices _avatarEntityDataChanged and here we are...
|
|
||||||
|
|
||||||
// AVATAR ENTITY DELETE FLOW
|
|
||||||
// - EntityScriptingInterface::deleteEntity calls _myAvatar->clearAvatarEntity() for deleted avatar entities
|
|
||||||
// - clearAvatarEntity removes the avatar entity and flags the trait instance for the entity as deleted
|
|
||||||
// - ClientTraitsHandler::sendChangedTraitsToMixer sends a deletion to the mixer which relays to other interfaces
|
|
||||||
// - AvatarHashMap::processBulkAvatarTraits on other interfaces calls avatar->processDeletedTraitInstace
|
|
||||||
// - AvatarData::processDeletedTraitInstance calls clearAvatarEntity
|
|
||||||
// - AvatarData::clearAvatarEntity sets _avatarEntityDataChanged = true and adds the ID to the detached list
|
|
||||||
// - Avatar::simulate notices _avatarEntityDataChanged and here we are...
|
|
||||||
|
|
||||||
if (!_avatarEntityDataChanged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getID().isNull() ||
|
|
||||||
getID() == AVATAR_SELF_ID ||
|
|
||||||
DependencyManager::get<NodeList>()->getSessionUUID() == QUuid()) {
|
|
||||||
// wait until MyAvatar and this Node gets an ID before doing this. Otherwise, various things go wrong --
|
|
||||||
// things get their parent fixed up from AVATAR_SELF_ID to a null uuid which means "no parent".
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
|
||||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
|
||||||
if (!entityTree) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QScriptEngine scriptEngine;
|
|
||||||
entityTree->withWriteLock([&] {
|
|
||||||
AvatarEntityMap avatarEntities = getAvatarEntityData();
|
|
||||||
AvatarEntityMap::const_iterator dataItr = avatarEntities.begin();
|
|
||||||
while (dataItr != avatarEntities.end()) {
|
|
||||||
// compute hash of data. TODO? cache this?
|
|
||||||
QByteArray data = dataItr.value();
|
|
||||||
uint32_t newHash = qHash(data);
|
|
||||||
|
|
||||||
// check to see if we recognize this hash and whether it was already successfully processed
|
|
||||||
QUuid entityID = dataItr.key();
|
|
||||||
MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID);
|
|
||||||
if (stateItr != _avatarEntityDataHashes.end()) {
|
|
||||||
if (stateItr.value().success) {
|
|
||||||
if (newHash == stateItr.value().hash) {
|
|
||||||
// data hasn't changed --> nothing to do
|
|
||||||
++dataItr;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// NOTE: if the data was unsuccessful in producing an entity in the past
|
|
||||||
// we will try again just in case something changed (unlikely).
|
|
||||||
// Unfortunately constantly trying to build the entity for this data costs
|
|
||||||
// CPU cycles that we'd rather not spend.
|
|
||||||
// TODO? put a maximum number of tries on this?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// remember this hash for the future
|
|
||||||
stateItr = _avatarEntityDataHashes.insert(entityID, AvatarEntityDataHash(newHash));
|
|
||||||
}
|
|
||||||
++dataItr;
|
|
||||||
|
|
||||||
// see EntityEditPacketSender::queueEditEntityMessage for the other end of this. unpack properties
|
|
||||||
// and either add or update the entity.
|
|
||||||
QJsonDocument jsonProperties = QJsonDocument::fromBinaryData(data);
|
|
||||||
if (!jsonProperties.isObject()) {
|
|
||||||
qCDebug(avatars_renderer) << "got bad avatarEntity json" << QString(data.toHex());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
QVariant variantProperties = jsonProperties.toVariant();
|
|
||||||
QVariantMap asMap = variantProperties.toMap();
|
|
||||||
QScriptValue scriptProperties = variantMapToScriptValue(asMap, scriptEngine);
|
|
||||||
EntityItemProperties properties;
|
|
||||||
EntityItemPropertiesFromScriptValueIgnoreReadOnly(scriptProperties, properties);
|
|
||||||
properties.setEntityHostType(entity::HostType::AVATAR);
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: if this avatar entity is not attached to us, strip its entity script completely...
|
|
||||||
auto attachedScript = properties.getScript();
|
|
||||||
if (!isMyAvatar() && !attachedScript.isEmpty()) {
|
|
||||||
QString noScript;
|
|
||||||
properties.setScript(noScript);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto specifiedHref = properties.getHref();
|
|
||||||
if (!isMyAvatar() && !specifiedHref.isEmpty()) {
|
|
||||||
qCDebug(avatars_renderer) << "removing entity href from avatar attached entity:" << entityID << "old href:" << specifiedHref;
|
|
||||||
QString noHref;
|
|
||||||
properties.setHref(noHref);
|
|
||||||
}
|
|
||||||
|
|
||||||
// When grabbing avatar entities, they are parented to the joint moving them, then when un-grabbed
|
|
||||||
// they go back to the default parent (null uuid). When un-gripped, others saw the entity disappear.
|
|
||||||
// The thinking here is the local position was noticed as changing, but not the parentID (since it is now
|
|
||||||
// back to the default), and the entity flew off somewhere. Marking all changed definitely fixes this,
|
|
||||||
// and seems safe (per Seth).
|
|
||||||
properties.markAllChanged();
|
|
||||||
|
|
||||||
// try to build the entity
|
|
||||||
EntityItemPointer entity = entityTree->findEntityByEntityItemID(EntityItemID(entityID));
|
|
||||||
bool success = true;
|
|
||||||
if (entity) {
|
|
||||||
if (entityTree->updateEntity(entityID, properties)) {
|
|
||||||
entity->updateLastEditedFromRemote();
|
|
||||||
} else {
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
entity = entityTree->addEntity(entityID, properties);
|
|
||||||
if (!entity) {
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stateItr.value().success = success;
|
|
||||||
}
|
|
||||||
|
|
||||||
AvatarEntityIDs recentlyDetachedAvatarEntities = getAndClearRecentlyDetachedIDs();
|
|
||||||
if (!recentlyDetachedAvatarEntities.empty()) {
|
|
||||||
// only lock this thread when absolutely necessary
|
|
||||||
AvatarEntityMap avatarEntityData;
|
|
||||||
_avatarEntitiesLock.withReadLock([&] {
|
|
||||||
avatarEntityData = _avatarEntityData;
|
|
||||||
});
|
|
||||||
foreach (auto entityID, recentlyDetachedAvatarEntities) {
|
|
||||||
if (!avatarEntityData.contains(entityID)) {
|
|
||||||
entityTree->deleteEntity(entityID, true, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove stale data hashes
|
|
||||||
foreach (auto entityID, recentlyDetachedAvatarEntities) {
|
|
||||||
MapOfAvatarEntityDataHashes::iterator stateItr = _avatarEntityDataHashes.find(entityID);
|
|
||||||
if (stateItr != _avatarEntityDataHashes.end()) {
|
|
||||||
_avatarEntityDataHashes.erase(stateItr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (avatarEntities.size() != _avatarEntityForRecording.size()) {
|
|
||||||
createRecordingIDs();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setAvatarEntityDataChanged(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Avatar::removeAvatarEntitiesFromTree() {
|
void Avatar::removeAvatarEntitiesFromTree() {
|
||||||
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
|
||||||
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
|
||||||
if (entityTree) {
|
if (entityTree) {
|
||||||
|
QList<QUuid> avatarEntityIDs;
|
||||||
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
|
avatarEntityIDs = _packedAvatarEntityData.keys();
|
||||||
|
});
|
||||||
entityTree->withWriteLock([&] {
|
entityTree->withWriteLock([&] {
|
||||||
AvatarEntityMap avatarEntities = getAvatarEntityData();
|
for (const auto& entityID : avatarEntityIDs) {
|
||||||
for (auto entityID : avatarEntities.keys()) {
|
|
||||||
entityTree->deleteEntity(entityID, true, true);
|
entityTree->deleteEntity(entityID, true, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -653,87 +494,6 @@ void Avatar::relayJointDataToChildren() {
|
||||||
_reconstructSoftEntitiesJointMap = false;
|
_reconstructSoftEntitiesJointMap = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Avatar::simulate(float deltaTime, bool inView) {
|
|
||||||
PROFILE_RANGE(simulation, "simulate");
|
|
||||||
|
|
||||||
_globalPosition = _transit.isActive() ? _transit.getCurrentPosition() : _serverPosition;
|
|
||||||
if (!hasParent()) {
|
|
||||||
setLocalPosition(_globalPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
_simulationRate.increment();
|
|
||||||
if (inView) {
|
|
||||||
_simulationInViewRate.increment();
|
|
||||||
}
|
|
||||||
|
|
||||||
PerformanceTimer perfTimer("simulate");
|
|
||||||
{
|
|
||||||
PROFILE_RANGE(simulation, "updateJoints");
|
|
||||||
if (inView) {
|
|
||||||
Head* head = getHead();
|
|
||||||
if (_hasNewJointData || _transit.isActive()) {
|
|
||||||
_skeletonModel->getRig().copyJointsFromJointData(_jointData);
|
|
||||||
glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset());
|
|
||||||
_skeletonModel->getRig().computeExternalPoses(rootTransform);
|
|
||||||
_jointDataSimulationRate.increment();
|
|
||||||
|
|
||||||
_skeletonModel->simulate(deltaTime, true);
|
|
||||||
|
|
||||||
locationChanged(); // joints changed, so if there are any children, update them.
|
|
||||||
_hasNewJointData = false;
|
|
||||||
|
|
||||||
glm::vec3 headPosition = getWorldPosition();
|
|
||||||
if (!_skeletonModel->getHeadPosition(headPosition)) {
|
|
||||||
headPosition = getWorldPosition();
|
|
||||||
}
|
|
||||||
head->setPosition(headPosition);
|
|
||||||
}
|
|
||||||
head->setScale(getModelScale());
|
|
||||||
head->simulate(deltaTime);
|
|
||||||
relayJointDataToChildren();
|
|
||||||
} else {
|
|
||||||
// a non-full update is still required so that the position, rotation, scale and bounds of the skeletonModel are updated.
|
|
||||||
_skeletonModel->simulate(deltaTime, false);
|
|
||||||
}
|
|
||||||
_skeletonModelSimulationRate.increment();
|
|
||||||
}
|
|
||||||
|
|
||||||
// update animation for display name fade in/out
|
|
||||||
if ( _displayNameTargetAlpha != _displayNameAlpha) {
|
|
||||||
// the alpha function is
|
|
||||||
// Fade out => alpha(t) = factor ^ t => alpha(t+dt) = alpha(t) * factor^(dt)
|
|
||||||
// Fade in => alpha(t) = 1 - factor^t => alpha(t+dt) = 1-(1-alpha(t))*coef^(dt)
|
|
||||||
// factor^(dt) = coef
|
|
||||||
float coef = pow(DISPLAYNAME_FADE_FACTOR, deltaTime);
|
|
||||||
if (_displayNameTargetAlpha < _displayNameAlpha) {
|
|
||||||
// Fading out
|
|
||||||
_displayNameAlpha *= coef;
|
|
||||||
} else {
|
|
||||||
// Fading in
|
|
||||||
_displayNameAlpha = 1 - (1 - _displayNameAlpha) * coef;
|
|
||||||
}
|
|
||||||
_displayNameAlpha = abs(_displayNameAlpha - _displayNameTargetAlpha) < 0.01f ? _displayNameTargetAlpha : _displayNameAlpha;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
PROFILE_RANGE(simulation, "misc");
|
|
||||||
measureMotionDerivatives(deltaTime);
|
|
||||||
simulateAttachments(deltaTime);
|
|
||||||
updatePalms();
|
|
||||||
}
|
|
||||||
{
|
|
||||||
PROFILE_RANGE(simulation, "entities");
|
|
||||||
updateAvatarEntities();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
PROFILE_RANGE(simulation, "grabs");
|
|
||||||
updateGrabs();
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFadingStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
float Avatar::getSimulationRate(const QString& rateName) const {
|
float Avatar::getSimulationRate(const QString& rateName) const {
|
||||||
if (rateName == "") {
|
if (rateName == "") {
|
||||||
return _simulationRate.rate();
|
return _simulationRate.rate();
|
||||||
|
@ -1049,7 +809,6 @@ void Avatar::render(RenderArgs* renderArgs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void Avatar::setEnableMeshVisible(bool isEnabled) {
|
void Avatar::setEnableMeshVisible(bool isEnabled) {
|
||||||
if (_isMeshVisible != isEnabled) {
|
if (_isMeshVisible != isEnabled) {
|
||||||
_isMeshVisible = isEnabled;
|
_isMeshVisible = isEnabled;
|
||||||
|
|
|
@ -140,9 +140,8 @@ public:
|
||||||
typedef render::Payload<AvatarData> Payload;
|
typedef render::Payload<AvatarData> Payload;
|
||||||
|
|
||||||
void init();
|
void init();
|
||||||
void updateAvatarEntities();
|
|
||||||
void removeAvatarEntitiesFromTree();
|
void removeAvatarEntitiesFromTree();
|
||||||
void simulate(float deltaTime, bool inView);
|
virtual void simulate(float deltaTime, bool inView) = 0;
|
||||||
virtual void simulateAttachments(float deltaTime);
|
virtual void simulateAttachments(float deltaTime);
|
||||||
|
|
||||||
virtual void render(RenderArgs* renderArgs);
|
virtual void render(RenderArgs* renderArgs);
|
||||||
|
@ -241,8 +240,6 @@ public:
|
||||||
static void renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2,
|
static void renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2,
|
||||||
float radius1, float radius2, const glm::vec4& color);
|
float radius1, float radius2, const glm::vec4& color);
|
||||||
|
|
||||||
virtual void applyCollision(const glm::vec3& contactPoint, const glm::vec3& penetration) { }
|
|
||||||
|
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* Set the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example,
|
* Set the offset applied to the current avatar. The offset adjusts the position that the avatar is rendered. For example,
|
||||||
* with an offset of <code>{ x: 0, y: 0.1, z: 0 }</code>, your avatar will appear to be raised off the ground slightly.
|
* with an offset of <code>{ x: 0, y: 0.1, z: 0 }</code>, your avatar will appear to be raised off the ground slightly.
|
||||||
|
|
|
@ -1908,10 +1908,9 @@ qint64 AvatarData::packAvatarEntityTraitInstance(AvatarTraits::TraitType traitTy
|
||||||
|
|
||||||
// grab a read lock on the avatar entities and check for entity data for the given ID
|
// grab a read lock on the avatar entities and check for entity data for the given ID
|
||||||
QByteArray entityBinaryData;
|
QByteArray entityBinaryData;
|
||||||
|
|
||||||
_avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] {
|
_avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] {
|
||||||
if (_avatarEntityData.contains(traitInstanceID)) {
|
if (_packedAvatarEntityData.contains(traitInstanceID)) {
|
||||||
entityBinaryData = _avatarEntityData[traitInstanceID];
|
entityBinaryData = _packedAvatarEntityData[traitInstanceID];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1987,9 +1986,9 @@ qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTr
|
||||||
qint64 bytesWritten = 0;
|
qint64 bytesWritten = 0;
|
||||||
|
|
||||||
if (traitType == AvatarTraits::AvatarEntity) {
|
if (traitType == AvatarTraits::AvatarEntity) {
|
||||||
packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion);
|
bytesWritten += packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion);
|
||||||
} else if (traitType == AvatarTraits::Grab) {
|
} else if (traitType == AvatarTraits::Grab) {
|
||||||
packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion);
|
bytesWritten += packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytesWritten;
|
return bytesWritten;
|
||||||
|
@ -1998,7 +1997,7 @@ qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTr
|
||||||
void AvatarData::prepareResetTraitInstances() {
|
void AvatarData::prepareResetTraitInstances() {
|
||||||
if (_clientTraitsHandler) {
|
if (_clientTraitsHandler) {
|
||||||
_avatarEntitiesLock.withReadLock([this]{
|
_avatarEntitiesLock.withReadLock([this]{
|
||||||
foreach (auto entityID, _avatarEntityData.keys()) {
|
foreach (auto entityID, _packedAvatarEntityData.keys()) {
|
||||||
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
|
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
|
||||||
}
|
}
|
||||||
foreach (auto grabID, _avatarGrabData.keys()) {
|
foreach (auto grabID, _avatarGrabData.keys()) {
|
||||||
|
@ -2019,7 +2018,7 @@ void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray trai
|
||||||
void AvatarData::processTraitInstance(AvatarTraits::TraitType traitType,
|
void AvatarData::processTraitInstance(AvatarTraits::TraitType traitType,
|
||||||
AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData) {
|
AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData) {
|
||||||
if (traitType == AvatarTraits::AvatarEntity) {
|
if (traitType == AvatarTraits::AvatarEntity) {
|
||||||
updateAvatarEntity(instanceID, traitBinaryData);
|
storeAvatarEntityDataPayload(instanceID, traitBinaryData);
|
||||||
} else if (traitType == AvatarTraits::Grab) {
|
} else if (traitType == AvatarTraits::Grab) {
|
||||||
updateAvatarGrabData(instanceID, traitBinaryData);
|
updateAvatarGrabData(instanceID, traitBinaryData);
|
||||||
}
|
}
|
||||||
|
@ -2367,7 +2366,7 @@ void AvatarData::setRecordingBasis(std::shared_ptr<Transform> recordingBasis) {
|
||||||
void AvatarData::createRecordingIDs() {
|
void AvatarData::createRecordingIDs() {
|
||||||
_avatarEntitiesLock.withReadLock([&] {
|
_avatarEntitiesLock.withReadLock([&] {
|
||||||
_avatarEntityForRecording.clear();
|
_avatarEntityForRecording.clear();
|
||||||
for (int i = 0; i < _avatarEntityData.size(); i++) {
|
for (int i = 0; i < _packedAvatarEntityData.size(); i++) {
|
||||||
_avatarEntityForRecording.insert(QUuid::createUuid());
|
_avatarEntityForRecording.insert(QUuid::createUuid());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -2422,6 +2421,10 @@ JointData jointDataFromJsonValue(int version, const QJsonValue& json) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AvatarData::avatarEntityDataToJson(QJsonObject& root) const {
|
||||||
|
// overridden where needed
|
||||||
|
}
|
||||||
|
|
||||||
QJsonObject AvatarData::toJson() const {
|
QJsonObject AvatarData::toJson() const {
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
|
|
||||||
|
@ -2433,20 +2436,8 @@ QJsonObject AvatarData::toJson() const {
|
||||||
if (!getDisplayName().isEmpty()) {
|
if (!getDisplayName().isEmpty()) {
|
||||||
root[JSON_AVATAR_DISPLAY_NAME] = getDisplayName();
|
root[JSON_AVATAR_DISPLAY_NAME] = getDisplayName();
|
||||||
}
|
}
|
||||||
_avatarEntitiesLock.withReadLock([&] {
|
|
||||||
if (!_avatarEntityData.empty()) {
|
avatarEntityDataToJson(root);
|
||||||
QJsonArray avatarEntityJson;
|
|
||||||
int entityCount = 0;
|
|
||||||
for (auto entityID : _avatarEntityData.keys()) {
|
|
||||||
QVariantMap entityData;
|
|
||||||
QUuid newId = _avatarEntityForRecording.size() == _avatarEntityData.size() ? _avatarEntityForRecording.values()[entityCount++] : entityID;
|
|
||||||
entityData.insert("id", newId);
|
|
||||||
entityData.insert("properties", _avatarEntityData.value(entityID).toBase64());
|
|
||||||
avatarEntityJson.push_back(QVariant(entityData).toJsonObject());
|
|
||||||
}
|
|
||||||
root[JSON_AVATAR_ENTITIES] = avatarEntityJson;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
auto recordingBasis = getRecordingBasis();
|
auto recordingBasis = getRecordingBasis();
|
||||||
bool success;
|
bool success;
|
||||||
|
@ -2568,9 +2559,9 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) {
|
||||||
for (auto attachmentJson : attachmentsJson) {
|
for (auto attachmentJson : attachmentsJson) {
|
||||||
if (attachmentJson.isObject()) {
|
if (attachmentJson.isObject()) {
|
||||||
QVariantMap entityData = attachmentJson.toObject().toVariantMap();
|
QVariantMap entityData = attachmentJson.toObject().toVariantMap();
|
||||||
QUuid entityID = entityData.value("id").toUuid();
|
QUuid id = entityData.value("id").toUuid();
|
||||||
QByteArray properties = QByteArray::fromBase64(entityData.value("properties").toByteArray());
|
QByteArray data = QByteArray::fromBase64(entityData.value("properties").toByteArray());
|
||||||
updateAvatarEntity(entityID, properties);
|
updateAvatarEntity(id, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2752,17 +2743,15 @@ void AvatarData::setAttachmentsVariant(const QVariantList& variant) {
|
||||||
setAttachmentData(newAttachments);
|
setAttachmentData(newAttachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int MAX_NUM_AVATAR_ENTITIES = 42;
|
void AvatarData::storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& data) {
|
||||||
|
|
||||||
void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) {
|
|
||||||
_avatarEntitiesLock.withWriteLock([&] {
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
AvatarEntityMap::iterator itr = _avatarEntityData.find(entityID);
|
PackedAvatarEntityMap::iterator itr = _packedAvatarEntityData.find(entityID);
|
||||||
if (itr == _avatarEntityData.end()) {
|
if (itr == _packedAvatarEntityData.end()) {
|
||||||
if (_avatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) {
|
if (_packedAvatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) {
|
||||||
_avatarEntityData.insert(entityID, entityData);
|
_packedAvatarEntityData.insert(entityID, data);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
itr.value() = entityData;
|
itr.value() = data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2775,15 +2764,20 @@ void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& ent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) {
|
||||||
|
// overridden where needed
|
||||||
|
// expects 'entityData' to be a JavaScript EntityItemProperties Object in QByteArray form
|
||||||
|
}
|
||||||
|
|
||||||
void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) {
|
void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) {
|
||||||
|
|
||||||
bool removedEntity = false;
|
bool removedEntity = false;
|
||||||
|
|
||||||
_avatarEntitiesLock.withWriteLock([this, &removedEntity, &entityID] {
|
_avatarEntitiesLock.withWriteLock([this, &removedEntity, &entityID] {
|
||||||
removedEntity = _avatarEntityData.remove(entityID);
|
removedEntity = _packedAvatarEntityData.remove(entityID);
|
||||||
});
|
});
|
||||||
|
|
||||||
insertDetachedEntityID(entityID);
|
insertRemovedEntityID(entityID);
|
||||||
|
|
||||||
if (removedEntity && _clientTraitsHandler) {
|
if (removedEntity && _clientTraitsHandler) {
|
||||||
// we have a client traits handler, so we need to mark this removed instance trait as deleted
|
// we have a client traits handler, so we need to mark this removed instance trait as deleted
|
||||||
|
@ -2793,75 +2787,29 @@ void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFr
|
||||||
}
|
}
|
||||||
|
|
||||||
AvatarEntityMap AvatarData::getAvatarEntityData() const {
|
AvatarEntityMap AvatarData::getAvatarEntityData() const {
|
||||||
AvatarEntityMap result;
|
// overridden where needed
|
||||||
_avatarEntitiesLock.withReadLock([&] {
|
// NOTE: the return value is expected to be a map of unfortunately-formatted-binary-blobs
|
||||||
result = _avatarEntityData;
|
return AvatarEntityMap();
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
void AvatarData::insertDetachedEntityID(const QUuid entityID) {
|
|
||||||
_avatarEntitiesLock.withWriteLock([&] {
|
|
||||||
_avatarEntityDetached.insert(entityID);
|
|
||||||
});
|
|
||||||
|
|
||||||
_avatarEntityDataChanged = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
|
void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
|
||||||
if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) {
|
// overridden where needed
|
||||||
// the data is suspect
|
// avatarEntityData is expected to be a map of QByteArrays
|
||||||
qCDebug(avatars) << "discard suspect AvatarEntityData with size =" << avatarEntityData.size();
|
// each QByteArray represents an EntityItemProperties object from JavaScript
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<QUuid> deletedEntityIDs;
|
|
||||||
QList<QUuid> updatedEntityIDs;
|
|
||||||
|
|
||||||
_avatarEntitiesLock.withWriteLock([&] {
|
|
||||||
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);
|
|
||||||
|
|
||||||
deletedEntityIDs.reserve(previousAvatarEntityIDs.size());
|
|
||||||
|
|
||||||
foreach (auto entityID, previousAvatarEntityIDs) {
|
|
||||||
if (!_avatarEntityData.contains(entityID)) {
|
|
||||||
_avatarEntityDetached.insert(entityID);
|
|
||||||
deletedEntityIDs.push_back(entityID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedEntityIDs = _avatarEntityData.keys();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (_clientTraitsHandler) {
|
|
||||||
// we have a client traits handler
|
|
||||||
|
|
||||||
// flag removed entities as deleted so that changes are sent next frame
|
|
||||||
for (auto& deletedEntityID : deletedEntityIDs) {
|
|
||||||
_clientTraitsHandler->markInstancedTraitDeleted(AvatarTraits::AvatarEntity, deletedEntityID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// flag any updated or created entities so that we send changes for them next frame
|
|
||||||
for (auto& entityID : updatedEntityIDs) {
|
|
||||||
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() {
|
void AvatarData::insertRemovedEntityID(const QUuid entityID) {
|
||||||
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
|
_avatarEntityRemoved.insert(entityID);
|
||||||
|
});
|
||||||
|
_avatarEntityDataChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AvatarEntityIDs AvatarData::getAndClearRecentlyRemovedIDs() {
|
||||||
AvatarEntityIDs result;
|
AvatarEntityIDs result;
|
||||||
_avatarEntitiesLock.withWriteLock([&] {
|
_avatarEntitiesLock.withWriteLock([&] {
|
||||||
result = _avatarEntityDetached;
|
result = _avatarEntityRemoved;
|
||||||
_avatarEntityDetached.clear();
|
_avatarEntityRemoved.clear();
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@ using AvatarWeakPointer = std::weak_ptr<AvatarData>;
|
||||||
using AvatarHash = QHash<QUuid, AvatarSharedPointer>;
|
using AvatarHash = QHash<QUuid, AvatarSharedPointer>;
|
||||||
|
|
||||||
using AvatarEntityMap = QMap<QUuid, QByteArray>;
|
using AvatarEntityMap = QMap<QUuid, QByteArray>;
|
||||||
|
using PackedAvatarEntityMap = QMap<QUuid, QByteArray>; // similar to AvatarEntityMap, but different internal format
|
||||||
using AvatarEntityIDs = QSet<QUuid>;
|
using AvatarEntityIDs = QSet<QUuid>;
|
||||||
|
|
||||||
using AvatarGrabDataMap = QMap<QUuid, QByteArray>;
|
using AvatarGrabDataMap = QMap<QUuid, QByteArray>;
|
||||||
|
@ -71,6 +72,8 @@ using AvatarGrabMap = QMap<QUuid, GrabPointer>;
|
||||||
|
|
||||||
using AvatarDataSequenceNumber = uint16_t;
|
using AvatarDataSequenceNumber = uint16_t;
|
||||||
|
|
||||||
|
const int MAX_NUM_AVATAR_ENTITIES = 42;
|
||||||
|
|
||||||
// avatar motion behaviors
|
// avatar motion behaviors
|
||||||
const quint32 AVATAR_MOTION_ACTION_MOTOR_ENABLED = 1U << 0;
|
const quint32 AVATAR_MOTION_ACTION_MOTOR_ENABLED = 1U << 0;
|
||||||
const quint32 AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED = 1U << 1;
|
const quint32 AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED = 1U << 1;
|
||||||
|
@ -952,19 +955,20 @@ public:
|
||||||
// FIXME: Can this name be improved? Can it be deprecated?
|
// FIXME: Can this name be improved? Can it be deprecated?
|
||||||
Q_INVOKABLE virtual void setAttachmentsVariant(const QVariantList& variant);
|
Q_INVOKABLE virtual void setAttachmentsVariant(const QVariantList& variant);
|
||||||
|
|
||||||
|
virtual void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload);
|
||||||
|
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* @function MyAvatar.updateAvatarEntity
|
* @function MyAvatar.updateAvatarEntity
|
||||||
* @param {Uuid} entityID
|
* @param {Uuid} entityID
|
||||||
* @param {string} entityData
|
* @param {string} entityData
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData);
|
Q_INVOKABLE virtual void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData);
|
||||||
|
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* @function MyAvatar.clearAvatarEntity
|
* @function MyAvatar.clearAvatarEntity
|
||||||
* @param {Uuid} entityID
|
* @param {Uuid} entityID
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true);
|
Q_INVOKABLE virtual void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true);
|
||||||
|
|
||||||
|
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
|
@ -1125,6 +1129,7 @@ public:
|
||||||
TransformPointer getRecordingBasis() const;
|
TransformPointer getRecordingBasis() const;
|
||||||
void setRecordingBasis(TransformPointer recordingBasis = TransformPointer());
|
void setRecordingBasis(TransformPointer recordingBasis = TransformPointer());
|
||||||
void createRecordingIDs();
|
void createRecordingIDs();
|
||||||
|
virtual void avatarEntityDataToJson(QJsonObject& root) const;
|
||||||
QJsonObject toJson() const;
|
QJsonObject toJson() const;
|
||||||
void fromJson(const QJsonObject& json, bool useFrameSkeleton = true);
|
void fromJson(const QJsonObject& json, bool useFrameSkeleton = true);
|
||||||
|
|
||||||
|
@ -1136,17 +1141,16 @@ public:
|
||||||
* @function MyAvatar.getAvatarEntityData
|
* @function MyAvatar.getAvatarEntityData
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const;
|
Q_INVOKABLE virtual AvatarEntityMap getAvatarEntityData() const;
|
||||||
|
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* @function MyAvatar.setAvatarEntityData
|
* @function MyAvatar.setAvatarEntityData
|
||||||
* @param {object} avatarEntityData
|
* @param {object} avatarEntityData
|
||||||
*/
|
*/
|
||||||
Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData);
|
Q_INVOKABLE virtual void setAvatarEntityData(const AvatarEntityMap& avatarEntityData);
|
||||||
|
|
||||||
virtual void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; }
|
virtual void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; }
|
||||||
void insertDetachedEntityID(const QUuid entityID);
|
AvatarEntityIDs getAndClearRecentlyRemovedIDs();
|
||||||
AvatarEntityIDs getAndClearRecentlyDetachedIDs();
|
|
||||||
|
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* @function MyAvatar.getSensorToWorldMatrix
|
* @function MyAvatar.getSensorToWorldMatrix
|
||||||
|
@ -1333,6 +1337,7 @@ public slots:
|
||||||
void resetLastSent() { _lastToByteArray = 0; }
|
void resetLastSent() { _lastToByteArray = 0; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void insertRemovedEntityID(const QUuid entityID);
|
||||||
void lazyInitHeadData() const;
|
void lazyInitHeadData() const;
|
||||||
|
|
||||||
float getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition) const;
|
float getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition) const;
|
||||||
|
@ -1461,9 +1466,9 @@ protected:
|
||||||
AABox _defaultBubbleBox;
|
AABox _defaultBubbleBox;
|
||||||
|
|
||||||
mutable ReadWriteLockable _avatarEntitiesLock;
|
mutable ReadWriteLockable _avatarEntitiesLock;
|
||||||
AvatarEntityIDs _avatarEntityDetached; // recently detached from this avatar
|
AvatarEntityIDs _avatarEntityRemoved; // recently removed AvatarEntity ids
|
||||||
AvatarEntityIDs _avatarEntityForRecording; // create new entities id for avatar recording
|
AvatarEntityIDs _avatarEntityForRecording; // create new entities id for avatar recording
|
||||||
AvatarEntityMap _avatarEntityData;
|
PackedAvatarEntityMap _packedAvatarEntityData;
|
||||||
bool _avatarEntityDataChanged { false };
|
bool _avatarEntityDataChanged { false };
|
||||||
|
|
||||||
mutable ReadWriteLockable _avatarGrabsLock;
|
mutable ReadWriteLockable _avatarGrabsLock;
|
||||||
|
|
|
@ -328,6 +328,19 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
|
||||||
}
|
}
|
||||||
|
|
||||||
void AvatarHashMap::processBulkAvatarTraits(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
void AvatarHashMap::processBulkAvatarTraits(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode) {
|
||||||
|
AvatarTraits::TraitMessageSequence seq;
|
||||||
|
|
||||||
|
message->readPrimitive(&seq);
|
||||||
|
|
||||||
|
auto traitsAckPacket = NLPacket::create(PacketType::BulkAvatarTraitsAck, sizeof(AvatarTraits::TraitMessageSequence), true);
|
||||||
|
traitsAckPacket->writePrimitive(seq);
|
||||||
|
auto nodeList = DependencyManager::get<LimitedNodeList>();
|
||||||
|
SharedNodePointer avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer);
|
||||||
|
if (!avatarMixer.isNull()) {
|
||||||
|
// we have a mixer to send to, acknowledge that we received these
|
||||||
|
// traits.
|
||||||
|
nodeList->sendPacket(std::move(traitsAckPacket), *avatarMixer);
|
||||||
|
}
|
||||||
|
|
||||||
while (message->getBytesLeftToRead()) {
|
while (message->getBytesLeftToRead()) {
|
||||||
// read the avatar ID to figure out which avatar this is for
|
// read the avatar ID to figure out which avatar this is for
|
||||||
|
|
|
@ -42,6 +42,10 @@ namespace AvatarTraits {
|
||||||
const TraitWireSize DELETED_TRAIT_SIZE = -1;
|
const TraitWireSize DELETED_TRAIT_SIZE = -1;
|
||||||
const TraitWireSize MAXIMUM_TRAIT_SIZE = INT16_MAX;
|
const TraitWireSize MAXIMUM_TRAIT_SIZE = INT16_MAX;
|
||||||
|
|
||||||
|
using TraitMessageSequence = int64_t;
|
||||||
|
const TraitMessageSequence FIRST_TRAIT_SEQUENCE = 0;
|
||||||
|
const TraitMessageSequence MAX_TRAIT_SEQUENCE = INT64_MAX;
|
||||||
|
|
||||||
inline qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination,
|
inline qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination,
|
||||||
TraitVersion traitVersion = NULL_TRAIT_VERSION) {
|
TraitVersion traitVersion = NULL_TRAIT_VERSION) {
|
||||||
qint64 bytesWritten = 0;
|
qint64 bytesWritten = 0;
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
// These properties have JSDoc documentation in HMDScriptingInterface.h.
|
// These properties have JSDoc documentation in HMDScriptingInterface.h.
|
||||||
class AbstractHMDScriptingInterface : public QObject {
|
class AbstractHMDScriptingInterface : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(bool active READ isHMDMode)
|
Q_PROPERTY(bool active READ isHMDMode NOTIFY mountedChanged)
|
||||||
Q_PROPERTY(float ipd READ getIPD)
|
Q_PROPERTY(float ipd READ getIPD)
|
||||||
Q_PROPERTY(float eyeHeight READ getEyeHeight)
|
Q_PROPERTY(float eyeHeight READ getEyeHeight)
|
||||||
Q_PROPERTY(float playerHeight READ getPlayerHeight)
|
Q_PROPERTY(float playerHeight READ getPlayerHeight)
|
||||||
|
@ -43,7 +43,7 @@ signals:
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
* Triggered when Interface's display mode changes and when the user puts on or takes off their HMD.
|
* Triggered when Interface's display mode changes and when the user puts on or takes off their HMD.
|
||||||
* @function HMD.displayModeChanged
|
* @function HMD.displayModeChanged
|
||||||
* @param {boolean} isHMDMode - <code>true</code> if the display mode is HMD, otherwise <code>false</code>. This is the
|
* @param {boolean} isHMDMode - <code>true</code> if the display mode is HMD, otherwise <code>false</code>. This is the
|
||||||
* same value as provided by <code>HMD.active</code>.
|
* same value as provided by <code>HMD.active</code>.
|
||||||
* @returns {Signal}
|
* @returns {Signal}
|
||||||
* @example <caption>Report when the display mode changes.</caption>
|
* @example <caption>Report when the display mode changes.</caption>
|
||||||
|
|
|
@ -39,13 +39,12 @@ void EntityEditPacketSender::adjustEditPacketForClockSkew(PacketType type, QByte
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type,
|
void EntityEditPacketSender::queueEditAvatarEntityMessage(EntityTreePointer entityTree,
|
||||||
EntityTreePointer entityTree,
|
|
||||||
EntityItemID entityItemID,
|
EntityItemID entityItemID,
|
||||||
const EntityItemProperties& properties) {
|
const EntityItemProperties& properties) {
|
||||||
assert(_myAvatar);
|
assert(_myAvatar);
|
||||||
if (!entityTree) {
|
if (!entityTree) {
|
||||||
qCDebug(entities) << "EntityEditPacketSender::queueEditEntityMessage null entityTree.";
|
qCDebug(entities) << "EntityEditPacketSender::queueEditAvatarEntityMessage null entityTree.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
EntityItemPointer entity = entityTree->findEntityByEntityItemID(entityItemID);
|
EntityItemPointer entity = entityTree->findEntityByEntityItemID(entityItemID);
|
||||||
|
@ -53,33 +52,27 @@ void EntityEditPacketSender::queueEditAvatarEntityMessage(PacketType type,
|
||||||
qCDebug(entities) << "EntityEditPacketSender::queueEditAvatarEntityMessage can't find entity: " << entityItemID;
|
qCDebug(entities) << "EntityEditPacketSender::queueEditAvatarEntityMessage can't find entity: " << entityItemID;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
entity->setLastBroadcast(usecTimestampNow());
|
||||||
|
|
||||||
// the properties that get serialized into the avatar identity packet should be the entire set
|
// serialize ALL properties in an "AvatarEntity" packet
|
||||||
// rather than just the ones being edited.
|
// rather than just the ones being edited.
|
||||||
EntityItemProperties entityProperties = entity->getProperties();
|
EntityItemProperties entityProperties = entity->getProperties();
|
||||||
entityProperties.merge(properties);
|
entityProperties.merge(properties);
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(_mutex);
|
OctreePacketData packetData(false, AvatarTraits::MAXIMUM_TRAIT_SIZE);
|
||||||
QScriptValue scriptProperties = EntityItemNonDefaultPropertiesToScriptValue(&_scriptEngine, entityProperties);
|
EncodeBitstreamParams params;
|
||||||
QVariant variantProperties = scriptProperties.toVariant();
|
EntityTreeElementExtraEncodeDataPointer extra { nullptr };
|
||||||
QJsonDocument jsonProperties = QJsonDocument::fromVariant(variantProperties);
|
OctreeElement::AppendState appendState = entity->appendEntityData(&packetData, params, extra);
|
||||||
|
|
||||||
// the ID of the parent/avatar changes from session to session. use a special UUID to indicate the avatar
|
if (appendState != OctreeElement::COMPLETED) {
|
||||||
QJsonObject jsonObject = jsonProperties.object();
|
// this entity's payload is too big
|
||||||
if (jsonObject.contains("parentID")) {
|
return;
|
||||||
if (QUuid(jsonObject["parentID"].toString()) == _myAvatar->getID()) {
|
|
||||||
jsonObject["parentID"] = AVATAR_SELF_ID.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
jsonProperties = QJsonDocument(jsonObject);
|
|
||||||
|
|
||||||
QByteArray binaryProperties = jsonProperties.toBinaryData();
|
QByteArray tempArray((const char*)packetData.getUncompressedData(), packetData.getUncompressedSize());
|
||||||
_myAvatar->updateAvatarEntity(entityItemID, binaryProperties);
|
_myAvatar->storeAvatarEntityDataPayload(entityItemID, tempArray);
|
||||||
|
|
||||||
entity->setLastBroadcast(usecTimestampNow());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void EntityEditPacketSender::queueEditEntityMessage(PacketType type,
|
void EntityEditPacketSender::queueEditEntityMessage(PacketType type,
|
||||||
EntityTreePointer entityTree,
|
EntityTreePointer entityTree,
|
||||||
EntityItemID entityItemID,
|
EntityItemID entityItemID,
|
||||||
|
@ -89,7 +82,7 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type,
|
||||||
qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit with no myAvatar";
|
qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit with no myAvatar";
|
||||||
} else if (properties.getOwningAvatarID() == _myAvatar->getID()) {
|
} else if (properties.getOwningAvatarID() == _myAvatar->getID()) {
|
||||||
// this is an avatar-based entity --> update our avatar-data rather than sending to the entity-server
|
// this is an avatar-based entity --> update our avatar-data rather than sending to the entity-server
|
||||||
queueEditAvatarEntityMessage(type, entityTree, entityItemID, properties);
|
queueEditAvatarEntityMessage(entityTree, entityItemID, properties);
|
||||||
} else {
|
} else {
|
||||||
qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit for another avatar";
|
qCWarning(entities) << "Suppressing entity edit message: cannot send avatar entity edit for another avatar";
|
||||||
}
|
}
|
||||||
|
@ -127,7 +120,13 @@ void EntityEditPacketSender::queueEditEntityMessage(PacketType type,
|
||||||
while (encodeResult == OctreeElement::PARTIAL) {
|
while (encodeResult == OctreeElement::PARTIAL) {
|
||||||
encodeResult = EntityItemProperties::encodeEntityEditPacket(type, entityItemID, propertiesCopy, bufferOut, requestedProperties, didntFitProperties);
|
encodeResult = EntityItemProperties::encodeEntityEditPacket(type, entityItemID, propertiesCopy, bufferOut, requestedProperties, didntFitProperties);
|
||||||
|
|
||||||
if (encodeResult != OctreeElement::NONE) {
|
if (encodeResult == OctreeElement::NONE) {
|
||||||
|
// This can happen for two reasons:
|
||||||
|
// 1. One of the properties is too large to fit in a single packet.
|
||||||
|
// 2. The requested properties don't exist in this entity type (e.g., 'modelUrl' in a Zone Entity).
|
||||||
|
// Since case #1 is more likely (and more critical), that's the one we warn about.
|
||||||
|
qCWarning(entities).nospace() << "queueEditEntityMessage: some of the properties don't fit and can't be sent. entityID=" << uuidStringWithoutCurlyBraces(entityItemID);
|
||||||
|
} else {
|
||||||
#ifdef WANT_DEBUG
|
#ifdef WANT_DEBUG
|
||||||
qCDebug(entities) << "calling queueOctreeEditMessage()...";
|
qCDebug(entities) << "calling queueOctreeEditMessage()...";
|
||||||
qCDebug(entities) << " id:" << entityItemID;
|
qCDebug(entities) << " id:" << entityItemID;
|
||||||
|
|
|
@ -50,7 +50,7 @@ public slots:
|
||||||
void processEntityEditNackPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
|
void processEntityEditNackPacket(QSharedPointer<ReceivedMessage> message, SharedNodePointer sendingNode);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void queueEditAvatarEntityMessage(PacketType type, EntityTreePointer entityTree,
|
void queueEditAvatarEntityMessage(EntityTreePointer entityTree,
|
||||||
EntityItemID entityItemID, const EntityItemProperties& properties);
|
EntityItemID entityItemID, const EntityItemProperties& properties);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
#include <GLMHelpers.h>
|
#include <GLMHelpers.h>
|
||||||
#include <RegisteredMetaTypes.h>
|
#include <RegisteredMetaTypes.h>
|
||||||
#include <Extents.h>
|
#include <Extents.h>
|
||||||
|
#include <VariantMapToScriptValue.h>
|
||||||
|
|
||||||
#include "EntitiesLogging.h"
|
#include "EntitiesLogging.h"
|
||||||
#include "EntityItem.h"
|
#include "EntityItem.h"
|
||||||
|
@ -90,6 +91,16 @@ void EntityItemProperties::setLastEdited(quint64 usecTime) {
|
||||||
_lastEdited = usecTime > _created ? usecTime : _created;
|
_lastEdited = usecTime > _created ? usecTime : _created;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EntityItemProperties::constructFromBuffer(const unsigned char* data, int dataLength) {
|
||||||
|
ReadBitstreamToTreeParams args;
|
||||||
|
EntityItemPointer tempEntity = EntityTypes::constructEntityItem(data, dataLength);
|
||||||
|
if (!tempEntity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tempEntity->readEntityDataFromBuffer(data, dataLength, args);
|
||||||
|
(*this) = tempEntity->getProperties();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
QHash<QString, ShapeType> stringToShapeTypeLookup;
|
QHash<QString, ShapeType> stringToShapeTypeLookup;
|
||||||
|
|
||||||
|
@ -2023,6 +2034,18 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
|
||||||
_lastEdited = usecTimestampNow();
|
_lastEdited = usecTimestampNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EntityItemProperties::copyFromJSONString(QScriptEngine& scriptEngine, const QString& jsonString) {
|
||||||
|
// DANGER: this method is expensive
|
||||||
|
QJsonDocument propertiesDoc = QJsonDocument::fromJson(jsonString.toUtf8());
|
||||||
|
QJsonObject propertiesObj = propertiesDoc.object();
|
||||||
|
QVariant propertiesVariant(propertiesObj);
|
||||||
|
QVariantMap propertiesMap = propertiesVariant.toMap();
|
||||||
|
QScriptValue propertiesScriptValue = variantMapToScriptValue(propertiesMap, scriptEngine);
|
||||||
|
bool honorReadOnly = true;
|
||||||
|
copyFromScriptValue(propertiesScriptValue, honorReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void EntityItemProperties::merge(const EntityItemProperties& other) {
|
void EntityItemProperties::merge(const EntityItemProperties& other) {
|
||||||
// Core
|
// Core
|
||||||
COPY_PROPERTY_IF_CHANGED(simulationOwner);
|
COPY_PROPERTY_IF_CHANGED(simulationOwner);
|
||||||
|
@ -2252,7 +2275,6 @@ void EntityItemPropertiesFromScriptValueHonorReadOnly(const QScriptValue &object
|
||||||
properties.copyFromScriptValue(object, true);
|
properties.copyFromScriptValue(object, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
QScriptValue EntityPropertyFlagsToScriptValue(QScriptEngine* engine, const EntityPropertyFlags& flags) {
|
QScriptValue EntityPropertyFlagsToScriptValue(QScriptEngine* engine, const EntityPropertyFlags& flags) {
|
||||||
return EntityItemProperties::entityPropertyFlagsToScriptValue(engine, flags);
|
return EntityItemProperties::entityPropertyFlagsToScriptValue(engine, flags);
|
||||||
}
|
}
|
||||||
|
@ -4590,6 +4612,40 @@ void EntityItemProperties::convertToCloneProperties(const EntityItemID& entityID
|
||||||
setCloneAvatarEntity(ENTITY_ITEM_DEFAULT_CLONE_AVATAR_ENTITY);
|
setCloneAvatarEntity(ENTITY_ITEM_DEFAULT_CLONE_AVATAR_ENTITY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EntityItemProperties::blobToProperties(QScriptEngine& scriptEngine, const QByteArray& blob, EntityItemProperties& properties) {
|
||||||
|
// DANGER: this method is NOT efficient.
|
||||||
|
// begin recipe for converting unfortunately-formatted-binary-blob to EntityItemProperties
|
||||||
|
QJsonDocument jsonProperties = QJsonDocument::fromBinaryData(blob);
|
||||||
|
if (!jsonProperties.isObject()) {
|
||||||
|
qCDebug(entities) << "bad avatarEntityData json" << QString(blob.toHex());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
QVariant variant = jsonProperties.toVariant();
|
||||||
|
QVariantMap variantMap = variant.toMap();
|
||||||
|
QScriptValue scriptValue = variantMapToScriptValue(variantMap, scriptEngine);
|
||||||
|
EntityItemPropertiesFromScriptValueHonorReadOnly(scriptValue, properties);
|
||||||
|
// end recipe
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EntityItemProperties::propertiesToBlob(QScriptEngine& scriptEngine, const QUuid& myAvatarID, const EntityItemProperties& properties, QByteArray& blob) {
|
||||||
|
// DANGER: this method is NOT efficient.
|
||||||
|
// begin recipe for extracting unfortunately-formatted-binary-blob from EntityItem
|
||||||
|
QScriptValue scriptValue = EntityItemNonDefaultPropertiesToScriptValue(&scriptEngine, properties);
|
||||||
|
QVariant variantProperties = scriptValue.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 (jsonObject.contains("parentID")) {
|
||||||
|
if (QUuid(jsonObject["parentID"].toString()) == myAvatarID) {
|
||||||
|
jsonObject["parentID"] = AVATAR_SELF_ID.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsonProperties = QJsonDocument(jsonObject);
|
||||||
|
blob = jsonProperties.toBinaryData();
|
||||||
|
// end recipe
|
||||||
|
}
|
||||||
|
|
||||||
QDebug& operator<<(QDebug& dbg, const EntityPropertyFlags& f) {
|
QDebug& operator<<(QDebug& dbg, const EntityPropertyFlags& f) {
|
||||||
QString result = "[ ";
|
QString result = "[ ";
|
||||||
|
|
||||||
|
|
|
@ -98,6 +98,9 @@ class EntityItemProperties {
|
||||||
friend class ZoneEntityItem;
|
friend class ZoneEntityItem;
|
||||||
friend class MaterialEntityItem;
|
friend class MaterialEntityItem;
|
||||||
public:
|
public:
|
||||||
|
static bool blobToProperties(QScriptEngine& scriptEngine, const QByteArray& blob, EntityItemProperties& properties);
|
||||||
|
static void propertiesToBlob(QScriptEngine& scriptEngine, const QUuid& myAvatarID, const EntityItemProperties& properties, QByteArray& blob);
|
||||||
|
|
||||||
EntityItemProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags());
|
EntityItemProperties(EntityPropertyFlags desiredProperties = EntityPropertyFlags());
|
||||||
virtual ~EntityItemProperties() = default;
|
virtual ~EntityItemProperties() = default;
|
||||||
|
|
||||||
|
@ -109,6 +112,7 @@ public:
|
||||||
virtual QScriptValue copyToScriptValue(QScriptEngine* engine, bool skipDefaults, bool allowUnknownCreateTime = false,
|
virtual QScriptValue copyToScriptValue(QScriptEngine* engine, bool skipDefaults, bool allowUnknownCreateTime = false,
|
||||||
bool strictSemantics = false, EntityPsuedoPropertyFlags psueudoPropertyFlags = EntityPsuedoPropertyFlags()) const;
|
bool strictSemantics = false, EntityPsuedoPropertyFlags psueudoPropertyFlags = EntityPsuedoPropertyFlags()) const;
|
||||||
virtual void copyFromScriptValue(const QScriptValue& object, bool honorReadOnly);
|
virtual void copyFromScriptValue(const QScriptValue& object, bool honorReadOnly);
|
||||||
|
void copyFromJSONString(QScriptEngine& scriptEngine, const QString& jsonString);
|
||||||
|
|
||||||
static QScriptValue entityPropertyFlagsToScriptValue(QScriptEngine* engine, const EntityPropertyFlags& flags);
|
static QScriptValue entityPropertyFlagsToScriptValue(QScriptEngine* engine, const EntityPropertyFlags& flags);
|
||||||
static void entityPropertyFlagsFromScriptValue(const QScriptValue& object, EntityPropertyFlags& flags);
|
static void entityPropertyFlagsFromScriptValue(const QScriptValue& object, EntityPropertyFlags& flags);
|
||||||
|
@ -135,6 +139,8 @@ public:
|
||||||
EntityPropertyFlags getDesiredProperties() { return _desiredProperties; }
|
EntityPropertyFlags getDesiredProperties() { return _desiredProperties; }
|
||||||
void setDesiredProperties(EntityPropertyFlags properties) { _desiredProperties = properties; }
|
void setDesiredProperties(EntityPropertyFlags properties) { _desiredProperties = properties; }
|
||||||
|
|
||||||
|
bool constructFromBuffer(const unsigned char* data, int dataLength);
|
||||||
|
|
||||||
// Note: DEFINE_PROPERTY(PROP_FOO, Foo, foo, type, value) creates the following methods and variables:
|
// Note: DEFINE_PROPERTY(PROP_FOO, Foo, foo, type, value) creates the following methods and variables:
|
||||||
// type getFoo() const;
|
// type getFoo() const;
|
||||||
// void setFoo(type);
|
// void setFoo(type);
|
||||||
|
@ -157,7 +163,7 @@ public:
|
||||||
DEFINE_PROPERTY(PROP_CREATED, Created, created, quint64, UNKNOWN_CREATED_TIME);
|
DEFINE_PROPERTY(PROP_CREATED, Created, created, quint64, UNKNOWN_CREATED_TIME);
|
||||||
DEFINE_PROPERTY_REF(PROP_LAST_EDITED_BY, LastEditedBy, lastEditedBy, QUuid, ENTITY_ITEM_DEFAULT_LAST_EDITED_BY);
|
DEFINE_PROPERTY_REF(PROP_LAST_EDITED_BY, LastEditedBy, lastEditedBy, QUuid, ENTITY_ITEM_DEFAULT_LAST_EDITED_BY);
|
||||||
DEFINE_PROPERTY_REF_ENUM(PROP_ENTITY_HOST_TYPE, EntityHostType, entityHostType, entity::HostType, entity::HostType::DOMAIN);
|
DEFINE_PROPERTY_REF_ENUM(PROP_ENTITY_HOST_TYPE, EntityHostType, entityHostType, entity::HostType, entity::HostType::DOMAIN);
|
||||||
DEFINE_PROPERTY_REF(PROP_OWNING_AVATAR_ID, OwningAvatarID, owningAvatarID, QUuid, UNKNOWN_ENTITY_ID);
|
DEFINE_PROPERTY_REF_WITH_SETTER(PROP_OWNING_AVATAR_ID, OwningAvatarID, owningAvatarID, QUuid, UNKNOWN_ENTITY_ID);
|
||||||
DEFINE_PROPERTY_REF(PROP_PARENT_ID, ParentID, parentID, QUuid, UNKNOWN_ENTITY_ID);
|
DEFINE_PROPERTY_REF(PROP_PARENT_ID, ParentID, parentID, QUuid, UNKNOWN_ENTITY_ID);
|
||||||
DEFINE_PROPERTY_REF(PROP_PARENT_JOINT_INDEX, ParentJointIndex, parentJointIndex, quint16, -1);
|
DEFINE_PROPERTY_REF(PROP_PARENT_JOINT_INDEX, ParentJointIndex, parentJointIndex, quint16, -1);
|
||||||
DEFINE_PROPERTY_REF(PROP_QUERY_AA_CUBE, QueryAACube, queryAACube, AACube, AACube());
|
DEFINE_PROPERTY_REF(PROP_QUERY_AA_CUBE, QueryAACube, queryAACube, AACube, AACube());
|
||||||
|
@ -499,6 +505,16 @@ void EntityPropertyInfoFromScriptValue(const QScriptValue& object, EntityPropert
|
||||||
inline void EntityItemProperties::setPosition(const glm::vec3& value)
|
inline void EntityItemProperties::setPosition(const glm::vec3& value)
|
||||||
{ _position = glm::clamp(value, (float)-HALF_TREE_SCALE, (float)HALF_TREE_SCALE); _positionChanged = true; }
|
{ _position = glm::clamp(value, (float)-HALF_TREE_SCALE, (float)HALF_TREE_SCALE); _positionChanged = true; }
|
||||||
|
|
||||||
|
inline void EntityItemProperties::setOwningAvatarID(const QUuid& id) {
|
||||||
|
_owningAvatarID = id;
|
||||||
|
if (!_owningAvatarID.isNull()) {
|
||||||
|
// for AvatarEntities 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.
|
||||||
|
setSimulationOwner(_owningAvatarID, AVATAR_ENTITY_SIMULATION_PRIORITY);
|
||||||
|
}
|
||||||
|
_owningAvatarIDChanged = true;
|
||||||
|
}
|
||||||
|
|
||||||
QDebug& operator<<(QDebug& dbg, const EntityPropertyFlags& f);
|
QDebug& operator<<(QDebug& dbg, const EntityPropertyFlags& f);
|
||||||
|
|
||||||
inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) {
|
inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) {
|
||||||
|
|
|
@ -174,7 +174,7 @@ int EntityTree::readEntityDataFromBuffer(const unsigned char* data, int bytesLef
|
||||||
addToNeedsParentFixupList(entity);
|
addToNeedsParentFixupList(entity);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
entity = EntityTypes::constructEntityItem(dataAt, bytesLeftToRead, args);
|
entity = EntityTypes::constructEntityItem(dataAt, bytesLeftToRead);
|
||||||
if (entity) {
|
if (entity) {
|
||||||
bytesForThisEntity = entity->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args);
|
bytesForThisEntity = entity->readEntityDataFromBuffer(dataAt, bytesLeftToRead, args);
|
||||||
|
|
||||||
|
@ -490,7 +490,6 @@ bool EntityTree::updateEntity(EntityItemPointer entity, const EntityItemProperti
|
||||||
}
|
}
|
||||||
|
|
||||||
EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties, bool isClone) {
|
EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties, bool isClone) {
|
||||||
EntityItemPointer result = NULL;
|
|
||||||
EntityItemProperties props = properties;
|
EntityItemProperties props = properties;
|
||||||
|
|
||||||
auto nodeList = DependencyManager::get<NodeList>();
|
auto nodeList = DependencyManager::get<NodeList>();
|
||||||
|
@ -517,12 +516,12 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti
|
||||||
if (containingElement) {
|
if (containingElement) {
|
||||||
qCWarning(entities) << "EntityTree::addEntity() on existing entity item with entityID=" << entityID
|
qCWarning(entities) << "EntityTree::addEntity() on existing entity item with entityID=" << entityID
|
||||||
<< "containingElement=" << containingElement.get();
|
<< "containingElement=" << containingElement.get();
|
||||||
return result;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// construct the instance of the entity
|
// construct the instance of the entity
|
||||||
EntityTypes::EntityType type = props.getType();
|
EntityTypes::EntityType type = props.getType();
|
||||||
result = EntityTypes::constructEntityItem(type, entityID, props);
|
EntityItemPointer result = EntityTypes::constructEntityItem(type, entityID, props);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
if (recordCreationTime) {
|
if (recordCreationTime) {
|
||||||
|
@ -531,10 +530,6 @@ EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const Enti
|
||||||
// Recurse the tree and store the entity in the correct tree element
|
// Recurse the tree and store the entity in the correct tree element
|
||||||
AddEntityOperator theOperator(getThisPointer(), result);
|
AddEntityOperator theOperator(getThisPointer(), result);
|
||||||
recurseTreeWithOperator(&theOperator);
|
recurseTreeWithOperator(&theOperator);
|
||||||
if (!result->getParentID().isNull()) {
|
|
||||||
addToNeedsParentFixupList(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
postAddEntity(result);
|
postAddEntity(result);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
@ -2969,27 +2964,30 @@ void EntityTree::updateEntityQueryAACubeWorker(SpatiallyNestablePointer object,
|
||||||
MovingEntitiesOperator& moveOperator, bool force, bool tellServer) {
|
MovingEntitiesOperator& moveOperator, bool force, bool tellServer) {
|
||||||
// if the queryBox has changed, tell the entity-server
|
// if the queryBox has changed, tell the entity-server
|
||||||
EntityItemPointer entity = std::dynamic_pointer_cast<EntityItem>(object);
|
EntityItemPointer entity = std::dynamic_pointer_cast<EntityItem>(object);
|
||||||
if (entity && (entity->updateQueryAACube() || force)) {
|
if (entity) {
|
||||||
bool success;
|
bool tellServerThis = tellServer && (entity->getEntityHostType() != entity::HostType::AVATAR);
|
||||||
AACube newCube = entity->getQueryAACube(success);
|
if ((entity->updateQueryAACube() || force)) {
|
||||||
if (success) {
|
bool success;
|
||||||
moveOperator.addEntityToMoveList(entity, newCube);
|
AACube newCube = entity->getQueryAACube(success);
|
||||||
}
|
if (success) {
|
||||||
// send an edit packet to update the entity-server about the queryAABox. We do this for domain-hosted
|
moveOperator.addEntityToMoveList(entity, newCube);
|
||||||
// entities as well as for avatar-entities; the packet-sender will route the update accordingly
|
}
|
||||||
if (tellServer && packetSender && (entity->isDomainEntity() || entity->isAvatarEntity())) {
|
// send an edit packet to update the entity-server about the queryAABox. We do this for domain-hosted
|
||||||
quint64 now = usecTimestampNow();
|
// entities as well as for avatar-entities; the packet-sender will route the update accordingly
|
||||||
EntityItemProperties properties = entity->getProperties();
|
if (tellServerThis && packetSender && (entity->isDomainEntity() || entity->isAvatarEntity())) {
|
||||||
properties.setQueryAACubeDirty();
|
quint64 now = usecTimestampNow();
|
||||||
properties.setLocationDirty();
|
EntityItemProperties properties = entity->getProperties();
|
||||||
properties.setLastEdited(now);
|
properties.setQueryAACubeDirty();
|
||||||
|
properties.setLocationDirty();
|
||||||
|
properties.setLastEdited(now);
|
||||||
|
|
||||||
packetSender->queueEditEntityMessage(PacketType::EntityEdit, getThisPointer(), entity->getID(), properties);
|
packetSender->queueEditEntityMessage(PacketType::EntityEdit, getThisPointer(), entity->getID(), properties);
|
||||||
entity->setLastBroadcast(now); // for debug/physics status icons
|
entity->setLastBroadcast(now); // for debug/physics status icons
|
||||||
}
|
}
|
||||||
|
|
||||||
entity->markDirtyFlags(Simulation::DIRTY_POSITION);
|
entity->markDirtyFlags(Simulation::DIRTY_POSITION);
|
||||||
entityChanged(entity);
|
entityChanged(entity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object->forEachDescendant([&](SpatiallyNestablePointer descendant) {
|
object->forEachDescendant([&](SpatiallyNestablePointer descendant) {
|
||||||
|
|
|
@ -58,6 +58,10 @@ REGISTER_ENTITY_TYPE(Light)
|
||||||
REGISTER_ENTITY_TYPE(Zone)
|
REGISTER_ENTITY_TYPE(Zone)
|
||||||
REGISTER_ENTITY_TYPE(Material)
|
REGISTER_ENTITY_TYPE(Material)
|
||||||
|
|
||||||
|
bool EntityTypes::typeIsValid(EntityType type) {
|
||||||
|
return type > EntityType::Unknown && type <= EntityType::NUM_TYPES;
|
||||||
|
}
|
||||||
|
|
||||||
const QString& EntityTypes::getEntityTypeName(EntityType entityType) {
|
const QString& EntityTypes::getEntityTypeName(EntityType entityType) {
|
||||||
QMap<EntityType, QString>::iterator matchedTypeName = _typeToNameMap.find(entityType);
|
QMap<EntityType, QString>::iterator matchedTypeName = _typeToNameMap.find(entityType);
|
||||||
if (matchedTypeName != _typeToNameMap.end()) {
|
if (matchedTypeName != _typeToNameMap.end()) {
|
||||||
|
@ -107,8 +111,7 @@ EntityItemPointer EntityTypes::constructEntityItem(EntityType entityType, const
|
||||||
return newEntityItem;
|
return newEntityItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
EntityItemPointer EntityTypes::constructEntityItem(const unsigned char* data, int bytesToRead,
|
void EntityTypes::extractEntityTypeAndID(const unsigned char* data, int dataLength, EntityTypes::EntityType& typeOut, QUuid& idOut) {
|
||||||
ReadBitstreamToTreeParams& args) {
|
|
||||||
|
|
||||||
// Header bytes
|
// Header bytes
|
||||||
// object ID [16 bytes]
|
// object ID [16 bytes]
|
||||||
|
@ -119,28 +122,36 @@ EntityItemPointer EntityTypes::constructEntityItem(const unsigned char* data, in
|
||||||
// ~27-35 bytes...
|
// ~27-35 bytes...
|
||||||
const int MINIMUM_HEADER_BYTES = 27;
|
const int MINIMUM_HEADER_BYTES = 27;
|
||||||
|
|
||||||
int bytesRead = 0;
|
if (dataLength >= MINIMUM_HEADER_BYTES) {
|
||||||
if (bytesToRead >= MINIMUM_HEADER_BYTES) {
|
int bytesRead = 0;
|
||||||
int originalLength = bytesToRead;
|
QByteArray originalDataBuffer = QByteArray::fromRawData((const char*)data, dataLength);
|
||||||
QByteArray originalDataBuffer((const char*)data, originalLength);
|
|
||||||
|
|
||||||
// id
|
// id
|
||||||
QByteArray encodedID = originalDataBuffer.mid(bytesRead, NUM_BYTES_RFC4122_UUID); // maximum possible size
|
QByteArray encodedID = originalDataBuffer.mid(bytesRead, NUM_BYTES_RFC4122_UUID); // maximum possible size
|
||||||
QUuid actualID = QUuid::fromRfc4122(encodedID);
|
idOut = QUuid::fromRfc4122(encodedID);
|
||||||
bytesRead += encodedID.size();
|
bytesRead += encodedID.size();
|
||||||
|
|
||||||
// type
|
// type
|
||||||
QByteArray encodedType = originalDataBuffer.mid(bytesRead); // maximum possible size
|
QByteArray encodedType = originalDataBuffer.mid(bytesRead); // maximum possible size
|
||||||
ByteCountCoded<quint32> typeCoder = encodedType;
|
ByteCountCoded<quint32> typeCoder = encodedType;
|
||||||
encodedType = typeCoder; // determine true length
|
encodedType = typeCoder; // determine true length
|
||||||
bytesRead += encodedType.size();
|
|
||||||
quint32 type = typeCoder;
|
quint32 type = typeCoder;
|
||||||
EntityTypes::EntityType entityType = (EntityTypes::EntityType)type;
|
typeOut = (EntityTypes::EntityType)type;
|
||||||
|
|
||||||
EntityItemID tempEntityID(actualID);
|
|
||||||
EntityItemProperties tempProperties;
|
|
||||||
return constructEntityItem(entityType, tempEntityID, tempProperties);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return NULL;
|
|
||||||
|
EntityItemPointer EntityTypes::constructEntityItem(const unsigned char* data, int bytesToRead) {
|
||||||
|
QUuid id;
|
||||||
|
EntityTypes::EntityType type = EntityTypes::Unknown;
|
||||||
|
extractEntityTypeAndID(data, bytesToRead, type, id);
|
||||||
|
if (type > EntityTypes::Unknown && type <= EntityTypes::NUM_TYPES) {
|
||||||
|
EntityItemID tempEntityID(id);
|
||||||
|
EntityItemProperties tempProperties;
|
||||||
|
return constructEntityItem(type, tempEntityID, tempProperties);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityItemPointer EntityTypes::constructEntityItem(const QUuid& id, const EntityItemProperties& properties) {
|
||||||
|
return constructEntityItem(properties.getType(), id, properties);
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,11 +109,14 @@ public:
|
||||||
NUM_TYPES
|
NUM_TYPES
|
||||||
} EntityType;
|
} EntityType;
|
||||||
|
|
||||||
|
static bool typeIsValid(EntityType type);
|
||||||
static const QString& getEntityTypeName(EntityType entityType);
|
static const QString& getEntityTypeName(EntityType entityType);
|
||||||
static EntityTypes::EntityType getEntityTypeFromName(const QString& name);
|
static EntityTypes::EntityType getEntityTypeFromName(const QString& name);
|
||||||
static bool registerEntityType(EntityType entityType, const char* name, EntityTypeFactory factoryMethod);
|
static bool registerEntityType(EntityType entityType, const char* name, EntityTypeFactory factoryMethod);
|
||||||
|
static void extractEntityTypeAndID(const unsigned char* data, int dataLength, EntityTypes::EntityType& typeOut, QUuid& idOut);
|
||||||
static EntityItemPointer constructEntityItem(EntityType entityType, const EntityItemID& entityID, const EntityItemProperties& properties);
|
static EntityItemPointer constructEntityItem(EntityType entityType, const EntityItemID& entityID, const EntityItemProperties& properties);
|
||||||
static EntityItemPointer constructEntityItem(const unsigned char* data, int bytesToRead, ReadBitstreamToTreeParams& args);
|
static EntityItemPointer constructEntityItem(const unsigned char* data, int bytesToRead);
|
||||||
|
static EntityItemPointer constructEntityItem(const QUuid& id, const EntityItemProperties& properties);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static QMap<EntityType, QString> _typeToNameMap;
|
static QMap<EntityType, QString> _typeToNameMap;
|
||||||
|
|
|
@ -96,7 +96,7 @@ const uint8_t RECRUIT_SIMULATION_PRIORITY = VOLUNTEER_SIMULATION_PRIORITY + 1;
|
||||||
// When poking objects with scripts an observer will bid at SCRIPT_EDIT priority.
|
// When poking objects with scripts an observer will bid at SCRIPT_EDIT priority.
|
||||||
const uint8_t SCRIPT_GRAB_SIMULATION_PRIORITY = 128;
|
const uint8_t SCRIPT_GRAB_SIMULATION_PRIORITY = 128;
|
||||||
const uint8_t SCRIPT_POKE_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY - 1;
|
const uint8_t SCRIPT_POKE_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY - 1;
|
||||||
const uint8_t AVATAR_ENTITY_SIMULATION_PRIORITY = SCRIPT_GRAB_SIMULATION_PRIORITY + 1;
|
const uint8_t AVATAR_ENTITY_SIMULATION_PRIORITY = 255;
|
||||||
|
|
||||||
// PERSONAL priority (needs a better name) is the level at which a simulation observer owns its own avatar
|
// 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
|
// which really just means: things that collide with it will be bid at a priority level one lower
|
||||||
|
|
|
@ -96,6 +96,8 @@ class ExtractedMesh;
|
||||||
|
|
||||||
class FBXSerializer : public HFMSerializer {
|
class FBXSerializer : public HFMSerializer {
|
||||||
public:
|
public:
|
||||||
|
virtual ~FBXSerializer() {}
|
||||||
|
|
||||||
MediaType getMediaType() const override;
|
MediaType getMediaType() const override;
|
||||||
std::unique_ptr<hfm::Serializer::Factory> getFactory() const override;
|
std::unique_ptr<hfm::Serializer::Factory> getFactory() const override;
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ class Serializer {
|
||||||
public:
|
public:
|
||||||
class Factory {
|
class Factory {
|
||||||
public:
|
public:
|
||||||
|
virtual ~Factory() {}
|
||||||
virtual std::shared_ptr<Serializer> get() = 0;
|
virtual std::shared_ptr<Serializer> get() = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ PacketVersion versionForPacketType(PacketType packetType) {
|
||||||
return static_cast<PacketVersion>(AvatarMixerPacketVersion::CollisionFlag);
|
return static_cast<PacketVersion>(AvatarMixerPacketVersion::CollisionFlag);
|
||||||
case PacketType::BulkAvatarData:
|
case PacketType::BulkAvatarData:
|
||||||
case PacketType::KillAvatar:
|
case PacketType::KillAvatar:
|
||||||
return static_cast<PacketVersion>(AvatarMixerPacketVersion::GrabTraits);
|
return static_cast<PacketVersion>(AvatarMixerPacketVersion::FasterAvatarEntities);
|
||||||
case PacketType::MessagesData:
|
case PacketType::MessagesData:
|
||||||
return static_cast<PacketVersion>(MessageDataVersion::TextOrBinaryData);
|
return static_cast<PacketVersion>(MessageDataVersion::TextOrBinaryData);
|
||||||
// ICE packets
|
// ICE packets
|
||||||
|
@ -97,6 +97,9 @@ PacketVersion versionForPacketType(PacketType packetType) {
|
||||||
return 22;
|
return 22;
|
||||||
case PacketType::EntityQueryInitialResultsComplete:
|
case PacketType::EntityQueryInitialResultsComplete:
|
||||||
return static_cast<PacketVersion>(EntityVersion::ParticleSpin);
|
return static_cast<PacketVersion>(EntityVersion::ParticleSpin);
|
||||||
|
case PacketType::BulkAvatarTraitsAck:
|
||||||
|
case PacketType::BulkAvatarTraits:
|
||||||
|
return static_cast<PacketVersion>(AvatarMixerPacketVersion::AvatarTraitsAck);
|
||||||
default:
|
default:
|
||||||
return 22;
|
return 22;
|
||||||
}
|
}
|
||||||
|
|
|
@ -133,7 +133,7 @@ public:
|
||||||
EntityQueryInitialResultsComplete,
|
EntityQueryInitialResultsComplete,
|
||||||
BulkAvatarTraits,
|
BulkAvatarTraits,
|
||||||
AudioSoloRequest,
|
AudioSoloRequest,
|
||||||
|
BulkAvatarTraitsAck,
|
||||||
NUM_PACKET_TYPE
|
NUM_PACKET_TYPE
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -310,7 +310,9 @@ enum class AvatarMixerPacketVersion : PacketVersion {
|
||||||
FarGrabJointsRedux,
|
FarGrabJointsRedux,
|
||||||
JointTransScaled,
|
JointTransScaled,
|
||||||
GrabTraits,
|
GrabTraits,
|
||||||
CollisionFlag
|
CollisionFlag,
|
||||||
|
AvatarTraitsAck,
|
||||||
|
FasterAvatarEntities
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class DomainConnectRequestVersion : PacketVersion {
|
enum class DomainConnectRequestVersion : PacketVersion {
|
||||||
|
|
|
@ -16,7 +16,8 @@
|
||||||
using namespace udt;
|
using namespace udt;
|
||||||
|
|
||||||
PacketQueue::PacketQueue(MessageNumber messageNumber) : _currentMessageNumber(messageNumber) {
|
PacketQueue::PacketQueue(MessageNumber messageNumber) : _currentMessageNumber(messageNumber) {
|
||||||
_channels.emplace_back(new std::list<PacketPointer>());
|
_channels.emplace_front(new std::list<PacketPointer>());
|
||||||
|
_currentChannel = _channels.begin();
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageNumber PacketQueue::getNextMessageNumber() {
|
MessageNumber PacketQueue::getNextMessageNumber() {
|
||||||
|
@ -27,21 +28,28 @@ MessageNumber PacketQueue::getNextMessageNumber() {
|
||||||
|
|
||||||
bool PacketQueue::isEmpty() const {
|
bool PacketQueue::isEmpty() const {
|
||||||
LockGuard locker(_packetsLock);
|
LockGuard locker(_packetsLock);
|
||||||
|
|
||||||
// Only the main channel and it is empty
|
// Only the main channel and it is empty
|
||||||
return (_channels.size() == 1) && _channels.front()->empty();
|
return _channels.size() == 1 && _channels.front()->empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
PacketQueue::PacketPointer PacketQueue::takePacket() {
|
PacketQueue::PacketPointer PacketQueue::takePacket() {
|
||||||
LockGuard locker(_packetsLock);
|
LockGuard locker(_packetsLock);
|
||||||
|
|
||||||
if (isEmpty()) {
|
if (isEmpty()) {
|
||||||
return PacketPointer();
|
return PacketPointer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find next non empty channel
|
// handle the case where we are looking at the first channel and it is empty
|
||||||
if (_channels[nextIndex()]->empty()) {
|
if (_currentChannel == _channels.begin() && (*_currentChannel)->empty()) {
|
||||||
nextIndex();
|
++_currentChannel;
|
||||||
}
|
}
|
||||||
auto& channel = _channels[_currentIndex];
|
|
||||||
|
// at this point the current channel should always not be at the end and should also not be empty
|
||||||
|
Q_ASSERT(_currentChannel != _channels.end());
|
||||||
|
|
||||||
|
auto& channel = *_currentChannel;
|
||||||
|
|
||||||
Q_ASSERT(!channel->empty());
|
Q_ASSERT(!channel->empty());
|
||||||
|
|
||||||
// Take front packet
|
// Take front packet
|
||||||
|
@ -49,20 +57,28 @@ PacketQueue::PacketPointer PacketQueue::takePacket() {
|
||||||
channel->pop_front();
|
channel->pop_front();
|
||||||
|
|
||||||
// Remove now empty channel (Don't remove the main channel)
|
// Remove now empty channel (Don't remove the main channel)
|
||||||
if (channel->empty() && _currentIndex != 0) {
|
if (channel->empty() && _currentChannel != _channels.begin()) {
|
||||||
channel->swap(*_channels.back());
|
// erase the current channel and slide the iterator to the next channel
|
||||||
_channels.pop_back();
|
_currentChannel = _channels.erase(_currentChannel);
|
||||||
--_currentIndex;
|
} else {
|
||||||
|
++_currentChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// push forward our number of channels taken from
|
||||||
|
++_channelsVisitedCount;
|
||||||
|
|
||||||
|
// check if we need to restart back at the front channel (main)
|
||||||
|
// to respect our capped number of channels considered concurrently
|
||||||
|
static const int MAX_CHANNELS_SENT_CONCURRENTLY = 16;
|
||||||
|
|
||||||
|
if (_currentChannel == _channels.end() || _channelsVisitedCount >= MAX_CHANNELS_SENT_CONCURRENTLY) {
|
||||||
|
_channelsVisitedCount = 0;
|
||||||
|
_currentChannel = _channels.begin();
|
||||||
}
|
}
|
||||||
|
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned int PacketQueue::nextIndex() {
|
|
||||||
_currentIndex = (_currentIndex + 1) % _channels.size();
|
|
||||||
return _currentIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PacketQueue::queuePacket(PacketPointer packet) {
|
void PacketQueue::queuePacket(PacketPointer packet) {
|
||||||
LockGuard locker(_packetsLock);
|
LockGuard locker(_packetsLock);
|
||||||
_channels.front()->push_back(std::move(packet));
|
_channels.front()->push_back(std::move(packet));
|
||||||
|
|
|
@ -30,8 +30,9 @@ class PacketQueue {
|
||||||
using LockGuard = std::lock_guard<Mutex>;
|
using LockGuard = std::lock_guard<Mutex>;
|
||||||
using PacketPointer = std::unique_ptr<Packet>;
|
using PacketPointer = std::unique_ptr<Packet>;
|
||||||
using PacketListPointer = std::unique_ptr<PacketList>;
|
using PacketListPointer = std::unique_ptr<PacketList>;
|
||||||
using Channel = std::unique_ptr<std::list<PacketPointer>>;
|
using RawChannel = std::list<PacketPointer>;
|
||||||
using Channels = std::vector<Channel>;
|
using Channel = std::unique_ptr<RawChannel>;
|
||||||
|
using Channels = std::list<Channel>;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
PacketQueue(MessageNumber messageNumber = 0);
|
PacketQueue(MessageNumber messageNumber = 0);
|
||||||
|
@ -47,16 +48,17 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
MessageNumber getNextMessageNumber();
|
MessageNumber getNextMessageNumber();
|
||||||
unsigned int nextIndex();
|
|
||||||
|
|
||||||
MessageNumber _currentMessageNumber { 0 };
|
MessageNumber _currentMessageNumber { 0 };
|
||||||
|
|
||||||
mutable Mutex _packetsLock; // Protects the packets to be sent.
|
mutable Mutex _packetsLock; // Protects the packets to be sent.
|
||||||
Channels _channels; // One channel per packet list + Main channel
|
Channels _channels; // One channel per packet list + Main channel
|
||||||
unsigned int _currentIndex { 0 };
|
|
||||||
|
Channels::iterator _currentChannel;
|
||||||
|
unsigned int _channelsVisitedCount { 0 };
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#endif // hifi_PacketQueue_h
|
#endif // hifi_PacketQueue_h
|
||||||
|
|
|
@ -61,6 +61,9 @@ private:
|
||||||
Mutex2& _mutex2;
|
Mutex2& _mutex2;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const microseconds SendQueue::MAXIMUM_ESTIMATED_TIMEOUT = seconds(5);
|
||||||
|
const microseconds SendQueue::MINIMUM_ESTIMATED_TIMEOUT = milliseconds(10);
|
||||||
|
|
||||||
std::unique_ptr<SendQueue> SendQueue::create(Socket* socket, HifiSockAddr destination, SequenceNumber currentSequenceNumber,
|
std::unique_ptr<SendQueue> SendQueue::create(Socket* socket, HifiSockAddr destination, SequenceNumber currentSequenceNumber,
|
||||||
MessageNumber currentMessageNumber, bool hasReceivedHandshakeACK) {
|
MessageNumber currentMessageNumber, bool hasReceivedHandshakeACK) {
|
||||||
Q_ASSERT_X(socket, "SendQueue::create", "Must be called with a valid Socket*");
|
Q_ASSERT_X(socket, "SendQueue::create", "Must be called with a valid Socket*");
|
||||||
|
@ -507,12 +510,8 @@ bool SendQueue::isInactive(bool attemptedToSendPacket) {
|
||||||
|
|
||||||
auto estimatedTimeout = std::chrono::microseconds(_estimatedTimeout);
|
auto estimatedTimeout = std::chrono::microseconds(_estimatedTimeout);
|
||||||
|
|
||||||
// cap our maximum estimated timeout to the already unreasonable 5 seconds
|
// Clamp timeout beween 10 ms and 5 s
|
||||||
const auto MAXIMUM_ESTIMATED_TIMEOUT = std::chrono::seconds(5);
|
estimatedTimeout = std::min(MAXIMUM_ESTIMATED_TIMEOUT, std::max(MINIMUM_ESTIMATED_TIMEOUT, estimatedTimeout));
|
||||||
|
|
||||||
if (estimatedTimeout > MAXIMUM_ESTIMATED_TIMEOUT) {
|
|
||||||
estimatedTimeout = MAXIMUM_ESTIMATED_TIMEOUT;
|
|
||||||
}
|
|
||||||
|
|
||||||
// use our condition_variable_any to wait
|
// use our condition_variable_any to wait
|
||||||
auto cvStatus = _emptyCondition.wait_for(locker, estimatedTimeout);
|
auto cvStatus = _emptyCondition.wait_for(locker, estimatedTimeout);
|
||||||
|
|
|
@ -140,6 +140,9 @@ private:
|
||||||
std::condition_variable_any _emptyCondition;
|
std::condition_variable_any _emptyCondition;
|
||||||
|
|
||||||
std::chrono::high_resolution_clock::time_point _lastPacketSentAt;
|
std::chrono::high_resolution_clock::time_point _lastPacketSentAt;
|
||||||
|
|
||||||
|
static const std::chrono::microseconds MAXIMUM_ESTIMATED_TIMEOUT;
|
||||||
|
static const std::chrono::microseconds MINIMUM_ESTIMATED_TIMEOUT;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,11 @@ void OctreePacketData::changeSettings(bool enableCompression, unsigned int targe
|
||||||
_enableCompression = enableCompression;
|
_enableCompression = enableCompression;
|
||||||
_targetSize = targetSize;
|
_targetSize = targetSize;
|
||||||
_uncompressedByteArray.resize(_targetSize);
|
_uncompressedByteArray.resize(_targetSize);
|
||||||
_compressedByteArray.resize(_targetSize);
|
if (_enableCompression) {
|
||||||
|
_compressedByteArray.resize(_targetSize);
|
||||||
|
} else {
|
||||||
|
_compressedByteArray.resize(0);
|
||||||
|
}
|
||||||
|
|
||||||
_uncompressed = (unsigned char*)_uncompressedByteArray.data();
|
_uncompressed = (unsigned char*)_uncompressedByteArray.data();
|
||||||
_compressed = (unsigned char*)_compressedByteArray.data();
|
_compressed = (unsigned char*)_compressedByteArray.data();
|
||||||
|
@ -586,13 +590,10 @@ bool OctreePacketData::appendRawData(QByteArray data) {
|
||||||
AtomicUIntStat OctreePacketData::_compressContentTime { 0 };
|
AtomicUIntStat OctreePacketData::_compressContentTime { 0 };
|
||||||
AtomicUIntStat OctreePacketData::_compressContentCalls { 0 };
|
AtomicUIntStat OctreePacketData::_compressContentCalls { 0 };
|
||||||
|
|
||||||
bool OctreePacketData::compressContent() {
|
bool OctreePacketData::compressContent() {
|
||||||
PerformanceWarning warn(false, "OctreePacketData::compressContent()", false, &_compressContentTime, &_compressContentCalls);
|
PerformanceWarning warn(false, "OctreePacketData::compressContent()", false, &_compressContentTime, &_compressContentCalls);
|
||||||
|
assert(_dirty);
|
||||||
// without compression, we always pass...
|
assert(_enableCompression);
|
||||||
if (!_enableCompression) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_bytesInUseLastCheck = _bytesInUse;
|
_bytesInUseLastCheck = _bytesInUse;
|
||||||
|
|
||||||
|
@ -605,13 +606,13 @@ bool OctreePacketData::compressContent() {
|
||||||
|
|
||||||
QByteArray compressedData = qCompress(uncompressedData, uncompressedSize, MAX_COMPRESSION);
|
QByteArray compressedData = qCompress(uncompressedData, uncompressedSize, MAX_COMPRESSION);
|
||||||
|
|
||||||
if (compressedData.size() < (int)MAX_OCTREE_PACKET_DATA_SIZE) {
|
if (compressedData.size() < _compressedByteArray.size()) {
|
||||||
_compressedBytes = compressedData.size();
|
_compressedBytes = compressedData.size();
|
||||||
memcpy(_compressed, compressedData.constData(), _compressedBytes);
|
memcpy(_compressed, compressedData.constData(), _compressedBytes);
|
||||||
_dirty = false;
|
_dirty = false;
|
||||||
success = true;
|
success = true;
|
||||||
} else {
|
} else {
|
||||||
qCWarning(octree) << "OctreePacketData::compressContent -- compressedData.size >= MAX_OCTREE_PACKET_DATA_SIZE";
|
qCWarning(octree) << "OctreePacketData::compressContent -- compressedData.size >= " << _compressedByteArray.size();
|
||||||
assert(false);
|
assert(false);
|
||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
|
@ -644,8 +645,7 @@ void OctreePacketData::loadFinalizedContent(const unsigned char* data, int lengt
|
||||||
memcpy(_uncompressed, uncompressedData.constData(), _bytesInUse);
|
memcpy(_uncompressed, uncompressedData.constData(), _bytesInUse);
|
||||||
} else {
|
} else {
|
||||||
memcpy(_uncompressed, data, length);
|
memcpy(_uncompressed, data, length);
|
||||||
memcpy(_compressed, data, length);
|
_bytesInUse = length;
|
||||||
_bytesInUse = _compressedBytes = length;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (_debug) {
|
if (_debug) {
|
||||||
|
|
|
@ -306,6 +306,8 @@ const btCollisionShape* EntityMotionState::computeNewShape() {
|
||||||
return getShapeManager()->getShape(shapeInfo);
|
return getShapeManager()->getShape(shapeInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uint8_t MAX_NUM_INACTIVE_UPDATES = 20;
|
||||||
|
|
||||||
bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) {
|
bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) {
|
||||||
// NOTE: this method is only ever called when the entity simulation is locally owned
|
// NOTE: this method is only ever called when the entity simulation is locally owned
|
||||||
DETAILED_PROFILE_RANGE(simulation_physics, "CheckOutOfSync");
|
DETAILED_PROFILE_RANGE(simulation_physics, "CheckOutOfSync");
|
||||||
|
@ -315,15 +317,10 @@ bool EntityMotionState::remoteSimulationOutOfSync(uint32_t simulationStep) {
|
||||||
// TODO: need to be able to detect when logic dictates we *decrease* priority
|
// TODO: need to be able to detect when logic dictates we *decrease* priority
|
||||||
// WIP: print info whenever _bidPriority mismatches what is known to the entity
|
// WIP: print info whenever _bidPriority mismatches what is known to the entity
|
||||||
|
|
||||||
if (_entity->dynamicDataNeedsTransmit()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int numSteps = simulationStep - _lastStep;
|
int numSteps = simulationStep - _lastStep;
|
||||||
float dt = (float)(numSteps) * PHYSICS_ENGINE_FIXED_SUBSTEP;
|
float dt = (float)(numSteps) * PHYSICS_ENGINE_FIXED_SUBSTEP;
|
||||||
|
|
||||||
if (_numInactiveUpdates > 0) {
|
if (_numInactiveUpdates > 0) {
|
||||||
const uint8_t MAX_NUM_INACTIVE_UPDATES = 20;
|
|
||||||
if (_numInactiveUpdates > MAX_NUM_INACTIVE_UPDATES) {
|
if (_numInactiveUpdates > MAX_NUM_INACTIVE_UPDATES) {
|
||||||
// clear local ownership (stop sending updates) and let the server clear itself
|
// clear local ownership (stop sending updates) and let the server clear itself
|
||||||
_entity->clearSimulationOwnership();
|
_entity->clearSimulationOwnership();
|
||||||
|
@ -451,8 +448,13 @@ void EntityMotionState::updateSendVelocities() {
|
||||||
if (!_body->isKinematicObject()) {
|
if (!_body->isKinematicObject()) {
|
||||||
clearObjectVelocities();
|
clearObjectVelocities();
|
||||||
}
|
}
|
||||||
// we pretend we sent the inactive update for this object
|
if (_entity->getEntityHostType() == entity::HostType::AVATAR) {
|
||||||
_numInactiveUpdates = 1;
|
// AvatarEntities only ever need to send one update (their updates are sent over a lossless protocol)
|
||||||
|
// so we set the count to the max to prevent resends
|
||||||
|
_numInactiveUpdates = MAX_NUM_INACTIVE_UPDATES;
|
||||||
|
} else {
|
||||||
|
++_numInactiveUpdates;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
glm::vec3 gravity = _entity->getGravity();
|
glm::vec3 gravity = _entity->getGravity();
|
||||||
|
|
||||||
|
|
|
@ -44,33 +44,24 @@ PluginManagerPointer PluginManager::getInstance() {
|
||||||
return DependencyManager::get<PluginManager>();
|
return DependencyManager::get<PluginManager>();
|
||||||
}
|
}
|
||||||
|
|
||||||
QString getPluginNameFromMetaData(QJsonObject object) {
|
QString getPluginNameFromMetaData(const QJsonObject& object) {
|
||||||
static const char* METADATA_KEY = "MetaData";
|
static const char* METADATA_KEY = "MetaData";
|
||||||
static const char* NAME_KEY = "name";
|
static const char* NAME_KEY = "name";
|
||||||
|
return object[METADATA_KEY][NAME_KEY].toString("");
|
||||||
if (!object.contains(METADATA_KEY) || !object[METADATA_KEY].isObject()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto metaDataObject = object[METADATA_KEY].toObject();
|
|
||||||
|
|
||||||
if (!metaDataObject.contains(NAME_KEY) || !metaDataObject[NAME_KEY].isString()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return metaDataObject[NAME_KEY].toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QString getPluginIIDFromMetaData(QJsonObject object) {
|
QString getPluginIIDFromMetaData(const QJsonObject& object) {
|
||||||
static const char* IID_KEY = "IID";
|
static const char* IID_KEY = "IID";
|
||||||
|
return object[IID_KEY].toString("");
|
||||||
if (!object.contains(IID_KEY) || !object[IID_KEY].isString()) {
|
|
||||||
return QString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return object[IID_KEY].toString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int getPluginInterfaceVersionFromMetaData(const QJsonObject& object) {
|
||||||
|
static const QString METADATA_KEY = "MetaData";
|
||||||
|
static const QString NAME_KEY = "version";
|
||||||
|
return object[METADATA_KEY][NAME_KEY].toInt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
QStringList preferredDisplayPlugins;
|
QStringList preferredDisplayPlugins;
|
||||||
QStringList disabledDisplays;
|
QStringList disabledDisplays;
|
||||||
QStringList disabledInputs;
|
QStringList disabledInputs;
|
||||||
|
@ -117,10 +108,16 @@ const LoaderList& getLoadedPlugins() {
|
||||||
QSharedPointer<QPluginLoader> loader(new QPluginLoader(pluginPath + plugin));
|
QSharedPointer<QPluginLoader> loader(new QPluginLoader(pluginPath + plugin));
|
||||||
|
|
||||||
if (isDisabled(loader->metaData())) {
|
if (isDisabled(loader->metaData())) {
|
||||||
qWarning() << "Plugin" << qPrintable(plugin) << "is disabled";
|
qCWarning(plugins) << "Plugin" << qPrintable(plugin) << "is disabled";
|
||||||
// Skip this one, it's disabled
|
// Skip this one, it's disabled
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (getPluginInterfaceVersionFromMetaData(loader->metaData()) != HIFI_PLUGIN_INTERFACE_VERSION) {
|
||||||
|
qCWarning(plugins) << "Plugin" << qPrintable(plugin) << "interface version doesn't match, not loading:"
|
||||||
|
<< getPluginInterfaceVersionFromMetaData(loader->metaData())
|
||||||
|
<< "doesn't match" << HIFI_PLUGIN_INTERFACE_VERSION;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (loader->load()) {
|
if (loader->load()) {
|
||||||
qCDebug(plugins) << "Plugin" << qPrintable(plugin) << "loaded successfully";
|
qCDebug(plugins) << "Plugin" << qPrintable(plugin) << "loaded successfully";
|
||||||
|
|
|
@ -61,3 +61,12 @@ private:
|
||||||
DisplayPluginList _displayPlugins;
|
DisplayPluginList _displayPlugins;
|
||||||
InputPluginList _inputPlugins;
|
InputPluginList _inputPlugins;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: we should define this value in CMake, and then use CMake
|
||||||
|
// templating to generate the individual plugin.json files, so that we
|
||||||
|
// don't have to update every plugin.json file whenever we update this
|
||||||
|
// value. The value should match "version" in
|
||||||
|
// plugins/*/src/plugin.json
|
||||||
|
// plugins/oculus/src/oculus.json
|
||||||
|
// etc
|
||||||
|
static const int HIFI_PLUGIN_INTERFACE_VERSION = 1;
|
||||||
|
|
|
@ -61,12 +61,11 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende
|
||||||
// FIXME: calling this here before the zones/lights are drawn during the deferred/forward passes means we're actually using the frames from the previous draw
|
// FIXME: calling this here before the zones/lights are drawn during the deferred/forward passes means we're actually using the frames from the previous draw
|
||||||
// Fetch the current frame stacks from all the stages
|
// Fetch the current frame stacks from all the stages
|
||||||
// Starting with the Light Frame genreated in previous tasks
|
// Starting with the Light Frame genreated in previous tasks
|
||||||
|
|
||||||
const auto& lightFrame = input.getN<Input>(0);
|
|
||||||
|
|
||||||
const auto setupOutput = task.addJob<RenderShadowSetup>("ShadowSetup", input);
|
const auto setupOutput = task.addJob<RenderShadowSetup>("ShadowSetup", input);
|
||||||
const auto queryResolution = setupOutput.getN<RenderShadowSetup::Output>(1);
|
const auto queryResolution = setupOutput.getN<RenderShadowSetup::Output>(1);
|
||||||
const auto shadowFrame = setupOutput.getN<RenderShadowSetup::Output>(3);
|
const auto shadowFrame = setupOutput.getN<RenderShadowSetup::Output>(3);
|
||||||
|
const auto currentKeyLight = setupOutput.getN<RenderShadowSetup::Output>(4);
|
||||||
// Fetch and cull the items from the scene
|
// Fetch and cull the items from the scene
|
||||||
|
|
||||||
static const auto shadowCasterReceiverFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask);
|
static const auto shadowCasterReceiverFilter = ItemFilter::Builder::visibleWorldItems().withTypeShape().withOpaque().withoutLayered().withTagBits(tagBits, tagMask);
|
||||||
|
@ -108,7 +107,7 @@ void RenderShadowTask::build(JobModel& task, const render::Varying& input, rende
|
||||||
antiFrustum = cascadeFrustums[i - 2];
|
antiFrustum = cascadeFrustums[i - 2];
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto cullInputs = CullShadowBounds::Inputs(sortedShapes, shadowFilter, antiFrustum, lightFrame, cascadeSetupOutput.getN<RenderShadowCascadeSetup::Outputs>(2)).asVarying();
|
const auto cullInputs = CullShadowBounds::Inputs(sortedShapes, shadowFilter, antiFrustum, currentKeyLight, cascadeSetupOutput.getN<RenderShadowCascadeSetup::Outputs>(2)).asVarying();
|
||||||
sprintf(jobName, "CullShadowCascade%d", i);
|
sprintf(jobName, "CullShadowCascade%d", i);
|
||||||
const auto culledShadowItemsAndBounds = task.addJob<CullShadowBounds>(jobName, cullInputs);
|
const auto culledShadowItemsAndBounds = task.addJob<CullShadowBounds>(jobName, cullInputs);
|
||||||
|
|
||||||
|
@ -356,16 +355,17 @@ void RenderShadowSetup::run(const render::RenderContextPointer& renderContext, c
|
||||||
_shadowFrameCache->_objects.clear();
|
_shadowFrameCache->_objects.clear();
|
||||||
output.edit3() = _shadowFrameCache;
|
output.edit3() = _shadowFrameCache;
|
||||||
|
|
||||||
if (!lightingModel->isShadowEnabled() || !lightStage->getCurrentKeyLight(lightFrame) || !lightStage->getCurrentKeyLight(lightFrame)->getCastShadows()) {
|
const auto currentKeyLight = lightStage->getCurrentKeyLight(lightFrame);
|
||||||
|
if (!lightingModel->isShadowEnabled() || !currentKeyLight || !currentKeyLight->getCastShadows()) {
|
||||||
renderContext->taskFlow.abortTask();
|
renderContext->taskFlow.abortTask();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
output.edit4() = currentKeyLight;
|
||||||
|
|
||||||
// Cache old render args
|
// Cache old render args
|
||||||
RenderArgs* args = renderContext->args;
|
RenderArgs* args = renderContext->args;
|
||||||
|
|
||||||
output.edit0() = args->_renderMode;
|
output.edit0() = args->_renderMode;
|
||||||
output.edit1() = glm::ivec2(0, 0);
|
|
||||||
// Save main camera frustum
|
// Save main camera frustum
|
||||||
*_cameraFrustum = args->getViewFrustum();
|
*_cameraFrustum = args->getViewFrustum();
|
||||||
output.edit2() = _cameraFrustum;
|
output.edit2() = _cameraFrustum;
|
||||||
|
@ -373,75 +373,72 @@ void RenderShadowSetup::run(const render::RenderContextPointer& renderContext, c
|
||||||
if (!_globalShadowObject) {
|
if (!_globalShadowObject) {
|
||||||
_globalShadowObject = std::make_shared<LightStage::Shadow>(graphics::LightPointer(), SHADOW_MAX_DISTANCE, SHADOW_CASCADE_COUNT);
|
_globalShadowObject = std::make_shared<LightStage::Shadow>(graphics::LightPointer(), SHADOW_MAX_DISTANCE, SHADOW_CASCADE_COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto theGlobalLight = lightStage->getCurrentKeyLight(lightFrame);
|
|
||||||
if (theGlobalLight && theGlobalLight->getCastShadows()) {
|
|
||||||
_globalShadowObject->setLight(theGlobalLight);
|
|
||||||
_globalShadowObject->setKeylightFrustum(args->getViewFrustum(), SHADOW_FRUSTUM_NEAR, SHADOW_FRUSTUM_FAR);
|
|
||||||
|
|
||||||
auto& firstCascade = _globalShadowObject->getCascade(0);
|
_globalShadowObject->setLight(currentKeyLight);
|
||||||
auto& firstCascadeFrustum = firstCascade.getFrustum();
|
_globalShadowObject->setKeylightFrustum(args->getViewFrustum(), SHADOW_FRUSTUM_NEAR, SHADOW_FRUSTUM_FAR);
|
||||||
unsigned int cascadeIndex;
|
|
||||||
|
|
||||||
// Adjust each cascade frustum
|
auto& firstCascade = _globalShadowObject->getCascade(0);
|
||||||
for (cascadeIndex = 0; cascadeIndex < _globalShadowObject->getCascadeCount(); ++cascadeIndex) {
|
auto& firstCascadeFrustum = firstCascade.getFrustum();
|
||||||
auto& bias = _bias[cascadeIndex];
|
unsigned int cascadeIndex;
|
||||||
_globalShadowObject->setKeylightCascadeFrustum(cascadeIndex, args->getViewFrustum(),
|
|
||||||
SHADOW_FRUSTUM_NEAR, SHADOW_FRUSTUM_FAR,
|
|
||||||
bias._constant, bias._slope);
|
|
||||||
}
|
|
||||||
|
|
||||||
_shadowFrameCache->pushShadow(_globalShadowObject);
|
// Adjust each cascade frustum
|
||||||
|
for (cascadeIndex = 0; cascadeIndex < _globalShadowObject->getCascadeCount(); ++cascadeIndex) {
|
||||||
// Now adjust coarse frustum bounds
|
auto& bias = _bias[cascadeIndex];
|
||||||
auto frustumPosition = firstCascadeFrustum->getPosition();
|
_globalShadowObject->setKeylightCascadeFrustum(cascadeIndex, args->getViewFrustum(),
|
||||||
auto farTopLeft = firstCascadeFrustum->getFarTopLeft() - frustumPosition;
|
SHADOW_FRUSTUM_NEAR, SHADOW_FRUSTUM_FAR,
|
||||||
auto farBottomRight = firstCascadeFrustum->getFarBottomRight() - frustumPosition;
|
bias._constant, bias._slope);
|
||||||
|
|
||||||
auto left = glm::dot(farTopLeft, firstCascadeFrustum->getRight());
|
|
||||||
auto right = glm::dot(farBottomRight, firstCascadeFrustum->getRight());
|
|
||||||
auto top = glm::dot(farTopLeft, firstCascadeFrustum->getUp());
|
|
||||||
auto bottom = glm::dot(farBottomRight, firstCascadeFrustum->getUp());
|
|
||||||
auto near = firstCascadeFrustum->getNearClip();
|
|
||||||
auto far = firstCascadeFrustum->getFarClip();
|
|
||||||
|
|
||||||
for (cascadeIndex = 1; cascadeIndex < _globalShadowObject->getCascadeCount(); ++cascadeIndex) {
|
|
||||||
auto& cascadeFrustum = _globalShadowObject->getCascade(cascadeIndex).getFrustum();
|
|
||||||
|
|
||||||
farTopLeft = cascadeFrustum->getFarTopLeft() - frustumPosition;
|
|
||||||
farBottomRight = cascadeFrustum->getFarBottomRight() - frustumPosition;
|
|
||||||
|
|
||||||
auto cascadeLeft = glm::dot(farTopLeft, cascadeFrustum->getRight());
|
|
||||||
auto cascadeRight = glm::dot(farBottomRight, cascadeFrustum->getRight());
|
|
||||||
auto cascadeTop = glm::dot(farTopLeft, cascadeFrustum->getUp());
|
|
||||||
auto cascadeBottom = glm::dot(farBottomRight, cascadeFrustum->getUp());
|
|
||||||
auto cascadeNear = cascadeFrustum->getNearClip();
|
|
||||||
auto cascadeFar = cascadeFrustum->getFarClip();
|
|
||||||
left = glm::min(left, cascadeLeft);
|
|
||||||
right = glm::max(right, cascadeRight);
|
|
||||||
bottom = glm::min(bottom, cascadeBottom);
|
|
||||||
top = glm::max(top, cascadeTop);
|
|
||||||
near = glm::min(near, cascadeNear);
|
|
||||||
far = glm::max(far, cascadeFar);
|
|
||||||
}
|
|
||||||
|
|
||||||
_coarseShadowFrustum->setPosition(firstCascadeFrustum->getPosition());
|
|
||||||
_coarseShadowFrustum->setOrientation(firstCascadeFrustum->getOrientation());
|
|
||||||
_coarseShadowFrustum->setProjection(glm::ortho<float>(left, right, bottom, top, near, far));
|
|
||||||
_coarseShadowFrustum->calculate();
|
|
||||||
|
|
||||||
// Push frustum for further culling and selection
|
|
||||||
args->pushViewFrustum(*_coarseShadowFrustum);
|
|
||||||
|
|
||||||
args->_renderMode = RenderArgs::SHADOW_RENDER_MODE;
|
|
||||||
|
|
||||||
// We want for the octree query enough resolution to catch the details in the lowest cascade. So compute
|
|
||||||
// the desired resolution for the first cascade frustum and extrapolate it to the coarse frustum.
|
|
||||||
glm::ivec2 queryResolution = firstCascade.framebuffer->getSize();
|
|
||||||
queryResolution.x = int(queryResolution.x * _coarseShadowFrustum->getWidth() / firstCascadeFrustum->getWidth());
|
|
||||||
queryResolution.y = int(queryResolution.y * _coarseShadowFrustum->getHeight() / firstCascadeFrustum->getHeight());
|
|
||||||
output.edit1() = queryResolution;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_shadowFrameCache->pushShadow(_globalShadowObject);
|
||||||
|
|
||||||
|
// Now adjust coarse frustum bounds
|
||||||
|
auto frustumPosition = firstCascadeFrustum->getPosition();
|
||||||
|
auto farTopLeft = firstCascadeFrustum->getFarTopLeft() - frustumPosition;
|
||||||
|
auto farBottomRight = firstCascadeFrustum->getFarBottomRight() - frustumPosition;
|
||||||
|
|
||||||
|
auto left = glm::dot(farTopLeft, firstCascadeFrustum->getRight());
|
||||||
|
auto right = glm::dot(farBottomRight, firstCascadeFrustum->getRight());
|
||||||
|
auto top = glm::dot(farTopLeft, firstCascadeFrustum->getUp());
|
||||||
|
auto bottom = glm::dot(farBottomRight, firstCascadeFrustum->getUp());
|
||||||
|
auto near = firstCascadeFrustum->getNearClip();
|
||||||
|
auto far = firstCascadeFrustum->getFarClip();
|
||||||
|
|
||||||
|
for (cascadeIndex = 1; cascadeIndex < _globalShadowObject->getCascadeCount(); ++cascadeIndex) {
|
||||||
|
auto& cascadeFrustum = _globalShadowObject->getCascade(cascadeIndex).getFrustum();
|
||||||
|
|
||||||
|
farTopLeft = cascadeFrustum->getFarTopLeft() - frustumPosition;
|
||||||
|
farBottomRight = cascadeFrustum->getFarBottomRight() - frustumPosition;
|
||||||
|
|
||||||
|
auto cascadeLeft = glm::dot(farTopLeft, cascadeFrustum->getRight());
|
||||||
|
auto cascadeRight = glm::dot(farBottomRight, cascadeFrustum->getRight());
|
||||||
|
auto cascadeTop = glm::dot(farTopLeft, cascadeFrustum->getUp());
|
||||||
|
auto cascadeBottom = glm::dot(farBottomRight, cascadeFrustum->getUp());
|
||||||
|
auto cascadeNear = cascadeFrustum->getNearClip();
|
||||||
|
auto cascadeFar = cascadeFrustum->getFarClip();
|
||||||
|
left = glm::min(left, cascadeLeft);
|
||||||
|
right = glm::max(right, cascadeRight);
|
||||||
|
bottom = glm::min(bottom, cascadeBottom);
|
||||||
|
top = glm::max(top, cascadeTop);
|
||||||
|
near = glm::min(near, cascadeNear);
|
||||||
|
far = glm::max(far, cascadeFar);
|
||||||
|
}
|
||||||
|
|
||||||
|
_coarseShadowFrustum->setPosition(firstCascadeFrustum->getPosition());
|
||||||
|
_coarseShadowFrustum->setOrientation(firstCascadeFrustum->getOrientation());
|
||||||
|
_coarseShadowFrustum->setProjection(glm::ortho<float>(left, right, bottom, top, near, far));
|
||||||
|
_coarseShadowFrustum->calculate();
|
||||||
|
|
||||||
|
// Push frustum for further culling and selection
|
||||||
|
args->pushViewFrustum(*_coarseShadowFrustum);
|
||||||
|
|
||||||
|
args->_renderMode = RenderArgs::SHADOW_RENDER_MODE;
|
||||||
|
|
||||||
|
// We want for the octree query enough resolution to catch the details in the lowest cascade. So compute
|
||||||
|
// the desired resolution for the first cascade frustum and extrapolate it to the coarse frustum.
|
||||||
|
glm::ivec2 queryResolution = firstCascade.framebuffer->getSize();
|
||||||
|
queryResolution.x = int(queryResolution.x * _coarseShadowFrustum->getWidth() / firstCascadeFrustum->getWidth());
|
||||||
|
queryResolution.y = int(queryResolution.y * _coarseShadowFrustum->getHeight() / firstCascadeFrustum->getHeight());
|
||||||
|
output.edit1() = queryResolution;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RenderShadowCascadeSetup::run(const render::RenderContextPointer& renderContext, const Inputs& input, Outputs& output) {
|
void RenderShadowCascadeSetup::run(const render::RenderContextPointer& renderContext, const Inputs& input, Outputs& output) {
|
||||||
|
@ -539,20 +536,20 @@ void CullShadowBounds::run(const render::RenderContextPointer& renderContext, co
|
||||||
outShapes.clear();
|
outShapes.clear();
|
||||||
outBounds = AABox();
|
outBounds = AABox();
|
||||||
|
|
||||||
const auto& lightFrame = *inputs.get3();
|
const auto currentKeyLight = inputs.get3();
|
||||||
auto cullFunctor = inputs.get4();
|
auto cullFunctor = inputs.get4();
|
||||||
|
|
||||||
render::CullFunctor shadowCullFunctor = [cullFunctor](const RenderArgs* args, const AABox& bounds) {
|
render::CullFunctor shadowCullFunctor = [cullFunctor](const RenderArgs* args, const AABox& bounds) {
|
||||||
return cullFunctor(args, bounds);
|
return cullFunctor(args, bounds);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!filter.selectsNothing()) {
|
if (!filter.selectsNothing() && currentKeyLight) {
|
||||||
auto& details = args->_details.edit(RenderDetails::SHADOW);
|
auto& details = args->_details.edit(RenderDetails::SHADOW);
|
||||||
render::CullTest test(shadowCullFunctor, args, details, antiFrustum);
|
render::CullTest test(shadowCullFunctor, args, details, antiFrustum);
|
||||||
auto scene = args->_scene;
|
auto scene = args->_scene;
|
||||||
auto lightStage = renderContext->_scene->getStage<LightStage>();
|
auto lightStage = renderContext->_scene->getStage<LightStage>();
|
||||||
assert(lightStage);
|
assert(lightStage);
|
||||||
const auto globalLightDir = lightStage->getCurrentKeyLight(lightFrame)->getDirection();
|
const auto globalLightDir = currentKeyLight->getDirection();
|
||||||
auto castersFilter = render::ItemFilter::Builder(filter).withShadowCaster().build();
|
auto castersFilter = render::ItemFilter::Builder(filter).withShadowCaster().build();
|
||||||
const auto& receiversFilter = filter;
|
const auto& receiversFilter = filter;
|
||||||
|
|
||||||
|
|
|
@ -104,7 +104,7 @@ signals:
|
||||||
class RenderShadowSetup {
|
class RenderShadowSetup {
|
||||||
public:
|
public:
|
||||||
using Input = RenderShadowTask::Input;
|
using Input = RenderShadowTask::Input;
|
||||||
using Output = render::VaryingSet4<RenderArgs::RenderMode, glm::ivec2, ViewFrustumPointer, LightStage::ShadowFramePointer>;
|
using Output = render::VaryingSet5<RenderArgs::RenderMode, glm::ivec2, ViewFrustumPointer, LightStage::ShadowFramePointer, graphics::LightPointer>;
|
||||||
using Config = RenderShadowSetupConfig;
|
using Config = RenderShadowSetupConfig;
|
||||||
using JobModel = render::Job::ModelIO<RenderShadowSetup, Input, Output, Config>;
|
using JobModel = render::Job::ModelIO<RenderShadowSetup, Input, Output, Config>;
|
||||||
|
|
||||||
|
@ -161,7 +161,7 @@ public:
|
||||||
|
|
||||||
class CullShadowBounds {
|
class CullShadowBounds {
|
||||||
public:
|
public:
|
||||||
using Inputs = render::VaryingSet5<render::ShapeBounds, render::ItemFilter, ViewFrustumPointer, LightStage::FramePointer, RenderShadowTask::CullFunctor>;
|
using Inputs = render::VaryingSet5<render::ShapeBounds, render::ItemFilter, ViewFrustumPointer, graphics::LightPointer, RenderShadowTask::CullFunctor>;
|
||||||
using Outputs = render::VaryingSet2<render::ShapeBounds, AABox>;
|
using Outputs = render::VaryingSet2<render::ShapeBounds, AABox>;
|
||||||
using JobModel = render::Job::ModelIO<CullShadowBounds, Inputs, Outputs>;
|
using JobModel = render::Job::ModelIO<CullShadowBounds, Inputs, Outputs>;
|
||||||
|
|
||||||
|
|
|
@ -87,7 +87,7 @@ void ScriptCache::getScriptContents(const QString& scriptOrURL, contentAvailable
|
||||||
if (_scriptCache.contains(url) && !forceDownload) {
|
if (_scriptCache.contains(url) && !forceDownload) {
|
||||||
auto scriptContent = _scriptCache[url];
|
auto scriptContent = _scriptCache[url];
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
qCDebug(scriptengine) << "Found script in cache:" << url.toString();
|
qCDebug(scriptengine) << "Found script in cache:" << url.fileName();
|
||||||
contentAvailable(url.toString(), scriptContent, true, true, STATUS_CACHED);
|
contentAvailable(url.toString(), scriptContent, true, true, STATUS_CACHED);
|
||||||
} else {
|
} else {
|
||||||
auto& scriptRequest = _activeScriptRequests[url];
|
auto& scriptRequest = _activeScriptRequests[url];
|
||||||
|
|
|
@ -164,7 +164,9 @@ ScriptEnginePointer scriptEngineFactory(ScriptEngine::Context context,
|
||||||
const QString& fileNameString) {
|
const QString& fileNameString) {
|
||||||
ScriptEngine* engine = new ScriptEngine(context, scriptContents, fileNameString);
|
ScriptEngine* engine = new ScriptEngine(context, scriptContents, fileNameString);
|
||||||
ScriptEnginePointer engineSP = ScriptEnginePointer(engine);
|
ScriptEnginePointer engineSP = ScriptEnginePointer(engine);
|
||||||
DependencyManager::get<ScriptEngines>()->addScriptEngine(qSharedPointerCast<ScriptEngine>(engineSP));
|
auto scriptEngines = DependencyManager::get<ScriptEngines>();
|
||||||
|
scriptEngines->addScriptEngine(qSharedPointerCast<ScriptEngine>(engineSP));
|
||||||
|
engine->setScriptEngines(scriptEngines);
|
||||||
return engineSP;
|
return engineSP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,7 +261,7 @@ bool ScriptEngine::isDebugMode() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
ScriptEngine::~ScriptEngine() {
|
ScriptEngine::~ScriptEngine() {
|
||||||
auto scriptEngines = DependencyManager::get<ScriptEngines>();
|
QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines);
|
||||||
if (scriptEngines) {
|
if (scriptEngines) {
|
||||||
scriptEngines->removeScriptEngine(qSharedPointerCast<ScriptEngine>(sharedFromThis()));
|
scriptEngines->removeScriptEngine(qSharedPointerCast<ScriptEngine>(sharedFromThis()));
|
||||||
}
|
}
|
||||||
|
@ -555,6 +557,10 @@ using ScriptableResourceRawPtr = ScriptableResource*;
|
||||||
|
|
||||||
static QScriptValue scriptableResourceToScriptValue(QScriptEngine* engine,
|
static QScriptValue scriptableResourceToScriptValue(QScriptEngine* engine,
|
||||||
const ScriptableResourceRawPtr& resource) {
|
const ScriptableResourceRawPtr& resource) {
|
||||||
|
if (!resource) {
|
||||||
|
return QScriptValue(); // probably shutting down
|
||||||
|
}
|
||||||
|
|
||||||
// The first script to encounter this resource will track its memory.
|
// The first script to encounter this resource will track its memory.
|
||||||
// In this way, it will be more likely to GC.
|
// In this way, it will be more likely to GC.
|
||||||
// This fails in the case that the resource is used across many scripts, but
|
// This fails in the case that the resource is used across many scripts, but
|
||||||
|
@ -1012,7 +1018,8 @@ QScriptValue ScriptEngine::evaluateInClosure(const QScriptValue& closure, const
|
||||||
}
|
}
|
||||||
|
|
||||||
QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
|
QScriptValue ScriptEngine::evaluate(const QString& sourceCode, const QString& fileName, int lineNumber) {
|
||||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines);
|
||||||
|
if (!scriptEngines || scriptEngines->isStopped()) {
|
||||||
return QScriptValue(); // bail early
|
return QScriptValue(); // bail early
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1062,7 +1069,8 @@ void ScriptEngine::run() {
|
||||||
auto name = filenameParts.size() > 0 ? filenameParts[filenameParts.size() - 1] : "unknown";
|
auto name = filenameParts.size() > 0 ? filenameParts[filenameParts.size() - 1] : "unknown";
|
||||||
PROFILE_SET_THREAD_NAME("Script: " + name);
|
PROFILE_SET_THREAD_NAME("Script: " + name);
|
||||||
|
|
||||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines);
|
||||||
|
if (!scriptEngines || scriptEngines->isStopped()) {
|
||||||
return; // bail early - avoid setting state in init(), as evaluate() will bail too
|
return; // bail early - avoid setting state in init(), as evaluate() will bail too
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1319,8 +1327,8 @@ void ScriptEngine::updateMemoryCost(const qint64& deltaSize) {
|
||||||
|
|
||||||
void ScriptEngine::timerFired() {
|
void ScriptEngine::timerFired() {
|
||||||
{
|
{
|
||||||
auto engine = DependencyManager::get<ScriptEngines>();
|
QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines);
|
||||||
if (!engine || engine->isStopped()) {
|
if (!scriptEngines || scriptEngines->isStopped()) {
|
||||||
scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename());
|
scriptWarningMessage("Script.timerFired() while shutting down is ignored... parent script:" + getFilename());
|
||||||
return; // bail early
|
return; // bail early
|
||||||
}
|
}
|
||||||
|
@ -1373,7 +1381,8 @@ QObject* ScriptEngine::setupTimerWithInterval(const QScriptValue& function, int
|
||||||
}
|
}
|
||||||
|
|
||||||
QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) {
|
QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS) {
|
||||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines);
|
||||||
|
if (!scriptEngines || scriptEngines->isStopped()) {
|
||||||
scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename());
|
scriptWarningMessage("Script.setInterval() while shutting down is ignored... parent script:" + getFilename());
|
||||||
return NULL; // bail early
|
return NULL; // bail early
|
||||||
}
|
}
|
||||||
|
@ -1382,7 +1391,8 @@ QObject* ScriptEngine::setInterval(const QScriptValue& function, int intervalMS)
|
||||||
}
|
}
|
||||||
|
|
||||||
QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) {
|
QObject* ScriptEngine::setTimeout(const QScriptValue& function, int timeoutMS) {
|
||||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines);
|
||||||
|
if (!scriptEngines || scriptEngines->isStopped()) {
|
||||||
scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename());
|
scriptWarningMessage("Script.setTimeout() while shutting down is ignored... parent script:" + getFilename());
|
||||||
return NULL; // bail early
|
return NULL; // bail early
|
||||||
}
|
}
|
||||||
|
@ -1595,7 +1605,7 @@ QScriptValue ScriptEngine::newModule(const QString& modulePath, const QScriptVal
|
||||||
auto closure = newObject();
|
auto closure = newObject();
|
||||||
auto exports = newObject();
|
auto exports = newObject();
|
||||||
auto module = newObject();
|
auto module = newObject();
|
||||||
qCDebug(scriptengine_module) << "newModule" << modulePath << parent.property("filename").toString();
|
qCDebug(scriptengine_module) << "newModule" << parent.property("filename").toString();
|
||||||
|
|
||||||
closure.setProperty("module", module, READONLY_PROP_FLAGS);
|
closure.setProperty("module", module, READONLY_PROP_FLAGS);
|
||||||
|
|
||||||
|
@ -1813,7 +1823,8 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
|
||||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines);
|
||||||
|
if (!scriptEngines || scriptEngines->isStopped()) {
|
||||||
scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:"
|
scriptWarningMessage("Script.include() while shutting down is ignored... includeFiles:"
|
||||||
+ includeFiles.join(",") + "parent script:" + getFilename());
|
+ includeFiles.join(",") + "parent script:" + getFilename());
|
||||||
return; // bail early
|
return; // bail early
|
||||||
|
@ -1907,7 +1918,8 @@ void ScriptEngine::include(const QStringList& includeFiles, QScriptValue callbac
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScriptEngine::include(const QString& includeFile, QScriptValue callback) {
|
void ScriptEngine::include(const QString& includeFile, QScriptValue callback) {
|
||||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines);
|
||||||
|
if (!scriptEngines || scriptEngines->isStopped()) {
|
||||||
scriptWarningMessage("Script.include() while shutting down is ignored... includeFile:"
|
scriptWarningMessage("Script.include() while shutting down is ignored... includeFile:"
|
||||||
+ includeFile + "parent script:" + getFilename());
|
+ includeFile + "parent script:" + getFilename());
|
||||||
return; // bail early
|
return; // bail early
|
||||||
|
@ -1925,7 +1937,8 @@ void ScriptEngine::load(const QString& loadFile) {
|
||||||
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
if (!IS_THREADSAFE_INVOCATION(thread(), __FUNCTION__)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (DependencyManager::get<ScriptEngines>()->isStopped()) {
|
QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines);
|
||||||
|
if (!scriptEngines || scriptEngines->isStopped()) {
|
||||||
scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:"
|
scriptWarningMessage("Script.load() while shutting down is ignored... loadFile:"
|
||||||
+ loadFile + "parent script:" + getFilename());
|
+ loadFile + "parent script:" + getFilename());
|
||||||
return; // bail early
|
return; // bail early
|
||||||
|
@ -2073,10 +2086,11 @@ void ScriptEngine::loadEntityScript(const EntityItemID& entityID, const QString&
|
||||||
}
|
}
|
||||||
PROFILE_RANGE(script, __FUNCTION__);
|
PROFILE_RANGE(script, __FUNCTION__);
|
||||||
|
|
||||||
if (isStopping() || DependencyManager::get<ScriptEngines>()->isStopped()) {
|
QSharedPointer<ScriptEngines> scriptEngines(_scriptEngines);
|
||||||
|
if (isStopping() || !scriptEngines || scriptEngines->isStopped()) {
|
||||||
qCDebug(scriptengine) << "loadEntityScript.start " << entityID.toString()
|
qCDebug(scriptengine) << "loadEntityScript.start " << entityID.toString()
|
||||||
<< " but isStopping==" << isStopping()
|
<< " but isStopping==" << isStopping()
|
||||||
<< " || engines->isStopped==" << DependencyManager::get<ScriptEngines>()->isStopped();
|
<< " || engines->isStopped==" << scriptEngines->isStopped();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -563,6 +563,8 @@ public:
|
||||||
bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const;
|
bool getEntityScriptDetails(const EntityItemID& entityID, EntityScriptDetails &details) const;
|
||||||
bool hasEntityScriptDetails(const EntityItemID& entityID) const;
|
bool hasEntityScriptDetails(const EntityItemID& entityID) const;
|
||||||
|
|
||||||
|
void setScriptEngines(QSharedPointer<ScriptEngines>& scriptEngines) { _scriptEngines = scriptEngines; }
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
|
||||||
/**jsdoc
|
/**jsdoc
|
||||||
|
@ -814,6 +816,8 @@ protected:
|
||||||
static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
|
static const QString _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS;
|
||||||
|
|
||||||
Setting::Handle<bool> _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
|
Setting::Handle<bool> _enableExtendedJSExceptions { _SETTINGS_ENABLE_EXTENDED_EXCEPTIONS, true };
|
||||||
|
|
||||||
|
QWeakPointer<ScriptEngines> _scriptEngines;
|
||||||
};
|
};
|
||||||
|
|
||||||
ScriptEnginePointer scriptEngineFactory(ScriptEngine::Context context,
|
ScriptEnginePointer scriptEngineFactory(ScriptEngine::Context context,
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"HiFi 4:1 Audio Codec"}
|
{
|
||||||
|
"name":"HiFi 4:1 Audio Codec",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"Kinect"}
|
{
|
||||||
|
"name":"Kinect",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"Leap Motion"}
|
{
|
||||||
|
"name":"Leap Motion",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"Neuron"}
|
{
|
||||||
|
"name":"Neuron",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"SDL2"}
|
{
|
||||||
|
"name":"SDL2",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"Sixense"}
|
{
|
||||||
|
"name":"Sixense",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"Spacemouse"}
|
{
|
||||||
|
"name":"Spacemouse",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"Oculus Rift"}
|
{
|
||||||
|
"name":"Oculus Rift",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"Oculus Rift"}
|
{
|
||||||
|
"name":"Oculus Rift",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"OpenVR (Vive)"}
|
{
|
||||||
|
"name":"OpenVR (Vive)",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"PCM Codec"}
|
{
|
||||||
|
"name":"PCM Codec",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
{"name":"Steam Client"}
|
{
|
||||||
|
"name":"Steam Client",
|
||||||
|
"version":1
|
||||||
|
}
|
||||||
|
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 60 KiB |
|
@ -151,13 +151,16 @@ local packet_types = {
|
||||||
[96] = "OctreeDataFileReply",
|
[96] = "OctreeDataFileReply",
|
||||||
[97] = "OctreeDataPersist",
|
[97] = "OctreeDataPersist",
|
||||||
[98] = "EntityClone",
|
[98] = "EntityClone",
|
||||||
[99] = "EntityQueryInitialResultsComplete"
|
[99] = "EntityQueryInitialResultsComplete",
|
||||||
|
[100] = "BulkAvatarTraits"
|
||||||
}
|
}
|
||||||
|
|
||||||
local unsourced_packet_types = {
|
local unsourced_packet_types = {
|
||||||
["DomainList"] = true
|
["DomainList"] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local fragments = {}
|
||||||
|
|
||||||
function p_hfudt.dissector(buf, pinfo, tree)
|
function p_hfudt.dissector(buf, pinfo, tree)
|
||||||
|
|
||||||
-- make sure this isn't a STUN packet - those don't follow HFUDT format
|
-- make sure this isn't a STUN packet - those don't follow HFUDT format
|
||||||
|
@ -235,6 +238,10 @@ function p_hfudt.dissector(buf, pinfo, tree)
|
||||||
|
|
||||||
local payload_offset = 4
|
local payload_offset = 4
|
||||||
|
|
||||||
|
local message_number = 0
|
||||||
|
local message_part_number = 0
|
||||||
|
local message_position = 0
|
||||||
|
|
||||||
-- if the message bit is set, handle the second word
|
-- if the message bit is set, handle the second word
|
||||||
if message_bit == 1 then
|
if message_bit == 1 then
|
||||||
payload_offset = 12
|
payload_offset = 12
|
||||||
|
@ -242,7 +249,7 @@ function p_hfudt.dissector(buf, pinfo, tree)
|
||||||
local second_word = buf(4, 4):le_uint()
|
local second_word = buf(4, 4):le_uint()
|
||||||
|
|
||||||
-- read message position from upper 2 bits
|
-- read message position from upper 2 bits
|
||||||
local message_position = bit32.rshift(second_word, 30)
|
message_position = bit32.rshift(second_word, 30)
|
||||||
local position = subtree:add(f_message_position, message_position)
|
local position = subtree:add(f_message_position, message_position)
|
||||||
|
|
||||||
if message_positions[message_position] ~= nil then
|
if message_positions[message_position] ~= nil then
|
||||||
|
@ -251,10 +258,12 @@ function p_hfudt.dissector(buf, pinfo, tree)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- read message number from lower 30 bits
|
-- read message number from lower 30 bits
|
||||||
subtree:add(f_message_number, bit32.band(second_word, 0x3FFFFFFF))
|
message_number = bit32.band(second_word, 0x3FFFFFFF)
|
||||||
|
subtree:add(f_message_number, message_number)
|
||||||
|
|
||||||
-- read the message part number
|
-- read the message part number
|
||||||
subtree:add(f_message_part_number, buf(8, 4):le_uint())
|
message_part_number = buf(8, 4):le_uint()
|
||||||
|
subtree:add(f_message_part_number, message_part_number)
|
||||||
end
|
end
|
||||||
|
|
||||||
if obfuscation_bits ~= 0 then
|
if obfuscation_bits ~= 0 then
|
||||||
|
@ -288,25 +297,85 @@ function p_hfudt.dissector(buf, pinfo, tree)
|
||||||
i = i + 16
|
i = i + 16
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Domain packets
|
local payload_to_dissect = nil
|
||||||
if packet_type_text == "DomainList" then
|
|
||||||
Dissector.get("hf-domain"):call(buf(i):tvb(), pinfo, tree)
|
-- check if we have part of a message that we need to re-assemble
|
||||||
|
-- before it can be dissected
|
||||||
|
if obfuscation_bits == 0 then
|
||||||
|
if message_bit == 1 and message_position ~= 0 then
|
||||||
|
if fragments[message_number] == nil then
|
||||||
|
fragments[message_number] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
if fragments[message_number][message_part_number] == nil then
|
||||||
|
fragments[message_number][message_part_number] = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- set the properties for this fragment
|
||||||
|
fragments[message_number][message_part_number] = {
|
||||||
|
payload = buf(i):bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
-- if this is the last part, set our maximum part number
|
||||||
|
if message_position == 1 then
|
||||||
|
fragments[message_number].last_part_number = message_part_number
|
||||||
|
end
|
||||||
|
|
||||||
|
-- if we have the last part
|
||||||
|
-- enumerate our parts for this message and see if everything is present
|
||||||
|
if fragments[message_number].last_part_number ~= nil then
|
||||||
|
local i = 0
|
||||||
|
local has_all = true
|
||||||
|
|
||||||
|
local finalMessage = ByteArray.new()
|
||||||
|
local message_complete = true
|
||||||
|
|
||||||
|
while i <= fragments[message_number].last_part_number do
|
||||||
|
if fragments[message_number][i] ~= nil then
|
||||||
|
finalMessage = finalMessage .. fragments[message_number][i].payload
|
||||||
|
else
|
||||||
|
-- missing this part, have to break until we have it
|
||||||
|
message_complete = false
|
||||||
|
end
|
||||||
|
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
if message_complete then
|
||||||
|
debug("Message " .. message_number .. " is " .. finalMessage:len())
|
||||||
|
payload_to_dissect = ByteArray.tvb(finalMessage, message_number)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
else
|
||||||
|
payload_to_dissect = buf(i):tvb()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- AvatarData or BulkAvatarDataPacket
|
if payload_to_dissect ~= nil then
|
||||||
if packet_type_text == "AvatarData" or packet_type_text == "BulkAvatarData" then
|
-- Domain packets
|
||||||
Dissector.get("hf-avatar"):call(buf(i):tvb(), pinfo, tree)
|
if packet_type_text == "DomainList" then
|
||||||
|
Dissector.get("hf-domain"):call(payload_to_dissect, pinfo, tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- AvatarData or BulkAvatarDataPacket
|
||||||
|
if packet_type_text == "AvatarData" or
|
||||||
|
packet_type_text == "BulkAvatarData" or
|
||||||
|
packet_type_text == "BulkAvatarTraits" then
|
||||||
|
Dissector.get("hf-avatar"):call(payload_to_dissect, pinfo, tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
if packet_type_text == "EntityEdit" then
|
||||||
|
Dissector.get("hf-entity"):call(payload_to_dissect, pinfo, tree)
|
||||||
|
end
|
||||||
|
|
||||||
|
if packet_types[packet_type] == "MicrophoneAudioNoEcho" or
|
||||||
|
packet_types[packet_type] == "MicrophoneAudioWithEcho" or
|
||||||
|
packet_types[packet_type] == "SilentAudioFrame" then
|
||||||
|
Dissector.get("hf-audio"):call(payload_to_dissect, pinfo, tree)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if packet_type_text == "EntityEdit" then
|
|
||||||
Dissector.get("hf-entity"):call(buf(i):tvb(), pinfo, tree)
|
|
||||||
end
|
|
||||||
|
|
||||||
if packet_types[packet_type] == "MicrophoneAudioNoEcho" or
|
|
||||||
packet_types[packet_type] == "MicrophoneAudioWithEcho" or
|
|
||||||
packet_types[packet_type] == "SilentAudioFrame" then
|
|
||||||
Dissector.get("hf-audio"):call(buf(i):tvb(), pinfo, tree)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- return the size of the header
|
-- return the size of the header
|
||||||
|
|
|
@ -21,13 +21,31 @@ local f_avatar_data_default_rotations = ProtoField.string("hf_avatar.avatar_data
|
||||||
local f_avatar_data_default_translations = ProtoField.string("hf_avatar.avatar_data_default_translations", "Valid Default")
|
local f_avatar_data_default_translations = ProtoField.string("hf_avatar.avatar_data_default_translations", "Valid Default")
|
||||||
local f_avatar_data_sizes = ProtoField.string("hf_avatar.avatar_sizes", "Sizes")
|
local f_avatar_data_sizes = ProtoField.string("hf_avatar.avatar_sizes", "Sizes")
|
||||||
|
|
||||||
|
-- avatar trait data fields
|
||||||
|
local f_avatar_trait_data = ProtoField.bytes("hf_avatar.avatar_trait_data", "Avatar Trait Data")
|
||||||
|
|
||||||
|
local f_avatar_trait_id = ProtoField.guid("hf_avatar.trait_avatar_id", "Trait Avatar ID")
|
||||||
|
local f_avatar_trait_type = ProtoField.int8("hf_avatar.trait_type", "Trait Type")
|
||||||
|
local f_avatar_trait_version = ProtoField.int32("hf_avatar.trait_version", "Trait Version")
|
||||||
|
local f_avatar_trait_instance_id = ProtoField.guid("hf_avatar.trait_instance_id", "Trait Instance ID")
|
||||||
|
local f_avatar_trait_binary = ProtoField.bytes("hf_avatar.trait_binary", "Trait Binary Data")
|
||||||
|
|
||||||
p_hf_avatar.fields = {
|
p_hf_avatar.fields = {
|
||||||
f_avatar_id, f_avatar_data_parent_id
|
f_avatar_id, f_avatar_data_parent_id,
|
||||||
|
f_avatar_trait_data,
|
||||||
|
f_avatar_trait_type, f_avatar_trait_id,
|
||||||
|
f_avatar_trait_version, f_avatar_trait_binary,
|
||||||
|
f_avatar_trait_instance_id
|
||||||
}
|
}
|
||||||
|
|
||||||
local packet_type_extractor = Field.new('hfudt.type')
|
local packet_type_extractor = Field.new('hfudt.type')
|
||||||
|
|
||||||
|
INSTANCED_TYPES = {
|
||||||
|
[1] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
TOTAL_TRAIT_TYPES = 2
|
||||||
|
|
||||||
function p_hf_avatar.dissector(buf, pinfo, tree)
|
function p_hf_avatar.dissector(buf, pinfo, tree)
|
||||||
pinfo.cols.protocol = p_hf_avatar.name
|
pinfo.cols.protocol = p_hf_avatar.name
|
||||||
|
|
||||||
|
@ -52,7 +70,7 @@ function p_hf_avatar.dissector(buf, pinfo, tree)
|
||||||
|
|
||||||
add_avatar_data_subtrees(avatar_data)
|
add_avatar_data_subtrees(avatar_data)
|
||||||
|
|
||||||
else
|
elseif packet_type == 11 then
|
||||||
-- BulkAvatarData packet
|
-- BulkAvatarData packet
|
||||||
while i < buf:len() do
|
while i < buf:len() do
|
||||||
-- avatar_id is first 16 bytes
|
-- avatar_id is first 16 bytes
|
||||||
|
@ -65,9 +83,88 @@ function p_hf_avatar.dissector(buf, pinfo, tree)
|
||||||
|
|
||||||
add_avatar_data_subtrees(avatar_data)
|
add_avatar_data_subtrees(avatar_data)
|
||||||
end
|
end
|
||||||
|
elseif packet_type == 100 then
|
||||||
|
-- BulkAvatarTraits packet
|
||||||
|
|
||||||
|
-- loop over the packet until we're done reading it
|
||||||
|
while i < buf:len() do
|
||||||
|
i = i + read_avatar_trait_data(buf(i))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function read_avatar_trait_data(buf)
|
||||||
|
local i = 0
|
||||||
|
|
||||||
|
-- avatar_id is first 16 bytes
|
||||||
|
local avatar_id_bytes = buf(i, 16)
|
||||||
|
i = i + 16
|
||||||
|
|
||||||
|
local traits_map = {}
|
||||||
|
|
||||||
|
-- loop over all of the traits for this avatar
|
||||||
|
while i < buf:len() do
|
||||||
|
-- pull out this trait type
|
||||||
|
local trait_type = buf(i, 1):le_int()
|
||||||
|
i = i + 1
|
||||||
|
|
||||||
|
debug("The trait type is " .. trait_type)
|
||||||
|
|
||||||
|
-- bail on our while if the trait type is null (-1)
|
||||||
|
if trait_type == -1 then break end
|
||||||
|
|
||||||
|
local trait_map = {}
|
||||||
|
|
||||||
|
-- pull out the trait version
|
||||||
|
trait_map.version = buf(i, 4):le_int()
|
||||||
|
i = i + 4
|
||||||
|
|
||||||
|
if INSTANCED_TYPES[trait_type] ~= nil then
|
||||||
|
-- pull out the trait instance ID
|
||||||
|
trait_map.instance_ID = buf(i, 16)
|
||||||
|
i = i + 16
|
||||||
|
end
|
||||||
|
|
||||||
|
-- pull out the trait binary size
|
||||||
|
trait_map.binary_size = buf(i, 2):le_int()
|
||||||
|
i = i + 2
|
||||||
|
|
||||||
|
-- unpack the binary data as long as this wasn't a delete
|
||||||
|
if trait_map.binary_size ~= -1 then
|
||||||
|
-- pull out the binary data for the trait
|
||||||
|
trait_map.binary_data = buf(i, trait_map.binary_size)
|
||||||
|
i = i + trait_map.binary_size
|
||||||
|
end
|
||||||
|
|
||||||
|
traits_map[trait_type] = trait_map
|
||||||
|
end
|
||||||
|
|
||||||
|
-- add a subtree including all of the data for this avatar
|
||||||
|
debug("Adding trait data of " .. i .. " bytes to the avatar tree")
|
||||||
|
local this_avatar_tree = avatar_subtree:add(f_avatar_trait_data, buf(0, i))
|
||||||
|
|
||||||
|
this_avatar_tree:add(f_avatar_trait_id, avatar_id_bytes)
|
||||||
|
|
||||||
|
-- enumerate the pulled traits and add them to the tree
|
||||||
|
local trait_type = 0
|
||||||
|
while trait_type < TOTAL_TRAIT_TYPES do
|
||||||
|
trait = traits_map[trait_type]
|
||||||
|
|
||||||
|
if trait ~= nil then
|
||||||
|
this_avatar_tree:add(f_avatar_trait_type, trait_type)
|
||||||
|
this_avatar_tree:add(f_avatar_trait_version, trait.version)
|
||||||
|
this_avatar_tree:add(f_avatar_trait_binary, trait.binary_data)
|
||||||
|
|
||||||
|
if trait.instance_ID ~= nil then
|
||||||
|
this_avatar_tree:add(f_avatar_trait_instance_id, trait.instance_ID)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
trait_type = trait_type + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
|
||||||
function add_avatar_data_subtrees(avatar_data)
|
function add_avatar_data_subtrees(avatar_data)
|
||||||
if avatar_data["has_flags"] then
|
if avatar_data["has_flags"] then
|
||||||
|
|