Merge remote-tracking branch 'upstream/master' into matFallthrough

This commit is contained in:
SamGondelman 2019-01-17 10:27:43 -08:00
commit 8d2e81a13b
297 changed files with 9934 additions and 5123 deletions
.gitignore
android
assignment-client
interface
libraries

1
.gitignore vendored
View file

@ -98,6 +98,7 @@ tools/jsdoc/package-lock.json
# ignore unneeded unity project files for avatar exporter
tools/unity-avatar-exporter/Library
tools/unity-avatar-exporter/Logs
tools/unity-avatar-exporter/Packages
tools/unity-avatar-exporter/ProjectSettings
tools/unity-avatar-exporter/Temp

View file

@ -1,4 +1,4 @@
#!/usr/bin/env bash
set -xeuo pipefail
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} setupDependencies
./gradlew -PHIFI_ANDROID_PRECOMPILED=${HIFI_ANDROID_PRECOMPILED} -PVERSION_CODE=${VERSION_CODE} -PRELEASE_NUMBER=${RELEASE_NUMBER} -PRELEASE_TYPE=${RELEASE_TYPE} app:${ANDROID_BUILD_TARGET}

View file

@ -9,14 +9,19 @@ docker run \
--rm \
--security-opt seccomp:unconfined \
-v "${WORKSPACE}":/home/jenkins/hifi \
-e "RELEASE_NUMBER=${RELEASE_NUMBER}" \
-e "RELEASE_TYPE=${RELEASE_TYPE}" \
-e "ANDROID_BUILD_TARGET=assembleDebug" \
-e "CMAKE_BACKTRACE_URL=${CMAKE_BACKTRACE_URL}" \
-e "CMAKE_BACKTRACE_TOKEN=${CMAKE_BACKTRACE_TOKEN}" \
-e "CMAKE_BACKTRACE_SYMBOLS_TOKEN=${CMAKE_BACKTRACE_SYMBOLS_TOKEN}" \
-e "GA_TRACKING_ID=${GA_TRACKING_ID}" \
-e "GIT_PR_COMMIT=${GIT_PR_COMMIT}" \
-e "VERSION_CODE=${VERSION_CODE}" \
-e RELEASE_NUMBER \
-e RELEASE_TYPE \
-e ANDROID_BUILD_TARGET \
-e ANDROID_BUILD_DIR \
-e CMAKE_BACKTRACE_URL \
-e CMAKE_BACKTRACE_TOKEN \
-e CMAKE_BACKTRACE_SYMBOLS_TOKEN \
-e GA_TRACKING_ID \
-e OAUTH_CLIENT_SECRET \
-e OAUTH_CLIENT_ID \
-e OAUTH_REDIRECT_URI \
-e VERSION_CODE \
"${DOCKER_IMAGE_NAME}" \
sh -c "./build_android.sh"

View file

@ -14,6 +14,7 @@ link_hifi_libraries(
audio avatars octree gpu graphics shaders fbx hfm entities
networking animation recording shared script-engine embedded-webserver
controllers physics plugins midi image
model-networking ktx shaders
)
add_dependencies(${TARGET_NAME} oven)

View file

@ -915,59 +915,52 @@ void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, Sha
void AssetServer::sendStatsPacket() {
QJsonObject serverStats;
auto stats = DependencyManager::get<NodeList>()->sampleStatsForAllConnections();
auto nodeList = DependencyManager::get<NodeList>();
nodeList->eachNode([&](auto& node) {
auto& stats = node->getConnectionStats();
for (const auto& stat : stats) {
QJsonObject nodeStats;
auto endTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stat.second.endTime);
auto endTimeMs = std::chrono::duration_cast<std::chrono::milliseconds>(stats.endTime);
QDateTime date = QDateTime::fromMSecsSinceEpoch(endTimeMs.count());
static const float USEC_PER_SEC = 1000000.0f;
static const float MEGABITS_PER_BYTE = 8.0f / 1000000.0f; // Bytes => Mbits
float elapsed = (float)(stat.second.endTime - stat.second.startTime).count() / USEC_PER_SEC; // sec
float elapsed = (float)(stats.endTime - stats.startTime).count() / USEC_PER_SEC; // sec
float megabitsPerSecPerByte = MEGABITS_PER_BYTE / elapsed; // Bytes => Mb/s
QJsonObject connectionStats;
connectionStats["1. Last Heard"] = date.toString();
connectionStats["2. Est. Max (P/s)"] = stat.second.estimatedBandwith;
connectionStats["3. RTT (ms)"] = stat.second.rtt;
connectionStats["4. CW (P)"] = stat.second.congestionWindowSize;
connectionStats["5. Period (us)"] = stat.second.packetSendPeriod;
connectionStats["6. Up (Mb/s)"] = stat.second.sentBytes * megabitsPerSecPerByte;
connectionStats["7. Down (Mb/s)"] = stat.second.receivedBytes * megabitsPerSecPerByte;
connectionStats["2. Est. Max (P/s)"] = stats.estimatedBandwith;
connectionStats["3. RTT (ms)"] = stats.rtt;
connectionStats["4. CW (P)"] = stats.congestionWindowSize;
connectionStats["5. Period (us)"] = stats.packetSendPeriod;
connectionStats["6. Up (Mb/s)"] = stats.sentBytes * megabitsPerSecPerByte;
connectionStats["7. Down (Mb/s)"] = stats.receivedBytes * megabitsPerSecPerByte;
nodeStats["Connection Stats"] = connectionStats;
using Events = udt::ConnectionStats::Stats::Event;
const auto& events = stat.second.events;
const auto& events = stats.events;
QJsonObject upstreamStats;
upstreamStats["1. Sent (P/s)"] = stat.second.sendRate;
upstreamStats["2. Sent Packets"] = stat.second.sentPackets;
upstreamStats["1. Sent (P/s)"] = stats.sendRate;
upstreamStats["2. Sent Packets"] = (int)stats.sentPackets;
upstreamStats["3. Recvd ACK"] = events[Events::ReceivedACK];
upstreamStats["4. Procd ACK"] = events[Events::ProcessedACK];
upstreamStats["5. Retransmitted"] = events[Events::Retransmission];
upstreamStats["5. Retransmitted"] = (int)stats.retransmittedPackets;
nodeStats["Upstream Stats"] = upstreamStats;
QJsonObject downstreamStats;
downstreamStats["1. Recvd (P/s)"] = stat.second.receiveRate;
downstreamStats["2. Recvd Packets"] = stat.second.receivedPackets;
downstreamStats["1. Recvd (P/s)"] = stats.receiveRate;
downstreamStats["2. Recvd Packets"] = (int)stats.receivedPackets;
downstreamStats["3. Sent ACK"] = events[Events::SentACK];
downstreamStats["4. Duplicates"] = events[Events::Duplicate];
downstreamStats["4. Duplicates"] = (int)stats.duplicatePackets;
nodeStats["Downstream Stats"] = downstreamStats;
QString uuid;
auto nodelist = DependencyManager::get<NodeList>();
if (stat.first == nodelist->getDomainHandler().getSockAddr()) {
uuid = uuidStringWithoutCurlyBraces(nodelist->getDomainHandler().getUUID());
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = "DomainServer";
} else {
auto node = nodelist->findNodeWithAddr(stat.first);
uuid = uuidStringWithoutCurlyBraces(node ? node->getUUID() : QUuid());
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuid;
}
QString uuid = uuidStringWithoutCurlyBraces(node->getUUID());
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuid;
serverStats[uuid] = nodeStats;
}
});
// send off the stats packets
ThreadedAssignment::addPacketStatsAndSendStatsPacket(serverStats);

View file

@ -338,7 +338,7 @@ void AudioMixer::sendStatsPacket() {
QJsonObject nodeStats;
QString uuidString = uuidStringWithoutCurlyBraces(node->getUUID());
nodeStats["outbound_kbps"] = node->getOutboundBandwidth();
nodeStats["outbound_kbps"] = node->getOutboundKbps();
nodeStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidString;
nodeStats["jitter"] = clientData->getAudioStreamStats();

View file

@ -504,7 +504,7 @@ void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStre
float distance = glm::max(glm::length(relativePosition), EPSILON);
float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition);
float gain = 1.0f;
float gain = masterListenerGain;
if (!isSoloing) {
gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho);
}

View file

@ -56,6 +56,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
packetReceiver.registerListener(PacketType::AvatarIdentityRequest, this, "handleAvatarIdentityRequestPacket");
packetReceiver.registerListener(PacketType::SetAvatarTraits, this, "queueIncomingPacket");
packetReceiver.registerListener(PacketType::BulkAvatarTraitsAck, this, "queueIncomingPacket");
packetReceiver.registerListenerForTypes({
PacketType::ReplicatedAvatarIdentity,
@ -746,65 +747,30 @@ void AvatarMixer::sendStatsPacket() {
AvatarMixerSlaveStats aggregateStats;
QJsonObject slavesObject;
float secondsSinceLastStats = (float)(start - _lastStatsTime) / (float)USECS_PER_SECOND;
// gather stats
int slaveNumber = 1;
_slavePool.each([&](AvatarMixerSlave& slave) {
QJsonObject slaveObject;
AvatarMixerSlaveStats stats;
slave.harvestStats(stats);
slaveObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(stats.nodesProcessed);
slaveObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(stats.packetsProcessed);
slaveObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(stats.nodesBroadcastedTo);
slaveObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(stats.numBytesSent);
slaveObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(stats.numPacketsSent);
slaveObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(stats.numIdentityPackets);
float averageNodes = ((float)stats.nodesBroadcastedTo / (float)tightLoopFrames);
float averageOutboundAvatarKbps = averageNodes ? ((stats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
slaveObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
float averageOthersIncluded = averageNodes ? stats.numOthersIncluded / averageNodes : 0.0f;
slaveObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
float averageOverBudgetAvatars = averageNodes ? stats.overBudgetAvatars / averageNodes : 0.0f;
slaveObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars);
slaveObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(stats.processIncomingPacketsElapsedTime);
slaveObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(stats.ignoreCalculationElapsedTime);
slaveObject["timing_3_toByteArray"] = TIGHT_LOOP_STAT_UINT64(stats.toByteArrayElapsedTime);
slaveObject["timing_4_avatarDataPacking"] = TIGHT_LOOP_STAT_UINT64(stats.avatarDataPackingElapsedTime);
slaveObject["timing_5_packetSending"] = TIGHT_LOOP_STAT_UINT64(stats.packetSendingElapsedTime);
slaveObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(stats.jobElapsedTime);
slavesObject[QString::number(slaveNumber)] = slaveObject;
slaveNumber++;
aggregateStats += stats;
});
QJsonObject slavesAggregatObject;
slavesAggregatObject["recevied_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
slavesAggregatObject["received_2_numPacketsReceived"] = TIGHT_LOOP_STAT(aggregateStats.packetsProcessed);
slavesAggregatObject["received_1_nodesProcessed"] = TIGHT_LOOP_STAT(aggregateStats.nodesProcessed);
slavesAggregatObject["sent_1_nodesBroadcastedTo"] = TIGHT_LOOP_STAT(aggregateStats.nodesBroadcastedTo);
slavesAggregatObject["sent_2_numBytesSent"] = TIGHT_LOOP_STAT(aggregateStats.numBytesSent);
slavesAggregatObject["sent_3_numPacketsSent"] = TIGHT_LOOP_STAT(aggregateStats.numPacketsSent);
slavesAggregatObject["sent_4_numIdentityPackets"] = TIGHT_LOOP_STAT(aggregateStats.numIdentityPackets);
float averageNodes = ((float)aggregateStats.nodesBroadcastedTo / (float)tightLoopFrames);
float averageOutboundAvatarKbps = averageNodes ? ((aggregateStats.numBytesSent / secondsSinceLastStats) / BYTES_PER_KILOBIT) / averageNodes : 0.0f;
slavesAggregatObject["sent_5_averageOutboundAvatarKbps"] = averageOutboundAvatarKbps;
float averageOthersIncluded = averageNodes ? aggregateStats.numOthersIncluded / averageNodes : 0.0f;
slavesAggregatObject["sent_6_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
slavesAggregatObject["sent_2_averageOthersIncluded"] = TIGHT_LOOP_STAT(averageOthersIncluded);
float averageOverBudgetAvatars = averageNodes ? aggregateStats.overBudgetAvatars / averageNodes : 0.0f;
slavesAggregatObject["sent_7_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_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime);
@ -813,8 +779,7 @@ void AvatarMixer::sendStatsPacket() {
slavesAggregatObject["timing_5_packetSending"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.packetSendingElapsedTime);
slavesAggregatObject["timing_6_jobElapsedTime"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.jobElapsedTime);
statsObject["slaves_aggregate"] = slavesAggregatObject;
statsObject["slaves_individual"] = slavesObject;
statsObject["slaves_aggregate (per frame)"] = slavesAggregatObject;
_handleViewFrustumPacketElapsedTime = 0;
_handleAvatarIdentityPacketElapsedTime = 0;
@ -839,8 +804,9 @@ void AvatarMixer::sendStatsPacket() {
// 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[NODE_OUTBOUND_KBPS_STAT_KEY] = node->getOutboundBandwidth();
avatarStats[NODE_INBOUND_KBPS_STAT_KEY] = node->getInboundBandwidth();
float outboundAvatarDataKbps = node->getOutboundKbps();
avatarStats[NODE_OUTBOUND_KBPS_STAT_KEY] = outboundAvatarDataKbps;
avatarStats[NODE_INBOUND_KBPS_STAT_KEY] = node->getInboundKbps();
AvatarMixerClientData* clientData = static_cast<AvatarMixerClientData*>(node->getLinkedData());
if (clientData) {
@ -850,7 +816,7 @@ void AvatarMixer::sendStatsPacket() {
// add the diff between the full outbound bandwidth and the measured bandwidth for AvatarData send only
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();
}
}

View file

@ -19,9 +19,8 @@
#include "AvatarMixerSlave.h"
AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) :
NodeData(nodeID, nodeLocalID)
{
AvatarMixerClientData::AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID) :
NodeData(nodeID, nodeLocalID) {
// in case somebody calls getSessionUUID on the AvatarData instance, make sure it has the right ID
_avatar->setID(nodeID);
}
@ -68,6 +67,9 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
case PacketType::SetAvatarTraits:
processSetTraitsMessage(*packet, slaveSharedData, *node);
break;
case PacketType::BulkAvatarTraitsAck:
processBulkAvatarTraitsAckMessage(*packet);
break;
default:
Q_UNREACHABLE();
}
@ -79,12 +81,11 @@ int AvatarMixerClientData::processPackets(const SlaveSharedData& slaveSharedData
}
int AvatarMixerClientData::parseData(ReceivedMessage& message) {
// pull the sequence number from the data first
uint16_t sequenceNumber;
message.readPrimitive(&sequenceNumber);
if (sequenceNumber < _lastReceivedSequenceNumber && _lastReceivedSequenceNumber != UINT16_MAX) {
incrementNumOutOfOrderSends();
}
@ -95,7 +96,8 @@ int AvatarMixerClientData::parseData(ReceivedMessage& message) {
}
void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
const SlaveSharedData& slaveSharedData, Node& sendingNode) {
const SlaveSharedData& slaveSharedData,
Node& sendingNode) {
// pull the trait version from the message
AvatarTraits::TraitVersion packetTraitVersion;
message.readPrimitive(&packetTraitVersion);
@ -134,7 +136,7 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
AvatarTraits::TraitInstanceID instanceID = QUuid::fromRfc4122(message.readWithoutCopy(NUM_BYTES_RFC4122_UUID));
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;
}
@ -142,7 +144,8 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
message.readPrimitive(&traitSize);
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;
}
@ -154,7 +157,7 @@ void AvatarMixerClientData::processSetTraitsMessage(ReceivedMessage& message,
if (traitSize == AvatarTraits::DELETED_TRAIT_SIZE) {
_avatar->processDeletedTraitInstance(traitType, instanceID);
// Mixer doesn't need deleted IDs.
_avatar->getAndClearRecentlyDetachedIDs();
_avatar->getAndClearRecentlyRemovedIDs();
// to track a deleted instance but keep version information
// 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);
}
} 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;
}
}
@ -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) {
const auto& whitelist = slaveSharedData.skeletonURLWhitelist;
@ -282,14 +342,18 @@ void AvatarMixerClientData::removeFromRadiusIgnoringSet(const QUuid& other) {
void AvatarMixerClientData::resetSentTraitData(Node::LocalID nodeLocalID) {
_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) {
_currentViewFrustums.clear();
auto sourceBuffer = reinterpret_cast<const unsigned char*>(message.constData());
uint8_t numFrustums = 0;
memcpy(&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[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["recent_other_av_in_view"] = _recentOtherAvatarsInView;
@ -338,5 +403,5 @@ void AvatarMixerClientData::cleanupKilledNode(const QUuid&, Node::LocalID nodeLo
removeLastBroadcastSequenceNumber(nodeLocalID);
removeLastBroadcastTime(nodeLocalID);
_lastSentTraitsTimestamps.erase(nodeLocalID);
_sentTraitVersions.erase(nodeLocalID);
_perNodeSentTraitVersions.erase(nodeLocalID);
}

View file

@ -32,6 +32,7 @@
#include <shared/ConicalViewFrustum.h>
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";
struct SlaveSharedData;
@ -42,6 +43,7 @@ public:
AvatarMixerClientData(const QUuid& nodeID, Node::LocalID nodeLocalID);
virtual ~AvatarMixerClientData() {}
using HRCTime = p_high_resolution_clock::time_point;
using PerNodeTraitVersions = std::unordered_map<Node::LocalID, AvatarTraits::TraitVersions>;
int parseData(ReceivedMessage& message) override;
AvatarData& getAvatar() { return *_avatar; }
@ -85,10 +87,15 @@ public:
void incrementNumFramesSinceFRDAdjustment() { ++_numFramesSinceAdjustment; }
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
{ return _avgOtherAvatarDataRate.getAverageSampleValuePerSecond() / (float) BYTES_PER_KILOBIT; }
float getOutboundAvatarTraitsKbps() const
{ return _avgOtherAvatarTraitsRate.getAverageSampleValuePerSecond() / BYTES_PER_KILOBIT; }
void loadJSONStats(QJsonObject& jsonObject) const;
@ -124,6 +131,7 @@ public:
int processPackets(const SlaveSharedData& slaveSharedData); // returns number of packets processed
void processSetTraitsMessage(ReceivedMessage& message, const SlaveSharedData& slaveSharedData, Node& sendingNode);
void processBulkAvatarTraitsAckMessage(ReceivedMessage& message);
void checkSkeletonURLAgainstWhitelist(const SlaveSharedData& slaveSharedData, Node& sendingNode,
AvatarTraits::TraitVersion traitVersion);
@ -138,7 +146,14 @@ public:
void setLastOtherAvatarTraitsSendPoint(Node::LocalID otherAvatar, TraitsCheckTimestamp 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);
@ -171,6 +186,7 @@ private:
int _numOutOfOrderSends = 0;
SimpleMovingAverage _avgOtherAvatarDataRate;
SimpleMovingAverage _avgOtherAvatarTraitsRate;
std::vector<QUuid> _radiusIgnoredOthers;
ConicalViewFrustums _currentViewFrustums;
@ -183,8 +199,27 @@ private:
AvatarTraits::TraitVersions _lastReceivedTraitVersions;
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, 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 };
};

View file

@ -73,52 +73,82 @@ int AvatarMixerSlave::sendIdentityPacket(NLPacketList& packetList, const AvatarM
QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray();
individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious
packetList.write(individualData);
_stats.numIdentityPackets++;
_stats.numIdentityPacketsSent++;
_stats.numIdentityBytesSent += individualData.size();
return individualData.size();
} else {
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,
const AvatarMixerClientData* sendingNodeData,
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
// 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();
bool allTraitsUpdated = true;
qint64 bytesWritten = 0;
if (timeOfLastTraitsChange > timeOfLastTraitsSent) {
// 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();
// 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();
auto simpleReceivedIt = lastReceivedVersions.simpleCBegin();
while (simpleReceivedIt != lastReceivedVersions.simpleCEnd()) {
auto traitType = static_cast<AvatarTraits::TraitType>(std::distance(lastReceivedVersions.simpleCBegin(),
simpleReceivedIt));
auto lastReceivedVersion = *simpleReceivedIt;
auto& lastSentVersionRef = lastSentVersions[traitType];
auto& lastAckedVersionRef = lastAckedVersions[traitType];
if (lastReceivedVersions[traitType] > lastSentVersionRef) {
// there is an update to this trait, add it to the traits packet
bytesWritten += sendingAvatar->packTrait(traitType, traitsPacketList, lastReceivedVersion);
// update the last sent version
lastSentVersionRef = lastReceivedVersion;
// hold sending more traits until we've been acked that the last one we sent was received
if (lastSentVersionRef == lastAckedVersionRef) {
if (lastReceivedVersion > lastSentVersionRef) {
bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten);
// there is an update to this trait, add it to the traits packet
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;
@ -131,6 +161,7 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
// get or create the sent trait versions for this trait type
auto& sentIDValuePairs = lastSentVersions.getInstanceIDValuePairs(traitType);
auto& ackIDValuePairs = lastAckedVersions.getInstanceIDValuePairs(traitType);
// enumerate each received instance
for (auto& receivedInstance : instancedReceivedIt->instances) {
@ -148,8 +179,19 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
{
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)) {
bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten);
// 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);
@ -159,25 +201,40 @@ qint64 AvatarMixerSlave::addChangedTraitsToBulkPacket(AvatarMixerClientData* lis
} else {
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) {
bytesWritten += addTraitsNodeHeader(listeningNodeData, sendingNodeData, traitsPacketList, bytesWritten);
// this instance version was deleted and we haven't sent the delete to this client yet
bytesWritten += AvatarTraits::packInstancedTraitDelete(traitType, instanceID, traitsPacketList, absoluteReceivedVersion);
// update the last sent version for this trait instance to the absolute value of the deleted version
sentInstanceIt->value = absoluteReceivedVersion;
auto& pendingTraitVersions =
listeningNodeData->getPendingTraitVersions(listeningNodeData->getTraitsMessageSequence(),
sendingNodeLocalID);
pendingTraitVersions.instanceInsert(traitType, instanceID, absoluteReceivedVersion);
}
}
++instancedReceivedIt;
}
// write a null trait type to mark the end of trait data for this avatar
bytesWritten += traitsPacketList.writePrimitive(AvatarTraits::NullTrait);
// since we send all traits for this other avatar, update the time of last traits sent
// to match the time of last traits change
listeningNodeData->setLastOtherAvatarTraitsSendPoint(otherNodeLocalID, timeOfLastTraitsChange);
if (bytesWritten) {
// write a null trait type to mark the end of trait data for this avatar
bytesWritten += traitsPacketList.writePrimitive(AvatarTraits::NullTrait);
// since we send all traits for this other avatar, update the time of last traits sent
// to match the time of last traits change
if (allTraitsUpdated) {
listeningNodeData->setLastOtherAvatarTraitsSendPoint(sendingNodeLocalID, timeOfLastTraitsChange);
}
}
}
@ -191,7 +248,8 @@ int AvatarMixerSlave::sendReplicatedIdentityPacket(const Node& agentNode, const
auto identityPacket = NLPacketList::create(PacketType::ReplicatedAvatarIdentity, QByteArray(), true, true);
identityPacket->write(individualData);
DependencyManager::get<NodeList>()->sendPacketList(std::move(identityPacket), destinationNode);
_stats.numIdentityPackets++;
_stats.numIdentityPacketsSent++;
_stats.numIdentityBytesSent += individualData.size();
return individualData.size();
} else {
return 0;
@ -419,6 +477,7 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
int remainingAvatars = (int)sortedAvatars.size();
auto traitsPacketList = NLPacketList::create(PacketType::BulkAvatarTraits, QByteArray(), true, true);
auto avatarPacket = NLPacket::create(PacketType::BulkAvatarData);
const int avatarPacketCapacity = avatarPacket->getPayloadCapacity();
int avatarSpaceAvailable = avatarPacketCapacity;
@ -539,17 +598,16 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
++numPacketsSent;
}
_stats.numPacketsSent += numPacketsSent;
_stats.numBytesSent += numAvatarDataBytes;
// record the bytes sent for other avatar data in the AvatarMixerClientData
nodeData->recordSentAvatarData(numAvatarDataBytes);
_stats.numDataPacketsSent += numPacketsSent;
_stats.numDataBytesSent += numAvatarDataBytes;
// close the current traits packet list
traitsPacketList->closeCurrentPacket();
if (traitsPacketList->getNumPackets() >= 1) {
// send the traits packet list
_stats.numTraitsBytesSent += traitBytesSent;
_stats.numTraitsPacketsSent += (int) traitsPacketList->getNumPackets();
nodeList->sendPacketList(std::move(traitsPacketList), *destinationNode);
}
@ -559,6 +617,10 @@ void AvatarMixerSlave::broadcastAvatarDataToAgent(const SharedNodePointer& node)
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
nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack);
nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames);
@ -685,8 +747,8 @@ void AvatarMixerSlave::broadcastAvatarDataToDownstreamMixer(const SharedNodePoin
// close the current packet so that we're always sending something
avatarPacketList->closeCurrentPacket(true);
_stats.numPacketsSent += (int)avatarPacketList->getNumPackets();
_stats.numBytesSent += numAvatarDataBytes;
_stats.numDataPacketsSent += (int)avatarPacketList->getNumPackets();
_stats.numDataBytesSent += numAvatarDataBytes;
// send the replicated bulk avatar data
auto nodeList = DependencyManager::get<NodeList>();

View file

@ -24,9 +24,12 @@ public:
int nodesBroadcastedTo { 0 };
int downstreamMixersBroadcastedTo { 0 };
int numPacketsSent { 0 };
int numBytesSent { 0 };
int numIdentityPackets { 0 };
int numDataBytesSent { 0 };
int numTraitsBytesSent { 0 };
int numIdentityBytesSent { 0 };
int numDataPacketsSent { 0 };
int numTraitsPacketsSent { 0 };
int numIdentityPacketsSent { 0 };
int numOthersIncluded { 0 };
int overBudgetAvatars { 0 };
@ -45,9 +48,13 @@ public:
// sending job stats
nodesBroadcastedTo = 0;
downstreamMixersBroadcastedTo = 0;
numPacketsSent = 0;
numBytesSent = 0;
numIdentityPackets = 0;
numDataBytesSent = 0;
numTraitsBytesSent = 0;
numIdentityBytesSent = 0;
numDataPacketsSent = 0;
numTraitsPacketsSent = 0;
numIdentityPacketsSent = 0;
numOthersIncluded = 0;
overBudgetAvatars = 0;
@ -65,9 +72,12 @@ public:
nodesBroadcastedTo += rhs.nodesBroadcastedTo;
downstreamMixersBroadcastedTo += rhs.downstreamMixersBroadcastedTo;
numPacketsSent += rhs.numPacketsSent;
numBytesSent += rhs.numBytesSent;
numIdentityPackets += rhs.numIdentityPackets;
numDataBytesSent += rhs.numDataBytesSent;
numTraitsBytesSent += rhs.numTraitsBytesSent;
numIdentityBytesSent += rhs.numIdentityBytesSent;
numDataPacketsSent += rhs.numDataPacketsSent;
numTraitsPacketsSent += rhs.numTraitsPacketsSent;
numIdentityPacketsSent += rhs.numIdentityPacketsSent;
numOthersIncluded += rhs.numOthersIncluded;
overBudgetAvatars += rhs.overBudgetAvatars;
@ -104,6 +114,11 @@ private:
int sendIdentityPacket(NLPacketList& packet, 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,
const AvatarMixerClientData* sendingNodeData,
NLPacketList& traitsPacketList);

View file

@ -21,6 +21,8 @@
#include <GLMHelpers.h>
#include <ResourceRequestObserver.h>
#include <AvatarLogging.h>
#include <EntityItem.h>
#include <EntityItemProperties.h>
ScriptableAvatar::ScriptableAvatar() {
@ -249,3 +251,157 @@ void ScriptableAvatar::setHasProceduralEyeFaceMovement(bool hasProceduralEyeFace
void ScriptableAvatar::setHasAudioEnabledFaceMovement(bool 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);
}
}
}
}

View file

@ -16,6 +16,7 @@
#include <AnimSkeleton.h>
#include <AvatarData.h>
#include <ScriptEngine.h>
#include <EntityItem.h>
/**jsdoc
* 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);
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:
void update(float deltatime);
@ -202,6 +223,8 @@ private:
QHash<QString, int> _fstJointIndices; ///< 1-based, since zero is returned for missing keys
QStringList _fstJointNames; ///< in order of depth-first traversal
QUrl _skeletonFBXURL;
mutable QScriptEngine _scriptEngine;
std::map<QUuid, EntityItemPointer> _entities;
/// Loads the joint indices, names from the FST file (if any)
void updateJointMappings();

View file

@ -23,6 +23,7 @@
#include <EntityEditFilters.h>
#include <NetworkingConstants.h>
#include <AddressManager.h>
#include <hfm/ModelFormatRegistry.h>
#include "../AssignmentDynamicFactory.h"
#include "AssignmentParentFinder.h"
@ -45,6 +46,8 @@ EntityServer::EntityServer(ReceivedMessage& message) :
DependencyManager::registerInheritance<EntityDynamicFactoryInterface, AssignmentDynamicFactory>();
DependencyManager::set<AssignmentDynamicFactory>();
DependencyManager::set<ModelFormatRegistry>(); // ModelFormatRegistry must be defined before ModelCache. See the ModelCache ctor
DependencyManager::set<ModelCache>();
auto& packetReceiver = DependencyManager::get<NodeList>()->getPacketReceiver();
packetReceiver.registerListenerForTypes({ PacketType::EntityAdd,

View file

@ -75,8 +75,8 @@ void MessagesMixer::sendStatsPacket() {
DependencyManager::get<NodeList>()->eachNode([&](const SharedNodePointer& node) {
QJsonObject clientStats;
clientStats[USERNAME_UUID_REPLACEMENT_STATS_KEY] = uuidStringWithoutCurlyBraces(node->getUUID());
clientStats["outbound_kbps"] = node->getOutboundBandwidth();
clientStats["inbound_kbps"] = node->getInboundBandwidth();
clientStats["outbound_kbps"] = node->getOutboundKbps();
clientStats["inbound_kbps"] = node->getInboundKbps();
messagesMixerObject[uuidStringWithoutCurlyBraces(node->getUUID())] = clientStats;
});

Binary file not shown.

Binary file not shown.

Before

(image error) Size: 37 KiB

After

(image error) Size: 40 KiB

Binary file not shown.

Binary file not shown.

Before

(image error) Size: 51 KiB

After

(image error) Size: 39 KiB

View file

@ -123,7 +123,16 @@
{ "from": { "makeAxis" : ["Keyboard.MouseMoveLeft", "Keyboard.MouseMoveRight"] },
"when": "Keyboard.RightMouseButton",
"to": "Actions.Yaw",
"to": "Actions.DeltaYaw",
"filters":
[
{ "type": "scale", "scale": 0.6 }
]
},
{ "from": { "makeAxis" : ["Keyboard.MouseMoveUp", "Keyboard.MouseMoveDown"] },
"when": "Keyboard.RightMouseButton",
"to": "Actions.DeltaPitch",
"filters":
[
{ "type": "scale", "scale": 0.6 }
@ -144,20 +153,6 @@
{ "from": "Keyboard.PgDown", "to": "Actions.VERTICAL_DOWN" },
{ "from": "Keyboard.PgUp", "to": "Actions.VERTICAL_UP" },
{ "from": "Keyboard.MouseMoveUp", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_UP",
"filters":
[
{ "type": "scale", "scale": 0.6 }
]
},
{ "from": "Keyboard.MouseMoveDown", "when": "Keyboard.RightMouseButton", "to": "Actions.PITCH_DOWN",
"filters":
[
{ "type": "scale", "scale": 0.6 }
]
},
{ "from": "Keyboard.TouchpadDown", "to": "Actions.PITCH_DOWN" },
{ "from": "Keyboard.TouchpadUp", "to": "Actions.PITCH_UP" },

View file

@ -0,0 +1,4 @@
<svg width="149" height="150" viewBox="0 0 149 150" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M74.3055 0C115.543 0 149 33.5916 149 74.6047C149 116.008 115.543 149.6 74.3055 149.6C33.4569 149.6 0 116.008 0 74.6047C0 33.5916 33.4569 0 74.3055 0ZM74.3055 139.054C109.708 139.054 138.496 110.149 138.496 74.6047C138.496 39.4507 109.708 10.5462 74.3055 10.5462C39.2924 10.5462 10.5039 39.4507 10.5039 74.6047C10.5039 110.149 39.2924 139.054 74.3055 139.054Z" fill="#1FC6A6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M65.3575 89.8376L106.595 43.3562C110.874 38.2784 119.044 45.3092 114.376 50.387L70.0259 100.384C68.0807 102.727 64.9684 102.727 63.0233 101.165L35.0128 78.9008C29.9554 74.6042 36.569 66.4016 41.6264 70.6982L65.3575 89.8376Z" fill="#1FC6A6"/>
</svg>

After

(image error) Size: 824 B

Binary file not shown.

After

(image error) Size: 27 KiB

Binary file not shown.

After

(image error) Size: 26 KiB

Binary file not shown.

After

(image error) Size: 463 B

View file

@ -192,13 +192,13 @@ Item {
}
StatText {
visible: root.expanded;
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
"Silent: " + root.audioSilentInboundPPS + " pps";
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
root.audioMixerOutPps + "pps";
}
StatText {
visible: root.expanded;
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
root.audioMixerOutPps + "pps";
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
"Silent: " + root.audioSilentInboundPPS + " pps";
}
StatText {
visible: root.expanded;

View file

@ -210,13 +210,13 @@ Item {
}
StatText {
visible: root.expanded;
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
"Silent: " + root.audioSilentInboundPPS + " pps";
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
root.audioMixerOutPps + "pps";
}
StatText {
visible: root.expanded;
text: "Audio Mixer Out: " + root.audioMixerOutKbps + " kbps, " +
root.audioMixerOutPps + "pps";
text: "Audio In Audio: " + root.audioAudioInboundPPS + " pps, " +
"Silent: " + root.audioSilentInboundPPS + " pps";
}
StatText {
visible: root.expanded;

View file

@ -32,6 +32,10 @@ Original.Button {
width: hifi.dimensions.buttonWidth
height: hifi.dimensions.controlLineHeight
property size implicitPadding: Qt.size(20, 16)
property int implicitWidth: buttonContentItem.implicitWidth + implicitPadding.width
property int implicitHeight: buttonContentItem.implicitHeight + implicitPadding.height
HifiConstants { id: hifi }
onHoveredChanged: {
@ -94,6 +98,8 @@ Original.Button {
contentItem: Item {
id: buttonContentItem
implicitWidth: (buttonGlyph.visible ? buttonGlyph.implicitWidth : 0) + buttonText.implicitWidth
implicitHeight: buttonText.implicitHeight
TextMetrics {
id: buttonGlyphTextMetrics;
font: buttonGlyph.font;

View file

@ -10,7 +10,7 @@ import "avatarapp"
Rectangle {
id: root
width: 480
height: 706
height: 706
property bool keyboardEnabled: true
property bool keyboardRaised: false
@ -254,7 +254,8 @@ Rectangle {
onSaveClicked: function() {
var avatarSettings = {
dominantHand : settings.dominantHandIsLeft ? 'left' : 'right',
collisionsEnabled : settings.avatarCollisionsOn,
collisionsEnabled : settings.environmentCollisionsOn,
otherAvatarsCollisionsEnabled : settings.otherAvatarsCollisionsOn,
animGraphOverrideUrl : settings.avatarAnimationOverrideJSON,
collisionSoundUrl : settings.avatarCollisionSoundUrl
};
@ -415,7 +416,7 @@ Rectangle {
width: 21.2
height: 19.3
source: isAvatarInFavorites ? '../../images/FavoriteIconActive.svg' : '../../images/FavoriteIconInActive.svg'
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
}
// TextStyle5
@ -424,7 +425,7 @@ Rectangle {
Layout.fillWidth: true
text: isAvatarInFavorites ? avatarName : "Add to Favorites"
elide: Qt.ElideRight
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
}
}

View file

@ -0,0 +1,24 @@
import QtQuick 2.6
import "../stylesUit" 1.0
import "../windows" as Windows
import "avatarPackager" 1.0
Windows.ScrollingWindow {
id: root
objectName: "AvatarPackager"
width: 480
height: 706
title: "Avatar Packager"
resizable: false
opacity: parent.opacity
destroyOnHidden: true
implicitWidth: 384; implicitHeight: 640
minSize: Qt.vector2d(480, 706)
HifiConstants { id: hifi }
AvatarPackagerApp {
height: pane.height
width: pane.width
}
}

View file

@ -0,0 +1,396 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtQml.Models 2.1
import QtGraphicalEffects 1.0
import Hifi.AvatarPackager.AvatarProjectStatus 1.0
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import "../../controls" 1.0
import "../../dialogs"
import "../avatarapp" 1.0 as AvatarApp
Item {
id: windowContent
HifiConstants { id: hifi }
property alias desktopObject: avatarPackager.desktopObject
MouseArea {
anchors.fill: parent
onClicked: {
unfocusser.forceActiveFocus();
}
Item {
id: unfocusser
visible: false
}
}
InfoBox {
id: fileListPopup
title: "List of Files"
content: Rectangle {
id: fileList
color: "#404040"
anchors.fill: parent
anchors.topMargin: 10
anchors.bottomMargin: 10
anchors.leftMargin: 29
anchors.rightMargin: 29
clip: true
ListView {
anchors.fill: parent
model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles
delegate: Rectangle {
width: parent.width
height: fileText.implicitHeight + 8
color: "#404040"
RalewaySemiBold {
id: fileText
size: 16
elide: Text.ElideLeft
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.topMargin: 4
width: parent.width - 10
color: "white"
text: modelData
}
}
}
}
}
InfoBox {
id: errorPopup
property string errorMessage
boxWidth: 380
boxHeight: 293
content: RalewayRegular {
id: bodyMessage
anchors.fill: parent
anchors.bottomMargin: 10
anchors.leftMargin: 29
anchors.rightMargin: 29
size: 20
color: "white"
text: errorPopup.errorMessage
width: parent.width
wrapMode: Text.WordWrap
}
function show(title, message) {
errorPopup.title = title;
errorMessage = message;
errorPopup.open();
}
}
Rectangle {
id: modalOverlay
anchors.fill: parent
z: 20
color: "#a15d5d5d"
visible: false
// This mouse area captures the cursor events while the modalOverlay is active
MouseArea {
anchors.fill: parent
propagateComposedEvents: false
hoverEnabled: true
}
}
AvatarApp.MessageBox {
id: popup
anchors.fill: parent
visible: false
closeOnClickOutside: true
}
Column {
id: avatarPackager
anchors.fill: parent
state: "main"
states: [
State {
name: AvatarPackagerState.main
PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; backButtonVisible: false }
PropertyChanges { target: avatarPackagerMain; visible: true }
PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer }
},
State {
name: AvatarPackagerState.createProject
PropertyChanges { target: avatarPackagerHeader; title: qsTr("Create Project") }
PropertyChanges { target: createAvatarProject; visible: true }
PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer }
},
State {
name: AvatarPackagerState.project
PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; canRename: true }
PropertyChanges { target: avatarProject; visible: true }
PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer }
},
State {
name: AvatarPackagerState.projectUpload
PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; backButtonEnabled: false }
PropertyChanges { target: avatarUploader; visible: true }
PropertyChanges { target: avatarPackagerFooter; visible: false }
}
]
property alias showModalOverlay: modalOverlay.visible
property var desktopObject: desktop
function openProject(path) {
let status = AvatarPackagerCore.openAvatarProject(path);
if (status !== AvatarProjectStatus.SUCCESS) {
displayErrorMessage(status);
return status;
}
avatarProject.reset();
avatarPackager.state = AvatarPackagerState.project;
return status;
}
function displayErrorMessage(status) {
if (status === AvatarProjectStatus.SUCCESS) {
return;
}
switch (status) {
case AvatarProjectStatus.ERROR_CREATE_PROJECT_NAME:
errorPopup.show("Project Folder Already Exists", "A folder with that name already exists at that location. Please choose a different project name or location.");
break;
case AvatarProjectStatus.ERROR_CREATE_CREATING_DIRECTORIES:
errorPopup.show("Project Folders Creation Error", "There was a problem creating the Avatar Project directory. Please check the project location and try again.");
break;
case AvatarProjectStatus.ERROR_CREATE_FIND_MODEL:
errorPopup.show("Cannot Find Model File", "There was a problem while trying to find the specified model file. Please verify that it exists at the specified location.");
break;
case AvatarProjectStatus.ERROR_CREATE_OPEN_MODEL:
errorPopup.show("Cannot Open Model File", "There was a problem while trying to open the specified model file.");
break;
case AvatarProjectStatus.ERROR_CREATE_READ_MODEL:
errorPopup.show("Error Read Model File", "There was a problem while trying to read the specified model file. Please check that the file is a valid FBX file and try again.");
break;
case AvatarProjectStatus.ERROR_CREATE_WRITE_FST:
errorPopup.show("Error Writing Project File", "There was a problem while trying to write the FST file.");
break;
case AvatarProjectStatus.ERROR_OPEN_INVALID_FILE_TYPE:
errorPopup.show("Invalid Project Path", "The avatar packager can only open FST files.");
break;
case AvatarProjectStatus.ERROR_OPEN_PROJECT_FOLDER:
errorPopup.show("Project Missing", "Project folder cannot be found. Please locate the folder and copy/move it to its original location.");
break;
case AvatarProjectStatus.ERROR_OPEN_FIND_FST:
errorPopup.show("File Missing", "We cannot find the project file (.fst) in the project folder. Please locate it and move it to the project folder.");
break;
case AvatarProjectStatus.ERROR_OPEN_OPEN_FST:
errorPopup.show("File Read Error", "We cannot read the project file (.fst).");
break;
case AvatarProjectStatus.ERROR_OPEN_FIND_MODEL:
errorPopup.show("File Missing", "We cannot find the avatar model file (.fbx) in the project folder. Please locate it and move it to the project folder.");
break;
default:
errorPopup.show("Error Message Missing", "Error message missing for status " + status);
}
}
function openDocs() {
Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/create-avatars#how-to-package-your-avatar");
}
AvatarPackagerHeader {
z: 100
id: avatarPackagerHeader
colorScheme: root.colorScheme
onBackButtonClicked: {
avatarPackager.state = AvatarPackagerState.main;
}
onDocsButtonClicked: {
avatarPackager.openDocs();
}
}
Item {
height: windowContent.height - avatarPackagerHeader.height - avatarPackagerFooter.height
width: windowContent.width
Rectangle {
anchors.fill: parent
color: "#404040"
}
AvatarProject {
id: avatarProject
colorScheme: root.colorScheme
anchors.fill: parent
}
AvatarProjectUpload {
id: avatarUploader
anchors.fill: parent
root: avatarProject
}
CreateAvatarProject {
id: createAvatarProject
colorScheme: root.colorScheme
anchors.fill: parent
}
Item {
id: avatarPackagerMain
visible: false
anchors.fill: parent
property var footer: Item {
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: createProjectButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: openProjectButton.left
anchors.rightMargin: 22
height: 40
width: 134
text: qsTr("New Project")
colorScheme: root.colorScheme
onClicked: {
createAvatarProject.clearInputs();
avatarPackager.state = AvatarPackagerState.createProject;
}
}
HifiControls.Button {
id: openProjectButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
height: 40
width: 133
text: qsTr("Open Project")
color: hifi.buttons.blue
colorScheme: root.colorScheme
onClicked: {
avatarPackager.showModalOverlay = true;
let browser = avatarPackager.desktopObject.fileDialog({
selectDirectory: false,
dir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH),
filter: "Avatar Project FST Files (*.fst)",
title: "Open Project (.fst)",
});
browser.canceled.connect(function() {
avatarPackager.showModalOverlay = false;
});
browser.selectedFile.connect(function(fileUrl) {
let fstFilePath = fileDialogHelper.urlToPath(fileUrl);
avatarPackager.showModalOverlay = false;
avatarPackager.openProject(fstFilePath);
});
}
}
}
Flow {
visible: AvatarPackagerCore.recentProjects.length === 0
anchors {
fill: parent
topMargin: 18
leftMargin: 16
rightMargin: 16
}
RalewayRegular {
size: 20
color: "white"
text: "Use a custom avatar of your choice."
width: parent.width
wrapMode: Text.WordWrap
}
RalewayRegular {
size: 20
color: "white"
text: "<a href='javascript:void'>Visit our docs</a> to learn more about using the packager."
linkColor: "#00B4EF"
width: parent.width
wrapMode: Text.WordWrap
onLinkActivated: {
avatarPackager.openDocs();
}
}
}
Item {
anchors.fill: parent
visible: AvatarPackagerCore.recentProjects.length > 0
RalewayRegular {
id: recentProjectsText
color: 'white'
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: 16
anchors.leftMargin: 16
size: 20
text: "Recent Projects"
onLinkActivated: fileListPopup.open()
}
Column {
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
top: recentProjectsText.bottom
topMargin: 16
leftMargin: 16
rightMargin: 16
}
spacing: 10
Repeater {
model: AvatarPackagerCore.recentProjects
AvatarProjectCard {
title: modelData.name
path: modelData.projectPath
onOpen: avatarPackager.openProject(modelData.path)
}
}
}
}
}
}
AvatarPackagerFooter {
id: avatarPackagerFooter
}
}
}

View file

@ -0,0 +1,41 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Rectangle {
id: avatarPackagerFooter
color: "#404040"
height: content === defaultContent ? 0 : 74
visible: content !== defaultContent
width: parent.width
property var content: Item { id: defaultContent }
children: [background, content]
property var background: Rectangle {
anchors.fill: parent
color: "#404040"
Rectangle {
id: topBorder1
anchors.top: parent.top
color: "#252525"
height: 1
width: parent.width
}
Rectangle {
id: topBorder2
anchors.top: topBorder1.bottom
color: "#575757"
height: 1
width: parent.width
}
}
}

View file

@ -0,0 +1,144 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import "../avatarapp" 1.0
ShadowRectangle {
id: root
width: parent.width
height: 74
color: "#252525"
property string title: qsTr("Avatar Packager")
property alias docsEnabled: docs.visible
property bool backButtonVisible: true // If false, is not visible and does not take up space
property bool backButtonEnabled: true // If false, is not visible but does not affect space
property bool canRename: false
property int colorScheme
property color textColor: "white"
property color hoverTextColor: "gray"
property color pressedTextColor: "#6A6A6A"
signal backButtonClicked
signal docsButtonClicked
RalewayButton {
id: back
visible: backButtonEnabled && backButtonVisible
size: 28
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: 16
text: "◀"
onClicked: root.backButtonClicked()
}
Item {
id: titleArea
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left: root.backButtonVisible ? back.right : parent.left
anchors.leftMargin: root.backButtonVisible ? 11 : 21
anchors.right: docs.left
states: [
State {
name: "renaming"
PropertyChanges { target: title; visible: false }
PropertyChanges { target: titleInputArea; visible: true }
}
]
Item {
id: title
anchors.fill: parent
RalewaySemiBold {
id: titleNotRenameable
visible: !root.canRename
size: 28
anchors.fill: parent
text: root.title
color: "white"
}
RalewayButton {
id: titleRenameable
visible: root.canRename
enabled: root.canRename
size: 28
anchors.fill: parent
text: root.title
onClicked: {
if (!root.canRename || AvatarPackagerCore.currentAvatarProject === null) {
return;
}
titleArea.state = "renaming";
titleInput.text = AvatarPackagerCore.currentAvatarProject.name;
titleInput.selectAll();
titleInput.forceActiveFocus(Qt.MouseFocusReason);
}
}
}
Item {
id: titleInputArea
visible: false
anchors.fill: parent
HifiControls.TextField {
id: titleInput
anchors.fill: parent
text: ""
colorScheme: root.colorScheme
font.family: "Fira Sans"
font.pixelSize: 28
z: 200
onFocusChanged: {
if (titleArea.state === "renaming" && !focus) {
accepted();
}
}
Keys.onPressed: {
if (event.key === Qt.Key_Escape) {
titleArea.state = "";
}
}
onAccepted: {
if (acceptableInput) {
AvatarPackagerCore.currentAvatarProject.name = text;
}
titleArea.state = "";
}
}
}
}
RalewayButton {
id: docs
visible: false
size: 28
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: 16
text: qsTr("Docs")
onClicked: {
docsButtonClicked();
}
}
}

View file

@ -0,0 +1,10 @@
pragma Singleton
import QtQuick 2.6
Item {
id: singleton
readonly property string main: "main"
readonly property string project: "project"
readonly property string createProject: "createProject"
readonly property string projectUpload: "projectUpload"
}

View file

@ -0,0 +1,336 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.2 as Original
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: root
HifiConstants { id: hifi }
Style { id: style }
property int colorScheme
property var uploader: null
property bool hasSuccessfullyUploaded: true
visible: false
anchors.fill: parent
anchors.margins: 10
function reset() {
hasSuccessfullyUploaded = false;
uploader = null;
}
property var footer: Item {
anchors.fill: parent
Item {
id: uploadFooter
visible: !root.uploader || root.finished || root.uploader.state !== 4
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: uploadButton
visible: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded
enabled: Account.loggedIn
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("Upload")
color: hifi.buttons.blue
colorScheme: root.colorScheme
width: 133
height: 40
onClicked: {
uploadNew();
}
}
HifiControls.Button {
id: updateButton
visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded
enabled: Account.loggedIn
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("Update")
color: hifi.buttons.blue
colorScheme: root.colorScheme
width: 134
height: 40
onClicked: {
showConfirmUploadPopup(uploadNew, uploadUpdate);
}
}
Item {
anchors.fill: parent
visible: root.hasSuccessfullyUploaded
HifiControls.Button {
enabled: Account.loggedIn
anchors.verticalCenter: parent.verticalCenter
anchors.right: viewInInventoryButton.left
anchors.rightMargin: 16
text: qsTr("Update")
color: hifi.buttons.white
colorScheme: root.colorScheme
width: 134
height: 40
onClicked: {
showConfirmUploadPopup(uploadNew, uploadUpdate);
}
}
HifiControls.Button {
id: viewInInventoryButton
enabled: Account.loggedIn
width: 168
height: 40
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("View in Inventory")
color: hifi.buttons.blue
colorScheme: root.colorScheme
onClicked: AvatarPackagerCore.currentAvatarProject.openInInventory()
}
}
}
Rectangle {
id: uploadingItemFooter
anchors.fill: parent
anchors.topMargin: 1
visible: !!root.uploader && !root.finished && root.uploader.state === 4
color: "#00B4EF"
LoadingCircle {
id: runningImage
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 16
width: 28
height: 28
white: true
}
RalewayRegular {
id: stepText
size: 20
anchors.verticalCenter: parent.verticalCenter
anchors.left: runningImage.right
anchors.leftMargin: 16
text: "Adding item to Inventory"
color: "white"
}
}
}
function uploadNew() {
upload(false);
}
function uploadUpdate() {
upload(true);
}
Connections {
target: root.uploader
onStateChanged: {
root.hasSuccessfullyUploaded = newState >= 4;
}
}
function upload(updateExisting) {
root.uploader = AvatarPackagerCore.currentAvatarProject.upload(updateExisting);
console.log("uploader: "+ root.uploader);
root.uploader.send();
avatarPackager.state = AvatarPackagerState.projectUpload;
}
function showConfirmUploadPopup() {
popup.titleText = 'Overwrite Avatar';
popup.bodyText = 'You have previously uploaded the avatar file from this project.' +
' This will overwrite that avatar and you wont be able to access the older version.';
popup.button1text = 'CREATE NEW';
popup.button2text = 'OVERWRITE';
popup.onButton2Clicked = function() {
popup.close();
uploadUpdate();
};
popup.onButton1Clicked = function() {
popup.close();
showConfirmCreateNewPopup();
};
popup.open();
}
function showConfirmCreateNewPopup(confirmCallback) {
popup.titleText = 'Create New';
popup.bodyText = 'This will upload your current files with the same avatar name.' +
' You will lose the ability to update the previously uploaded avatar. Are you sure you want to continue?';
popup.button1text = 'CANCEL';
popup.button2text = 'CONFIRM';
popup.onButton1Clicked = function() {
popup.close()
};
popup.onButton2Clicked = function() {
popup.close();
uploadNew();
};
popup.open();
}
RalewayRegular {
id: infoMessage
states: [
State {
when: root.hasSuccessfullyUploaded
name: "upload-success"
PropertyChanges {
target: infoMessage
text: "Your avatar has been successfully uploaded to our servers. Make changes to your avatar by editing and uploading the project files."
}
},
State {
name: "has-previous-success"
when: !!AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID
PropertyChanges {
target: infoMessage
text: "Click \"Update\" to overwrite the hosted files and update the avatar in your inventory. You will have to “Wear” the avatar again to see changes."
}
}
]
color: 'white'
size: 20
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottomMargin: 24
wrapMode: Text.Wrap
text: "You can upload your files to our servers to always access them, and to make your avatar visible to other users."
}
HifiControls.Button {
id: openFolderButton
visible: false
width: parent.width
anchors.top: infoMessage.bottom
anchors.topMargin: 10
text: qsTr("Open Project Folder")
colorScheme: root.colorScheme
height: 30
onClicked: {
fileDialogHelper.openDirectory(fileDialogHelper.pathToUrl(AvatarPackagerCore.currentAvatarProject.projectFolderPath));
}
}
RalewayRegular {
id: showFilesText
color: 'white'
linkColor: style.colors.blueHighlight
visible: AvatarPackagerCore.currentAvatarProject !== null
anchors.bottom: loginRequiredMessage.top
anchors.bottomMargin: 10
size: 20
text: AvatarPackagerCore.currentAvatarProject ? AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>See list</a>" : ""
onLinkActivated: fileListPopup.open()
}
Rectangle {
id: loginRequiredMessage
visible: !Account.loggedIn
height: !Account.loggedIn ? loginRequiredTextRow.height + 20 : 0
anchors {
bottom: parent.bottom
left: parent.left
right: parent.right
}
color: "#FFD6AD"
border.color: "#F39622"
border.width: 2
radius: 2
Item {
id: loginRequiredTextRow
height: Math.max(loginWarningGlyph.implicitHeight, loginWarningText.implicitHeight)
anchors.fill: parent
anchors.margins: 10
HiFiGlyphs {
id: loginWarningGlyph
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
width: implicitWidth
size: 48
text: "+"
color: "black"
}
RalewayRegular {
id: loginWarningText
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 16
anchors.left: loginWarningGlyph.right
anchors.right: parent.right
text: "Please login to upload your avatar to High Fidelity hosting."
size: 18
wrapMode: Text.Wrap
}
}
}
}

View file

@ -0,0 +1,102 @@
import QtQuick 2.0
import QtGraphicalEffects 1.0
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: projectCard
height: 80
width: parent.width
property alias title: title.text
property alias path: path.text
property color textColor: "#E3E3E3"
property color hoverTextColor: "#121212"
property color pressedTextColor: "#121212"
property color backgroundColor: "#121212"
property color hoverBackgroundColor: "#E3E3E3"
property color pressedBackgroundColor: "#6A6A6A"
signal open
state: mouseArea.pressed ? "pressed" : (mouseArea.containsMouse ? "hover" : "normal")
states: [
State {
name: "normal"
PropertyChanges { target: background; color: backgroundColor }
PropertyChanges { target: title; color: textColor }
PropertyChanges { target: path; color: textColor }
},
State {
name: "hover"
PropertyChanges { target: background; color: hoverBackgroundColor }
PropertyChanges { target: title; color: hoverTextColor }
PropertyChanges { target: path; color: hoverTextColor }
},
State {
name: "pressed"
PropertyChanges { target: background; color: pressedBackgroundColor }
PropertyChanges { target: title; color: pressedTextColor }
PropertyChanges { target: path; color: pressedTextColor }
}
]
Rectangle {
id: background
width: parent.width
height: parent.height
color: "#121212"
radius: 4
RalewayBold {
id: title
elide: "ElideRight"
anchors {
top: parent.top
topMargin: 13
left: parent.left
leftMargin: 16
right: parent.right
rightMargin: 16
}
text: "<title missing>"
size: 24
}
RalewayRegular {
id: path
anchors {
top: title.bottom
left: parent.left
leftMargin: 32
right: background.right
rightMargin: 16
}
elide: "ElideLeft"
horizontalAlignment: Text.AlignRight
text: "<path missing>"
size: 20
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: open()
}
}
DropShadow {
id: shadow
anchors.fill: background
radius: 4
horizontalOffset: 0
verticalOffset: 4
color: Qt.rgba(0, 0, 0, 0.25)
source: background
}
}

View file

@ -0,0 +1,202 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.2 as Original
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: uploadingScreen
property var root: undefined
visible: false
anchors.fill: parent
Timer {
id: backToProjectTimer
interval: 2000
running: false
repeat: false
onTriggered: {
if (avatarPackager.state === AvatarPackagerState.projectUpload) {
avatarPackager.state = AvatarPackagerState.project;
}
}
}
function stateChangedCallback(newState) {
if (newState >= 4) {
root.uploader.stateChanged.disconnect(stateChangedCallback);
backToProjectTimer.start();
}
}
onVisibleChanged: {
if (visible) {
root.uploader.stateChanged.connect(stateChangedCallback);
root.uploader.finishedChanged.connect(function() {
if (root.uploader.error === 0) {
backToProjectTimer.start();
}
});
}
}
Item {
id: uploadStatus
anchors.fill: parent
Item {
id: statusItem
width: parent.width
height: 256
states: [
State {
name: "success"
when: root.uploader.state >= 4 && root.uploader.error === 0
PropertyChanges { target: uploadSpinner; visible: false }
PropertyChanges { target: errorIcon; visible: false }
PropertyChanges { target: successIcon; visible: true }
},
State {
name: "error"
when: root.uploader.finished && root.uploader.error !== 0
PropertyChanges { target: uploadSpinner; visible: false }
PropertyChanges { target: errorIcon; visible: true }
PropertyChanges { target: successIcon; visible: false }
PropertyChanges { target: errorFooter; visible: true }
PropertyChanges { target: errorMessage; visible: true }
}
]
AnimatedImage {
id: uploadSpinner
visible: true
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
width: 164
height: 164
source: "../../../icons/loader-snake-256.gif"
playing: true
}
HiFiGlyphs {
id: errorIcon
visible: false
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
}
size: 315
text: "+"
color: "#EA4C5F"
}
Image {
id: successIcon
visible: false
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: 148
height: 148
source: "../../../icons/checkmark-stroke.svg"
}
}
Item {
id: statusRows
anchors.top: statusItem.bottom
anchors.left: parent.left
anchors.leftMargin: 12
AvatarUploadStatusItem {
id: statusCategories
uploader: root.uploader
text: "Retrieving categories"
uploaderState: 1
}
AvatarUploadStatusItem {
id: statusUploading
uploader: root.uploader
anchors.top: statusCategories.bottom
text: "Uploading data"
uploaderState: 2
}
AvatarUploadStatusItem {
id: statusResponse
uploader: root.uploader
anchors.top: statusUploading.bottom
text: "Waiting for response"
uploaderState: 3
}
}
RalewayRegular {
id: errorMessage
visible: false
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: errorFooter.top
anchors.leftMargin: 16
anchors.rightMargin: 16
anchors.bottomMargin: 32
size: 28
wrapMode: Text.Wrap
color: "white"
text: "We couldn't upload your avatar at this time. Please try again later."
}
AvatarPackagerFooter {
id: errorFooter
anchors.bottom: parent.bottom
visible: false
content: Item {
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: backButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
text: qsTr("Back")
color: hifi.buttons.blue
colorScheme: root.colorScheme
width: 133
height: 40
onClicked: {
avatarPackager.state = AvatarPackagerState.project;
}
}
}
}
}
}

View file

@ -0,0 +1,96 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: root
height: 48
property string text: "NO STEP TEXT"
property int uploaderState;
property var uploader;
states: [
State {
name: ""
when: root.uploader === null
},
State {
name: "success"
when: root.uploader.state > uploaderState
PropertyChanges { target: stepText; color: "white" }
PropertyChanges { target: successGlyph; visible: true }
},
State {
name: "fail"
when: root.uploader.error !== 0
PropertyChanges { target: stepText; color: "#EA4C5F" }
PropertyChanges { target: failGlyph; visible: true }
},
State {
name: "running"
when: root.uploader.state === uploaderState
PropertyChanges { target: stepText; color: "white" }
PropertyChanges { target: runningImage; visible: true; playing: true }
}
]
Item {
id: statusItem
width: 48
height: parent.height
LoadingCircle {
id: runningImage
visible: false
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: 32
height: 32
}
Image {
id: successGlyph
visible: false
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: 30
height: 30
source: "../../../icons/checkmark-stroke.svg"
}
HiFiGlyphs {
id: failGlyph
visible: false
width: implicitWidth
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
size: 48
text: "+"
color: "#EA4C5F"
}
}
RalewayRegular {
id: stepText
anchors.left: statusItem.right
anchors.verticalCenter: parent.verticalCenter
text: root.text
size: 28
color: "#777777"
}
}

View file

@ -0,0 +1,63 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import TabletScriptingInterface 1.0
Item {
id: root
readonly property bool pressed: mouseArea.state == "pressed"
readonly property bool hovered: mouseArea.state == "hovering"
signal clicked()
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.focus = true
Tablet.playSound(TabletEnums.ButtonClick);
root.clicked();
}
property string lastState: ""
states: [
State {
name: ""
StateChangeScript {
script: {
mouseArea.lastState = mouseArea.state;
}
}
},
State {
name: "pressed"
when: mouseArea.containsMouse && mouseArea.pressed
StateChangeScript {
script: {
mouseArea.lastState = mouseArea.state;
}
}
},
State {
name: "hovering"
when: mouseArea.containsMouse
StateChangeScript {
script: {
if (mouseArea.lastState == "") {
Tablet.playSound(TabletEnums.ButtonHover);
}
mouseArea.lastState = mouseArea.state;
}
}
}
]
}
}

View file

@ -0,0 +1,135 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import Hifi.AvatarPackager.AvatarProjectStatus 1.0
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Item {
id: root
HifiConstants { id: hifi }
property int colorScheme
property var footer: Item {
anchors.fill: parent
anchors.rightMargin: 17
HifiControls.Button {
id: createButton
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
height: 30
width: 133
text: qsTr("Create")
enabled: false
onClicked: {
let status = AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text);
if (status !== AvatarProjectStatus.SUCCESS) {
avatarPackager.displayErrorMessage(status);
return;
}
avatarProject.reset();
avatarPackager.state = AvatarPackagerState.project;
}
}
}
visible: false
anchors.fill: parent
height: parent.height
width: parent.width
function clearInputs() {
name.text = projectLocation.text = avatarModel.text = textureFolder.text = "";
}
function checkErrors() {
let newErrorMessageText = "";
let projectName = name.text;
let projectFolder = projectLocation.text;
let hasProjectNameError = projectName !== "" && projectFolder !== "" && !AvatarPackagerCore.isValidNewProjectName(projectFolder, projectName);
if (hasProjectNameError) {
newErrorMessageText = "A folder with that name already exists at that location. Please choose a different project name or location.";
}
name.error = projectLocation.error = hasProjectNameError;
errorMessage.text = newErrorMessageText;
createButton.enabled = newErrorMessageText === "" && requiredFieldsFilledIn();
}
function requiredFieldsFilledIn() {
return name.text !== "" && projectLocation.text !== "" && avatarModel.text !== "";
}
RalewayRegular {
id: errorMessage
visible: text !== ""
text: ""
color: "#EA4C5F"
wrapMode: Text.WordWrap
size: 20
anchors {
left: parent.left
right: parent.right
}
}
Column {
id: createAvatarColumns
anchors.top: errorMessage.visible ? errorMessage.bottom : parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 10
spacing: 17
property string defaultFileBrowserPath: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH)
ProjectInputControl {
id: name
label: "Name"
colorScheme: root.colorScheme
onTextChanged: checkErrors()
}
ProjectInputControl {
id: projectLocation
label: "Specify Project Location"
colorScheme: root.colorScheme
browseEnabled: true
browseFolder: true
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
browseTitle: "Project Location"
onTextChanged: checkErrors()
}
ProjectInputControl {
id: avatarModel
label: "Specify Avatar Model (.fbx)"
colorScheme: root.colorScheme
browseEnabled: true
browseFolder: false
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
browseFilter: "Avatar Model File (*.fbx)"
browseTitle: "Open Avatar Model (.fbx)"
onTextChanged: checkErrors()
}
ProjectInputControl {
id: textureFolder
label: "Specify Texture Folder - <i>Optional</i>"
colorScheme: root.colorScheme
browseEnabled: true
browseFolder: true
browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath
browseTitle: "Texture Folder"
onTextChanged: checkErrors()
}
}
}

View file

@ -0,0 +1,120 @@
import Hifi 1.0 as Hifi
import QtQuick 2.5
import stylesUit 1.0
import controlsUit 1.0 as HifiControlsUit
import "../../controls" as HifiControls
Rectangle {
id: root
visible: false
color: Qt.rgba(.34, .34, .34, 0.6)
z: 999;
anchors.fill: parent
property alias title: titleText.text
property alias content: loader.sourceComponent
property bool closeOnClickOutside: false;
property alias boxWidth: mainContainer.width
property alias boxHeight: mainContainer.height
onVisibleChanged: {
if (visible) {
focus = true;
}
}
function open() {
visible = true;
}
function close() {
visible = false;
}
HifiConstants {
id: hifi
}
// This object is always used in a popup.
// This MouseArea is used to prevent a user from being
// able to click on a button/mouseArea underneath the popup.
MouseArea {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
onClicked: {
if (closeOnClickOutside) {
root.close()
}
}
}
Rectangle {
id: mainContainer
width: Math.max(parent.width * 0.8, 400)
height: parent.height * 0.6
MouseArea {
anchors.fill: parent
propagateComposedEvents: false
hoverEnabled: true
onClicked: function(ev) {
ev.accepted = true;
}
}
anchors.centerIn: parent
color: "#252525"
// TextStyle1
RalewaySemiBold {
id: titleText
size: 24
color: "white"
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 30
text: "Title not defined"
}
Item {
anchors.topMargin: 10
anchors.top: titleText.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: button.top
Loader {
id: loader
anchors.fill: parent
}
}
Item {
id: button
height: 40
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: 12
HifiControlsUit.Button {
anchors.centerIn: parent
text: "CLOSE"
onClicked: close()
color: hifi.buttons.noneBorderlessWhite;
colorScheme: hifi.colorSchemes.dark;
}
}
}
}

View file

@ -0,0 +1,16 @@
import QtQuick 2.6
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
AnimatedImage {
id: root
width: 128
height: 128
property bool white: false
source: white ? "../../../icons/loader-snake-256-wf.gif" : "../../../icons/loader-snake-256.gif"
playing: true
}

View file

@ -0,0 +1,78 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
Column {
id: control
anchors.left: parent.left
anchors.leftMargin: 21
anchors.right: parent.right
anchors.rightMargin: 16
height: 75
spacing: 4
property alias label: label.text
property alias browseEnabled: browseButton.visible
property bool browseFolder: false
property string browseFilter: "All Files (*.*)"
property string browseTitle: "Open file"
property string browseDir: ""
property alias text: input.text
property alias error: input.error
property int colorScheme
Row {
RalewaySemiBold {
id: label
size: 20
font.weight: Font.Medium
text: ""
color: "white"
}
}
Row {
width: control.width
spacing: 16
height: 40
HifiControls.TextField {
id: input
colorScheme: control.colorScheme
font.family: "Fira Sans"
font.pixelSize: 18
height: parent.height
width: browseButton.visible ? parent.width - browseButton.width - parent.spacing : parent.width
}
HifiControls.Button {
id: browseButton
visible: false
height: parent.height
width: 133
text: qsTr("Browse")
colorScheme: root.colorScheme
onClicked: {
avatarPackager.showModalOverlay = true;
let browser = avatarPackager.desktopObject.fileDialog({
selectDirectory: browseFolder,
dir: browseDir,
filter: browseFilter,
title: browseTitle,
});
browser.canceled.connect(function() {
avatarPackager.showModalOverlay = false;
});
browser.selectedFile.connect(function(fileUrl) {
input.text = fileDialogHelper.urlToPath(fileUrl);
avatarPackager.showModalOverlay = false;
});
}
}
}
}

View file

@ -0,0 +1,26 @@
import QtQuick 2.6
import "../../controlsUit" 1.0 as HifiControls
import "../../stylesUit" 1.0
import TabletScriptingInterface 1.0
RalewaySemiBold {
id: root
property color idleColor: "white"
property color hoverColor: "#AFAFAF"
property color pressedColor: "#575757"
color: clickable.hovered ? root.hoverColor : (clickable.pressed ? root.pressedColor : root.idleColor)
signal clicked()
ClickableArea {
id: clickable
anchors.fill: root
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,20 @@
import QtQuick 2.5
import QtQuick.Window 2.2
import "../../stylesUit" 1.0
QtObject {
readonly property QtObject colors: QtObject {
readonly property color lightGrayBackground: "#f2f2f2"
readonly property color black: "#000000"
readonly property color white: "#ffffff"
readonly property color blueHighlight: "#00b4ef"
readonly property color inputFieldBackground: "#d4d4d4"
readonly property color yellowishOrange: "#ffb017"
readonly property color blueAccent: "#0093c5"
readonly property color greenHighlight: "#1fc6a6"
readonly property color lightGray: "#afafaf"
readonly property color redHighlight: "#ea4c5f"
readonly property color orangeAccent: "#ff6309"
}
}

View file

@ -0,0 +1,2 @@
module AvatarPackager
singleton AvatarPackagerState 1.0 AvatarPackagerState.qml

View file

@ -157,7 +157,7 @@ Rectangle {
visible = false;
adjustWearablesClosed(status, avatarName);
}
HifiConstants { id: hifi }
@ -230,7 +230,7 @@ Rectangle {
lineHeightMode: Text.FixedHeight
lineHeight: 18;
text: "Wearable"
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
}
spacing: 10
@ -241,7 +241,7 @@ Rectangle {
lineHeight: 18;
text: "<a href='#'>Get more</a>"
linkColor: hifi.colors.blueHighlight
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
onLinkActivated: {
popup.showGetWearables(function() {
emitSendToScript({'method' : 'navigate', 'url' : 'hifi://AvatarIsland/11.5848,-8.10862,-2.80195'})

View file

@ -23,6 +23,8 @@ Rectangle {
property string button2color: hifi.buttons.blue;
property string button2text: ''
property bool closeOnClickOutside: false;
property var onButton2Clicked;
property var onButton1Clicked;
property var onLinkClicked;
@ -56,6 +58,11 @@ Rectangle {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
onClicked: {
if (closeOnClickOutside) {
root.close()
}
}
}
Rectangle {
@ -68,6 +75,15 @@ Rectangle {
console.debug('mainContainer: height = ', height)
}
MouseArea {
anchors.fill: parent;
propagateComposedEvents: false;
hoverEnabled: true;
onClicked: function(ev) {
ev.accepted = true;
}
}
anchors.centerIn: parent
color: "white"

View file

@ -35,7 +35,8 @@ Rectangle {
property real scaleValue: scaleSlider.value / 10
property alias dominantHandIsLeft: leftHandRadioButton.checked
property alias avatarCollisionsOn: collisionsEnabledRadiobutton.checked
property alias otherAvatarsCollisionsOn: otherAvatarsCollisionsEnabledCheckBox.checked
property alias environmentCollisionsOn: environmentCollisionsEnabledCheckBox.checked
property alias avatarAnimationOverrideJSON: avatarAnimationUrlInputText.text
property alias avatarAnimationJSON: avatarAnimationUrlInputText.placeholderText
property alias avatarCollisionSoundUrl: avatarCollisionSoundUrlInputText.text
@ -54,11 +55,11 @@ Rectangle {
} else {
rightHandRadioButton.checked = true;
}
if (settings.otherAvatarsCollisionsEnabled) {
otherAvatarsCollisionsEnabledCheckBox.checked = true;
}
if (settings.collisionsEnabled) {
collisionsEnabledRadiobutton.checked = true;
} else {
collisionsDisabledRadioButton.checked = true;
environmentCollisionsEnabledCheckBox.checked = true;
}
avatarAnimationJSON = settings.animGraphUrl;
@ -103,11 +104,11 @@ Rectangle {
size: 17;
text: "Avatar Scale"
verticalAlignment: Text.AlignVCenter
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
}
RowLayout {
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
spacing: 0
@ -117,7 +118,7 @@ Rectangle {
text: 'T'
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
}
HifiControlsUit.Slider {
@ -135,7 +136,7 @@ Rectangle {
}
}
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
// TextStyle9
@ -164,7 +165,7 @@ Rectangle {
text: 'T'
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
anchors.verticalCenter: parent.verticalCenter
Layout.alignment: Qt.AlignVCenter
}
}
@ -255,55 +256,43 @@ Rectangle {
text: "Right"
boxSize: 20
}
HifiConstants {
id: hifi
}
// TextStyle9
RalewaySemiBold {
size: 17;
Layout.row: 1
Layout.column: 0
text: "Avatar Collisions"
text: "Avatar collides with other avatars"
}
ButtonGroup {
id: onOff
}
HifiControlsUit.RadioButton {
id: collisionsEnabledRadiobutton
Layout.row: 1
Layout.column: 1
Layout.leftMargin: -40
ButtonGroup.group: onOff
colorScheme: hifi.colorSchemes.light
fontSize: 17
letterSpacing: 1.4
checked: true
text: "ON"
boxSize: 20
}
HifiConstants {
id: hifi
}
HifiControlsUit.RadioButton {
id: collisionsDisabledRadioButton
HifiControlsUit.CheckBox {
id: otherAvatarsCollisionsEnabledCheckBox;
boxSize: 20;
Layout.row: 1
Layout.column: 2
Layout.rightMargin: 20
ButtonGroup.group: onOff
Layout.leftMargin: 60
colorScheme: hifi.colorSchemes.light
fontSize: 17
letterSpacing: 1.4
}
text: "OFF"
boxSize: 20
// TextStyle9
RalewaySemiBold {
size: 17;
Layout.row: 2
Layout.column: 0
text: "Avatar collides with environment"
}
HifiControlsUit.CheckBox {
id: environmentCollisionsEnabledCheckBox;
boxSize: 20;
Layout.row: 2
Layout.column: 2
Layout.leftMargin: 60
colorScheme: hifi.colorSchemes.light
}
}
@ -327,8 +316,7 @@ Rectangle {
InputTextStyle4 {
id: avatarAnimationUrlInputText
font.pixelSize: 17
anchors.left: parent.left
anchors.right: parent.right
Layout.fillWidth: true
placeholderText: 'user\\file\\dir'
onFocusChanged: {
@ -357,8 +345,7 @@ Rectangle {
InputTextStyle4 {
id: avatarCollisionSoundUrlInputText
font.pixelSize: 17
anchors.left: parent.left
anchors.right: parent.right
Layout.fillWidth: true
placeholderText: 'https://hifi-public.s3.amazonaws.com/sounds/Collisions-'
onFocusChanged: {

View file

@ -0,0 +1,15 @@
import QtQuick 2.0
import "../avatarPackager" 1.0
Item {
id: root
width: 480
height: 706
AvatarPackagerApp {
width: parent.width
height: parent.height
desktopObject: tabletRoot
}
}

View file

@ -869,7 +869,7 @@ Flickable {
id: outOfRangeDataStrategyComboBox
height: 25
width: 100
width: 150
editable: true
colorScheme: hifi.colorSchemes.dark

View file

@ -158,6 +158,7 @@
#include "audio/AudioScope.h"
#include "avatar/AvatarManager.h"
#include "avatar/MyHead.h"
#include "avatar/AvatarPackager.h"
#include "CrashRecoveryHandler.h"
#include "CrashHandler.h"
#include "devices/DdeFaceTracker.h"
@ -170,6 +171,7 @@
#include "scripting/Audio.h"
#include "networking/CloseEventSender.h"
#include "scripting/TestScriptingInterface.h"
#include "scripting/PlatformInfoScriptingInterface.h"
#include "scripting/AssetMappingsScriptingInterface.h"
#include "scripting/ClipboardScriptingInterface.h"
#include "scripting/DesktopScriptingInterface.h"
@ -722,6 +724,8 @@ const QString TEST_RESULTS_LOCATION_COMMAND{ "--testResultsLocation" };
bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
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
// --allowMultipleInstances
auto reportAndQuit = [&](const char* commandSwitch, std::function<void(FILE* fp)> report) {
@ -861,7 +865,6 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
DependencyManager::set<LODManager>();
DependencyManager::set<StandAloneJSConsole>();
DependencyManager::set<DialogsManager>();
DependencyManager::set<BandwidthRecorder>();
DependencyManager::set<ResourceCacheSharedItems>();
DependencyManager::set<DesktopScriptingInterface>();
DependencyManager::set<EntityScriptingInterface>(true);
@ -922,6 +925,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) {
DependencyManager::set<Keyboard>();
DependencyManager::set<KeyboardScriptingInterface>();
DependencyManager::set<GrabManager>();
DependencyManager::set<AvatarPackager>();
return previousSessionCrashed;
}
@ -972,6 +976,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
QApplication(argc, argv),
_window(new MainWindow(desktop())),
_sessionRunTimer(startupTimer),
_logger(new FileLogger(this)),
_previousSessionCrashed(setupEssentials(argc, argv, runningMarkerExisted)),
_entitySimulation(new PhysicalEntitySimulation()),
_physicsEngine(new PhysicsEngine(Vectors::ZERO)),
@ -1061,9 +1066,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
installNativeEventFilter(&MyNativeEventFilter::getInstance());
#endif
_logger = new FileLogger(this);
qInstallMessageHandler(messageHandler);
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf");
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/fontawesome-webfont.ttf");
QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "fonts/hifi-glyphs.ttf");
@ -1577,13 +1579,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
connect(this, SIGNAL(aboutToQuit()), this, SLOT(onAboutToQuit()));
// hook up bandwidth estimator
QSharedPointer<BandwidthRecorder> bandwidthRecorder = DependencyManager::get<BandwidthRecorder>();
connect(nodeList.data(), &LimitedNodeList::dataSent,
bandwidthRecorder.data(), &BandwidthRecorder::updateOutboundData);
connect(nodeList.data(), &LimitedNodeList::dataReceived,
bandwidthRecorder.data(), &BandwidthRecorder::updateInboundData);
// FIXME -- I'm a little concerned about this.
connect(myAvatar->getSkeletonModel().get(), &SkeletonModel::skeletonLoaded,
this, &Application::checkSkeleton, Qt::QueuedConnection);
@ -2049,15 +2044,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
properties["deadlock_watchdog_maxElapsed"] = (int)DeadlockWatchdogThread::_maxElapsed;
properties["deadlock_watchdog_maxElapsedAverage"] = (int)DeadlockWatchdogThread::_maxElapsedAverage;
auto bandwidthRecorder = DependencyManager::get<BandwidthRecorder>();
properties["packet_rate_in"] = bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond();
properties["packet_rate_out"] = bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond();
properties["kbps_in"] = bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond();
properties["kbps_out"] = bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond();
properties["atp_in_kbps"] = bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AssetServer);
auto nodeList = DependencyManager::get<NodeList>();
properties["packet_rate_in"] = nodeList->getInboundPPS();
properties["packet_rate_out"] = nodeList->getOutboundPPS();
properties["kbps_in"] = nodeList->getInboundKbps();
properties["kbps_out"] = nodeList->getOutboundKbps();
SharedNodePointer entityServerNode = nodeList->soloNodeOfType(NodeType::EntityServer);
SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer);
SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer);
@ -2068,6 +2060,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
properties["avatar_ping"] = avatarMixerNode ? avatarMixerNode->getPingMs() : -1;
properties["asset_ping"] = assetServerNode ? assetServerNode->getPingMs() : -1;
properties["messages_ping"] = messagesMixerNode ? messagesMixerNode->getPingMs() : -1;
properties["atp_in_kbps"] = assetServerNode ? assetServerNode->getInboundKbps() : 0.0f;
auto loadingRequests = ResourceCache::getLoadingRequests();
@ -2293,7 +2286,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// Setup the mouse ray pick and related operators
{
auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES() | PickScriptingInterface::PICK_INCLUDE_NONCOLLIDABLE()), 0.0f, true);
auto mouseRayPick = std::make_shared<RayPick>(Vectors::ZERO, Vectors::UP, PickFilter(PickScriptingInterface::PICK_ENTITIES()), 0.0f, true);
mouseRayPick->parentTransform = std::make_shared<MouseTransformNode>();
mouseRayPick->setJointState(PickQuery::JOINT_STATE_MOUSE);
auto mouseRayPickID = DependencyManager::get<PickManager>()->addPick(PickQuery::Ray, mouseRayPick);
@ -2466,6 +2459,9 @@ void Application::updateHeartbeat() const {
}
void Application::onAboutToQuit() {
// quickly save AvatarEntityData before the EntityTree is dismantled
getMyAvatar()->saveAvatarEntityDataToSettings();
emit beforeAboutToQuit();
if (getLoginDialogPoppedUp() && _firstRun.get()) {
@ -2626,6 +2622,7 @@ void Application::cleanupBeforeQuit() {
DependencyManager::destroy<PickManager>();
DependencyManager::destroy<KeyboardScriptingInterface>();
DependencyManager::destroy<Keyboard>();
DependencyManager::destroy<AvatarPackager>();
qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete";
}
@ -5970,6 +5967,8 @@ void Application::update(float deltaTime) {
if (deltaTime > FLT_EPSILON) {
myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH));
myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW));
myAvatar->setDriveKey(MyAvatar::DELTA_PITCH, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_PITCH));
myAvatar->setDriveKey(MyAvatar::DELTA_YAW, -1.0f * userInputMapper->getActionState(controller::Action::DELTA_YAW));
myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW));
}
}
@ -6759,8 +6758,10 @@ void Application::updateWindowTitle() const {
}
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) {
return;
}
@ -6788,8 +6789,6 @@ void Application::clearDomainOctreeDetails() {
ShaderCache::instance().clearUnusedResources();
DependencyManager::get<TextureCache>()->clearUnusedResources();
DependencyManager::get<recording::ClipCache>()->clearUnusedResources();
getMyAvatar()->setAvatarEntityDataChanged(true);
}
void Application::domainURLChanged(QUrl domainURL) {
@ -6998,6 +6997,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe
scriptEngine->registerGlobalObject("Test", TestScriptingInterface::getInstance());
}
scriptEngine->registerGlobalObject("PlatformInfo", PlatformInfoScriptingInterface::getInstance());
scriptEngine->registerGlobalObject("Rates", new RatesScriptingInterface(this));
// hook our avatar and avatar hash map object into this script engine
@ -8715,6 +8715,14 @@ void Application::updateLoginDialogOverlayPosition() {
}
}
bool Application::hasRiftControllers() {
return PluginUtils::isOculusTouchControllerAvailable();
}
bool Application::hasViveControllers() {
return PluginUtils::isViveControllerAvailable();
}
void Application::onDismissedLoginDialog() {
_loginDialogPoppedUp = false;
loginDialogPoppedUp.set(false);
@ -8933,6 +8941,10 @@ void Application::copyToClipboard(const QString& text) {
QApplication::clipboard()->setText(text);
}
QString Application::getGraphicsCardType() {
return GPUIdent::getInstance()->getName();
}
#if defined(Q_OS_ANDROID)
void Application::beforeEnterBackground() {
auto nodeList = DependencyManager::get<NodeList>();

View file

@ -326,6 +326,10 @@ public:
void createLoginDialogOverlay();
void updateLoginDialogOverlayPosition();
// Check if a headset is connected
bool hasRiftControllers();
bool hasViveControllers();
#if defined(Q_OS_ANDROID)
void beforeEnterBackground();
void enterBackground();
@ -459,6 +463,8 @@ public slots:
void changeViewAsNeeded(float boomLength);
QString getGraphicsCardType();
private slots:
void onDesktopRootItemCreated(QQuickItem* qmlContext);
void onDesktopRootContextCreated(QQmlContext* qmlContext);
@ -588,6 +594,8 @@ private:
bool _aboutToQuit { false };
FileLogger* _logger { nullptr };
bool _previousSessionCrashed;
DisplayPluginPointer _displayPlugin;
@ -668,8 +676,6 @@ private:
QPointer<EntityScriptServerLogDialog> _entityScriptServerLogDialog;
QDir _defaultScriptsLocation;
FileLogger* _logger;
TouchEvent _lastTouchEvent;
quint64 _lastNackTime;
@ -787,6 +793,5 @@ private:
bool _showTrackedObjects { false };
bool _prevShowTrackedObjects { false };
};
#endif // hifi_Application_h

View file

@ -60,7 +60,6 @@ void addAvatarEntities(const QVariantList& avatarEntities) {
entityProperties.setParentID(myNodeID);
entityProperties.setEntityHostType(entity::HostType::AVATAR);
entityProperties.setOwningAvatarID(myNodeID);
entityProperties.setSimulationOwner(myNodeID, AVATAR_ENTITY_SIMULATION_PRIORITY);
entityProperties.markAllChanged();
EntityItemID id = EntityItemID(QUuid::createUuid());

View file

@ -35,6 +35,7 @@
#include "assets/ATPAssetMigrator.h"
#include "audio/AudioScope.h"
#include "avatar/AvatarManager.h"
#include "avatar/AvatarPackager.h"
#include "AvatarBookmarks.h"
#include "devices/DdeFaceTracker.h"
#include "MainWindow.h"
@ -144,9 +145,13 @@ Menu::Menu() {
assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets());
}
// Edit > Package Avatar as .fst...
addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0,
qApp, SLOT(packageModel()));
// Edit > Avatar Packager
#ifndef Q_OS_ANDROID
action = addActionToQMenuAndActionHash(editMenu, MenuOption::AvatarPackager);
connect(action, &QAction::triggered, [] {
DependencyManager::get<AvatarPackager>()->open();
});
#endif
// Edit > Reload All Content
addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches()));
@ -645,6 +650,8 @@ Menu::Menu() {
addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowTrackedObjects, 0, false, qApp, SLOT(setShowTrackedObjects(bool)));
addActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::PackageModel, 0, qApp, SLOT(packageModel()));
// Developer > Hands >>>
MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands");
addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false,

View file

@ -46,6 +46,7 @@ namespace MenuOption {
const QString AutoMuteAudio = "Auto Mute Microphone";
const QString AvatarReceiveStats = "Show Receive Stats";
const QString AvatarBookmarks = "Avatar Bookmarks";
const QString AvatarPackager = "Avatar Packager";
const QString Back = "Back";
const QString BinaryEyelidControl = "Binary Eyelid Control";
const QString BookmarkAvatar = "Bookmark Avatar";

View file

@ -28,7 +28,6 @@
#pragma GCC diagnostic pop
#endif
#include <shared/QtHelpers.h>
#include <AvatarData.h>
#include <PerfStat.h>
@ -268,6 +267,7 @@ void AvatarManager::updateOtherAvatars(float deltaTime) {
if (avatar->getSkeletonModel()->isLoaded()) {
// remove the orb if it is there
avatar->removeOrb();
avatar->updateCollisionGroup(_myAvatar->getOtherAvatarsCollisionsEnabled());
if (avatar->needsPhysicsUpdate()) {
_avatarsToChangeInPhysics.insert(avatar);
}
@ -529,6 +529,7 @@ void AvatarManager::handleChangedMotionStates(const VectorOfMotionStates& motion
}
void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents) {
bool playedCollisionSound { false };
for (Collision collision : collisionEvents) {
// TODO: The plan is to handle MOTIONSTATE_TYPE_AVATAR, and then MOTIONSTATE_TYPE_MYAVATAR. As it is, other
// people's avatars will have an id that doesn't match any entities, and one's own avatar will have
@ -536,43 +537,47 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents
// my avatar. (Other user machines will make a similar analysis and inject sound for their collisions.)
if (collision.idA.isNull() || collision.idB.isNull()) {
auto myAvatar = getMyAvatar();
auto collisionSound = myAvatar->getCollisionSound();
if (collisionSound) {
const auto characterController = myAvatar->getCharacterController();
const float avatarVelocityChange = (characterController ? glm::length(characterController->getVelocityChange()) : 0.0f);
const float velocityChange = glm::length(collision.velocityChange) + avatarVelocityChange;
const float MIN_AVATAR_COLLISION_ACCELERATION = 2.4f; // walking speed
const bool isSound = (collision.type == CONTACT_EVENT_TYPE_START) && (velocityChange > MIN_AVATAR_COLLISION_ACCELERATION);
myAvatar->collisionWithEntity(collision);
if (!isSound) {
return; // No sense iterating for others. We only have one avatar.
if (!playedCollisionSound) {
playedCollisionSound = true;
auto collisionSound = myAvatar->getCollisionSound();
if (collisionSound) {
const auto characterController = myAvatar->getCharacterController();
const float avatarVelocityChange =
(characterController ? glm::length(characterController->getVelocityChange()) : 0.0f);
const float velocityChange = glm::length(collision.velocityChange) + avatarVelocityChange;
const float MIN_AVATAR_COLLISION_ACCELERATION = 2.4f; // walking speed
const bool isSound =
(collision.type == CONTACT_EVENT_TYPE_START) && (velocityChange > MIN_AVATAR_COLLISION_ACCELERATION);
if (!isSound) {
return; // No sense iterating for others. We only have one avatar.
}
// Your avatar sound is personal to you, so let's say the "mass" part of the kinetic energy is already accounted for.
const float energy = velocityChange * velocityChange;
const float COLLISION_ENERGY_AT_FULL_VOLUME = 10.0f;
const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME);
// For general entity collisionSoundURL, playSound supports changing the pitch for the sound based on the size of the object,
// but most avatars are roughly the same size, so let's not be so fancy yet.
const float AVATAR_STRETCH_FACTOR = 1.0f;
_collisionInjectors.remove_if(
[](const AudioInjectorPointer& injector) { return !injector || injector->isFinished(); });
static const int MAX_INJECTOR_COUNT = 3;
if (_collisionInjectors.size() < MAX_INJECTOR_COUNT) {
AudioInjectorOptions options;
options.stereo = collisionSound->isStereo();
options.position = myAvatar->getWorldPosition();
options.volume = energyFactorOfFull;
options.pitch = 1.0f / AVATAR_STRETCH_FACTOR;
auto injector = AudioInjector::playSoundAndDelete(collisionSound, options);
_collisionInjectors.emplace_back(injector);
}
}
// Your avatar sound is personal to you, so let's say the "mass" part of the kinetic energy is already accounted for.
const float energy = velocityChange * velocityChange;
const float COLLISION_ENERGY_AT_FULL_VOLUME = 10.0f;
const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME);
// For general entity collisionSoundURL, playSound supports changing the pitch for the sound based on the size of the object,
// but most avatars are roughly the same size, so let's not be so fancy yet.
const float AVATAR_STRETCH_FACTOR = 1.0f;
_collisionInjectors.remove_if([](const AudioInjectorPointer& injector) {
return !injector || injector->isFinished();
});
static const int MAX_INJECTOR_COUNT = 3;
if (_collisionInjectors.size() < MAX_INJECTOR_COUNT) {
AudioInjectorOptions options;
options.stereo = collisionSound->isStereo();
options.position = myAvatar->getWorldPosition();
options.volume = energyFactorOfFull;
options.pitch = 1.0f / AVATAR_STRETCH_FACTOR;
auto injector = AudioInjector::playSoundAndDelete(collisionSound, options);
_collisionInjectors.emplace_back(injector);
}
myAvatar->collisionWithEntity(collision);
return;
}
}
}

View file

@ -19,6 +19,7 @@
AvatarMotionState::AvatarMotionState(OtherAvatarPointer avatar, const btCollisionShape* shape) : ObjectMotionState(shape), _avatar(avatar) {
assert(_avatar);
_type = MOTIONSTATE_TYPE_AVATAR;
_collisionGroup = BULLET_COLLISION_GROUP_OTHER_AVATAR;
cacheShapeDiameter();
}
@ -170,8 +171,8 @@ QUuid AvatarMotionState::getSimulatorID() const {
// virtual
void AvatarMotionState::computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const {
group = BULLET_COLLISION_GROUP_OTHER_AVATAR;
mask = Physics::getDefaultCollisionMask(group);
group = _collisionGroup;
mask = _collisionGroup == BULLET_COLLISION_GROUP_COLLISIONLESS ? 0 : Physics::getDefaultCollisionMask(group);
}
// virtual

View file

@ -66,6 +66,9 @@ public:
void addDirtyFlags(uint32_t flags) { _dirtyFlags |= flags; }
void setCollisionGroup(int32_t group) { _collisionGroup = group; }
int32_t getCollisionGroup() { return _collisionGroup; }
virtual void computeCollisionGroupAndMask(int32_t& group, int32_t& mask) const override;
virtual float getMass() const override;
@ -87,7 +90,7 @@ protected:
OtherAvatarPointer _avatar;
float _diameter { 0.0f };
int32_t _collisionGroup;
uint32_t _dirtyFlags;
};

View file

@ -0,0 +1,149 @@
//
// AvatarPackager.cpp
//
//
// Created by Thijs Wenker on 12/6/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AvatarPackager.h"
#include "Application.h"
#include <QQmlContext>
#include <QQmlEngine>
#include <QUrl>
#include <OffscreenUi.h>
#include "ModelSelector.h"
#include <avatar/MarketplaceItemUploader.h>
#include <mutex>
#include "ui/TabletScriptingInterface.h"
std::once_flag setupQMLTypesFlag;
AvatarPackager::AvatarPackager() {
std::call_once(setupQMLTypesFlag, []() {
qmlRegisterType<FST>();
qmlRegisterType<MarketplaceItemUploader>();
qRegisterMetaType<AvatarPackager*>();
qRegisterMetaType<AvatarProject*>();
qRegisterMetaType<AvatarProjectStatus::AvatarProjectStatus>();
qmlRegisterUncreatableMetaObject(
AvatarProjectStatus::staticMetaObject,
"Hifi.AvatarPackager.AvatarProjectStatus",
1, 0,
"AvatarProjectStatus",
"Error: only enums"
);
});
recentProjectsFromVariantList(_recentProjectsSetting.get());
QDir defaultProjectsDir(AvatarProject::getDefaultProjectsPath());
defaultProjectsDir.mkpath(".");
}
bool AvatarPackager::open() {
const auto packageModelDialogCreated = [=](QQmlContext* context, QObject* newObject) {
context->setContextProperty("AvatarPackagerCore", this);
};
static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system";
auto tablet = dynamic_cast<TabletProxy*>(DependencyManager::get<TabletScriptingInterface>()->getTablet(SYSTEM_TABLET));
if (tablet->getToolbarMode()) {
static const QUrl url{ "hifi/AvatarPackagerWindow.qml" };
DependencyManager::get<OffscreenUi>()->show(url, "AvatarPackager", packageModelDialogCreated);
return true;
}
static const QUrl url{ "hifi/tablet/AvatarPackager.qml" };
if (!tablet->isPathLoaded(url)) {
tablet->getTabletSurface()->getSurfaceContext()->setContextProperty("AvatarPackagerCore", this);
tablet->pushOntoStack(url);
return true;
}
return false;
}
void AvatarPackager::addCurrentProjectToRecentProjects() {
const int MAX_RECENT_PROJECTS = 5;
const QString& fstPath = _currentAvatarProject->getFSTPath();
auto removeProjects = QVector<RecentAvatarProject>();
for (const auto& project : _recentProjects) {
if (project.getProjectFSTPath() == fstPath) {
removeProjects.append(project);
}
}
for (const auto& removeProject : removeProjects) {
_recentProjects.removeOne(removeProject);
}
const auto newRecentProject = RecentAvatarProject(_currentAvatarProject->getProjectName(), fstPath);
_recentProjects.prepend(newRecentProject);
while (_recentProjects.size() > MAX_RECENT_PROJECTS) {
_recentProjects.pop_back();
}
_recentProjectsSetting.set(recentProjectsToVariantList(false));
emit recentProjectsChanged();
}
QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPaths) const {
QVariantList result;
for (const auto& project : _recentProjects) {
QVariantMap projectVariant;
projectVariant.insert("name", project.getProjectName());
projectVariant.insert("path", project.getProjectFSTPath());
if (includeProjectPaths) {
projectVariant.insert("projectPath", project.getProjectPath());
}
result.append(projectVariant);
}
return result;
}
void AvatarPackager::recentProjectsFromVariantList(QVariantList projectsVariant) {
_recentProjects.clear();
for (const auto& projectVariant : projectsVariant) {
auto map = projectVariant.toMap();
_recentProjects.append(RecentAvatarProject(map.value("name").toString(), map.value("path").toString()));
}
}
AvatarProjectStatus::AvatarProjectStatus AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) {
AvatarProjectStatus::AvatarProjectStatus status;
setAvatarProject(AvatarProject::openAvatarProject(avatarProjectFSTPath, status));
return status;
}
AvatarProjectStatus::AvatarProjectStatus AvatarPackager::createAvatarProject(const QString& projectsFolder,
const QString& avatarProjectName,
const QString& avatarModelPath,
const QString& textureFolder) {
AvatarProjectStatus::AvatarProjectStatus status;
setAvatarProject(AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder, status));
return status;
}
void AvatarPackager::setAvatarProject(AvatarProject* avatarProject) {
if (avatarProject == _currentAvatarProject) {
return;
}
if (_currentAvatarProject) {
_currentAvatarProject->deleteLater();
}
_currentAvatarProject = avatarProject;
if (_currentAvatarProject) {
addCurrentProjectToRecentProjects();
connect(_currentAvatarProject, &AvatarProject::nameChanged, this, &AvatarPackager::addCurrentProjectToRecentProjects);
QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership);
}
emit avatarProjectChanged();
}

View file

@ -0,0 +1,100 @@
//
// AvatarPackager.h
//
//
// Created by Thijs Wenker on 12/6/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#pragma once
#ifndef hifi_AvatarPackager_h
#define hifi_AvatarPackager_h
#include <QObject>
#include <DependencyManager.h>
#include "FileDialogHelper.h"
#include "avatar/AvatarProject.h"
#include "SettingHandle.h"
class RecentAvatarProject {
public:
RecentAvatarProject() = default;
RecentAvatarProject(QString projectName, QString projectFSTPath) {
_projectName = projectName;
_projectFSTPath = projectFSTPath;
}
RecentAvatarProject(const RecentAvatarProject& other) {
_projectName = other._projectName;
_projectFSTPath = other._projectFSTPath;
}
QString getProjectName() const { return _projectName; }
QString getProjectFSTPath() const { return _projectFSTPath; }
QString getProjectPath() const {
return QFileInfo(_projectFSTPath).absoluteDir().absolutePath();
}
bool operator==(const RecentAvatarProject& other) const {
return _projectName == other._projectName && _projectFSTPath == other._projectFSTPath;
}
private:
QString _projectName;
QString _projectFSTPath;
};
class AvatarPackager : public QObject, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
Q_PROPERTY(AvatarProject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged)
Q_PROPERTY(QString AVATAR_PROJECTS_PATH READ getAvatarProjectsPath CONSTANT)
Q_PROPERTY(QVariantList recentProjects READ getRecentProjects NOTIFY recentProjectsChanged)
public:
AvatarPackager();
bool open();
Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus createAvatarProject(const QString& projectsFolder,
const QString& avatarProjectName,
const QString& avatarModelPath,
const QString& textureFolder);
Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus openAvatarProject(const QString& avatarProjectFSTPath);
Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) const {
return AvatarProject::isValidNewProjectName(projectPath, projectName);
}
signals:
void avatarProjectChanged();
void recentProjectsChanged();
private:
Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; };
Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); }
Q_INVOKABLE QVariantList getRecentProjects() const { return recentProjectsToVariantList(true); }
void setAvatarProject(AvatarProject* avatarProject);
void addCurrentProjectToRecentProjects();
AvatarProject* _currentAvatarProject { nullptr };
QVector<RecentAvatarProject> _recentProjects;
QVariantList recentProjectsToVariantList(bool includeProjectPaths) const;
void recentProjectsFromVariantList(QVariantList projectsVariant);
Setting::Handle<QVariantList> _recentProjectsSetting { "io.highfidelity.avatarPackager.recentProjects", QVariantList() };
};
#endif // hifi_AvatarPackager_h

View file

@ -0,0 +1,260 @@
//
// AvatarProject.cpp
//
//
// Created by Thijs Wenker on 12/7/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AvatarProject.h"
#include <FSTReader.h>
#include <QFile>
#include <QFileInfo>
#include <QQmlEngine>
#include <QTimer>
#include "FBXSerializer.h"
#include <ui/TabletScriptingInterface.h>
#include "scripting/HMDScriptingInterface.h"
AvatarProject* AvatarProject::openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status) {
status = AvatarProjectStatus::NONE;
if (!path.toLower().endsWith(".fst")) {
status = AvatarProjectStatus::ERROR_OPEN_INVALID_FILE_TYPE;
return nullptr;
}
QFileInfo fstFileInfo{ path };
if (!fstFileInfo.absoluteDir().exists()) {
status = AvatarProjectStatus::ERROR_OPEN_PROJECT_FOLDER;
return nullptr;
}
if (!fstFileInfo.exists()) {
status = AvatarProjectStatus::ERROR_OPEN_FIND_FST;
return nullptr;
}
QFile file{ fstFileInfo.filePath() };
if (!file.open(QIODevice::ReadOnly)) {
status = AvatarProjectStatus::ERROR_OPEN_OPEN_FST;
return nullptr;
}
const auto project = new AvatarProject(path, file.readAll());
QFileInfo fbxFileInfo{ project->getFBXPath() };
if (!fbxFileInfo.exists()) {
project->deleteLater();
status = AvatarProjectStatus::ERROR_OPEN_FIND_MODEL;
return nullptr;
}
QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership);
status = AvatarProjectStatus::SUCCESS;
return project;
}
AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName,
const QString& avatarModelPath, const QString& textureFolder,
AvatarProjectStatus::AvatarProjectStatus& status) {
status = AvatarProjectStatus::NONE;
if (!isValidNewProjectName(projectsFolder, avatarProjectName)) {
status = AvatarProjectStatus::ERROR_CREATE_PROJECT_NAME;
return nullptr;
}
QDir projectDir(projectsFolder + "/" + avatarProjectName);
if (!projectDir.mkpath(".")) {
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
return nullptr;
}
QDir projectTexturesDir(projectDir.path() + "/textures");
if (!projectTexturesDir.mkpath(".")) {
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
return nullptr;
}
QDir projectScriptsDir(projectDir.path() + "/scripts");
if (!projectScriptsDir.mkpath(".")) {
status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES;
return nullptr;
}
const auto fileName = QFileInfo(avatarModelPath).fileName();
const auto newModelPath = projectDir.absoluteFilePath(fileName);
const auto newFSTPath = projectDir.absoluteFilePath("avatar.fst");
QFile::copy(avatarModelPath, newModelPath);
QFileInfo fbxInfo{ newModelPath };
if (!fbxInfo.exists() || !fbxInfo.isFile()) {
status = AvatarProjectStatus::ERROR_CREATE_FIND_MODEL;
return nullptr;
}
QFile fbx{ fbxInfo.filePath() };
if (!fbx.open(QIODevice::ReadOnly)) {
status = AvatarProjectStatus::ERROR_CREATE_OPEN_MODEL;
return nullptr;
}
std::shared_ptr<hfm::Model> hfmModel;
try {
const QByteArray fbxContents = fbx.readAll();
hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath());
} catch (const QString& error) {
Q_UNUSED(error)
status = AvatarProjectStatus::ERROR_CREATE_READ_MODEL;
return nullptr;
}
QStringList textures{};
auto addTextureToList = [&textures](hfm::Texture texture) mutable {
if (!texture.filename.isEmpty() && texture.content.isEmpty() && !textures.contains(texture.filename)) {
textures << texture.filename;
}
};
foreach(const HFMMaterial material, hfmModel->materials) {
addTextureToList(material.normalTexture);
addTextureToList(material.albedoTexture);
addTextureToList(material.opacityTexture);
addTextureToList(material.glossTexture);
addTextureToList(material.roughnessTexture);
addTextureToList(material.specularTexture);
addTextureToList(material.metallicTexture);
addTextureToList(material.emissiveTexture);
addTextureToList(material.occlusionTexture);
addTextureToList(material.scatteringTexture);
addTextureToList(material.lightmapTexture);
}
QDir textureDir(textureFolder.isEmpty() ? fbxInfo.absoluteDir() : textureFolder);
for (const auto& texture : textures) {
QString sourcePath = textureDir.path() + "/" + texture;
QString targetPath = projectTexturesDir.path() + "/" + texture;
QFileInfo sourceTexturePath(sourcePath);
if (sourceTexturePath.exists()) {
QFile::copy(sourcePath, targetPath);
}
}
auto fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel);
fst->setName(avatarProjectName);
if (!fst->write()) {
status = AvatarProjectStatus::ERROR_CREATE_WRITE_FST;
return nullptr;
}
status = AvatarProjectStatus::SUCCESS;
return new AvatarProject(fst);
}
QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) const {
QStringList result{};
constexpr auto flags = QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden;
if (!scriptsDir.exists()) {
return result;
}
for (const auto& script : scriptsDir.entryInfoList({}, flags)) {
if (script.fileName().toLower().endsWith(".js")) {
result.push_back("scripts/" + script.fileName());
}
}
return result;
}
bool AvatarProject::isValidNewProjectName(const QString& projectPath, const QString& projectName) {
if (projectPath.trimmed().isEmpty() || projectName.trimmed().isEmpty()) {
return false;
}
QDir dir(projectPath + "/" + projectName);
return !dir.exists();
}
AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) :
AvatarProject::AvatarProject(new FST(fstPath, FSTReader::readMapping(data))) {
}
AvatarProject::AvatarProject(FST* fst) {
_fst = fst;
auto fileInfo = QFileInfo(getFSTPath());
_directory = fileInfo.absoluteDir();
_fst->setScriptPaths(getScriptPaths(QDir(_directory.path() + "/scripts")));
_fst->write();
refreshProjectFiles();
_projectPath = fileInfo.absoluteDir().absolutePath();
}
void AvatarProject::appendDirectory(const QString& prefix, const QDir& dir) {
constexpr auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden;
for (auto& entry : dir.entryInfoList({}, flags)) {
if (entry.isFile()) {
_projectFiles.append({ entry.absoluteFilePath(), prefix + entry.fileName() });
} else if (entry.isDir()) {
appendDirectory(prefix + entry.fileName() + "/", entry.absoluteFilePath());
}
}
}
void AvatarProject::refreshProjectFiles() {
_projectFiles.clear();
appendDirectory("", _directory);
}
QStringList AvatarProject::getProjectFiles() const {
QStringList paths;
for (auto& path : _projectFiles) {
paths.append(path.relativePath);
}
return paths;
}
MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) {
QUuid itemID;
if (updateExisting) {
itemID = _fst->getMarketplaceID();
}
auto uploader = new MarketplaceItemUploader(getProjectName(), "", QFileInfo(getFSTPath()).fileName(),
itemID, _projectFiles);
connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() {
if (uploader->getError() == MarketplaceItemUploader::Error::None) {
_fst->setMarketplaceID(uploader->getMarketplaceID());
_fst->write();
}
});
return uploader;
}
void AvatarProject::openInInventory() const {
constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 };
auto tablet = dynamic_cast<TabletProxy*>(
DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system"));
tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml");
DependencyManager::get<HMDScriptingInterface>()->openTablet();
tablet->getTabletRoot()->forceActiveFocus();
auto name = getProjectName();
// I'm not a fan of this, but it's the only current option.
QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() {
tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } }));
});
}

View file

@ -0,0 +1,115 @@
//
// AvatarProject.h
//
//
// Created by Thijs Wenker on 12/7/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#pragma once
#ifndef hifi_AvatarProject_h
#define hifi_AvatarProject_h
#include "MarketplaceItemUploader.h"
#include "ProjectFile.h"
#include "FST.h"
#include <QObject>
#include <QDir>
#include <QVariantHash>
#include <QStandardPaths>
namespace AvatarProjectStatus {
Q_NAMESPACE
enum AvatarProjectStatus {
NONE,
SUCCESS,
ERROR_CREATE_PROJECT_NAME,
ERROR_CREATE_CREATING_DIRECTORIES,
ERROR_CREATE_FIND_MODEL,
ERROR_CREATE_OPEN_MODEL,
ERROR_CREATE_READ_MODEL,
ERROR_CREATE_WRITE_FST,
ERROR_OPEN_INVALID_FILE_TYPE,
ERROR_OPEN_PROJECT_FOLDER,
ERROR_OPEN_FIND_FST,
ERROR_OPEN_OPEN_FST,
ERROR_OPEN_FIND_MODEL
};
Q_ENUM_NS(AvatarProjectStatus)
}
class AvatarProject : public QObject {
Q_OBJECT
Q_PROPERTY(FST* fst READ getFST CONSTANT)
Q_PROPERTY(QStringList projectFiles READ getProjectFiles NOTIFY projectFilesChanged)
Q_PROPERTY(QString projectFolderPath READ getProjectPath CONSTANT)
Q_PROPERTY(QString projectFSTPath READ getFSTPath CONSTANT)
Q_PROPERTY(QString projectFBXPath READ getFBXPath CONSTANT)
Q_PROPERTY(QString name READ getProjectName WRITE setProjectName NOTIFY nameChanged)
public:
Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting);
Q_INVOKABLE void openInInventory() const;
Q_INVOKABLE QStringList getProjectFiles() const;
Q_INVOKABLE QString getProjectName() const { return _fst->getName(); }
Q_INVOKABLE void setProjectName(const QString& newProjectName) {
if (newProjectName.trimmed().length() > 0) {
_fst->setName(newProjectName);
_fst->write();
emit nameChanged();
}
}
Q_INVOKABLE QString getProjectPath() const { return _projectPath; }
Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); }
Q_INVOKABLE QString getFBXPath() const {
return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath()));
}
/**
* returns the AvatarProject or a nullptr on failure.
*/
static AvatarProject* openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status);
static AvatarProject* createAvatarProject(const QString& projectsFolder,
const QString& avatarProjectName,
const QString& avatarModelPath,
const QString& textureFolder,
AvatarProjectStatus::AvatarProjectStatus& status);
static bool isValidNewProjectName(const QString& projectPath, const QString& projectName);
static QString getDefaultProjectsPath() {
return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/High Fidelity Projects";
}
signals:
void nameChanged();
void projectFilesChanged();
private:
AvatarProject(const QString& fstPath, const QByteArray& data);
AvatarProject(FST* fst);
~AvatarProject() { _fst->deleteLater(); }
FST* getFST() { return _fst; }
void refreshProjectFiles();
void appendDirectory(const QString& prefix, const QDir& dir);
QStringList getScriptPaths(const QDir& scriptsDir) const;
FST* _fst;
QDir _directory;
QList<ProjectFilePath> _projectFiles{};
QString _projectPath;
};
#endif // hifi_AvatarProject_h

View file

@ -0,0 +1,321 @@
//
// MarketplaceItemUploader.cpp
//
//
// Created by Ryan Huffman on 12/10/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "MarketplaceItemUploader.h"
#include <AccountManager.h>
#include <DependencyManager.h>
#ifndef Q_OS_ANDROID
#include <quazip5/quazip.h>
#include <quazip5/quazipfile.h>
#endif
#include <QTimer>
#include <QBuffer>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
MarketplaceItemUploader::MarketplaceItemUploader(QString title,
QString description,
QString rootFilename,
QUuid marketplaceID,
QList<ProjectFilePath> filePaths) :
_title(title),
_description(description),
_rootFilename(rootFilename),
_marketplaceID(marketplaceID),
_filePaths(filePaths) {
}
void MarketplaceItemUploader::setState(State newState) {
Q_ASSERT(_state != State::Complete);
Q_ASSERT(_error == Error::None);
Q_ASSERT(newState != _state);
_state = newState;
emit stateChanged(newState);
if (newState == State::Complete) {
emit completed();
emit finishedChanged();
}
}
void MarketplaceItemUploader::setError(Error error) {
Q_ASSERT(_state != State::Complete);
Q_ASSERT(_error == Error::None);
_error = error;
emit errorChanged(error);
emit finishedChanged();
}
void MarketplaceItemUploader::send() {
doGetCategories();
}
void MarketplaceItemUploader::doGetCategories() {
setState(State::GettingCategories);
static const QString path = "/api/v1/marketplace/categories";
auto accountManager = DependencyManager::get<AccountManager>();
auto request = accountManager->createRequest(path, AccountManagerAuth::None);
qWarning() << "Request url is: " << request.url();
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* reply = networkAccessManager.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
auto error = reply->error();
if (error == QNetworkReply::NoError) {
auto doc = QJsonDocument::fromJson(reply->readAll());
auto extractCategoryID = [&doc]() -> std::pair<bool, int> {
auto items = doc.object()["data"].toObject()["items"];
if (!items.isArray()) {
qWarning() << "Categories parse error: data.items is not an array";
return { false, 0 };
}
auto itemsArray = items.toArray();
for (const auto item : itemsArray) {
if (!item.isObject()) {
qWarning() << "Categories parse error: item is not an object";
return { false, 0 };
}
auto itemObject = item.toObject();
if (itemObject["name"].toString() == "Avatars") {
auto idValue = itemObject["id"];
if (!idValue.isDouble()) {
qWarning() << "Categories parse error: id is not a number";
return { false, 0 };
}
return { true, (int)idValue.toDouble() };
}
}
qWarning() << "Categories parse error: could not find a category for 'Avatar'";
return { false, 0 };
};
bool success;
std::tie(success, _categoryID) = extractCategoryID();
if (!success) {
qWarning() << "Failed to find marketplace category id";
setError(Error::Unknown);
} else {
qDebug() << "Marketplace Avatar category ID is" << _categoryID;
doUploadAvatar();
}
} else {
setError(Error::Unknown);
}
});
}
void MarketplaceItemUploader::doUploadAvatar() {
#ifdef Q_OS_ANDROID
qWarning() << "Marketplace uploading is not supported on Android";
setError(Error::Unknown);
return;
#else
QBuffer buffer{ &_fileData };
QuaZip zip{ &buffer };
if (!zip.open(QuaZip::Mode::mdAdd)) {
qWarning() << "Failed to open zip";
setError(Error::Unknown);
return;
}
for (auto& filePath : _filePaths) {
qWarning() << "Zipping: " << filePath.absolutePath << filePath.relativePath;
QFileInfo fileInfo{ filePath.absolutePath };
QuaZipFile zipFile{ &zip };
if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(filePath.relativePath))) {
qWarning() << "Could not open zip file:" << zipFile.getZipError();
setError(Error::Unknown);
return;
}
QFile file{ filePath.absolutePath };
if (file.open(QIODevice::ReadOnly)) {
zipFile.write(file.readAll());
} else {
qWarning() << "Failed to open: " << filePath.absolutePath;
}
file.close();
zipFile.close();
if (zipFile.getZipError() != UNZ_OK) {
qWarning() << "Could not close zip file: " << zipFile.getZipError();
setState(State::Complete);
return;
}
}
zip.close();
qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB";
QString path = "/api/v1/marketplace/items";
bool creating = true;
if (!_marketplaceID.isNull()) {
creating = false;
auto idWithBraces = _marketplaceID.toString();
auto idWithoutBraces = idWithBraces.mid(1, idWithBraces.length() - 2);
path += "/" + idWithoutBraces;
}
auto accountManager = DependencyManager::get<AccountManager>();
auto request = accountManager->createRequest(path, AccountManagerAuth::Required);
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
// TODO(huffman) add JSON escaping
auto escapeJson = [](QString str) -> QString { return str; };
QString jsonString = "{\"marketplace_item\":{";
jsonString += "\"title\":\"" + escapeJson(_title) + "\"";
// Items cannot have their description updated after they have been submitted.
if (creating) {
jsonString += ",\"description\":\"" + escapeJson(_description) + "\"";
}
jsonString += ",\"root_file_key\":\"" + escapeJson(_rootFilename) + "\"";
jsonString += ",\"category_ids\":[" + QStringLiteral("%1").arg(_categoryID) + "]";
jsonString += ",\"license\":0";
jsonString += ",\"files\":\"" + QString::fromLatin1(_fileData.toBase64()) + "\"}}";
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* reply{ nullptr };
if (creating) {
reply = networkAccessManager.post(request, jsonString.toUtf8());
} else {
reply = networkAccessManager.put(request, jsonString.toUtf8());
}
connect(reply, &QNetworkReply::uploadProgress, this, [this](float bytesSent, float bytesTotal) {
if (_state == State::UploadingAvatar) {
emit uploadProgress(bytesSent, bytesTotal);
if (bytesSent >= bytesTotal) {
setState(State::WaitingForUploadResponse);
}
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
_responseData = reply->readAll();
auto error = reply->error();
if (error == QNetworkReply::NoError) {
auto doc = QJsonDocument::fromJson(_responseData.toLatin1());
auto status = doc.object()["status"].toString();
if (status == "success") {
_marketplaceID = QUuid::fromString(doc["data"].toObject()["marketplace_id"].toString());
_itemVersion = doc["data"].toObject()["version"].toDouble();
setState(State::WaitingForInventory);
doWaitForInventory();
} else {
qWarning() << "Got error response while uploading avatar: " << _responseData;
setError(Error::Unknown);
}
} else {
qWarning() << "Got error while uploading avatar: " << reply->error() << reply->errorString() << _responseData;
setError(Error::Unknown);
}
});
setState(State::UploadingAvatar);
#endif
}
void MarketplaceItemUploader::doWaitForInventory() {
static const QString path = "/api/v1/commerce/inventory";
auto accountManager = DependencyManager::get<AccountManager>();
auto request = accountManager->createRequest(path, AccountManagerAuth::Required);
QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
QNetworkReply* reply = networkAccessManager.post(request, "");
_numRequestsForInventory++;
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
auto data = reply->readAll();
bool success = false;
auto error = reply->error();
if (error == QNetworkReply::NoError) {
// Parse response data
auto doc = QJsonDocument::fromJson(data);
auto isAssetAvailable = [this, &doc]() -> bool {
if (!doc.isObject()) {
return false;
}
auto root = doc.object();
auto status = root["status"].toString();
if (status != "success") {
return false;
}
auto data = root["data"];
if (!data.isObject()) {
return false;
}
auto assets = data.toObject()["assets"];
if (!assets.isArray()) {
return false;
}
for (auto asset : assets.toArray()) {
auto assetObject = asset.toObject();
auto id = QUuid::fromString(assetObject["id"].toString());
if (id.isNull()) {
continue;
}
if (id == _marketplaceID) {
auto version = assetObject["version"];
auto valid = assetObject["valid"];
if (version.isDouble() && valid.isBool()) {
if ((int)version.toDouble() >= _itemVersion && valid.toBool()) {
return true;
}
}
}
}
return false;
};
success = isAssetAvailable();
}
if (success) {
qDebug() << "Found item in inventory";
setState(State::Complete);
} else {
constexpr int MAX_INVENTORY_REQUESTS { 8 };
constexpr int TIME_BETWEEN_INVENTORY_REQUESTS_MS { 5000 };
if (_numRequestsForInventory > MAX_INVENTORY_REQUESTS) {
qDebug() << "Failed to find item in inventory";
setError(Error::Unknown);
} else {
QTimer::singleShot(TIME_BETWEEN_INVENTORY_REQUESTS_MS, [this]() { doWaitForInventory(); });
}
}
});
}

View file

@ -0,0 +1,105 @@
//
// MarketplaceItemUploader.h
//
//
// Created by Ryan Huffman on 12/10/2018
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#pragma once
#ifndef hifi_MarketplaceItemUploader_h
#define hifi_MarketplaceItemUploader_h
#include "ProjectFile.h"
#include <QObject>
#include <QUuid>
class QNetworkReply;
class MarketplaceItemUploader : public QObject {
Q_OBJECT
Q_PROPERTY(bool finished READ getFinished NOTIFY finishedChanged)
Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged)
Q_PROPERTY(State state READ getState NOTIFY stateChanged)
Q_PROPERTY(Error error READ getError NOTIFY errorChanged)
Q_PROPERTY(QString responseData READ getResponseData)
public:
enum class Error {
None,
Unknown,
};
Q_ENUM(Error);
enum class State {
Idle,
GettingCategories,
UploadingAvatar,
WaitingForUploadResponse,
WaitingForInventory,
Complete,
};
Q_ENUM(State);
MarketplaceItemUploader(QString title,
QString description,
QString rootFilename,
QUuid marketplaceID,
QList<ProjectFilePath> filePaths);
Q_INVOKABLE void send();
void setError(Error error);
QString getResponseData() const { return _responseData; }
void setState(State newState);
State getState() const { return _state; }
bool getComplete() const { return _state == State::Complete; }
QUuid getMarketplaceID() const { return _marketplaceID; }
Error getError() const { return _error; }
bool getFinished() const { return _state == State::Complete || _error != Error::None; }
signals:
void uploadProgress(qint64 bytesSent, qint64 bytesTotal);
void completed();
void stateChanged(State newState);
void errorChanged(Error error);
// Triggered when the upload has finished, either succesfully completing, or stopping with an error
void finishedChanged();
private:
void doGetCategories();
void doUploadAvatar();
void doWaitForInventory();
QNetworkReply* _reply;
State _state { State::Idle };
Error _error { Error::None };
QString _title;
QString _description;
QString _rootFilename;
QUuid _marketplaceID;
int _categoryID;
int _itemVersion;
QString _responseData;
int _numRequestsForInventory { 0 };
QString _rootFilePath;
QList<ProjectFilePath> _filePaths;
QByteArray _fileData;
};
#endif // hifi_MarketplaceItemUploader_h

View file

@ -50,6 +50,7 @@
#include <RecordingScriptingInterface.h>
#include <trackers/FaceTracker.h>
#include <RenderableModelEntityItem.h>
#include <VariantMapToScriptValue.h>
#include "MyHead.h"
#include "MySkeletonModel.h"
@ -281,6 +282,8 @@ MyAvatar::MyAvatar(QThread* thread) :
MyAvatar::~MyAvatar() {
_lookAtTargetAvatar.reset();
delete _myScriptEngine;
_myScriptEngine = nullptr;
}
void MyAvatar::setDominantHand(const QString& hand) {
@ -671,7 +674,7 @@ void MyAvatar::update(float deltaTime) {
_clientTraitsHandler->sendChangedTraitsToMixer();
simulate(deltaTime);
simulate(deltaTime, true);
currentEnergy += energyChargeRate;
currentEnergy -= getAccelerationEnergy();
@ -743,7 +746,7 @@ void MyAvatar::updateChildCauterization(SpatiallyNestablePointer object, bool ca
}
}
void MyAvatar::simulate(float deltaTime) {
void MyAvatar::simulate(float deltaTime, bool inView) {
PerformanceTimer perfTimer("simulate");
animateScaleChanges(deltaTime);
@ -887,7 +890,7 @@ void MyAvatar::simulate(float deltaTime) {
_characterController.setCollisionlessAllowed(collisionlessAllowed);
}
updateAvatarEntities();
handleChangedAvatarEntityData();
updateFadingStatus();
}
@ -1251,7 +1254,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:
// Avatar/avatarEntityData/size: 5
// Avatar/avatarEntityData/1/id: ...
@ -1261,14 +1264,15 @@ void MyAvatar::resizeAvatarEntitySettingHandles(unsigned int avatarEntityIndex)
// Avatar/avatarEntityData/5/properties: ...
//
// Create Setting::Handles to mimic this.
while (_avatarEntityIDSettings.size() <= avatarEntityIndex) {
uint32_t settingsIndex = (uint32_t)_avatarEntityIDSettings.size() + 1;
while (settingsIndex <= maxIndex) {
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);
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);
settingsIndex++;
}
}
@ -1298,26 +1302,54 @@ void MyAvatar::saveData() {
_flyingHMDSetting.set(getFlyingHMDPref());
auto hmdInterface = DependencyManager::get<HMDScriptingInterface>();
_avatarEntitiesLock.withReadLock([&] {
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);
saveAvatarEntityDataToSettings();
}
unsigned int avatarEntityIndex = 0;
for (auto entityID : avatarEntityIDs) {
_avatarEntityIDSettings[avatarEntityIndex].set(entityID);
_avatarEntityDataSettings[avatarEntityIndex].set(_avatarEntityData.value(entityID));
avatarEntityIndex++;
}
void MyAvatar::saveAvatarEntityDataToSettings() {
if (!_needToSaveAvatarEntitySettings) {
return;
}
bool success = updateStaleAvatarEntityBlobs();
if (!success) {
return;
}
_needToSaveAvatarEntitySettings = false;
// clean up any left-over (due to the list shrinking) slots
for (; avatarEntityIndex < previousAvatarEntityCount; avatarEntityIndex++) {
_avatarEntityIDSettings[avatarEntityIndex].remove();
_avatarEntityDataSettings[avatarEntityIndex].remove();
uint32_t numEntities = (uint32_t)_cachedAvatarEntityBlobs.size();
uint32_t prevNumEntities = _avatarEntityCountSetting.get(0);
resizeAvatarEntitySettingHandles(std::max<uint32_t>(numEntities, prevNumEntities));
// 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) {
@ -1414,7 +1446,410 @@ void MyAvatar::setEnableInverseKinematics(bool 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() {
if (!_myScriptEngine) {
_myScriptEngine = new QScriptEngine();
}
getHead()->setBasePitch(_headPitchSetting.get());
_yawSpeed = _yawSpeedSetting.get(_yawSpeed);
@ -1426,14 +1861,7 @@ void MyAvatar::loadData() {
useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName);
int avatarEntityCount = _avatarEntityCountSetting.get(0);
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);
}
loadAvatarEntityDataFromSettings();
// Flying preferences must be loaded before calling setFlyingEnabled()
Setting::Handle<bool> firstRunVal { Settings::firstRun, true };
@ -1455,6 +1883,38 @@ void MyAvatar::loadData() {
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 {
Settings settings;
settings.beginGroup("savedAttachmentData");
@ -1531,35 +1991,74 @@ ScriptAvatarData* MyAvatar::getTargetAvatar() const {
}
}
void MyAvatar::updateLookAtTargetAvatar() {
//
// Look at the avatar whose eyes are closest to the ray in direction of my avatar's head
// And set the correctedLookAt for all (nearby) avatars that are looking at me.
_lookAtTargetAvatar.reset();
_targetAvatarPosition = glm::vec3(0.0f);
static float lookAtCostFunction(const glm::vec3& myForward, const glm::vec3& myPosition, const glm::vec3& otherForward, const glm::vec3& otherPosition,
bool otherIsTalking, bool lookingAtOtherAlready) {
const float DISTANCE_FACTOR = 3.14f;
const float MY_ANGLE_FACTOR = 1.0f;
const float OTHER_ANGLE_FACTOR = 1.0f;
const float OTHER_IS_TALKING_TERM = otherIsTalking ? 1.0f : 0.0f;
const float LOOKING_AT_OTHER_ALREADY_TERM = lookingAtOtherAlready ? -0.2f : 0.0f;
glm::vec3 lookForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD;
glm::vec3 cameraPosition = qApp->getCamera().getPosition();
const float GREATEST_LOOKING_AT_DISTANCE = 10.0f; // meters
const float MAX_MY_ANGLE = PI / 8.0f; // 22.5 degrees, Don't look too far away from the head facing.
const float MAX_OTHER_ANGLE = (3.0f * PI) / 4.0f; // 135 degrees, Don't stare at the back of another avatars head.
float smallestAngleTo = glm::radians(DEFAULT_FIELD_OF_VIEW_DEGREES) / 2.0f;
const float KEEP_LOOKING_AT_CURRENT_ANGLE_FACTOR = 1.3f;
const float GREATEST_LOOKING_AT_DISTANCE = 10.0f;
glm::vec3 d = otherPosition - myPosition;
float distance = glm::length(d);
glm::vec3 dUnit = d / distance;
float myAngle = acosf(glm::dot(myForward, dUnit));
float otherAngle = acosf(glm::dot(otherForward, -dUnit));
AvatarHash hash = DependencyManager::get<AvatarManager>()->getHashCopy();
if (distance > GREATEST_LOOKING_AT_DISTANCE || myAngle > MAX_MY_ANGLE || otherAngle > MAX_OTHER_ANGLE) {
return FLT_MAX;
} else {
return (DISTANCE_FACTOR * distance +
MY_ANGLE_FACTOR * myAngle +
OTHER_ANGLE_FACTOR * otherAngle +
OTHER_IS_TALKING_TERM +
LOOKING_AT_OTHER_ALREADY_TERM);
}
}
foreach (const AvatarSharedPointer& avatarPointer, hash) {
auto avatar = static_pointer_cast<Avatar>(avatarPointer);
bool isCurrentTarget = avatar->getIsLookAtTarget();
float distanceTo = glm::length(avatar->getHead()->getEyePosition() - cameraPosition);
avatar->setIsLookAtTarget(false);
if (!avatar->isMyAvatar() && avatar->isInitialized() &&
(distanceTo < GREATEST_LOOKING_AT_DISTANCE * getModelScale())) {
float radius = glm::length(avatar->getHead()->getEyePosition() - avatar->getHead()->getRightEyePosition());
float angleTo = coneSphereAngle(getHead()->getEyePosition(), lookForward, avatar->getHead()->getEyePosition(), radius);
if (angleTo < (smallestAngleTo * (isCurrentTarget ? KEEP_LOOKING_AT_CURRENT_ANGLE_FACTOR : 1.0f))) {
_lookAtTargetAvatar = avatarPointer;
_targetAvatarPosition = avatarPointer->getWorldPosition();
void MyAvatar::computeMyLookAtTarget(const AvatarHash& hash) {
glm::vec3 myForward = getHead()->getFinalOrientationInWorldFrame() * IDENTITY_FORWARD;
glm::vec3 myPosition = getHead()->getEyePosition();
CameraMode mode = qApp->getCamera().getMode();
if (mode == CAMERA_MODE_FIRST_PERSON) {
myPosition = qApp->getCamera().getPosition();
}
float bestCost = FLT_MAX;
std::shared_ptr<Avatar> bestAvatar;
foreach (const AvatarSharedPointer& avatarData, hash) {
std::shared_ptr<Avatar> avatar = std::static_pointer_cast<Avatar>(avatarData);
if (!avatar->isMyAvatar() && avatar->isInitialized()) {
glm::vec3 otherForward = avatar->getHead()->getForwardDirection();
glm::vec3 otherPosition = avatar->getHead()->getEyePosition();
const float TIME_WITHOUT_TALKING_THRESHOLD = 1.0f;
bool otherIsTalking = avatar->getHead()->getTimeWithoutTalking() <= TIME_WITHOUT_TALKING_THRESHOLD;
bool lookingAtOtherAlready = _lookAtTargetAvatar.lock().get() == avatar.get();
float cost = lookAtCostFunction(myForward, myPosition, otherForward, otherPosition, otherIsTalking, lookingAtOtherAlready);
if (cost < bestCost) {
bestCost = cost;
bestAvatar = avatar;
}
}
}
if (bestAvatar) {
_lookAtTargetAvatar = bestAvatar;
_targetAvatarPosition = bestAvatar->getWorldPosition();
} else {
_lookAtTargetAvatar.reset();
}
}
void MyAvatar::snapOtherAvatarLookAtTargetsToMe(const AvatarHash& hash) {
foreach (const AvatarSharedPointer& avatarData, hash) {
std::shared_ptr<Avatar> avatar = std::static_pointer_cast<Avatar>(avatarData);
if (!avatar->isMyAvatar() && avatar->isInitialized()) {
if (_lookAtSnappingEnabled && avatar->getLookAtSnappingEnabled() && isLookingAtMe(avatar)) {
// Alter their gaze to look directly at my camera; this looks more natural than looking at my avatar's face.
@ -1614,10 +2113,19 @@ void MyAvatar::updateLookAtTargetAvatar() {
avatar->getHead()->clearCorrectedLookAtPosition();
}
}
auto avatarPointer = _lookAtTargetAvatar.lock();
if (avatarPointer) {
static_pointer_cast<Avatar>(avatarPointer)->setIsLookAtTarget(true);
}
}
void MyAvatar::updateLookAtTargetAvatar() {
// The AvatarManager is a mutable class shared by many threads. We make a thread-safe deep copy of it,
// to avoid having to hold a lock while we iterate over all the avatars within.
AvatarHash hash = DependencyManager::get<AvatarManager>()->getHashCopy();
// determine what the best look at target for my avatar should be.
computeMyLookAtTarget(hash);
// snap look at position for avatars that are looking at me.
snapOtherAvatarLookAtTargetsToMe(hash);
}
void MyAvatar::clearLookAtTargetAvatar() {
@ -1858,8 +2366,11 @@ void MyAvatar::clearAvatarEntities() {
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
AvatarEntityMap avatarEntities = getAvatarEntityData();
for (auto entityID : avatarEntities.keys()) {
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
});
for (const auto& entityID : avatarEntityIDs) {
entityTree->withWriteLock([&entityID, &entityTree] {
// remove this entity first from the entity tree
entityTree->deleteEntity(entityID, true, true);
@ -1874,10 +2385,12 @@ void MyAvatar::clearAvatarEntities() {
void MyAvatar::removeWearableAvatarEntities() {
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
if (entityTree) {
AvatarEntityMap avatarEntities = getAvatarEntityData();
for (auto entityID : avatarEntities.keys()) {
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
});
for (const auto& entityID : avatarEntityIDs) {
auto entity = entityTree->findEntityByID(entityID);
if (entity && isWearableEntity(entity)) {
entityTree->withWriteLock([&entityID, &entityTree] {
@ -1894,13 +2407,16 @@ void MyAvatar::removeWearableAvatarEntities() {
}
QVariantList MyAvatar::getAvatarEntitiesVariant() {
// NOTE: this method is NOT efficient
QVariantList avatarEntitiesData;
QScriptEngine scriptEngine;
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
if (entityTree) {
AvatarEntityMap avatarEntities = getAvatarEntityData();
for (auto entityID : avatarEntities.keys()) {
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
});
for (const auto& entityID : avatarEntityIDs) {
auto entity = entityTree->findEntityByID(entityID);
if (!entity) {
continue;
@ -1911,7 +2427,7 @@ QVariantList MyAvatar::getAvatarEntitiesVariant() {
desiredProperties += PROP_LOCAL_POSITION;
desiredProperties += PROP_LOCAL_ROTATION;
EntityItemProperties entityProperties = entity->getProperties(desiredProperties);
QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(&scriptEngine, entityProperties);
QScriptValue scriptProperties = EntityItemPropertiesToScriptValue(_myScriptEngine, entityProperties);
avatarEntityData["properties"] = scriptProperties.toVariant();
avatarEntitiesData.append(QVariant(avatarEntityData));
}
@ -2300,17 +2816,17 @@ void MyAvatar::setAttachmentData(const QVector<AttachmentData>& attachmentData)
}
QVector<AttachmentData> MyAvatar::getAttachmentData() const {
QVector<AttachmentData> avatarData;
auto avatarEntities = getAvatarEntityData();
AvatarEntityMap::const_iterator dataItr = avatarEntities.begin();
while (dataItr != avatarEntities.end()) {
QUuid entityID = dataItr.key();
QVector<AttachmentData> attachmentData;
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
});
for (const auto& entityID : avatarEntityIDs) {
auto properties = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
AttachmentData data = entityPropertiesToAttachmentData(properties);
avatarData.append(data);
dataItr++;
attachmentData.append(data);
}
return avatarData;
return attachmentData;
}
QVariantList MyAvatar::getAttachmentsVariant() const {
@ -2339,16 +2855,16 @@ void MyAvatar::setAttachmentsVariant(const QVariantList& variant) {
}
bool MyAvatar::findAvatarEntity(const QString& modelURL, const QString& jointName, QUuid& entityID) {
auto avatarEntities = getAvatarEntityData();
AvatarEntityMap::const_iterator dataItr = avatarEntities.begin();
while (dataItr != avatarEntities.end()) {
entityID = dataItr.key();
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
});
for (const auto& entityID : avatarEntityIDs) {
auto props = DependencyManager::get<EntityScriptingInterface>()->getEntityProperties(entityID);
if (props.getModelURL() == modelURL &&
(jointName.isEmpty() || props.getParentJointIndex() == getJointIndex(jointName))) {
return true;
}
dataItr++;
}
return false;
}
@ -2662,9 +3178,10 @@ void MyAvatar::updateOrientation(float deltaTime) {
_bodyYawDelta = 0.0f;
}
}
float totalBodyYaw = _bodyYawDelta * deltaTime;
// Rotate directly proportional to delta yaw and delta pitch from right-click mouse movement.
totalBodyYaw += getDriveKey(DELTA_YAW) * _yawSpeed / YAW_SPEED_DEFAULT;
// Comfort Mode: If you press any of the left/right rotation drive keys or input, you'll
// get an instantaneous 15 degree turn. If you keep holding the key down you'll get another
@ -2732,7 +3249,8 @@ void MyAvatar::updateOrientation(float deltaTime) {
head->setBaseRoll(ROLL(euler));
} else {
head->setBaseYaw(0.0f);
head->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime);
head->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime
+ getDriveKey(DELTA_PITCH) * _pitchSpeed / PITCH_SPEED_DEFAULT);
head->setBaseRoll(0.0f);
}
}
@ -3326,7 +3844,6 @@ void MyAvatar::setCollisionsEnabled(bool enabled) {
QMetaObject::invokeMethod(this, "setCollisionsEnabled", Q_ARG(bool, enabled));
return;
}
_characterController.setCollisionless(!enabled);
emit collisionsEnabledChanged(enabled);
}
@ -3337,6 +3854,20 @@ bool MyAvatar::getCollisionsEnabled() {
return _characterController.computeCollisionGroup() != BULLET_COLLISION_GROUP_COLLISIONLESS;
}
void MyAvatar::setOtherAvatarsCollisionsEnabled(bool enabled) {
if (QThread::currentThread() != thread()) {
QMetaObject::invokeMethod(this, "setOtherAvatarsCollisionsEnabled", Q_ARG(bool, enabled));
return;
}
_collideWithOtherAvatars = enabled;
emit otherAvatarsCollisionsEnabledChanged(enabled);
}
bool MyAvatar::getOtherAvatarsCollisionsEnabled() {
return _collideWithOtherAvatars;
}
void MyAvatar::updateCollisionCapsuleCache() {
glm::vec3 start, end;
float radius;

View file

@ -225,6 +225,7 @@ class MyAvatar : public Avatar {
Q_PROPERTY(bool centerOfGravityModelEnabled READ getCenterOfGravityModelEnabled WRITE setCenterOfGravityModelEnabled)
Q_PROPERTY(bool hmdLeanRecenterEnabled READ getHMDLeanRecenterEnabled WRITE setHMDLeanRecenterEnabled)
Q_PROPERTY(bool collisionsEnabled READ getCollisionsEnabled WRITE setCollisionsEnabled)
Q_PROPERTY(bool otherAvatarsCollisionsEnabled READ getOtherAvatarsCollisionsEnabled WRITE setOtherAvatarsCollisionsEnabled)
Q_PROPERTY(bool characterControllerEnabled READ getCharacterControllerEnabled WRITE setCharacterControllerEnabled)
Q_PROPERTY(bool useAdvancedMovementControls READ useAdvancedMovementControls WRITE setUseAdvancedMovementControls)
Q_PROPERTY(bool showPlayArea READ getShowPlayArea WRITE setShowPlayArea)
@ -264,6 +265,8 @@ public:
STEP_YAW,
PITCH,
ZOOM,
DELTA_YAW,
DELTA_PITCH,
MAX_DRIVE_KEYS
};
Q_ENUM(DriveKeys)
@ -574,9 +577,11 @@ public:
float getHMDRollControlRate() const { return _hmdRollControlRate; }
// get/set avatar data
void resizeAvatarEntitySettingHandles(unsigned int avatarEntityIndex);
void resizeAvatarEntitySettingHandles(uint32_t maxIndex);
void saveData();
void saveAvatarEntityDataToSettings();
void loadData();
void loadAvatarEntityDataFromSettings();
void saveAttachmentData(const AttachmentData& attachment) const;
AttachmentData loadAttachmentData(const QUrl& modelURL, const QString& jointName = QString()) const;
@ -832,6 +837,8 @@ public:
AvatarWeakPointer getLookAtTargetAvatar() const { return _lookAtTargetAvatar; }
void updateLookAtTargetAvatar();
void computeMyLookAtTarget(const AvatarHash& hash);
void snapOtherAvatarLookAtTargetsToMe(const AvatarHash& hash);
void clearLookAtTargetAvatar();
virtual void setJointRotations(const QVector<glm::quat>& jointRotations) override;
@ -1062,6 +1069,18 @@ public:
*/
Q_INVOKABLE bool getCollisionsEnabled();
/**jsdoc
* @function MyAvatar.setOtherAvatarsCollisionsEnabled
* @param {boolean} enabled
*/
Q_INVOKABLE void setOtherAvatarsCollisionsEnabled(bool enabled);
/**jsdoc
* @function MyAvatar.getOtherAvatarsCollisionsEnabled
* @returns {boolean}
*/
Q_INVOKABLE bool getOtherAvatarsCollisionsEnabled();
/**jsdoc
* @function MyAvatar.getCollisionCapsule
* @returns {object}
@ -1169,6 +1188,7 @@ public:
virtual void setAttachmentsVariant(const QVariantList& variant) override;
glm::vec3 getNextPosition() { return _goToPending ? _goToPosition : getWorldPosition(); }
void prepareAvatarEntityDataForReload();
/**jsdoc
* Create a new grab.
@ -1189,6 +1209,11 @@ public:
*/
Q_INVOKABLE void releaseGrab(const QUuid& grabID);
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:
/**jsdoc
@ -1387,6 +1412,10 @@ public slots:
*/
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
* Set whether or not your avatar mesh is visible.
* @function MyAvatar.setEnableMeshVisible
@ -1489,6 +1518,14 @@ signals:
*/
void collisionsEnabledChanged(bool enabled);
/**jsdoc
* Triggered when collisions with other avatars enabled or disabled
* @function MyAvatar.otherAvatarsCollisionsEnabledChanged
* @param {boolean} enabled
* @returns {Signal}
*/
void otherAvatarsCollisionsEnabledChanged(bool enabled);
/**jsdoc
* Triggered when avatar's animation url changes
* @function MyAvatar.animGraphUrlChanged
@ -1578,23 +1615,24 @@ signals:
*/
void disableHandTouchForIDChanged(const QUuid& entityID, bool disable);
private slots:
void leaveDomain();
void updateCollisionCapsuleCache();
protected:
void handleChangedAvatarEntityData();
virtual void beParentOfChild(SpatiallyNestablePointer newChild) const override;
virtual void forgetChild(SpatiallyNestablePointer newChild) const override;
virtual void recalculateChildCauterization() const override;
private:
bool updateStaleAvatarEntityBlobs() const;
bool requiresSafeLanding(const glm::vec3& positionIn, glm::vec3& positionOut);
virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) override;
void simulate(float deltaTime);
void simulate(float deltaTime, bool inView) override;
void updateFromTrackers(float deltaTime);
void saveAvatarUrl();
virtual void render(RenderArgs* renderArgs) override;
@ -1897,6 +1935,7 @@ private:
bool _haveReceivedHeightLimitsFromDomain { false };
int _disableHandTouchCount { 0 };
bool _skeletonModelLoaded { false };
bool _reloadAvatarEntityDataFromSettings { true };
Setting::Handle<QString> _dominantHandSetting;
Setting::Handle<float> _headPitchSetting;
@ -1915,6 +1954,38 @@ private:
Setting::Handle<bool> _allowTeleportingSetting { "allowTeleporting", true };
std::vector<Setting::Handle<QUuid>> _avatarEntityIDSettings;
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);

View file

@ -7,10 +7,18 @@
//
#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"
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) {
const glm::u8vec3 NO_MODEL_COLOR(0xe3, 0xe3, 0xe3);
@ -120,7 +128,7 @@ bool OtherAvatar::shouldBeInPhysicsSimulation() const {
}
bool OtherAvatar::needsPhysicsUpdate() const {
constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION;
constexpr uint32_t FLAGS_OF_INTEREST = Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS | Simulation::DIRTY_POSITION | Simulation::DIRTY_COLLISION_GROUP;
return (_motionState && (bool)(_motionState->getIncomingDirtyFlags() & FLAGS_OF_INTEREST));
}
@ -129,3 +137,306 @@ void OtherAvatar::rebuildCollisionShape() {
_motionState->addDirtyFlags(Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS);
}
}
void OtherAvatar::updateCollisionGroup(bool myAvatarCollide) {
if (_motionState) {
bool collides = _motionState->getCollisionGroup() == BULLET_COLLISION_GROUP_OTHER_AVATAR && myAvatarCollide;
if (_collideWithOtherAvatars != collides) {
if (!myAvatarCollide) {
_collideWithOtherAvatars = false;
}
auto newCollisionGroup = _collideWithOtherAvatars ? BULLET_COLLISION_GROUP_OTHER_AVATAR : BULLET_COLLISION_GROUP_COLLISIONLESS;
_motionState->setCollisionGroup(newCollisionGroup);
_motionState->addDirtyFlags(Simulation::DIRTY_COLLISION_GROUP);
}
}
}
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);
}
}
}

View file

@ -10,6 +10,7 @@
#define hifi_OtherAvatar_h
#include <memory>
#include <vector>
#include <avatars-renderer/Avatar.h>
#include <workload/Space.h>
@ -45,9 +46,19 @@ public:
bool shouldBeInPhysicsSimulation() const;
bool needsPhysicsUpdate() const;
void updateCollisionGroup(bool myAvatarCollide);
void simulate(float deltaTime, bool inView) override;
friend AvatarManager;
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 };
OverlayID _otherAvatarOrbMeshPlaceholderID { UNKNOWN_OVERLAY_ID };
AvatarMotionState* _motionState { nullptr };

View file

@ -223,7 +223,7 @@ Pointer::Buttons PathPointer::getPressedButtons(const PickResultPointer& pickRes
std::string button = trigger.getButton();
TriggerState& state = _states[button];
// TODO: right now, LaserPointers don't support axes, only on/off buttons
if (trigger.getEndpoint()->peek() >= 1.0f) {
if (trigger.getEndpoint()->peek().value >= 1.0f) {
toReturn.insert(button);
if (_previousButtons.find(button) == _previousButtons.end()) {

View file

@ -25,12 +25,12 @@ float ClipboardScriptingInterface::getClipboardContentsLargestDimension() {
return qApp->getEntityClipboard()->getContentsLargestDimension();
}
bool ClipboardScriptingInterface::exportEntities(const QString& filename, const QVector<EntityItemID>& entityIDs) {
bool ClipboardScriptingInterface::exportEntities(const QString& filename, const QVector<QUuid>& entityIDs) {
bool retVal;
BLOCKING_INVOKE_METHOD(qApp, "exportEntities",
Q_RETURN_ARG(bool, retVal),
Q_ARG(const QString&, filename),
Q_ARG(const QVector<EntityItemID>&, entityIDs));
Q_ARG(const QVector<QUuid>&, entityIDs));
return retVal;
}

View file

@ -63,7 +63,7 @@ public:
* @param {Uuid[]} entityIDs Array of IDs of the entities to export.
* @returns {boolean} <code>true</code> if the export was successful, otherwise <code>false</code>.
*/
Q_INVOKABLE bool exportEntities(const QString& filename, const QVector<EntityItemID>& entityIDs);
Q_INVOKABLE bool exportEntities(const QString& filename, const QVector<QUuid>& entityIDs);
/**jsdoc
* Export the entities with centers within a cube to a JSON file.

View file

@ -104,7 +104,7 @@ class ScriptEngine;
* <ul>
* <li>{@link Controller.getValue|getValue}</li>
* <li>{@link Controller.getAxisValue|getAxisValue}</li>
* <li>{@link Controller.getPoseValue|getgetPoseValue}</li>
* <li>{@link Controller.getPoseValue|getPoseValue}</li>
* <li>{@link Controller.getActionValue|getActionValue}</li>
* </ul>
*

View file

@ -43,7 +43,7 @@ bool MenuScriptingInterface::menuExists(const QString& menu) {
if (QThread::currentThread() == qApp->thread()) {
return Menu::getInstance()->menuExists(menu);
}
bool result;
bool result { false };
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuExists",
Q_RETURN_ARG(bool, result),
Q_ARG(const QString&, menu));
@ -86,7 +86,7 @@ bool MenuScriptingInterface::menuItemExists(const QString& menu, const QString&
if (QThread::currentThread() == qApp->thread()) {
return Menu::getInstance()->menuItemExists(menu, menuitem);
}
bool result;
bool result { false };
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "menuItemExists",
Q_RETURN_ARG(bool, result),
Q_ARG(const QString&, menu),
@ -98,7 +98,7 @@ bool MenuScriptingInterface::isOptionChecked(const QString& menuOption) {
if (QThread::currentThread() == qApp->thread()) {
return Menu::getInstance()->isOptionChecked(menuOption);
}
bool result;
bool result { false };
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isOptionChecked",
Q_RETURN_ARG(bool, result),
Q_ARG(const QString&, menuOption));
@ -115,7 +115,7 @@ bool MenuScriptingInterface::isMenuEnabled(const QString& menuOption) {
if (QThread::currentThread() == qApp->thread()) {
return Menu::getInstance()->isOptionChecked(menuOption);
}
bool result;
bool result { false };
BLOCKING_INVOKE_METHOD(Menu::getInstance(), "isMenuEnabled",
Q_RETURN_ARG(bool, result),
Q_ARG(const QString&, menuOption));

View file

@ -0,0 +1,135 @@
//
// Created by Nissim Hadar on 2018/12/28
// Copyright 2013-2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "PlatformInfoScriptingInterface.h"
#include "Application.h"
#include <thread>
#ifdef Q_OS_WIN
#include <Windows.h>
#elif defined Q_OS_MAC
#include <sstream>
#endif
PlatformInfoScriptingInterface* PlatformInfoScriptingInterface::getInstance() {
static PlatformInfoScriptingInterface sharedInstance;
return &sharedInstance;
}
QString PlatformInfoScriptingInterface::getOperatingSystemType() {
#ifdef Q_OS_WIN
return "WINDOWS";
#elif defined Q_OS_MAC
return "MACOS";
#else
return "UNKNOWN";
#endif
}
QString PlatformInfoScriptingInterface::getCPUBrand() {
#ifdef Q_OS_WIN
int CPUInfo[4] = { -1 };
unsigned nExIds, i = 0;
char CPUBrandString[0x40];
// Get the information associated with each extended ID.
__cpuid(CPUInfo, 0x80000000);
nExIds = CPUInfo[0];
for (i = 0x80000000; i <= nExIds; ++i) {
__cpuid(CPUInfo, i);
// Interpret CPU brand string
if (i == 0x80000002) {
memcpy(CPUBrandString, CPUInfo, sizeof(CPUInfo));
} else if (i == 0x80000003) {
memcpy(CPUBrandString + 16, CPUInfo, sizeof(CPUInfo));
} else if (i == 0x80000004) {
memcpy(CPUBrandString + 32, CPUInfo, sizeof(CPUInfo));
}
}
return CPUBrandString;
#elif defined Q_OS_MAC
FILE* stream = popen("sysctl -n machdep.cpu.brand_string", "r");
std::ostringstream hostStream;
while (!feof(stream) && !ferror(stream)) {
char buf[128];
int bytesRead = fread(buf, 1, 128, stream);
hostStream.write(buf, bytesRead);
}
return QString::fromStdString(hostStream.str());
#else
return QString("NO IMPLEMENTED");
#endif
}
unsigned int PlatformInfoScriptingInterface::getNumLogicalCores() {
return std::thread::hardware_concurrency();
}
int PlatformInfoScriptingInterface::getTotalSystemMemoryMB() {
#ifdef Q_OS_WIN
MEMORYSTATUSEX statex;
statex.dwLength = sizeof (statex);
GlobalMemoryStatusEx(&statex);
return statex.ullTotalPhys / 1024 / 1024;
#elif defined Q_OS_MAC
FILE* stream = popen("sysctl -a | grep hw.memsize", "r");
std::ostringstream hostStream;
while (!feof(stream) && !ferror(stream)) {
char buf[128];
int bytesRead = fread(buf, 1, 128, stream);
hostStream.write(buf, bytesRead);
}
QString result = QString::fromStdString(hostStream.str());
QStringList parts = result.split(' ');
return (int)(parts[1].toDouble() / 1024 / 1024);
#else
return -1;
#endif
}
QString PlatformInfoScriptingInterface::getGraphicsCardType() {
#ifdef Q_OS_WIN
return qApp->getGraphicsCardType();
#elif defined Q_OS_MAC
FILE* stream = popen("system_profiler SPDisplaysDataType | grep Chipset", "r");
std::ostringstream hostStream;
while (!feof(stream) && !ferror(stream)) {
char buf[128];
int bytesRead = fread(buf, 1, 128, stream);
hostStream.write(buf, bytesRead);
}
QString result = QString::fromStdString(hostStream.str());
QStringList parts = result.split('\n');
for (int i = 0; i < parts.size(); ++i) {
if (parts[i].toLower().contains("radeon") || parts[i].toLower().contains("nvidia")) {
return parts[i];
}
}
// unkown graphics card
return "UNKNOWN";
#else
return QString("NO IMPLEMENTED");
#endif
}
bool PlatformInfoScriptingInterface::hasRiftControllers() {
return qApp->hasRiftControllers();
}
bool PlatformInfoScriptingInterface::hasViveControllers() {
return qApp->hasViveControllers();
}

View file

@ -0,0 +1,70 @@
//
// Created by Nissim Hadar on 2018/12/28
// Copyright 2013-2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_PlatformInfoScriptingInterface_h
#define hifi_PlatformInfoScriptingInterface_h
#include <QtCore/QObject>
class QScriptValue;
class PlatformInfoScriptingInterface : public QObject {
Q_OBJECT
public slots:
static PlatformInfoScriptingInterface* getInstance();
/**jsdoc
* Returns the Operating Sytem type
* @function Test.getOperatingSystemType
* @returns {string} "WINDOWS", "MACOS" or "UNKNOWN"
*/
QString getOperatingSystemType();
/**jsdoc
* Returns the CPU brand
*function PlatformInfo.getCPUBrand()
* @returns {string} brand of CPU
*/
QString getCPUBrand();
/**jsdoc
* Returns the number of logical CPU cores
*function PlatformInfo.getNumLogicalCores()
* @returns {int} number of logical CPU cores
*/
unsigned int getNumLogicalCores();
/**jsdoc
* Returns the total system memory in megabyte
*function PlatformInfo.getTotalSystemMemory()
* @returns {int} size of memory in megabytes
*/
int getTotalSystemMemoryMB();
/**jsdoc
* Returns the graphics card type
* @function Test.getGraphicsCardType
* @returns {string} graphics card type
*/
QString getGraphicsCardType();
/**jsdoc
* Returns true if Oculus Rift is connected (looks for hand controllers)
* @function Window.hasRift
* @returns {boolean} <code>true</code> if running on Windows, otherwise <code>false</code>.*/
bool hasRiftControllers();
/**jsdoc
* Returns true if HTC Vive is connected (looks for hand controllers)
* @function Window.hasRift
* @returns {boolean} <code>true</code> if running on Windows, otherwise <code>false</code>.*/
bool hasViveControllers();
};
#endif // hifi_PlatformInfoScriptingInterface_h

View file

@ -30,7 +30,6 @@
#include <gl/Context.h>
#include "BandwidthRecorder.h"
#include "Menu.h"
#include "Util.h"
#include "SequenceNumberStats.h"
@ -166,20 +165,25 @@ void Stats::updateStats(bool force) {
STAT_UPDATE(collisionPicksUpdated, updatedPicks[PickQuery::Collision]);
}
auto bandwidthRecorder = DependencyManager::get<BandwidthRecorder>();
STAT_UPDATE(packetInCount, (int)bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond());
STAT_UPDATE(packetOutCount, (int)bandwidthRecorder->getCachedTotalAverageOutputPacketsPerSecond());
STAT_UPDATE_FLOAT(mbpsIn, (float)bandwidthRecorder->getCachedTotalAverageInputKilobitsPerSecond() / 1000.0f, 0.01f);
STAT_UPDATE_FLOAT(mbpsOut, (float)bandwidthRecorder->getCachedTotalAverageOutputKilobitsPerSecond() / 1000.0f, 0.01f);
STAT_UPDATE(packetInCount, nodeList->getInboundPPS());
STAT_UPDATE(packetOutCount, nodeList->getOutboundPPS());
STAT_UPDATE_FLOAT(mbpsIn, nodeList->getInboundKbps() / 1000.0f, 0.01f);
STAT_UPDATE_FLOAT(mbpsOut, nodeList->getOutboundKbps() / 1000.0f, 0.01f);
STAT_UPDATE_FLOAT(assetMbpsIn, (float)bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AssetServer) / 1000.0f, 0.01f);
STAT_UPDATE_FLOAT(assetMbpsOut, (float)bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AssetServer) / 1000.0f, 0.01f);
// Second column: ping
SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer);
SharedNodePointer avatarMixerNode = nodeList->soloNodeOfType(NodeType::AvatarMixer);
SharedNodePointer assetServerNode = nodeList->soloNodeOfType(NodeType::AssetServer);
SharedNodePointer messageMixerNode = nodeList->soloNodeOfType(NodeType::MessagesMixer);
if (assetServerNode) {
STAT_UPDATE_FLOAT(assetMbpsIn, assetServerNode->getInboundKbps() / 1000.0f, 0.01f);
STAT_UPDATE_FLOAT(assetMbpsOut, assetServerNode->getOutboundKbps() / 1000.0f, 0.01f);
} else {
STAT_UPDATE_FLOAT(assetMbpsIn, 0.0f, 0.01f);
STAT_UPDATE_FLOAT(assetMbpsOut, 0.0f, 0.01f);
}
// Second column: ping
STAT_UPDATE(audioPing, audioMixerNode ? audioMixerNode->getPingMs() : -1);
const int mixerLossRate = (int)roundf(_audioStats->data()->getMixerStream()->lossRateWindow() * 100.0f);
const int clientLossRate = (int)roundf(_audioStats->data()->getClientStream()->lossRateWindow() * 100.0f);
@ -198,7 +202,7 @@ void Stats::updateStats(bool force) {
// TODO: this should also support entities
if (node->getType() == NodeType::EntityServer) {
totalPingOctree += node->getPingMs();
totalEntityKbps += node->getInboundBandwidth();
totalEntityKbps += node->getInboundKbps();
octreeServerCount++;
if (pingOctreeMax < node->getPingMs()) {
pingOctreeMax = node->getPingMs();
@ -218,10 +222,10 @@ void Stats::updateStats(bool force) {
if (_expanded || force) {
SharedNodePointer avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer);
if (avatarMixer) {
STAT_UPDATE(avatarMixerInKbps, (int)roundf(bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AvatarMixer)));
STAT_UPDATE(avatarMixerInPps, (int)roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AvatarMixer)));
STAT_UPDATE(avatarMixerOutKbps, (int)roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AvatarMixer)));
STAT_UPDATE(avatarMixerOutPps, (int)roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AvatarMixer)));
STAT_UPDATE(avatarMixerInKbps, (int)roundf(avatarMixer->getInboundKbps()));
STAT_UPDATE(avatarMixerInPps, avatarMixer->getInboundPPS());
STAT_UPDATE(avatarMixerOutKbps, (int)roundf(avatarMixer->getOutboundKbps()));
STAT_UPDATE(avatarMixerOutPps, avatarMixer->getOutboundPPS());
} else {
STAT_UPDATE(avatarMixerInKbps, -1);
STAT_UPDATE(avatarMixerInPps, -1);
@ -233,17 +237,15 @@ void Stats::updateStats(bool force) {
SharedNodePointer audioMixerNode = nodeList->soloNodeOfType(NodeType::AudioMixer);
auto audioClient = DependencyManager::get<AudioClient>().data();
if (audioMixerNode || force) {
STAT_UPDATE(audioMixerKbps, (int)roundf(
bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AudioMixer) +
bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerPps, (int)roundf(
bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer) +
bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerKbps, (int)roundf(audioMixerNode->getInboundKbps() +
audioMixerNode->getOutboundKbps()));
STAT_UPDATE(audioMixerPps, audioMixerNode->getInboundPPS() +
audioMixerNode->getOutboundPPS());
STAT_UPDATE(audioMixerInKbps, (int)roundf(bandwidthRecorder->getAverageInputKilobitsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerInPps, (int)roundf(bandwidthRecorder->getAverageInputPacketsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerOutKbps, (int)roundf(bandwidthRecorder->getAverageOutputKilobitsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerOutPps, (int)roundf(bandwidthRecorder->getAverageOutputPacketsPerSecond(NodeType::AudioMixer)));
STAT_UPDATE(audioMixerInKbps, (int)roundf(audioMixerNode->getInboundKbps()));
STAT_UPDATE(audioMixerInPps, audioMixerNode->getInboundPPS());
STAT_UPDATE(audioMixerOutKbps, (int)roundf(audioMixerNode->getOutboundKbps()));
STAT_UPDATE(audioMixerOutPps, audioMixerNode->getOutboundPPS());
STAT_UPDATE(audioAudioInboundPPS, (int)audioClient->getAudioInboundPPS());
STAT_UPDATE(audioSilentInboundPPS, (int)audioClient->getSilentInboundPPS());
STAT_UPDATE(audioOutboundPPS, (int)audioClient->getAudioOutboundPPS());

View file

@ -27,6 +27,9 @@ Base3DOverlay::Base3DOverlay() :
_drawInFront(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) :
@ -41,6 +44,9 @@ Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) :
_isVisibleInSecondaryCamera(base3DOverlay->_isVisibleInSecondaryCamera)
{
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) {
@ -209,6 +215,7 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) {
transaction.updateItem(itemID);
scene->enqueueTransaction(transaction);
}
_queryAACubeSet = true; // HACK: just in case some SpatiallyNestable code accidentally set it false
}
}

View file

@ -25,6 +25,7 @@ public:
Base3DOverlay(const Base3DOverlay* base3DOverlay);
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()); }
void setOverlayID(OverlayID overlayID) override { setID(overlayID); }

View file

@ -109,9 +109,6 @@ void AnimClip::copyFromNetworkAnim() {
jointMap.reserve(animJointCount);
for (int i = 0; i < animJointCount; 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);
}

View file

@ -425,7 +425,6 @@ int Rig::indexOfJoint(const QString& jointName) const {
// This is a content error, so we should issue a warning.
if (result < 0 && _jointNameWarningCount < MAX_JOINT_NAME_WARNING_COUNT) {
qCWarning(animation) << "Rig: Missing joint" << jointName << "in avatar model";
_jointNameWarningCount++;
}
return result;

View file

@ -1333,8 +1333,12 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
} 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
float gain = injector->getVolume();
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) {
mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain;
}

View file

@ -695,7 +695,7 @@ static void ifft_radix8_first(complex_t* x, complex_t* y, int n, int p) {
// n >= 4
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);
// 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* 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 ai = xp0[i].im;
@ -743,7 +743,7 @@ static void rfft_post(complex_t* x, const complex_t* w, int n) {
// n >= 4
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);
// 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* 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 ai = xp0[i].im;

View file

@ -973,8 +973,8 @@ FORCEINLINE static void ifft_radix8_first(complex_t* x, complex_t* y, int n, int
// n >= 32
static void rfft_post(complex_t* x, const complex_t* w, int n) {
size_t t = n/4;
assert(n/4 >= 8); // SIMD8
int t = n/4;
assert(t >= 8); // SIMD8
// NOTE: x[n/2].re is packed into x[0].im
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* 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 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
static void rifft_pre(complex_t* x, const complex_t* w, int n) {
size_t t = n/4;
assert(n/4 >= 8); // SIMD8
int t = n/4;
assert(t >= 8); // SIMD8
// NOTE: x[n/2].re is packed into x[0].im
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* 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 z1 = _mm256_loadu_ps(&xp0[i+4].re);

View file

@ -296,6 +296,7 @@ void Avatar::setTargetScale(float targetScale) {
if (_targetScale != newValue) {
_targetScale = newValue;
_scaleChanged = usecTimestampNow();
_avatarScaleChanged = _scaleChanged;
_isAnimatingScale = true;
emit targetScaleChanged(targetScale);
@ -307,175 +308,16 @@ void Avatar::setAvatarEntityDataChanged(bool value) {
_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;
EntityItemPropertiesFromScriptValueHonorReadOnly(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() {
auto treeRenderer = DependencyManager::get<EntityTreeRenderer>();
EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr;
if (entityTree) {
QList<QUuid> avatarEntityIDs;
_avatarEntitiesLock.withReadLock([&] {
avatarEntityIDs = _packedAvatarEntityData.keys();
});
entityTree->withWriteLock([&] {
AvatarEntityMap avatarEntities = getAvatarEntityData();
for (auto entityID : avatarEntities.keys()) {
for (const auto& entityID : avatarEntityIDs) {
entityTree->deleteEntity(entityID, true, true);
}
});
@ -650,87 +492,6 @@ void Avatar::relayJointDataToChildren() {
_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 {
if (rateName == "") {
return _simulationRate.rate();
@ -1045,7 +806,6 @@ void Avatar::render(RenderArgs* renderArgs) {
}
}
void Avatar::setEnableMeshVisible(bool isEnabled) {
if (_isMeshVisible != isEnabled) {
_isMeshVisible = isEnabled;

View file

@ -139,9 +139,8 @@ public:
typedef render::Payload<AvatarData> Payload;
void init();
void updateAvatarEntities();
void removeAvatarEntitiesFromTree();
void simulate(float deltaTime, bool inView);
virtual void simulate(float deltaTime, bool inView) = 0;
virtual void simulateAttachments(float deltaTime);
virtual void render(RenderArgs* renderArgs);
@ -156,9 +155,6 @@ public:
virtual void postUpdate(float deltaTime, const render::ScenePointer& scene);
//setters
void setIsLookAtTarget(const bool isLookAtTarget) { _isLookAtTarget = isLookAtTarget; }
bool getIsLookAtTarget() const { return _isLookAtTarget; }
//getters
bool isInitialized() const { return _initialized; }
SkeletonModelPointer getSkeletonModel() { return _skeletonModel; }
@ -243,8 +239,6 @@ public:
static void renderJointConnectingCone(gpu::Batch& batch, glm::vec3 position1, glm::vec3 position2,
float radius1, float radius2, const glm::vec4& color);
virtual void applyCollision(const glm::vec3& contactPoint, const glm::vec3& penetration) { }
/**jsdoc
* 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.
@ -552,6 +546,7 @@ protected:
glm::vec3 getBodyRightDirection() const { return getWorldOrientation() * IDENTITY_RIGHT; }
glm::vec3 getBodyUpDirection() const { return getWorldOrientation() * IDENTITY_UP; }
void measureMotionDerivatives(float deltaTime);
bool getCollideWithOtherAvatars() const { return _collideWithOtherAvatars; }
float getSkeletonHeight() const;
float getHeadHeight() const;
@ -595,7 +590,6 @@ protected:
int _rightPointerGeometryID { 0 };
int _nameRectGeometryID { 0 };
bool _initialized { false };
bool _isLookAtTarget { false };
bool _isAnimatingScale { false };
bool _mustFadeIn { false };
bool _isFading { false };

View file

@ -14,16 +14,33 @@
#include "AvatarTraits.h"
// This templated class is admittedly fairly confusing to look at. It is used
// to hold some associated value of type T for both simple (non-instanced) and instanced traits.
// Most of the complexity comes from the fact that simple and instanced trait types are
// handled differently. For each simple trait type there can be a value T, but for
// each instance of each instanced trait
// (keyed by a TraitInstanceID, which at the time of this writing is a UUID) there can be a value T.
// There are separate methods in most cases for simple traits and instanced traits
// because of this different behaviour. This class is not used to hold the values
// of the traits themselves, but instead an associated value like the latest version
// of each trait (see TraitVersions) or a state associated with each trait (like added/changed/deleted).
namespace AvatarTraits {
template<typename T, T defaultValue>
class AssociatedTraitValues {
public:
// constructor that pre-fills _simpleTypes with the default value specified by the template
AssociatedTraitValues() : _simpleTypes(FirstInstancedTrait, defaultValue) {}
/// inserts the given value for the given simple trait type
void insert(TraitType type, T value) { _simpleTypes[type] = value; }
/// resets the simple trait type value to the default
void erase(TraitType type) { _simpleTypes[type] = defaultValue; }
/// returns a reference to the value for a given instance for a given instanced trait type
T& getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID);
/// inserts the passed value for the given instance for the given instanced trait type
void instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value);
struct InstanceIDValuePair {
@ -34,24 +51,30 @@ namespace AvatarTraits {
};
using InstanceIDValuePairs = std::vector<InstanceIDValuePair>;
/// returns a vector of InstanceIDValuePair objects for the given instanced trait type
InstanceIDValuePairs& getInstanceIDValuePairs(TraitType traitType);
/// erases the a given instance for a given instanced trait type
void instanceErase(TraitType traitType, TraitInstanceID instanceID);
/// erases the value for all instances for a given instanced trait type
void eraseAllInstances(TraitType traitType);
// will return defaultValue for instanced traits
/// value getters for simple trait types, will be default value if value has been erased or not set
T operator[](TraitType traitType) const { return _simpleTypes[traitType]; }
T& operator[](TraitType traitType) { return _simpleTypes[traitType]; }
/// resets all simple trait types to the default value and erases all values for instanced trait types
void reset() {
std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue);
_instancedTypes.clear();
}
/// const iterators for the vector of simple type values
typename std::vector<T>::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); }
typename std::vector<T>::const_iterator simpleCEnd() const { return _simpleTypes.cend(); }
/// non-const iterators for the vector of simple type values
typename std::vector<T>::iterator simpleBegin() { return _simpleTypes.begin(); }
typename std::vector<T>::iterator simpleEnd() { return _simpleTypes.end(); }
@ -64,15 +87,18 @@ namespace AvatarTraits {
traitType(traitType), instances({{ instanceID, value }}) {};
};
/// const iterators for the vector of TraitWithInstances objects
typename std::vector<TraitWithInstances>::const_iterator instancedCBegin() const { return _instancedTypes.cbegin(); }
typename std::vector<TraitWithInstances>::const_iterator instancedCEnd() const { return _instancedTypes.cend(); }
/// non-const iterators for the vector of TraitWithInstances objects
typename std::vector<TraitWithInstances>::iterator instancedBegin() { return _instancedTypes.begin(); }
typename std::vector<TraitWithInstances>::iterator instancedEnd() { return _instancedTypes.end(); }
private:
std::vector<T> _simpleTypes;
/// return the iterator to the matching TraitWithInstances object for a given instanced trait type
typename std::vector<TraitWithInstances>::iterator instancesForTrait(TraitType traitType) {
return std::find_if(_instancedTypes.begin(), _instancedTypes.end(),
[traitType](TraitWithInstances& traitWithInstances){
@ -83,25 +109,34 @@ namespace AvatarTraits {
std::vector<TraitWithInstances> _instancedTypes;
};
/// returns a reference to the InstanceIDValuePairs object for a given instanced trait type
template <typename T, T defaultValue>
inline typename AssociatedTraitValues<T, defaultValue>::InstanceIDValuePairs&
AssociatedTraitValues<T, defaultValue>::getInstanceIDValuePairs(TraitType traitType) {
// first check if we already have some values for instances of this trait type
auto it = instancesForTrait(traitType);
if (it != _instancedTypes.end()) {
return it->instances;
} else {
// if we didn't have any values for instances of the instanced trait type
// add an empty InstanceIDValuePairs object first and then return the reference to it
_instancedTypes.emplace_back(traitType);
return _instancedTypes.back().instances;
}
}
// returns a reference to value for the given instance of the given instanced trait type
template <typename T, T defaultValue>
inline T& AssociatedTraitValues<T, defaultValue>::getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID) {
// first check if we already have some values for instances of this trait type
auto it = instancesForTrait(traitType);
if (it != _instancedTypes.end()) {
// grab the matching vector of instances
auto& instancesVector = it->instances;
// check if we have a value for this specific instance ID
auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(),
[instanceID](InstanceIDValuePair& idValuePair){
return idValuePair.id == instanceID;
@ -109,40 +144,53 @@ namespace AvatarTraits {
if (instanceIt != instancesVector.end()) {
return instanceIt->value;
} else {
// no value for this specific instance ID, insert the default value and return it
instancesVector.emplace_back(instanceID, defaultValue);
return instancesVector.back().value;
}
} else {
// no values for any instances of this trait type
// insert the default value for the specific instance for the instanced trait type
_instancedTypes.emplace_back(traitType, instanceID, defaultValue);
return _instancedTypes.back().instances.back().value;
}
}
/// inserts the passed value for the specific instance of the given instanced trait type
template <typename T, T defaultValue>
inline void AssociatedTraitValues<T, defaultValue>::instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value) {
// first check if we already have some instances for this trait type
auto it = instancesForTrait(traitType);
if (it != _instancedTypes.end()) {
// found some instances for the instanced trait type, check if our specific instance is one of them
auto& instancesVector = it->instances;
auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(),
[instanceID](InstanceIDValuePair& idValuePair){
return idValuePair.id == instanceID;
});
if (instanceIt != instancesVector.end()) {
// the instance already existed, update the value
instanceIt->value = value;
} else {
// the instance was not present, emplace the new value
instancesVector.emplace_back(instanceID, value);
}
} else {
// there were no existing instances for the given trait type
// setup the container for instances and insert the passed value for this instance ID
_instancedTypes.emplace_back(traitType, instanceID, value);
}
}
/// erases the value for a specific instance of the given instanced trait type
template <typename T, T defaultValue>
inline void AssociatedTraitValues<T, defaultValue>::instanceErase(TraitType traitType, TraitInstanceID instanceID) {
// check if we have any instances at all for this instanced trait type
auto it = instancesForTrait(traitType);
if (it != _instancedTypes.end()) {
// we have some instances, erase the value for the passed instance ID if it is present
auto& instancesVector = it->instances;
instancesVector.erase(std::remove_if(instancesVector.begin(),
instancesVector.end(),

View file

@ -540,6 +540,10 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
if (_headData->getHasProceduralBlinkFaceMovement()) {
setAtBit16(flags, PROCEDURAL_BLINK_FACE_MOVEMENT);
}
// avatar collisions enabled
if (_collideWithOtherAvatars) {
setAtBit16(flags, COLLIDE_WITH_OTHER_AVATARS);
}
data->flags = flags;
destinationBuffer += sizeof(AvatarDataPacket::AdditionalFlags);
@ -1116,7 +1120,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
auto newHasAudioEnabledFaceMovement = oneAtBit16(bitItems, AUDIO_ENABLED_FACE_MOVEMENT);
auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT);
auto newHasProceduralBlinkFaceMovement = oneAtBit16(bitItems, PROCEDURAL_BLINK_FACE_MOVEMENT);
auto newCollideWithOtherAvatars = oneAtBit16(bitItems, COLLIDE_WITH_OTHER_AVATARS);
bool keyStateChanged = (_keyState != newKeyState);
bool handStateChanged = (_handState != newHandState);
@ -1125,7 +1129,9 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
bool audioEnableFaceMovementChanged = (_headData->getHasAudioEnabledFaceMovement() != newHasAudioEnabledFaceMovement);
bool proceduralEyeFaceMovementChanged = (_headData->getHasProceduralEyeFaceMovement() != newHasProceduralEyeFaceMovement);
bool proceduralBlinkFaceMovementChanged = (_headData->getHasProceduralBlinkFaceMovement() != newHasProceduralBlinkFaceMovement);
bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged || proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged;
bool collideWithOtherAvatarsChanged = (_collideWithOtherAvatars != newCollideWithOtherAvatars);
bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged ||
proceduralEyeFaceMovementChanged || proceduralBlinkFaceMovementChanged || collideWithOtherAvatarsChanged;
_keyState = newKeyState;
_handState = newHandState;
@ -1134,6 +1140,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
_headData->setHasAudioEnabledFaceMovement(newHasAudioEnabledFaceMovement);
_headData->setHasProceduralEyeFaceMovement(newHasProceduralEyeFaceMovement);
_headData->setHasProceduralBlinkFaceMovement(newHasProceduralBlinkFaceMovement);
_collideWithOtherAvatars = newCollideWithOtherAvatars;
sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags);
@ -1901,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
QByteArray entityBinaryData;
_avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] {
if (_avatarEntityData.contains(traitInstanceID)) {
entityBinaryData = _avatarEntityData[traitInstanceID];
if (_packedAvatarEntityData.contains(traitInstanceID)) {
entityBinaryData = _packedAvatarEntityData[traitInstanceID];
}
});
@ -1980,9 +1986,9 @@ qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTr
qint64 bytesWritten = 0;
if (traitType == AvatarTraits::AvatarEntity) {
packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion);
bytesWritten += packAvatarEntityTraitInstance(traitType, traitInstanceID, destination, traitVersion);
} else if (traitType == AvatarTraits::Grab) {
packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion);
bytesWritten += packGrabTraitInstance(traitType, traitInstanceID, destination, traitVersion);
}
return bytesWritten;
@ -1991,7 +1997,7 @@ qint64 AvatarData::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTr
void AvatarData::prepareResetTraitInstances() {
if (_clientTraitsHandler) {
_avatarEntitiesLock.withReadLock([this]{
foreach (auto entityID, _avatarEntityData.keys()) {
foreach (auto entityID, _packedAvatarEntityData.keys()) {
_clientTraitsHandler->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID);
}
foreach (auto grabID, _avatarGrabData.keys()) {
@ -2012,7 +2018,7 @@ void AvatarData::processTrait(AvatarTraits::TraitType traitType, QByteArray trai
void AvatarData::processTraitInstance(AvatarTraits::TraitType traitType,
AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData) {
if (traitType == AvatarTraits::AvatarEntity) {
updateAvatarEntity(instanceID, traitBinaryData);
storeAvatarEntityDataPayload(instanceID, traitBinaryData);
} else if (traitType == AvatarTraits::Grab) {
updateAvatarGrabData(instanceID, traitBinaryData);
}
@ -2360,7 +2366,7 @@ void AvatarData::setRecordingBasis(std::shared_ptr<Transform> recordingBasis) {
void AvatarData::createRecordingIDs() {
_avatarEntitiesLock.withReadLock([&] {
_avatarEntityForRecording.clear();
for (int i = 0; i < _avatarEntityData.size(); i++) {
for (int i = 0; i < _packedAvatarEntityData.size(); i++) {
_avatarEntityForRecording.insert(QUuid::createUuid());
}
});
@ -2415,6 +2421,10 @@ JointData jointDataFromJsonValue(int version, const QJsonValue& json) {
return result;
}
void AvatarData::avatarEntityDataToJson(QJsonObject& root) const {
// overridden where needed
}
QJsonObject AvatarData::toJson() const {
QJsonObject root;
@ -2426,20 +2436,8 @@ QJsonObject AvatarData::toJson() const {
if (!getDisplayName().isEmpty()) {
root[JSON_AVATAR_DISPLAY_NAME] = getDisplayName();
}
_avatarEntitiesLock.withReadLock([&] {
if (!_avatarEntityData.empty()) {
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;
}
});
avatarEntityDataToJson(root);
auto recordingBasis = getRecordingBasis();
bool success;
@ -2561,9 +2559,9 @@ void AvatarData::fromJson(const QJsonObject& json, bool useFrameSkeleton) {
for (auto attachmentJson : attachmentsJson) {
if (attachmentJson.isObject()) {
QVariantMap entityData = attachmentJson.toObject().toVariantMap();
QUuid entityID = entityData.value("id").toUuid();
QByteArray properties = QByteArray::fromBase64(entityData.value("properties").toByteArray());
updateAvatarEntity(entityID, properties);
QUuid id = entityData.value("id").toUuid();
QByteArray data = QByteArray::fromBase64(entityData.value("properties").toByteArray());
updateAvatarEntity(id, data);
}
}
}
@ -2745,17 +2743,15 @@ void AvatarData::setAttachmentsVariant(const QVariantList& variant) {
setAttachmentData(newAttachments);
}
const int MAX_NUM_AVATAR_ENTITIES = 42;
void AvatarData::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) {
void AvatarData::storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& data) {
_avatarEntitiesLock.withWriteLock([&] {
AvatarEntityMap::iterator itr = _avatarEntityData.find(entityID);
if (itr == _avatarEntityData.end()) {
if (_avatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) {
_avatarEntityData.insert(entityID, entityData);
PackedAvatarEntityMap::iterator itr = _packedAvatarEntityData.find(entityID);
if (itr == _packedAvatarEntityData.end()) {
if (_packedAvatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) {
_packedAvatarEntityData.insert(entityID, data);
}
} else {
itr.value() = entityData;
itr.value() = data;
}
});
@ -2768,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) {
bool removedEntity = false;
_avatarEntitiesLock.withWriteLock([this, &removedEntity, &entityID] {
removedEntity = _avatarEntityData.remove(entityID);
removedEntity = _packedAvatarEntityData.remove(entityID);
});
insertDetachedEntityID(entityID);
insertRemovedEntityID(entityID);
if (removedEntity && _clientTraitsHandler) {
// we have a client traits handler, so we need to mark this removed instance trait as deleted
@ -2786,75 +2787,29 @@ void AvatarData::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFr
}
AvatarEntityMap AvatarData::getAvatarEntityData() const {
AvatarEntityMap result;
_avatarEntitiesLock.withReadLock([&] {
result = _avatarEntityData;
});
return result;
}
void AvatarData::insertDetachedEntityID(const QUuid entityID) {
_avatarEntitiesLock.withWriteLock([&] {
_avatarEntityDetached.insert(entityID);
});
_avatarEntityDataChanged = true;
// overridden where needed
// NOTE: the return value is expected to be a map of unfortunately-formatted-binary-blobs
return AvatarEntityMap();
}
void AvatarData::setAvatarEntityData(const AvatarEntityMap& avatarEntityData) {
if (avatarEntityData.size() > MAX_NUM_AVATAR_ENTITIES) {
// the data is suspect
qCDebug(avatars) << "discard suspect AvatarEntityData with size =" << avatarEntityData.size();
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);
}
}
// overridden where needed
// avatarEntityData is expected to be a map of QByteArrays
// each QByteArray represents an EntityItemProperties object from JavaScript
}
AvatarEntityIDs AvatarData::getAndClearRecentlyDetachedIDs() {
void AvatarData::insertRemovedEntityID(const QUuid entityID) {
_avatarEntitiesLock.withWriteLock([&] {
_avatarEntityRemoved.insert(entityID);
});
_avatarEntityDataChanged = true;
}
AvatarEntityIDs AvatarData::getAndClearRecentlyRemovedIDs() {
AvatarEntityIDs result;
_avatarEntitiesLock.withWriteLock([&] {
result = _avatarEntityDetached;
_avatarEntityDetached.clear();
result = _avatarEntityRemoved;
_avatarEntityRemoved.clear();
});
return result;
}

View file

@ -63,6 +63,7 @@ using AvatarWeakPointer = std::weak_ptr<AvatarData>;
using AvatarHash = QHash<QUuid, AvatarSharedPointer>;
using AvatarEntityMap = QMap<QUuid, QByteArray>;
using PackedAvatarEntityMap = QMap<QUuid, QByteArray>; // similar to AvatarEntityMap, but different internal format
using AvatarEntityIDs = QSet<QUuid>;
using AvatarGrabDataMap = QMap<QUuid, QByteArray>;
@ -71,6 +72,8 @@ using AvatarGrabMap = QMap<QUuid, GrabPointer>;
using AvatarDataSequenceNumber = uint16_t;
const int MAX_NUM_AVATAR_ENTITIES = 42;
// avatar motion behaviors
const quint32 AVATAR_MOTION_ACTION_MOTOR_ENABLED = 1U << 0;
const quint32 AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED = 1U << 1;
@ -110,6 +113,7 @@ const int HAND_STATE_FINGER_POINTING_BIT = 7; // 8th bit
const int AUDIO_ENABLED_FACE_MOVEMENT = 8; // 9th bit
const int PROCEDURAL_EYE_FACE_MOVEMENT = 9; // 10th bit
const int PROCEDURAL_BLINK_FACE_MOVEMENT = 10; // 11th bit
const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit
const char HAND_STATE_NULL = 0;
@ -951,19 +955,20 @@ public:
// FIXME: Can this name be improved? Can it be deprecated?
Q_INVOKABLE virtual void setAttachmentsVariant(const QVariantList& variant);
virtual void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload);
/**jsdoc
* @function MyAvatar.updateAvatarEntity
* @param {Uuid} entityID
* @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
* @function MyAvatar.clearAvatarEntity
* @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
@ -1124,6 +1129,7 @@ public:
TransformPointer getRecordingBasis() const;
void setRecordingBasis(TransformPointer recordingBasis = TransformPointer());
void createRecordingIDs();
virtual void avatarEntityDataToJson(QJsonObject& root) const;
QJsonObject toJson() const;
void fromJson(const QJsonObject& json, bool useFrameSkeleton = true);
@ -1135,17 +1141,16 @@ public:
* @function MyAvatar.getAvatarEntityData
* @returns {object}
*/
Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const;
Q_INVOKABLE virtual AvatarEntityMap getAvatarEntityData() const;
/**jsdoc
* @function MyAvatar.setAvatarEntityData
* @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; }
void insertDetachedEntityID(const QUuid entityID);
AvatarEntityIDs getAndClearRecentlyDetachedIDs();
AvatarEntityIDs getAndClearRecentlyRemovedIDs();
/**jsdoc
* @function MyAvatar.getSensorToWorldMatrix
@ -1332,6 +1337,7 @@ public slots:
void resetLastSent() { _lastToByteArray = 0; }
protected:
void insertRemovedEntityID(const QUuid entityID);
void lazyInitHeadData() const;
float getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition) const;
@ -1460,9 +1466,9 @@ protected:
AABox _defaultBubbleBox;
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
AvatarEntityMap _avatarEntityData;
PackedAvatarEntityMap _packedAvatarEntityData;
bool _avatarEntityDataChanged { false };
mutable ReadWriteLockable _avatarGrabsLock;
@ -1495,6 +1501,7 @@ protected:
int _replicaIndex { 0 };
bool _isNewAvatar { true };
bool _isClientAvatar { false };
bool _collideWithOtherAvatars { true };
// null unless MyAvatar or ScriptableAvatar sending traits data to mixer
std::unique_ptr<ClientTraitsHandler, LaterDeleter> _clientTraitsHandler;

View file

@ -328,6 +328,19 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
}
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()) {
// read the avatar ID to figure out which avatar this is for

View file

@ -42,6 +42,10 @@ namespace AvatarTraits {
const TraitWireSize DELETED_TRAIT_SIZE = -1;
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,
TraitVersion traitVersion = NULL_TRAIT_VERSION) {
qint64 bytesWritten = 0;

View file

@ -0,0 +1,13 @@
#ifndef hifi_AvatarProjectFile_h
#define hifi_AvatarProjectFile_h
#include <QObject>
class ProjectFilePath {
Q_GADGET;
public:
QString absolutePath;
QString relativePath;
};
#endif // hifi_AvatarProjectFile_h

View file

@ -52,11 +52,17 @@ namespace controller {
* <tr><td><code>TranslateZ</code></td><td>number</td><td>number</td><td>Move the user's avatar in the direction of its
* z-axis, if the camera isn't in independent or mirror modes.</td></tr>
* <tr><td><code>Pitch</code></td><td>number</td><td>number</td><td>Rotate the user's avatar head and attached camera
* about its negative x-axis (i.e., positive values pitch down), if the camera isn't in HMD, independent, or mirror
* modes.</td></tr>
* <tr><td><code>Yaw</code></td><td>number</td><td>number</td><td>Rotate the user's avatar about its y-axis, if the
* camera isn't in independent or mirror modes.</td></tr>
* about its negative x-axis (i.e., positive values pitch down) at a rate proportional to the control value, if the
* camera isn't in HMD, independent, or mirror modes.</td></tr>
* <tr><td><code>Yaw</code></td><td>number</td><td>number</td><td>Rotate the user's avatar about its y-axis at a rate
* proportional to the control value, if the camera isn't in independent or mirror modes.</td></tr>
* <tr><td><code>Roll</code></td><td>number</td><td>number</td><td>No action.</td></tr>
* <tr><td><code>DeltaPitch</code></td><td>number</td><td>number</td><td>Rotate the user's avatar head and attached
* camera about its negative x-axis (i.e., positive values pitch down) by an amount proportional to the control value,
* if the camera isn't in HMD, independent, or mirror modes.</td></tr>
* <tr><td><code>DeltaYaw</code></td><td>number</td><td>number</td><td>Rotate the user's avatar about its y-axis by an
* amount proportional to the control value, if the camera isn't in independent or mirror modes.</td></tr>
* <tr><td><code>DeltaRoll</code></td><td>number</td><td>number</td><td>No action.</td></tr>
* <tr><td><code>StepTranslateX</code></td><td>number</td><td>number</td><td>No action.</td></tr>
* <tr><td><code>StepTranslateY</code></td><td>number</td><td>number</td><td>No action.</td></tr>
* <tr><td><code>StepTranslateZ</code></td><td>number</td><td>number</td><td>No action.</td></tr>
@ -318,6 +324,9 @@ namespace controller {
makeAxisPair(Action::ROLL, "Roll"),
makeAxisPair(Action::PITCH, "Pitch"),
makeAxisPair(Action::YAW, "Yaw"),
makeAxisPair(Action::DELTA_YAW, "DeltaYaw"),
makeAxisPair(Action::DELTA_PITCH, "DeltaPitch"),
makeAxisPair(Action::DELTA_ROLL, "DeltaRoll"),
makeAxisPair(Action::STEP_YAW, "StepYaw"),
makeAxisPair(Action::STEP_PITCH, "StepPitch"),
makeAxisPair(Action::STEP_ROLL, "StepRoll"),

View file

@ -27,6 +27,10 @@ enum class Action {
ROTATE_Y, YAW = ROTATE_Y,
ROTATE_Z, ROLL = ROTATE_Z,
DELTA_PITCH,
DELTA_YAW,
DELTA_ROLL,
STEP_YAW,
// FIXME does this have a use case?
STEP_PITCH,

View file

@ -0,0 +1,21 @@
//
// AxisValue.cpp
//
// Created by David Rowe on 14 Dec 2018.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AxisValue.h"
namespace controller {
AxisValue::AxisValue(const float value, const quint64 timestamp) :
value(value), timestamp(timestamp) { }
bool AxisValue::operator==(const AxisValue& right) const {
return value == right.value && timestamp == right.timestamp;
}
}

View file

@ -0,0 +1,34 @@
//
// AxisValue.h
//
// Created by David Rowe on 13 Dec 2018.
// Copyright 2018 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#pragma once
#ifndef hifi_controllers_AxisValue_h
#define hifi_controllers_AxisValue_h
#include <QtCore/qglobal.h>
namespace controller {
struct AxisValue {
public:
float value { 0.0f };
// The value can be timestamped to determine if consecutive identical values should be output (e.g., mouse movement).
quint64 timestamp { 0 };
AxisValue() {}
AxisValue(const float value, const quint64 timestamp);
bool operator ==(const AxisValue& right) const;
bool operator !=(const AxisValue& right) const { return !(*this == right); }
};
}
#endif // hifi_controllers_AxisValue_h

View file

@ -26,12 +26,12 @@ namespace controller {
return 0.0f;
}
float InputDevice::getAxis(int channel) const {
AxisValue InputDevice::getAxis(int channel) const {
auto axis = _axisStateMap.find(channel);
if (axis != _axisStateMap.end()) {
return (*axis).second;
} else {
return 0.0f;
return AxisValue();
}
}
@ -68,26 +68,25 @@ namespace controller {
return Input::NamedPair(makeInput(pose), name);
}
float InputDevice::getValue(ChannelType channelType, uint16_t channel) const {
AxisValue InputDevice::getValue(ChannelType channelType, uint16_t channel) const {
switch (channelType) {
case ChannelType::AXIS:
return getAxis(channel);
case ChannelType::BUTTON:
return getButton(channel);
return { getButton(channel), 0 };
case ChannelType::POSE:
return getPose(channel).valid ? 1.0f : 0.0f;
return { getPose(channel).valid ? 1.0f : 0.0f, 0 };
default:
break;
}
return 0.0f;
return { 0.0f, 0 };
}
float InputDevice::getValue(const Input& input) const {
AxisValue InputDevice::getValue(const Input& input) const {
return getValue(input.getType(), input.channel);
}

View file

@ -16,6 +16,7 @@
#include <QtCore/QString>
#include "AxisValue.h"
#include "Pose.h"
#include "Input.h"
#include "StandardControls.h"
@ -103,16 +104,16 @@ public:
using Pointer = std::shared_ptr<InputDevice>;
typedef std::unordered_set<int> ButtonPressedMap;
typedef std::map<int, float> AxisStateMap;
typedef std::map<int, AxisValue> AxisStateMap;
typedef std::map<int, Pose> PoseStateMap;
// Get current state for each channel
float getButton(int channel) const;
float getAxis(int channel) const;
AxisValue getAxis(int channel) const;
Pose getPose(int channel) const;
float getValue(const Input& input) const;
float getValue(ChannelType channelType, uint16_t channel) const;
AxisValue getValue(const Input& input) const;
AxisValue getValue(ChannelType channelType, uint16_t channel) const;
Pose getPoseValue(uint16_t channel) const;
const QString& getName() const { return _name; }

View file

@ -297,6 +297,13 @@ namespace controller {
return 0.0f;
}
InputRecorder::ActionStates InputRecorder::getActionstates() {
if (_actionStateList.size() > 0) {
return _actionStateList[_playCount];
}
return {};
}
controller::Pose InputRecorder::getPoseState(const QString& action) {
if (_poseStateList.size() > 0) {
return _poseStateList[_playCount][action];

Some files were not shown because too many files have changed in this diff Show more