diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 2f91571d8b..5211c3beaa 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -434,8 +434,16 @@ void Agent::executeScript() { connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater); _avatarAudioTimerThread.start(); - // 60Hz timer for avatar - QObject::connect(_scriptEngine.get(), &ScriptEngine::update, this, &Agent::processAgentAvatar); + // Agents should run at 45hz + static const int AVATAR_DATA_HZ = 45; + static const int AVATAR_DATA_IN_MSECS = MSECS_PER_SECOND / AVATAR_DATA_HZ; + QTimer* avatarDataTimer = new QTimer(this); + connect(avatarDataTimer, &QTimer::timeout, this, &Agent::processAgentAvatar); + avatarDataTimer->setSingleShot(false); + avatarDataTimer->setInterval(AVATAR_DATA_IN_MSECS); + avatarDataTimer->setTimerType(Qt::PreciseTimer); + avatarDataTimer->start(); + _scriptEngine->run(); Frame::clearFrameHandler(AUDIO_FRAME_TYPE); @@ -538,7 +546,7 @@ void Agent::processAgentAvatar() { auto scriptedAvatar = DependencyManager::get(); AvatarData::AvatarDataDetail dataDetail = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO) ? AvatarData::SendAllData : AvatarData::CullSmallData; - QByteArray avatarByteArray = scriptedAvatar->toByteArray(dataDetail, 0, scriptedAvatar->getLastSentJointData()); + QByteArray avatarByteArray = scriptedAvatar->toByteArrayStateful(dataDetail); scriptedAvatar->doneEncoding(true); static AvatarDataSequenceNumber sequenceNumber = 0; diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 45b04c4189..0f6863f9ae 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -10,8 +10,10 @@ // #include -#include +#include #include +#include +#include #include #include @@ -21,6 +23,7 @@ #include #include +#include #include #include #include @@ -37,15 +40,15 @@ const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45; const unsigned int AVATAR_DATA_SEND_INTERVAL_MSECS = (1.0f / (float) AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND) * 1000; AvatarMixer::AvatarMixer(ReceivedMessage& message) : - ThreadedAssignment(message), - _broadcastThread() + ThreadedAssignment(message) { // make sure we hear about node kills so we can tell the other nodes connect(DependencyManager::get().data(), &NodeList::nodeKilled, this, &AvatarMixer::nodeKilled); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); + packetReceiver.registerListener(PacketType::AvatarData, this, "queueIncomingPacket"); + packetReceiver.registerListener(PacketType::AdjustAvatarSorting, this, "handleAdjustAvatarSorting"); packetReceiver.registerListener(PacketType::ViewFrustum, this, "handleViewFrustumPacket"); - packetReceiver.registerListener(PacketType::AvatarData, this, "handleAvatarDataPacket"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "handleAvatarIdentityPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); @@ -56,18 +59,16 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch); } -AvatarMixer::~AvatarMixer() { - if (_broadcastTimer) { - _broadcastTimer->deleteLater(); - } - - _broadcastThread.quit(); - _broadcastThread.wait(); +void AvatarMixer::queueIncomingPacket(QSharedPointer message, SharedNodePointer node) { + auto start = usecTimestampNow(); + getOrCreateClientData(node)->queuePacket(message, node); + auto end = usecTimestampNow(); + _queueIncomingPacketElapsedTime += (end - start); } -// An 80% chance of sending a identity packet within a 5 second interval. -// assuming 60 htz update rate. -const float IDENTITY_SEND_PROBABILITY = 1.0f / 187.0f; + +AvatarMixer::~AvatarMixer() { +} void AvatarMixer::sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode) { QByteArray individualData = nodeData->getAvatar().identityByteArray(); @@ -83,424 +84,190 @@ void AvatarMixer::sendIdentityPacket(AvatarMixerClientData* nodeData, const Shar ++_sumIdentityPackets; } -// NOTE: some additional optimizations to consider. -// 1) use the view frustum to cull those avatars that are out of view. Since avatar data doesn't need to be present -// if the avatar is not in view or in the keyhole. -void AvatarMixer::broadcastAvatarData() { - _broadcastRate.increment(); +std::chrono::microseconds AvatarMixer::timeFrame(p_high_resolution_clock::time_point& timestamp) { + // advance the next frame + auto nextTimestamp = timestamp + std::chrono::microseconds((int)((float)USECS_PER_SECOND / (float)AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND)); + auto now = p_high_resolution_clock::now(); - int idleTime = AVATAR_DATA_SEND_INTERVAL_MSECS; + // compute how long the last frame took + auto duration = std::chrono::duration_cast(now - timestamp); - if (_lastFrameTimestamp.time_since_epoch().count() > 0) { - auto idleDuration = p_high_resolution_clock::now() - _lastFrameTimestamp; - idleTime = std::chrono::duration_cast(idleDuration).count(); - } + // set the new frame timestamp + timestamp = std::max(now, nextTimestamp); - ++_numStatFrames; + // sleep until the next frame should start + // WIN32 sleep_until is broken until VS2015 Update 2 + // instead, std::max (above) guarantees that timestamp >= now, so we can sleep_for + std::this_thread::sleep_for(timestamp - now); - const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f; - const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f; + return duration; +} - const float RATIO_BACK_OFF = 0.02f; - const int TRAILING_AVERAGE_FRAMES = 100; - int framesSinceCutoffEvent = TRAILING_AVERAGE_FRAMES; - - const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES; - const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; - - // only send extra avatar data (avatars out of view, ignored) every Nth AvatarData frame - // Extra avatar data will be sent (AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND/EXTRA_AVATAR_DATA_FRAME_RATIO) times - // per second. - // This value should be a power of two for performance purposes, as the mixer performs a modulo operation every frame - // to determine whether the extra data should be sent. - const int EXTRA_AVATAR_DATA_FRAME_RATIO = 16; - - // NOTE: The following code calculates the _performanceThrottlingRatio based on how much the avatar-mixer was - // able to sleep. This will eventually be used to ask for an additional avatar-mixer to help out. Currently the value - // is unused as it is assumed this should not be hit before the avatar-mixer hits the desired bandwidth limit per client. - // It is reported in the domain-server stats for the avatar-mixer. - - _trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio) - + (idleTime * CURRENT_FRAME_RATIO / (float) AVATAR_DATA_SEND_INTERVAL_MSECS); - - float lastCutoffRatio = _performanceThrottlingRatio; - bool hasRatioChanged = false; - - if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) { - if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) { - // we're struggling - change our performance throttling ratio - _performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio)); - - qDebug() << "Mixer is struggling, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was" - << lastCutoffRatio << "and is now" << _performanceThrottlingRatio; - hasRatioChanged = true; - } else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) { - // we've recovered and can back off the performance throttling - _performanceThrottlingRatio = _performanceThrottlingRatio - RATIO_BACK_OFF; - - if (_performanceThrottlingRatio < 0) { - _performanceThrottlingRatio = 0; - } - - qDebug() << "Mixer is recovering, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was" - << lastCutoffRatio << "and is now" << _performanceThrottlingRatio; - hasRatioChanged = true; - } - - if (hasRatioChanged) { - framesSinceCutoffEvent = 0; - } - } - - if (!hasRatioChanged) { - ++framesSinceCutoffEvent; - } +void AvatarMixer::start() { auto nodeList = DependencyManager::get(); - // setup for distributed random floating point values - std::random_device randomDevice; - std::mt19937 generator(randomDevice()); - std::uniform_real_distribution distribution; + unsigned int frame = 1; + auto frameTimestamp = p_high_resolution_clock::now(); - nodeList->eachMatchingNode( - [&](const SharedNodePointer& node)->bool { - if (!node->getLinkedData()) { - return false; - } - if (node->getType() != NodeType::Agent) { - return false; - } - if (!node->getActiveSocket()) { - return false; - } - return true; - }, - [&](const SharedNodePointer& node) { - AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); - MutexTryLocker lock(nodeData->getMutex()); - if (!lock.isLocked()) { - return; - } - ++_sumListeners; - nodeData->resetInViewStats(); + while (!_isFinished) { - AvatarData& avatar = nodeData->getAvatar(); - glm::vec3 myPosition = avatar.getClientGlobalPosition(); + auto frameDuration = timeFrame(frameTimestamp); // calculates last frame duration and sleeps remainder of target amount + throttle(frameDuration, frame); // determines _throttlingRatio for upcoming mix frame - // reset the internal state for correct random number distribution - distribution.reset(); + int lockWait, nodeTransform, functor; - // reset the max distance for this frame - float maxAvatarDistanceThisFrame = 0.0f; + // Allow nodes to process any pending/queued packets across our worker threads + { + auto start = usecTimestampNow(); - // reset the number of sent avatars - nodeData->resetNumAvatarsSentLastFrame(); + nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + auto end = usecTimestampNow(); + _processQueuedAvatarDataPacketsLockWaitElapsedTime += (end - start); - // keep a counter of the number of considered avatars - int numOtherAvatars = 0; - - // keep track of outbound data rate specifically for avatar data - int numAvatarDataBytes = 0; - - // keep track of the number of other avatars held back in this frame - int numAvatarsHeldBack = 0; - - // keep track of the number of other avatar frames skipped - int numAvatarsWithSkippedFrames = 0; - - // use the data rate specifically for avatar data for FRD adjustment checks - float avatarDataRateLastSecond = nodeData->getOutboundAvatarDataKbps(); - - // When this is true, the AvatarMixer will send Avatar data to a client about avatars that are not in the view frustrum - bool getsOutOfView = nodeData->getRequestsDomainListData(); - - // When this is true, the AvatarMixer will send Avatar data to a client about avatars that they've ignored - bool getsIgnoredByMe = getsOutOfView; - - // When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them - bool getsAnyIgnored = getsIgnoredByMe && node->getCanKick(); - - // Check if it is time to adjust what we send this client based on the observed - // bandwidth to this node. We do this once a second, which is also the window for - // the bandwidth reported by node->getOutboundBandwidth(); - if (nodeData->getNumFramesSinceFRDAdjustment() > AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND) { - - const float FRD_ADJUSTMENT_ACCEPTABLE_RATIO = 0.8f; - const float HYSTERISIS_GAP = (1 - FRD_ADJUSTMENT_ACCEPTABLE_RATIO); - const float HYSTERISIS_MIDDLE_PERCENTAGE = (1 - (HYSTERISIS_GAP * 0.5f)); - - // get the current full rate distance so we can work with it - float currentFullRateDistance = nodeData->getFullRateDistance(); - - if (avatarDataRateLastSecond > _maxKbpsPerNode) { - - // is the FRD greater than the farthest avatar? - // if so, before we calculate anything, set it to that distance - currentFullRateDistance = std::min(currentFullRateDistance, nodeData->getMaxAvatarDistance()); - - // we're adjusting the full rate distance to target a bandwidth in the middle - // of the hysterisis gap - currentFullRateDistance *= (_maxKbpsPerNode * HYSTERISIS_MIDDLE_PERCENTAGE) / avatarDataRateLastSecond; - - nodeData->setFullRateDistance(currentFullRateDistance); - nodeData->resetNumFramesSinceFRDAdjustment(); - } else if (currentFullRateDistance < nodeData->getMaxAvatarDistance() - && avatarDataRateLastSecond < _maxKbpsPerNode * FRD_ADJUSTMENT_ACCEPTABLE_RATIO) { - // we are constrained AND we've recovered to below the acceptable ratio - // lets adjust the full rate distance to target a bandwidth in the middle of the hyterisis gap - currentFullRateDistance *= (_maxKbpsPerNode * HYSTERISIS_MIDDLE_PERCENTAGE) / avatarDataRateLastSecond; - - nodeData->setFullRateDistance(currentFullRateDistance); - nodeData->resetNumFramesSinceFRDAdjustment(); - } - } else { - nodeData->incrementNumFramesSinceFRDAdjustment(); - } - - // setup a PacketList for the avatarPackets - auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData); - - if (nodeData->getAvatarSessionDisplayNameMustChange()) { - const QString& existingBaseDisplayName = nodeData->getBaseDisplayName(); - if (--_sessionDisplayNames[existingBaseDisplayName].second <= 0) { - _sessionDisplayNames.remove(existingBaseDisplayName); - } - - QString baseName = avatar.getDisplayName().trimmed(); - const QRegularExpression curses{ "fuck|shit|damn|cock|cunt" }; // POC. We may eventually want something much more elaborate (subscription?). - baseName = baseName.replace(curses, "*"); // Replace rather than remove, so that people have a clue that the person's a jerk. - const QRegularExpression trailingDigits{ "\\s*_\\d+$" }; // whitespace "_123" - baseName = baseName.remove(trailingDigits); - if (baseName.isEmpty()) { - baseName = "anonymous"; - } - - QPair& soFar = _sessionDisplayNames[baseName]; // Inserts and answers 0, 0 if not already present, which is what we want. - int& highWater = soFar.first; - nodeData->setBaseDisplayName(baseName); - QString sessionDisplayName = (highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName; - avatar.setSessionDisplayName(sessionDisplayName); - highWater++; - soFar.second++; // refcount - nodeData->flagIdentityChange(); - nodeData->setAvatarSessionDisplayNameMustChange(false); - sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name. Others will find out below. - qDebug() << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID(); - } - - // this is an AGENT we have received head data from - // send back a packet with other active node data to this node - nodeList->eachMatchingNode( - [&](const SharedNodePointer& otherNode)->bool { - // make sure we have data for this avatar, that it isn't the same node, - // and isn't an avatar that the viewing node has ignored - // or that has ignored the viewing node - if (!otherNode->getLinkedData() - || otherNode->getUUID() == node->getUUID() - || (node->isIgnoringNodeWithID(otherNode->getUUID()) && !getsIgnoredByMe) - || (otherNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) { - return false; - } else { - AvatarMixerClientData* otherData = reinterpret_cast(otherNode->getLinkedData()); - AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); - // Check to see if the space bubble is enabled - if (node->isIgnoreRadiusEnabled() || otherNode->isIgnoreRadiusEnabled()) { - // Define the minimum bubble size - static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f); - // Define the scale of the box for the current node - glm::vec3 nodeBoxScale = (nodeData->getPosition() - nodeData->getGlobalBoundingBoxCorner()) * 2.0f; - // Define the scale of the box for the current other node - glm::vec3 otherNodeBoxScale = (otherData->getPosition() - otherData->getGlobalBoundingBoxCorner()) * 2.0f; - - // Set up the bounding box for the current node - AABox nodeBox(nodeData->getGlobalBoundingBoxCorner(), nodeBoxScale); - // Clamp the size of the bounding box to a minimum scale - if (glm::any(glm::lessThan(nodeBoxScale, minBubbleSize))) { - nodeBox.setScaleStayCentered(minBubbleSize); - } - // Set up the bounding box for the current other node - AABox otherNodeBox(otherData->getGlobalBoundingBoxCorner(), otherNodeBoxScale); - // Clamp the size of the bounding box to a minimum scale - if (glm::any(glm::lessThan(otherNodeBoxScale, minBubbleSize))) { - otherNodeBox.setScaleStayCentered(minBubbleSize); - } - // Quadruple the scale of both bounding boxes - nodeBox.embiggen(4.0f); - otherNodeBox.embiggen(4.0f); - - // Perform the collision check between the two bounding boxes - if (nodeBox.touches(otherNodeBox)) { - nodeData->ignoreOther(node, otherNode); - return getsAnyIgnored; - } - } - // Not close enough to ignore - nodeData->removeFromRadiusIgnoringSet(node, otherNode->getUUID()); - return true; - } - }, - [&](const SharedNodePointer& otherNode) { - ++numOtherAvatars; - - AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); - MutexTryLocker lock(otherNodeData->getMutex()); - if (!lock.isLocked()) { - return; - } - - // make sure we send out identity packets to and from new arrivals. - bool forceSend = !otherNodeData->checkAndSetHasReceivedFirstPacketsFrom(node->getUUID()); - - if (otherNodeData->getIdentityChangeTimestamp().time_since_epoch().count() > 0 - && (forceSend - || otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp - || distribution(generator) < IDENTITY_SEND_PROBABILITY)) { - sendIdentityPacket(otherNodeData, node); - } - - AvatarData& otherAvatar = otherNodeData->getAvatar(); - // Decide whether to send this avatar's data based on it's distance from us - - // The full rate distance is the distance at which EVERY update will be sent for this avatar - // at twice the full rate distance, there will be a 50% chance of sending this avatar's update - glm::vec3 otherPosition = otherAvatar.getClientGlobalPosition(); - float distanceToAvatar = glm::length(myPosition - otherPosition); - - // potentially update the max full rate distance for this frame - maxAvatarDistanceThisFrame = std::max(maxAvatarDistanceThisFrame, distanceToAvatar); - - if (distanceToAvatar != 0.0f - && !getsOutOfView - && distribution(generator) > (nodeData->getFullRateDistance() / distanceToAvatar)) { - return; - } - - AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(otherNode->getUUID()); - AvatarDataSequenceNumber lastSeqFromSender = otherNodeData->getLastReceivedSequenceNumber(); - - if (lastSeqToReceiver > lastSeqFromSender && lastSeqToReceiver != UINT16_MAX) { - // we got out out of order packets from the sender, track it - otherNodeData->incrementNumOutOfOrderSends(); - } - - // make sure we haven't already sent this data from this sender to this receiver - // or that somehow we haven't sent - if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) { - ++numAvatarsHeldBack; - return; - } else if (lastSeqFromSender - lastSeqToReceiver > 1) { - // this is a skip - we still send the packet but capture the presence of the skip so we see it happening - ++numAvatarsWithSkippedFrames; - } - - // we're going to send this avatar - - // increment the number of avatars sent to this reciever - nodeData->incrementNumAvatarsSentLastFrame(); - - // set the last sent sequence number for this sender on the receiver - nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(), - otherNodeData->getLastReceivedSequenceNumber()); - - // determine if avatar is in view, to determine how much data to include... - glm::vec3 otherNodeBoxScale = (otherPosition - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f; - AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale); - bool isInView = nodeData->otherAvatarInView(otherNodeBox); - - // this throttles the extra data to only be sent every Nth message - if (!isInView && !getsOutOfView && (lastSeqToReceiver % EXTRA_AVATAR_DATA_FRAME_RATIO > 0)) { - return; - } - - // start a new segment in the PacketList for this avatar - avatarPacketList->startSegment(); - - AvatarData::AvatarDataDetail detail; - if (!isInView && !getsOutOfView) { - detail = AvatarData::MinimumData; - nodeData->incrementAvatarOutOfView(); - } else { - detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO - ? AvatarData::SendAllData : AvatarData::CullSmallData; - nodeData->incrementAvatarInView(); - } - - numAvatarDataBytes += avatarPacketList->write(otherNode->getUUID().toRfc4122()); - auto lastEncodeForOther = nodeData->getLastOtherAvatarEncodeTime(otherNode->getUUID()); - QVector& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID()); - bool distanceAdjust = true; - glm::vec3 viewerPosition = myPosition; - auto bytes = otherAvatar.toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, distanceAdjust, viewerPosition, &lastSentJointsForOther); - numAvatarDataBytes += avatarPacketList->write(bytes); - - avatarPacketList->endSegment(); - }); - - // close the current packet so that we're always sending something - avatarPacketList->closeCurrentPacket(true); - - // send the avatar data PacketList - nodeList->sendPacketList(std::move(avatarPacketList), *node); - - // record the bytes sent for other avatar data in the AvatarMixerClientData - nodeData->recordSentAvatarData(numAvatarDataBytes); - - // record the number of avatars held back this frame - nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack); - nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames); - - if (numOtherAvatars == 0) { - // update the full rate distance to FLOAT_MAX since we didn't have any other avatars to send - nodeData->setMaxAvatarDistance(FLT_MAX); - } else { - nodeData->setMaxAvatarDistance(maxAvatarDistanceThisFrame); - } + _slavePool.processIncomingPackets(cbegin, cend); + }, &lockWait, &nodeTransform, &functor); + auto end = usecTimestampNow(); + _processQueuedAvatarDataPacketsElapsedTime += (end - start); } - ); - // We're done encoding this version of the otherAvatars. Update their "lastSent" joint-states so - // that we can notice differences, next time around. - // - // FIXME - this seems suspicious, the code seems to consider all avatars, but not all avatars will - // have had their joints sent, so actually we should consider the time since they actually were sent???? - nodeList->eachMatchingNode( - [&](const SharedNodePointer& otherNode)->bool { - if (!otherNode->getLinkedData()) { - return false; - } - if (otherNode->getType() != NodeType::Agent) { - return false; - } - if (!otherNode->getActiveSocket()) { - return false; - } - return true; - }, - [&](const SharedNodePointer& otherNode) { - AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); - MutexTryLocker lock(otherNodeData->getMutex()); - if (!lock.isLocked()) { - return; - } - AvatarData& otherAvatar = otherNodeData->getAvatar(); - otherAvatar.doneEncoding(false); - }); + // process pending display names... this doesn't currently run on multiple threads, because it + // side-effects the mixer's data, which is fine because it's a very low cost operation + { + auto start = usecTimestampNow(); + nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + std::for_each(cbegin, cend, [&](const SharedNodePointer& node) { + manageDisplayName(node); + ++_sumListeners; + }); + }, &lockWait, &nodeTransform, &functor); + auto end = usecTimestampNow(); + _displayNameManagementElapsedTime += (end - start); + } - _lastFrameTimestamp = p_high_resolution_clock::now(); + // this is where we need to put the real work... + { + auto start = usecTimestampNow(); + nodeList->nestedEach([&](NodeList::const_iterator cbegin, NodeList::const_iterator cend) { + auto start = usecTimestampNow(); + _slavePool.broadcastAvatarData(cbegin, cend, _lastFrameTimestamp, _maxKbpsPerNode, _throttlingRatio); + auto end = usecTimestampNow(); + _broadcastAvatarDataInner += (end - start); + }, &lockWait, &nodeTransform, &functor); + auto end = usecTimestampNow(); + _broadcastAvatarDataElapsedTime += (end - start); -#ifdef WANT_DEBUG - auto sinceLastDebug = p_high_resolution_clock::now() - _lastDebugMessage; - auto sinceLastDebugUsecs = std::chrono::duration_cast(sinceLastDebug).count(); - quint64 DEBUG_INTERVAL = USECS_PER_SECOND * 5; + _broadcastAvatarDataLockWait += lockWait; + _broadcastAvatarDataNodeTransform += nodeTransform; + _broadcastAvatarDataNodeFunctor += functor; + } + + ++frame; + ++_numTightLoopFrames; + _loopRate.increment(); + + // play nice with qt event-looping + { + // since we're a while loop we need to yield to qt's event processing + auto start = usecTimestampNow(); + QCoreApplication::processEvents(); + if (_isFinished) { + // alert qt eventing that this is finished + QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete); + break; + } + auto end = usecTimestampNow(); + _processEventsElapsedTime += (end - start); + } + + _lastFrameTimestamp = frameTimestamp; - if (sinceLastDebugUsecs > DEBUG_INTERVAL) { - qDebug() << "broadcast rate:" << _broadcastRate.rate() << "hz"; - _lastDebugMessage = p_high_resolution_clock::now(); } -#endif +} + +// NOTE: nodeData->getAvatar() might be side effected, must be called when access to node/nodeData +// is guarenteed to not be accessed by other thread +void AvatarMixer::manageDisplayName(const SharedNodePointer& node) { + AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); + if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) { + AvatarData& avatar = nodeData->getAvatar(); + const QString& existingBaseDisplayName = nodeData->getBaseDisplayName(); + if (--_sessionDisplayNames[existingBaseDisplayName].second <= 0) { + _sessionDisplayNames.remove(existingBaseDisplayName); + } + + QString baseName = avatar.getDisplayName().trimmed(); + const QRegularExpression curses { "fuck|shit|damn|cock|cunt" }; // POC. We may eventually want something much more elaborate (subscription?). + baseName = baseName.replace(curses, "*"); // Replace rather than remove, so that people have a clue that the person's a jerk. + const QRegularExpression trailingDigits { "\\s*_\\d+$" }; // whitespace "_123" + baseName = baseName.remove(trailingDigits); + if (baseName.isEmpty()) { + baseName = "anonymous"; + } + + QPair& soFar = _sessionDisplayNames[baseName]; // Inserts and answers 0, 0 if not already present, which is what we want. + int& highWater = soFar.first; + nodeData->setBaseDisplayName(baseName); + QString sessionDisplayName = (highWater > 0) ? baseName + "_" + QString::number(highWater) : baseName; + avatar.setSessionDisplayName(sessionDisplayName); + highWater++; + soFar.second++; // refcount + nodeData->flagIdentityChange(); + nodeData->setAvatarSessionDisplayNameMustChange(false); + sendIdentityPacket(nodeData, node); // Tell node whose name changed about its new session display name. + qCDebug(avatars) << "Giving session display name" << sessionDisplayName << "to node with ID" << node->getUUID(); + } +} + +void AvatarMixer::throttle(std::chrono::microseconds duration, int frame) { + // throttle using a modified proportional-integral controller + const float FRAME_TIME = USECS_PER_SECOND / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND; + float mixRatio = duration.count() / FRAME_TIME; + + // constants are determined based on a "regular" 16-CPU EC2 server + + // target different mix and backoff ratios (they also have different backoff rates) + // this is to prevent oscillation, and encourage throttling to find a steady state + const float TARGET = 0.9f; + // on a "regular" machine with 100 avatars, this is the largest value where + // - overthrottling can be recovered + // - oscillations will not occur after the recovery + const float BACKOFF_TARGET = 0.44f; + + // the mixer is known to struggle at about 150 on a "regular" machine + // so throttle 2/150 the streams to ensure smooth mixing (throttling is linear) + const float STRUGGLES_AT = 150.0f; + const float THROTTLE_RATE = 2 / STRUGGLES_AT; + const float BACKOFF_RATE = THROTTLE_RATE / 4; + + // recovery should be bounded so that large changes in user count is a tolerable experience + // throttling is linear, so most cases will not need a full recovery + const int RECOVERY_TIME = 180; + + // weight more recent frames to determine if throttling is necessary, + const int TRAILING_FRAMES = (int)(100 * RECOVERY_TIME * BACKOFF_RATE); + const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_FRAMES; + const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; + _trailingMixRatio = PREVIOUS_FRAMES_RATIO * _trailingMixRatio + CURRENT_FRAME_RATIO * mixRatio; + + if (frame % TRAILING_FRAMES == 0) { + if (_trailingMixRatio > TARGET) { + int proportionalTerm = 1 + (_trailingMixRatio - TARGET) / 0.1f; + _throttlingRatio += THROTTLE_RATE * proportionalTerm; + _throttlingRatio = std::min(_throttlingRatio, 1.0f); + qDebug("avatar-mixer is struggling (%f mix/sleep) - throttling %f of streams", + (double)_trailingMixRatio, (double)_throttlingRatio); + } + else if (_throttlingRatio > 0.0f && _trailingMixRatio <= BACKOFF_TARGET) { + int proportionalTerm = 1 + (TARGET - _trailingMixRatio) / 0.2f; + _throttlingRatio -= BACKOFF_RATE * proportionalTerm; + _throttlingRatio = std::max(_throttlingRatio, 0.0f); + qDebug("avatar-mixer is recovering (%f mix/sleep) - throttling %f of streams", + (double)_trailingMixRatio, (double)_throttlingRatio); + } + } } void AvatarMixer::nodeKilled(SharedNodePointer killedNode) { @@ -542,7 +309,7 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) { }, [&](const SharedNodePointer& node) { QMetaObject::invokeMethod(node->getLinkedData(), - "removeLastBroadcastSequenceNumber", + "cleanupKilledNode", Qt::AutoConnection, Q_ARG(const QUuid&, QUuid(killedNode->getUUID()))); } @@ -550,9 +317,30 @@ void AvatarMixer::nodeKilled(SharedNodePointer killedNode) { } } + +void AvatarMixer::handleAdjustAvatarSorting(QSharedPointer message, SharedNodePointer senderNode) { + auto start = usecTimestampNow(); + + // only allow admins with kick rights to change this value... + if (senderNode->getCanKick()) { + message->readPrimitive(&AvatarData::_avatarSortCoefficientSize); + message->readPrimitive(&AvatarData::_avatarSortCoefficientCenter); + message->readPrimitive(&AvatarData::_avatarSortCoefficientAge); + + qCDebug(avatars) << "New avatar sorting... " + << "size:" << AvatarData::_avatarSortCoefficientSize + << "center:" << AvatarData::_avatarSortCoefficientCenter + << "age:" << AvatarData::_avatarSortCoefficientAge; + } + + auto end = usecTimestampNow(); + _handleAdjustAvatarSortingElapsedTime += (end - start); +} + + void AvatarMixer::handleViewFrustumPacket(QSharedPointer message, SharedNodePointer senderNode) { - auto nodeList = DependencyManager::get(); - nodeList->getOrCreateLinkedData(senderNode); + auto start = usecTimestampNow(); + getOrCreateClientData(senderNode); if (senderNode->getLinkedData()) { AvatarMixerClientData* nodeData = dynamic_cast(senderNode->getLinkedData()); @@ -560,11 +348,15 @@ void AvatarMixer::handleViewFrustumPacket(QSharedPointer messag nodeData->readViewFrustumPacket(message->getMessage()); } } + + auto end = usecTimestampNow(); + _handleViewFrustumPacketElapsedTime += (end - start); } void AvatarMixer::handleRequestsDomainListDataPacket(QSharedPointer message, SharedNodePointer senderNode) { - auto nodeList = DependencyManager::get(); - nodeList->getOrCreateLinkedData(senderNode); + auto start = usecTimestampNow(); + + getOrCreateClientData(senderNode); if (senderNode->getLinkedData()) { AvatarMixerClientData* nodeData = dynamic_cast(senderNode->getLinkedData()); @@ -572,19 +364,17 @@ void AvatarMixer::handleRequestsDomainListDataPacket(QSharedPointerreadPrimitive(&isRequesting); nodeData->setRequestsDomainListData(isRequesting); - qDebug() << "node" << nodeData->getNodeID() << "requestsDomainListData" << isRequesting; + qCDebug(avatars) << "node" << nodeData->getNodeID() << "requestsDomainListData" << isRequesting; } } -} - -void AvatarMixer::handleAvatarDataPacket(QSharedPointer message, SharedNodePointer senderNode) { - auto nodeList = DependencyManager::get(); - nodeList->updateNodeWithDataFromPacket(message, senderNode); + auto end = usecTimestampNow(); + _handleRequestsDomainListDataPacketElapsedTime += (end - start); } void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer message, SharedNodePointer senderNode) { + auto start = usecTimestampNow(); auto nodeList = DependencyManager::get(); - nodeList->getOrCreateLinkedData(senderNode); + getOrCreateClientData(senderNode); if (senderNode->getLinkedData()) { AvatarMixerClientData* nodeData = dynamic_cast(senderNode->getLinkedData()); @@ -606,32 +396,174 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer mes } } } + auto end = usecTimestampNow(); + _handleAvatarIdentityPacketElapsedTime += (end - start); } void AvatarMixer::handleKillAvatarPacket(QSharedPointer message) { + auto start = usecTimestampNow(); DependencyManager::get()->processKillNode(*message); + auto end = usecTimestampNow(); + _handleKillAvatarPacketElapsedTime += (end - start); } void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode) { + auto start = usecTimestampNow(); senderNode->parseIgnoreRequestMessage(message); + auto end = usecTimestampNow(); + _handleNodeIgnoreRequestPacketElapsedTime += (end - start); } void AvatarMixer::handleRadiusIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + auto start = usecTimestampNow(); sendingNode->parseIgnoreRadiusRequestMessage(packet); + auto end = usecTimestampNow(); + _handleRadiusIgnoreRequestPacketElapsedTime += (end - start); } void AvatarMixer::sendStatsPacket() { + auto start = usecTimestampNow(); + + QJsonObject statsObject; - statsObject["average_listeners_last_second"] = (float) _sumListeners / (float) _numStatFrames; - statsObject["average_identity_packets_per_frame"] = (float) _sumIdentityPackets / (float) _numStatFrames; + statsObject["broadcast_loop_rate"] = _loopRate.rate(); + statsObject["threads"] = _slavePool.numThreads(); + statsObject["trailing_mix_ratio"] = _trailingMixRatio; + statsObject["throttling_ratio"] = _throttlingRatio; - statsObject["trailing_sleep_percentage"] = _trailingSleepRatio * 100; - statsObject["performance_throttling_ratio"] = _performanceThrottlingRatio; - statsObject["broadcast_loop_rate"] = _broadcastRate.rate(); + // this things all occur on the frequency of the tight loop + int tightLoopFrames = _numTightLoopFrames; + int tenTimesPerFrame = tightLoopFrames * 10; + #define TIGHT_LOOP_STAT(x) (x > tenTimesPerFrame) ? x / tightLoopFrames : ((float)x / (float)tightLoopFrames); + #define TIGHT_LOOP_STAT_UINT64(x) (x > (quint64)tenTimesPerFrame) ? x / tightLoopFrames : ((float)x / (float)tightLoopFrames); + + statsObject["average_listeners_last_second"] = TIGHT_LOOP_STAT(_sumListeners); + + QJsonObject singleCoreTasks; + singleCoreTasks["processEvents"] = TIGHT_LOOP_STAT_UINT64(_processEventsElapsedTime); + singleCoreTasks["queueIncomingPacket"] = TIGHT_LOOP_STAT_UINT64(_queueIncomingPacketElapsedTime); + + QJsonObject incomingPacketStats; + incomingPacketStats["handleAvatarIdentityPacket"] = TIGHT_LOOP_STAT_UINT64(_handleAvatarIdentityPacketElapsedTime); + incomingPacketStats["handleKillAvatarPacket"] = TIGHT_LOOP_STAT_UINT64(_handleKillAvatarPacketElapsedTime); + incomingPacketStats["handleNodeIgnoreRequestPacket"] = TIGHT_LOOP_STAT_UINT64(_handleNodeIgnoreRequestPacketElapsedTime); + incomingPacketStats["handleRadiusIgnoreRequestPacket"] = TIGHT_LOOP_STAT_UINT64(_handleRadiusIgnoreRequestPacketElapsedTime); + incomingPacketStats["handleRequestsDomainListDataPacket"] = TIGHT_LOOP_STAT_UINT64(_handleRequestsDomainListDataPacketElapsedTime); + incomingPacketStats["handleViewFrustumPacket"] = TIGHT_LOOP_STAT_UINT64(_handleViewFrustumPacketElapsedTime); + + singleCoreTasks["incoming_packets"] = incomingPacketStats; + singleCoreTasks["sendStats"] = (float)_sendStatsElapsedTime; + + statsObject["singleCoreTasks"] = singleCoreTasks; + + QJsonObject parallelTasks; + + QJsonObject processQueuedAvatarDataPacketsStats; + processQueuedAvatarDataPacketsStats["1_total"] = TIGHT_LOOP_STAT_UINT64(_processQueuedAvatarDataPacketsElapsedTime); + processQueuedAvatarDataPacketsStats["2_lockWait"] = TIGHT_LOOP_STAT_UINT64(_processQueuedAvatarDataPacketsLockWaitElapsedTime); + parallelTasks["processQueuedAvatarDataPackets"] = processQueuedAvatarDataPacketsStats; + + QJsonObject broadcastAvatarDataStats; + + broadcastAvatarDataStats["1_total"] = TIGHT_LOOP_STAT_UINT64(_broadcastAvatarDataElapsedTime); + broadcastAvatarDataStats["2_innner"] = TIGHT_LOOP_STAT_UINT64(_broadcastAvatarDataInner); + broadcastAvatarDataStats["3_lockWait"] = TIGHT_LOOP_STAT_UINT64(_broadcastAvatarDataLockWait); + broadcastAvatarDataStats["4_NodeTransform"] = TIGHT_LOOP_STAT_UINT64(_broadcastAvatarDataNodeTransform); + broadcastAvatarDataStats["5_Functor"] = TIGHT_LOOP_STAT_UINT64(_broadcastAvatarDataNodeFunctor); + + parallelTasks["broadcastAvatarData"] = broadcastAvatarDataStats; + + QJsonObject displayNameManagementStats; + displayNameManagementStats["1_total"] = TIGHT_LOOP_STAT_UINT64(_displayNameManagementElapsedTime); + parallelTasks["displayNameManagement"] = displayNameManagementStats; + + statsObject["parallelTasks"] = parallelTasks; + + + 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["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); + + float averageOverBudgetAvatars = averageNodes ? aggregateStats.overBudgetAvatars / averageNodes : 0.0f; + slavesAggregatObject["sent_7_averageOverBudgetAvatars"] = TIGHT_LOOP_STAT(averageOverBudgetAvatars); + + slavesAggregatObject["timing_1_processIncomingPackets"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.processIncomingPacketsElapsedTime); + slavesAggregatObject["timing_2_ignoreCalculation"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.ignoreCalculationElapsedTime); + slavesAggregatObject["timing_3_toByteArray"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.toByteArrayElapsedTime); + slavesAggregatObject["timing_4_avatarDataPacking"] = TIGHT_LOOP_STAT_UINT64(aggregateStats.avatarDataPackingElapsedTime); + 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; + + _handleViewFrustumPacketElapsedTime = 0; + _handleAvatarIdentityPacketElapsedTime = 0; + _handleKillAvatarPacketElapsedTime = 0; + _handleNodeIgnoreRequestPacketElapsedTime = 0; + _handleRadiusIgnoreRequestPacketElapsedTime = 0; + _handleRequestsDomainListDataPacketElapsedTime = 0; + _processEventsElapsedTime = 0; + _queueIncomingPacketElapsedTime = 0; + _processQueuedAvatarDataPacketsElapsedTime = 0; + _processQueuedAvatarDataPacketsLockWaitElapsedTime = 0; QJsonObject avatarsObject; - auto nodeList = DependencyManager::get(); // add stats for each listerner nodeList->eachNode([&](const SharedNodePointer& node) { @@ -661,33 +593,57 @@ void AvatarMixer::sendStatsPacket() { avatarsObject[uuidStringWithoutCurlyBraces(node->getUUID())] = avatarStats; }); - statsObject["avatars"] = avatarsObject; + statsObject["z_avatars"] = avatarsObject; + ThreadedAssignment::addPacketStatsAndSendStatsPacket(statsObject); _sumListeners = 0; _sumIdentityPackets = 0; - _numStatFrames = 0; + _numTightLoopFrames = 0; + + _broadcastAvatarDataElapsedTime = 0; + _broadcastAvatarDataInner = 0; + _broadcastAvatarDataLockWait = 0; + _broadcastAvatarDataNodeTransform = 0; + _broadcastAvatarDataNodeFunctor = 0; + + _displayNameManagementElapsedTime = 0; + _ignoreCalculationElapsedTime = 0; + _avatarDataPackingElapsedTime = 0; + _packetSendingElapsedTime = 0; + + + auto end = usecTimestampNow(); + _sendStatsElapsedTime = (end - start); + + _lastStatsTime = start; + } void AvatarMixer::run() { - qDebug() << "Waiting for connection to domain to request settings from domain-server."; + qCDebug(avatars) << "Waiting for connection to domain to request settings from domain-server."; // wait until we have the domain-server settings, otherwise we bail DomainHandler& domainHandler = DependencyManager::get()->getDomainHandler(); connect(&domainHandler, &DomainHandler::settingsReceived, this, &AvatarMixer::domainSettingsRequestComplete); connect(&domainHandler, &DomainHandler::settingsReceiveFail, this, &AvatarMixer::domainSettingsRequestFailed); - + ThreadedAssignment::commonInit(AVATAR_MIXER_LOGGING_NAME, NodeType::AvatarMixer); - // setup the timer that will be fired on the broadcast thread - _broadcastTimer = new QTimer; - _broadcastTimer->setTimerType(Qt::PreciseTimer); - _broadcastTimer->setInterval(AVATAR_DATA_SEND_INTERVAL_MSECS); - _broadcastTimer->moveToThread(&_broadcastThread); +} - // connect appropriate signals and slots - connect(_broadcastTimer, &QTimer::timeout, this, &AvatarMixer::broadcastAvatarData, Qt::DirectConnection); - connect(&_broadcastThread, SIGNAL(started()), _broadcastTimer, SLOT(start())); +AvatarMixerClientData* AvatarMixer::getOrCreateClientData(SharedNodePointer node) { + auto clientData = dynamic_cast(node->getLinkedData()); + + if (!clientData) { + node->setLinkedData(std::unique_ptr { new AvatarMixerClientData(node->getUUID()) }); + clientData = dynamic_cast(node->getLinkedData()); + auto& avatar = clientData->getAvatar(); + avatar.setDomainMinimumScale(_domainMinimumScale); + avatar.setDomainMaximumScale(_domainMaximumScale); + } + + return clientData; } void AvatarMixer::domainSettingsRequestComplete() { @@ -696,20 +652,9 @@ void AvatarMixer::domainSettingsRequestComplete() { // parse the settings to pull out the values we need parseDomainServerSettings(nodeList->getDomainHandler().getSettingsObject()); - - float domainMinimumScale = _domainMinimumScale; - float domainMaximumScale = _domainMaximumScale; - - nodeList->linkedDataCreateCallback = [domainMinimumScale, domainMaximumScale] (Node* node) { - auto clientData = std::unique_ptr { new AvatarMixerClientData(node->getUUID()) }; - clientData->getAvatar().setDomainMinimumScale(domainMinimumScale); - clientData->getAvatar().setDomainMaximumScale(domainMaximumScale); - - node->setLinkedData(std::move(clientData)); - }; - // start the broadcastThread - _broadcastThread.start(); + // start our tight loop... + start(); } void AvatarMixer::handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID) { @@ -728,17 +673,36 @@ void AvatarMixer::handlePacketVersionMismatch(PacketType type, const HifiSockAdd void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { const QString AVATAR_MIXER_SETTINGS_KEY = "avatar_mixer"; + QJsonObject avatarMixerGroupObject = domainSettings[AVATAR_MIXER_SETTINGS_KEY].toObject(); + + const QString NODE_SEND_BANDWIDTH_KEY = "max_node_send_bandwidth"; const float DEFAULT_NODE_SEND_BANDWIDTH = 5.0f; - QJsonValue nodeBandwidthValue = domainSettings[AVATAR_MIXER_SETTINGS_KEY].toObject()[NODE_SEND_BANDWIDTH_KEY]; + QJsonValue nodeBandwidthValue = avatarMixerGroupObject[NODE_SEND_BANDWIDTH_KEY]; if (!nodeBandwidthValue.isDouble()) { - qDebug() << NODE_SEND_BANDWIDTH_KEY << "is not a double - will continue with default value"; + qCDebug(avatars) << NODE_SEND_BANDWIDTH_KEY << "is not a double - will continue with default value"; } _maxKbpsPerNode = nodeBandwidthValue.toDouble(DEFAULT_NODE_SEND_BANDWIDTH) * KILO_PER_MEGA; - qDebug() << "The maximum send bandwidth per node is" << _maxKbpsPerNode << "kbps."; + qCDebug(avatars) << "The maximum send bandwidth per node is" << _maxKbpsPerNode << "kbps."; + const QString AUTO_THREADS = "auto_threads"; + bool autoThreads = avatarMixerGroupObject[AUTO_THREADS].toBool(); + if (!autoThreads) { + bool ok; + const QString NUM_THREADS = "num_threads"; + int numThreads = avatarMixerGroupObject[NUM_THREADS].toString().toInt(&ok); + if (!ok) { + qCWarning(avatars) << "Avatar mixer: Error reading thread count. Using 1 thread."; + numThreads = 1; + } + qCDebug(avatars) << "Avatar mixer will use specified number of threads:" << numThreads; + _slavePool.setNumThreads(numThreads); + } else { + qCDebug(avatars) << "Avatar mixer will automatically determine number of threads to use. Using:" << _slavePool.numThreads() << "threads."; + } + const QString AVATARS_SETTINGS_KEY = "avatars"; static const QString MIN_SCALE_OPTION = "min_avatar_scale"; @@ -754,6 +718,7 @@ void AvatarMixer::parseDomainServerSettings(const QJsonObject& domainSettings) { std::swap(_domainMinimumScale, _domainMaximumScale); } - qDebug() << "This domain requires a minimum avatar scale of" << _domainMinimumScale - << "and a maximum avatar scale of" << _domainMaximumScale; + qCDebug(avatars) << "This domain requires a minimum avatar scale of" << _domainMinimumScale + << "and a maximum avatar scale of" << _domainMaximumScale; + } diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index 5d54622ac9..1925ec1ebd 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -21,6 +21,8 @@ #include #include "AvatarMixerClientData.h" +#include "AvatarMixerSlavePool.h" + /// Handles assignments of type AvatarMixer - distribution of avatar data to various clients class AvatarMixer : public ThreadedAssignment { Q_OBJECT @@ -36,8 +38,9 @@ public slots: void sendStatsPacket() override; private slots: + void queueIncomingPacket(QSharedPointer message, SharedNodePointer node); + void handleAdjustAvatarSorting(QSharedPointer message, SharedNodePointer senderNode); void handleViewFrustumPacket(QSharedPointer message, SharedNodePointer senderNode); - void handleAvatarDataPacket(QSharedPointer message, SharedNodePointer senderNode); void handleAvatarIdentityPacket(QSharedPointer message, SharedNodePointer senderNode); void handleKillAvatarPacket(QSharedPointer message); void handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode); @@ -45,22 +48,29 @@ private slots: void handleRequestsDomainListDataPacket(QSharedPointer message, SharedNodePointer senderNode); void domainSettingsRequestComplete(); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); + void start(); private: - void broadcastAvatarData(); + AvatarMixerClientData* getOrCreateClientData(SharedNodePointer node); + std::chrono::microseconds timeFrame(p_high_resolution_clock::time_point& timestamp); + void throttle(std::chrono::microseconds duration, int frame); + void parseDomainServerSettings(const QJsonObject& domainSettings); void sendIdentityPacket(AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode); - QThread _broadcastThread; + void manageDisplayName(const SharedNodePointer& node); p_high_resolution_clock::time_point _lastFrameTimestamp; - float _trailingSleepRatio { 1.0f }; - float _performanceThrottlingRatio { 0.0f }; + // FIXME - new throttling - use these values somehow + float _trailingMixRatio { 0.0f }; + float _throttlingRatio { 0.0f }; + int _sumListeners { 0 }; int _numStatFrames { 0 }; + int _numTightLoopFrames { 0 }; int _sumIdentityPackets { 0 }; float _maxKbpsPerNode = 0.0f; @@ -68,11 +78,41 @@ private: float _domainMinimumScale { MIN_AVATAR_SCALE }; float _domainMaximumScale { MAX_AVATAR_SCALE }; - QTimer* _broadcastTimer = nullptr; - RateCounter<> _broadcastRate; p_high_resolution_clock::time_point _lastDebugMessage; QHash> _sessionDisplayNames; + + quint64 _displayNameManagementElapsedTime { 0 }; // total time spent in broadcastAvatarData/display name management... since last stats window + quint64 _ignoreCalculationElapsedTime { 0 }; + quint64 _avatarDataPackingElapsedTime { 0 }; + quint64 _packetSendingElapsedTime { 0 }; + + quint64 _broadcastAvatarDataElapsedTime { 0 }; // total time spent in broadcastAvatarData since last stats window + quint64 _broadcastAvatarDataInner { 0 }; + quint64 _broadcastAvatarDataLockWait { 0 }; + quint64 _broadcastAvatarDataNodeTransform { 0 }; + quint64 _broadcastAvatarDataNodeFunctor { 0 }; + + quint64 _handleAdjustAvatarSortingElapsedTime { 0 }; + quint64 _handleViewFrustumPacketElapsedTime { 0 }; + quint64 _handleAvatarIdentityPacketElapsedTime { 0 }; + quint64 _handleKillAvatarPacketElapsedTime { 0 }; + quint64 _handleNodeIgnoreRequestPacketElapsedTime { 0 }; + quint64 _handleRadiusIgnoreRequestPacketElapsedTime { 0 }; + quint64 _handleRequestsDomainListDataPacketElapsedTime { 0 }; + quint64 _processQueuedAvatarDataPacketsElapsedTime { 0 }; + quint64 _processQueuedAvatarDataPacketsLockWaitElapsedTime { 0 }; + + quint64 _processEventsElapsedTime { 0 }; + quint64 _sendStatsElapsedTime { 0 }; + quint64 _queueIncomingPacketElapsedTime { 0 }; + quint64 _lastStatsTime { usecTimestampNow() }; + + RateCounter<> _loopRate; // this is the rate that the main thread tight loop runs + + + AvatarMixerSlavePool _slavePool; + }; #endif // hifi_AvatarMixer_h diff --git a/assignment-client/src/avatars/AvatarMixerClientData.cpp b/assignment-client/src/avatars/AvatarMixerClientData.cpp index a7a506e1d8..717e14535f 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.cpp +++ b/assignment-client/src/avatars/AvatarMixerClientData.cpp @@ -16,10 +16,52 @@ #include "AvatarMixerClientData.h" + + +void AvatarMixerClientData::queuePacket(QSharedPointer message, SharedNodePointer node) { + if (!_packetQueue.node) { + _packetQueue.node = node; + } + _packetQueue.push(message); +} + +int AvatarMixerClientData::processPackets() { + int packetsProcessed = 0; + SharedNodePointer node = _packetQueue.node; + assert(_packetQueue.empty() || node); + _packetQueue.node.clear(); + + while (!_packetQueue.empty()) { + auto& packet = _packetQueue.front(); + + packetsProcessed++; + + switch (packet->getType()) { + case PacketType::AvatarData: + parseData(*packet); + break; + default: + Q_UNREACHABLE(); + } + _packetQueue.pop(); + } + assert(_packetQueue.empty()); + + return packetsProcessed; +} + int AvatarMixerClientData::parseData(ReceivedMessage& message) { + // pull the sequence number from the data first - message.readPrimitive(&_lastReceivedSequenceNumber); + uint16_t sequenceNumber; + + message.readPrimitive(&sequenceNumber); + if (sequenceNumber < _lastReceivedSequenceNumber && _lastReceivedSequenceNumber != UINT16_MAX) { + incrementNumOutOfOrderSends(); + } + _lastReceivedSequenceNumber = sequenceNumber; + // compute the offset to the data payload return _avatar->parseDataFromBuffer(message.readWithoutCopy(message.getBytesLeftToRead())); } @@ -32,14 +74,22 @@ bool AvatarMixerClientData::checkAndSetHasReceivedFirstPacketsFrom(const QUuid& return true; } +uint64_t AvatarMixerClientData::getLastBroadcastTime(const QUuid& nodeUUID) const { + // return the matching PacketSequenceNumber, or the default if we don't have it + auto nodeMatch = _lastBroadcastTimes.find(nodeUUID); + if (nodeMatch != _lastBroadcastTimes.end()) { + return nodeMatch->second; + } + return 0; +} + uint16_t AvatarMixerClientData::getLastBroadcastSequenceNumber(const QUuid& nodeUUID) const { // return the matching PacketSequenceNumber, or the default if we don't have it auto nodeMatch = _lastBroadcastSequenceNumbers.find(nodeUUID); if (nodeMatch != _lastBroadcastSequenceNumbers.end()) { return nodeMatch->second; - } else { - return 0; } + return 0; } void AvatarMixerClientData::ignoreOther(SharedNodePointer self, SharedNodePointer other) { @@ -76,8 +126,6 @@ bool AvatarMixerClientData::otherAvatarInView(const AABox& otherAvatarBox) { void AvatarMixerClientData::loadJSONStats(QJsonObject& jsonObject) const { jsonObject["display_name"] = _avatar->getDisplayName(); - jsonObject["full_rate_distance"] = _fullRateDistance; - jsonObject["max_av_distance"] = _maxAvatarDistance; jsonObject["num_avs_sent_last_frame"] = _numAvatarsSentLastFrame; jsonObject["avg_other_av_starves_per_second"] = getAvgNumOtherAvatarStarvesPerSecond(); jsonObject["avg_other_av_skips_per_second"] = getAvgNumOtherAvatarSkipsPerSecond(); diff --git a/assignment-client/src/avatars/AvatarMixerClientData.h b/assignment-client/src/avatars/AvatarMixerClientData.h index aa011f8baf..51b8d692e2 100644 --- a/assignment-client/src/avatars/AvatarMixerClientData.h +++ b/assignment-client/src/avatars/AvatarMixerClientData.h @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -41,6 +42,8 @@ public: int parseData(ReceivedMessage& message) override; AvatarData& getAvatar() { return *_avatar; } + const AvatarData* getConstAvatarData() const { return _avatar.get(); } + AvatarSharedPointer getAvatarSharedPointer() const { return _avatar; } bool checkAndSetHasReceivedFirstPacketsFrom(const QUuid& uuid); @@ -49,6 +52,15 @@ public: { _lastBroadcastSequenceNumbers[nodeUUID] = sequenceNumber; } Q_INVOKABLE void removeLastBroadcastSequenceNumber(const QUuid& nodeUUID) { _lastBroadcastSequenceNumbers.erase(nodeUUID); } + uint64_t getLastBroadcastTime(const QUuid& nodeUUID) const; + void setLastBroadcastTime(const QUuid& nodeUUID, uint64_t broadcastTime) { _lastBroadcastTimes[nodeUUID] = broadcastTime; } + Q_INVOKABLE void removeLastBroadcastTime(const QUuid& nodeUUID) { _lastBroadcastTimes.erase(nodeUUID); } + + Q_INVOKABLE void cleanupKilledNode(const QUuid& nodeUUID) { + removeLastBroadcastSequenceNumber(nodeUUID); + removeLastBroadcastTime(nodeUUID); + } + uint16_t getLastReceivedSequenceNumber() const { return _lastReceivedSequenceNumber; } HRCTime getIdentityChangeTimestamp() const { return _identityChangeTimestamp; } @@ -56,12 +68,6 @@ public: bool getAvatarSessionDisplayNameMustChange() const { return _avatarSessionDisplayNameMustChange; } void setAvatarSessionDisplayNameMustChange(bool set = true) { _avatarSessionDisplayNameMustChange = set; } - void setFullRateDistance(float fullRateDistance) { _fullRateDistance = fullRateDistance; } - float getFullRateDistance() const { return _fullRateDistance; } - - void setMaxAvatarDistance(float maxAvatarDistance) { _maxAvatarDistance = maxAvatarDistance; } - float getMaxAvatarDistance() const { return _maxAvatarDistance; } - void resetNumAvatarsSentLastFrame() { _numAvatarsSentLastFrame = 0; } void incrementNumAvatarsSentLastFrame() { ++_numAvatarsSentLastFrame; } int getNumAvatarsSentLastFrame() const { return _numAvatarsSentLastFrame; } @@ -85,9 +91,9 @@ public: void loadJSONStats(QJsonObject& jsonObject) const; - glm::vec3 getPosition() { return _avatar ? _avatar->getPosition() : glm::vec3(0); } - glm::vec3 getGlobalBoundingBoxCorner() { return _avatar ? _avatar->getGlobalBoundingBoxCorner() : glm::vec3(0); } - bool isRadiusIgnoring(const QUuid& other) { return _radiusIgnoredOthers.find(other) != _radiusIgnoredOthers.end(); } + glm::vec3 getPosition() const { return _avatar ? _avatar->getPosition() : glm::vec3(0); } + glm::vec3 getGlobalBoundingBoxCorner() const { return _avatar ? _avatar->getGlobalBoundingBoxCorner() : glm::vec3(0); } + bool isRadiusIgnoring(const QUuid& other) const { return _radiusIgnoredOthers.find(other) != _radiusIgnoredOthers.end(); } void addToRadiusIgnoringSet(const QUuid& other) { _radiusIgnoredOthers.insert(other); } void removeFromRadiusIgnoringSet(SharedNodePointer self, const QUuid& other); void ignoreOther(SharedNodePointer self, SharedNodePointer other); @@ -104,6 +110,8 @@ public: bool getRequestsDomainListData() { return _requestsDomainListData; } void setRequestsDomainListData(bool requesting) { _requestsDomainListData = requesting; } + ViewFrustum getViewFrustom() const { return _currentViewFrustum; } + quint64 getLastOtherAvatarEncodeTime(QUuid otherAvatar) { quint64 result = 0; if (_lastOtherAvatarEncodeTime.find(otherAvatar) != _lastOtherAvatarEncodeTime.end()) { @@ -118,14 +126,21 @@ public: return _lastOtherAvatarSentJoints[otherAvatar]; } - + void queuePacket(QSharedPointer message, SharedNodePointer node); + int processPackets(); // returns number of packets processed private: + struct PacketQueue : public std::queue> { + QWeakPointer node; + }; + PacketQueue _packetQueue; + AvatarSharedPointer _avatar { new AvatarData() }; uint16_t _lastReceivedSequenceNumber { 0 }; std::unordered_map _lastBroadcastSequenceNumbers; std::unordered_set _hasReceivedFirstPacketsFrom; + std::unordered_map _lastBroadcastTimes; // this is a map of the last time we encoded an "other" avatar for // sending to "this" node @@ -135,9 +150,6 @@ private: HRCTime _identityChangeTimestamp; bool _avatarSessionDisplayNameMustChange{ false }; - float _fullRateDistance = FLT_MAX; - float _maxAvatarDistance = FLT_MAX; - int _numAvatarsSentLastFrame = 0; int _numFramesSinceAdjustment = 0; diff --git a/assignment-client/src/avatars/AvatarMixerSlave.cpp b/assignment-client/src/avatars/AvatarMixerSlave.cpp new file mode 100644 index 0000000000..dd25aa4c4b --- /dev/null +++ b/assignment-client/src/avatars/AvatarMixerSlave.cpp @@ -0,0 +1,434 @@ +// +// AvatarMixerSlave.cpp +// assignment-client/src/avatar +// +// Created by Brad Hefta-Gaub on 2/14/2017. +// Copyright 2017 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 +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "AvatarMixer.h" +#include "AvatarMixerClientData.h" +#include "AvatarMixerSlave.h" + + +void AvatarMixerSlave::configure(ConstIter begin, ConstIter end) { + _begin = begin; + _end = end; +} + +void AvatarMixerSlave::configureBroadcast(ConstIter begin, ConstIter end, + p_high_resolution_clock::time_point lastFrameTimestamp, + float maxKbpsPerNode, float throttlingRatio) { + _begin = begin; + _end = end; + _lastFrameTimestamp = lastFrameTimestamp; + _maxKbpsPerNode = maxKbpsPerNode; + _throttlingRatio = throttlingRatio; +} + +void AvatarMixerSlave::harvestStats(AvatarMixerSlaveStats& stats) { + stats = _stats; + _stats.reset(); +} + + +void AvatarMixerSlave::processIncomingPackets(const SharedNodePointer& node) { + auto start = usecTimestampNow(); + auto nodeData = dynamic_cast(node->getLinkedData()); + if (nodeData) { + _stats.nodesProcessed++; + _stats.packetsProcessed += nodeData->processPackets(); + } + auto end = usecTimestampNow(); + _stats.processIncomingPacketsElapsedTime += (end - start); +} + + +int AvatarMixerSlave::sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode) { + int bytesSent = 0; + QByteArray individualData = nodeData->getConstAvatarData()->identityByteArray(); + auto identityPacket = NLPacket::create(PacketType::AvatarIdentity, individualData.size()); + individualData.replace(0, NUM_BYTES_RFC4122_UUID, nodeData->getNodeID().toRfc4122()); // FIXME, this looks suspicious + bytesSent += individualData.size(); + identityPacket->write(individualData); + DependencyManager::get()->sendPacket(std::move(identityPacket), *destinationNode); + _stats.numIdentityPackets++; + return bytesSent; +} + +static const int AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 45; + +// FIXME - There is some old logic (unchanged as of 2/17/17) that randomly decides to send an identity +// packet. That logic had the following comment about the constants it uses... +// +// An 80% chance of sending a identity packet within a 5 second interval. +// assuming 60 htz update rate. +// +// Assuming the calculation of the constant is in fact correct for 80% and 60hz and 5 seconds (an assumption +// that I have not verified) then the constant is definitely wrong now, since we send at 45hz. +const float IDENTITY_SEND_PROBABILITY = 1.0f / 187.0f; + +void AvatarMixerSlave::broadcastAvatarData(const SharedNodePointer& node) { + quint64 start = usecTimestampNow(); + + auto nodeList = DependencyManager::get(); + + // setup for distributed random floating point values + std::random_device randomDevice; + std::mt19937 generator(randomDevice()); + std::uniform_real_distribution distribution; + + if (node->getLinkedData() && (node->getType() == NodeType::Agent) && node->getActiveSocket()) { + _stats.nodesBroadcastedTo++; + + AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); + + nodeData->resetInViewStats(); + + const AvatarData& avatar = nodeData->getAvatar(); + glm::vec3 myPosition = avatar.getClientGlobalPosition(); + + // reset the internal state for correct random number distribution + distribution.reset(); + + // reset the number of sent avatars + nodeData->resetNumAvatarsSentLastFrame(); + + // keep a counter of the number of considered avatars + int numOtherAvatars = 0; + + // keep track of outbound data rate specifically for avatar data + int numAvatarDataBytes = 0; + int identityBytesSent = 0; + + // max number of avatarBytes per frame + auto maxAvatarBytesPerFrame = (_maxKbpsPerNode * BYTES_PER_KILOBIT) / AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND; + + // FIXME - find a way to not send the sessionID for every avatar + int minimumBytesPerAvatar = AvatarDataPacket::AVATAR_HAS_FLAGS_SIZE + NUM_BYTES_RFC4122_UUID; + + int overBudgetAvatars = 0; + + // keep track of the number of other avatars held back in this frame + int numAvatarsHeldBack = 0; + + // keep track of the number of other avatar frames skipped + int numAvatarsWithSkippedFrames = 0; + + // When this is true, the AvatarMixer will send Avatar data to a client about avatars that are not in the view frustrum + bool getsOutOfView = nodeData->getRequestsDomainListData(); + + // When this is true, the AvatarMixer will send Avatar data to a client about avatars that they've ignored + bool getsIgnoredByMe = getsOutOfView; + + // When this is true, the AvatarMixer will send Avatar data to a client about avatars that have ignored them + bool getsAnyIgnored = getsIgnoredByMe && node->getCanKick(); + + // setup a PacketList for the avatarPackets + auto avatarPacketList = NLPacketList::create(PacketType::BulkAvatarData); + + // Define the minimum bubble size + static const glm::vec3 minBubbleSize = glm::vec3(0.3f, 1.3f, 0.3f); + // Define the scale of the box for the current node + glm::vec3 nodeBoxScale = (nodeData->getPosition() - nodeData->getGlobalBoundingBoxCorner()) * 2.0f; + // Set up the bounding box for the current node + AABox nodeBox(nodeData->getGlobalBoundingBoxCorner(), nodeBoxScale); + // Clamp the size of the bounding box to a minimum scale + if (glm::any(glm::lessThan(nodeBoxScale, minBubbleSize))) { + nodeBox.setScaleStayCentered(minBubbleSize); + } + // Quadruple the scale of both bounding boxes + nodeBox.embiggen(4.0f); + + + // setup list of AvatarData as well as maps to map betweeen the AvatarData and the original nodes + // for calling the AvatarData::sortAvatars() function and getting our sorted list of client nodes + QList avatarList; + std::unordered_map avatarDataToNodes; + + int listItem = 0; + std::for_each(_begin, _end, [&](const SharedNodePointer& otherNode) { + const AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); + + // theoretically it's possible for a Node to be in the NodeList (and therefore end up here), + // but not have yet sent data that's linked to the node. Check for that case and don't + // consider those nodes. + if (otherNodeData) { + listItem++; + AvatarSharedPointer otherAvatar = otherNodeData->getAvatarSharedPointer(); + avatarList << otherAvatar; + avatarDataToNodes[otherAvatar] = otherNode; + } + }); + + AvatarSharedPointer thisAvatar = nodeData->getAvatarSharedPointer(); + ViewFrustum cameraView = nodeData->getViewFrustom(); + std::priority_queue sortedAvatars = AvatarData::sortAvatars( + avatarList, cameraView, + + [&](AvatarSharedPointer avatar)->uint64_t{ + auto avatarNode = avatarDataToNodes[avatar]; + assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map + return nodeData->getLastBroadcastTime(avatarNode->getUUID()); + }, + + [&](AvatarSharedPointer avatar)->float{ + glm::vec3 nodeBoxHalfScale = (avatar->getPosition() - avatar->getGlobalBoundingBoxCorner()); + return glm::max(nodeBoxHalfScale.x, glm::max(nodeBoxHalfScale.y, nodeBoxHalfScale.z)); + }, + + [&](AvatarSharedPointer avatar)->bool{ + if (avatar == thisAvatar) { + return true; // ignore ourselves... + } + + bool shouldIgnore = false; + + // We will also ignore other nodes for a couple of different reasons: + // 1) ignore bubbles and ignore specific node + // 2) the node hasn't really updated it's frame data recently, this can + // happen if for example the avatar is connected on a desktop and sending + // updates at ~30hz. So every 3 frames we skip a frame. + auto avatarNode = avatarDataToNodes[avatar]; + + assert(avatarNode); // we can't have gotten here without the avatarData being a valid key in the map + + const AvatarMixerClientData* avatarNodeData = reinterpret_cast(avatarNode->getLinkedData()); + assert(avatarNodeData); // we can't have gotten here without avatarNode having valid data + quint64 startIgnoreCalculation = usecTimestampNow(); + + // make sure we have data for this avatar, that it isn't the same node, + // and isn't an avatar that the viewing node has ignored + // or that has ignored the viewing node + if (!avatarNode->getLinkedData() + || avatarNode->getUUID() == node->getUUID() + || (node->isIgnoringNodeWithID(avatarNode->getUUID()) && !getsIgnoredByMe) + || (avatarNode->isIgnoringNodeWithID(node->getUUID()) && !getsAnyIgnored)) { + shouldIgnore = true; + } else { + + // Check to see if the space bubble is enabled + if (node->isIgnoreRadiusEnabled() || avatarNode->isIgnoreRadiusEnabled()) { + + // Define the scale of the box for the current other node + glm::vec3 otherNodeBoxScale = (avatarNodeData->getPosition() - avatarNodeData->getGlobalBoundingBoxCorner()) * 2.0f; + // Set up the bounding box for the current other node + AABox otherNodeBox(avatarNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale); + // Clamp the size of the bounding box to a minimum scale + if (glm::any(glm::lessThan(otherNodeBoxScale, minBubbleSize))) { + otherNodeBox.setScaleStayCentered(minBubbleSize); + } + // Quadruple the scale of both bounding boxes + otherNodeBox.embiggen(4.0f); + + // Perform the collision check between the two bounding boxes + if (nodeBox.touches(otherNodeBox)) { + nodeData->ignoreOther(node, avatarNode); + shouldIgnore = !getsAnyIgnored; + } + } + // Not close enough to ignore + if (!shouldIgnore) { + nodeData->removeFromRadiusIgnoringSet(node, avatarNode->getUUID()); + } + } + quint64 endIgnoreCalculation = usecTimestampNow(); + _stats.ignoreCalculationElapsedTime += (endIgnoreCalculation - startIgnoreCalculation); + + if (!shouldIgnore) { + AvatarDataSequenceNumber lastSeqToReceiver = nodeData->getLastBroadcastSequenceNumber(avatarNode->getUUID()); + AvatarDataSequenceNumber lastSeqFromSender = avatarNodeData->getLastReceivedSequenceNumber(); + + // FIXME - This code does appear to be working. But it seems brittle. + // It supports determining if the frame of data for this "other" + // avatar has already been sent to the reciever. This has been + // verified to work on a desktop display that renders at 60hz and + // therefore sends to mixer at 30hz. Each second you'd expect to + // have 15 (45hz-30hz) duplicate frames. In this case, the stat + // avg_other_av_skips_per_second does report 15. + // + // make sure we haven't already sent this data from this sender to this receiver + // or that somehow we haven't sent + if (lastSeqToReceiver == lastSeqFromSender && lastSeqToReceiver != 0) { + ++numAvatarsHeldBack; + shouldIgnore = true; + } else if (lastSeqFromSender - lastSeqToReceiver > 1) { + // this is a skip - we still send the packet but capture the presence of the skip so we see it happening + ++numAvatarsWithSkippedFrames; + } + } + return shouldIgnore; + }); + + // loop through our sorted avatars and allocate our bandwidth to them accordingly + int avatarRank = 0; + + // this is overly conservative, because it includes some avatars we might not consider + int remainingAvatars = (int)sortedAvatars.size(); + + while (!sortedAvatars.empty()) { + AvatarPriority sortData = sortedAvatars.top(); + sortedAvatars.pop(); + const auto& avatarData = sortData.avatar; + avatarRank++; + remainingAvatars--; + + auto otherNode = avatarDataToNodes[avatarData]; + assert(otherNode); // we can't have gotten here without the avatarData being a valid key in the map + + // NOTE: Here's where we determine if we are over budget and drop to bare minimum data + int minimRemainingAvatarBytes = minimumBytesPerAvatar * remainingAvatars; + bool overBudget = (identityBytesSent + numAvatarDataBytes + minimRemainingAvatarBytes) > maxAvatarBytesPerFrame; + + quint64 startAvatarDataPacking = usecTimestampNow(); + + ++numOtherAvatars; + + const AvatarMixerClientData* otherNodeData = reinterpret_cast(otherNode->getLinkedData()); + + // make sure we send out identity packets to and from new arrivals. + bool forceSend = !nodeData->checkAndSetHasReceivedFirstPacketsFrom(otherNode->getUUID()); + + // FIXME - this clause seems suspicious "... || otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp ..." + if (!overBudget + && otherNodeData->getIdentityChangeTimestamp().time_since_epoch().count() > 0 + && (forceSend + || otherNodeData->getIdentityChangeTimestamp() > _lastFrameTimestamp + || distribution(generator) < IDENTITY_SEND_PROBABILITY)) { + + identityBytesSent += sendIdentityPacket(otherNodeData, node); + } + + const AvatarData* otherAvatar = otherNodeData->getConstAvatarData(); + glm::vec3 otherPosition = otherAvatar->getClientGlobalPosition(); + + // determine if avatar is in view, to determine how much data to include... + glm::vec3 otherNodeBoxScale = (otherPosition - otherNodeData->getGlobalBoundingBoxCorner()) * 2.0f; + AABox otherNodeBox(otherNodeData->getGlobalBoundingBoxCorner(), otherNodeBoxScale); + bool isInView = nodeData->otherAvatarInView(otherNodeBox); + + // start a new segment in the PacketList for this avatar + avatarPacketList->startSegment(); + + AvatarData::AvatarDataDetail detail; + + if (overBudget) { + overBudgetAvatars++; + _stats.overBudgetAvatars++; + detail = AvatarData::NoData; + } else if (!isInView && !getsOutOfView) { + detail = AvatarData::NoData; + nodeData->incrementAvatarOutOfView(); + } else { + detail = distribution(generator) < AVATAR_SEND_FULL_UPDATE_RATIO + ? AvatarData::SendAllData : AvatarData::CullSmallData; + nodeData->incrementAvatarInView(); + } + + bool includeThisAvatar = true; + auto lastEncodeForOther = nodeData->getLastOtherAvatarEncodeTime(otherNode->getUUID()); + QVector& lastSentJointsForOther = nodeData->getLastOtherAvatarSentJoints(otherNode->getUUID()); + bool distanceAdjust = true; + glm::vec3 viewerPosition = myPosition; + AvatarDataPacket::HasFlags hasFlagsOut; // the result of the toByteArray + bool dropFaceTracking = false; + + quint64 start = usecTimestampNow(); + QByteArray bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, + hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther); + quint64 end = usecTimestampNow(); + _stats.toByteArrayElapsedTime += (end - start); + + static const int MAX_ALLOWED_AVATAR_DATA = (1400 - NUM_BYTES_RFC4122_UUID); + if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) { + qCWarning(avatars) << "otherAvatar.toByteArray() resulted in very large buffer:" << bytes.size() << "... attempt to drop facial data"; + + dropFaceTracking = true; // first try dropping the facial data + bytes = otherAvatar->toByteArray(detail, lastEncodeForOther, lastSentJointsForOther, + hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther); + + if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) { + qCWarning(avatars) << "otherAvatar.toByteArray() without facial data resulted in very large buffer:" << bytes.size() << "... reduce to MinimumData"; + bytes = otherAvatar->toByteArray(AvatarData::MinimumData, lastEncodeForOther, lastSentJointsForOther, + hasFlagsOut, dropFaceTracking, distanceAdjust, viewerPosition, &lastSentJointsForOther); + } + + if (bytes.size() > MAX_ALLOWED_AVATAR_DATA) { + qCWarning(avatars) << "otherAvatar.toByteArray() MinimumData resulted in very large buffer:" << bytes.size() << "... FAIL!!"; + includeThisAvatar = false; + } + } + + if (includeThisAvatar) { + numAvatarDataBytes += avatarPacketList->write(otherNode->getUUID().toRfc4122()); + numAvatarDataBytes += avatarPacketList->write(bytes); + + if (detail != AvatarData::NoData) { + _stats.numOthersIncluded++; + + // increment the number of avatars sent to this reciever + nodeData->incrementNumAvatarsSentLastFrame(); + + // set the last sent sequence number for this sender on the receiver + nodeData->setLastBroadcastSequenceNumber(otherNode->getUUID(), + otherNodeData->getLastReceivedSequenceNumber()); + + // remember the last time we sent details about this other node to the receiver + nodeData->setLastBroadcastTime(otherNode->getUUID(), start); + } + } + + avatarPacketList->endSegment(); + + quint64 endAvatarDataPacking = usecTimestampNow(); + _stats.avatarDataPackingElapsedTime += (endAvatarDataPacking - startAvatarDataPacking); + }; + + quint64 startPacketSending = usecTimestampNow(); + + // close the current packet so that we're always sending something + avatarPacketList->closeCurrentPacket(true); + + _stats.numPacketsSent += (int)avatarPacketList->getNumPackets(); + _stats.numBytesSent += numAvatarDataBytes; + + // send the avatar data PacketList + nodeList->sendPacketList(std::move(avatarPacketList), *node); + + // record the bytes sent for other avatar data in the AvatarMixerClientData + nodeData->recordSentAvatarData(numAvatarDataBytes); + + // record the number of avatars held back this frame + nodeData->recordNumOtherAvatarStarves(numAvatarsHeldBack); + nodeData->recordNumOtherAvatarSkips(numAvatarsWithSkippedFrames); + + quint64 endPacketSending = usecTimestampNow(); + _stats.packetSendingElapsedTime += (endPacketSending - startPacketSending); + } + + quint64 end = usecTimestampNow(); + _stats.jobElapsedTime += (end - start); +} + diff --git a/assignment-client/src/avatars/AvatarMixerSlave.h b/assignment-client/src/avatars/AvatarMixerSlave.h new file mode 100644 index 0000000000..04141d9d72 --- /dev/null +++ b/assignment-client/src/avatars/AvatarMixerSlave.h @@ -0,0 +1,107 @@ +// +// AvatarMixerSlave.h +// assignment-client/src/avatar +// +// Created by Brad Hefta-Gaub on 2/14/2017. +// Copyright 2017 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_AvatarMixerSlave_h +#define hifi_AvatarMixerSlave_h + +class AvatarMixerClientData; + +class AvatarMixerSlaveStats { +public: + int nodesProcessed { 0 }; + int packetsProcessed { 0 }; + quint64 processIncomingPacketsElapsedTime { 0 }; + + int nodesBroadcastedTo { 0 }; + int numPacketsSent { 0 }; + int numBytesSent { 0 }; + int numIdentityPackets { 0 }; + int numOthersIncluded { 0 }; + int overBudgetAvatars { 0 }; + + quint64 ignoreCalculationElapsedTime { 0 }; + quint64 avatarDataPackingElapsedTime { 0 }; + quint64 packetSendingElapsedTime { 0 }; + quint64 toByteArrayElapsedTime { 0 }; + quint64 jobElapsedTime { 0 }; + + void reset() { + // receiving job stats + nodesProcessed = 0; + packetsProcessed = 0; + processIncomingPacketsElapsedTime = 0; + + // sending job stats + nodesBroadcastedTo = 0; + numPacketsSent = 0; + numBytesSent = 0; + numIdentityPackets = 0; + numOthersIncluded = 0; + overBudgetAvatars = 0; + + ignoreCalculationElapsedTime = 0; + avatarDataPackingElapsedTime = 0; + packetSendingElapsedTime = 0; + toByteArrayElapsedTime = 0; + jobElapsedTime = 0; + } + + AvatarMixerSlaveStats& operator+=(const AvatarMixerSlaveStats& rhs) { + nodesProcessed += rhs.nodesProcessed; + packetsProcessed += rhs.packetsProcessed; + processIncomingPacketsElapsedTime += rhs.processIncomingPacketsElapsedTime; + + nodesBroadcastedTo += rhs.nodesBroadcastedTo; + numPacketsSent += rhs.numPacketsSent; + numBytesSent += rhs.numBytesSent; + numIdentityPackets += rhs.numIdentityPackets; + numOthersIncluded += rhs.numOthersIncluded; + overBudgetAvatars += rhs.overBudgetAvatars; + + ignoreCalculationElapsedTime += rhs.ignoreCalculationElapsedTime; + avatarDataPackingElapsedTime += rhs.avatarDataPackingElapsedTime; + packetSendingElapsedTime += rhs.packetSendingElapsedTime; + toByteArrayElapsedTime += rhs.toByteArrayElapsedTime; + jobElapsedTime += rhs.jobElapsedTime; + return *this; + } + +}; + +class AvatarMixerSlave { +public: + using ConstIter = NodeList::const_iterator; + + void configure(ConstIter begin, ConstIter end); + void configureBroadcast(ConstIter begin, ConstIter end, + p_high_resolution_clock::time_point lastFrameTimestamp, + float maxKbpsPerNode, float throttlingRatio); + + void processIncomingPackets(const SharedNodePointer& node); + void broadcastAvatarData(const SharedNodePointer& node); + + void harvestStats(AvatarMixerSlaveStats& stats); + +private: + int sendIdentityPacket(const AvatarMixerClientData* nodeData, const SharedNodePointer& destinationNode); + + // frame state + ConstIter _begin; + ConstIter _end; + + p_high_resolution_clock::time_point _lastFrameTimestamp; + float _maxKbpsPerNode { 0.0f }; + float _throttlingRatio { 0.0f }; + + AvatarMixerSlaveStats _stats; +}; + +#endif // hifi_AvatarMixerSlave_h diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.cpp b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp new file mode 100644 index 0000000000..07d4fa8851 --- /dev/null +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.cpp @@ -0,0 +1,208 @@ +// +// AvatarMixerSlavePool.cpp +// assignment-client/src/avatar +// +// Created by Brad Hefta-Gaub on 2/14/2017. +// Copyright 2017 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 +#include + +#include "AvatarMixerSlavePool.h" + +void AvatarMixerSlaveThread::run() { + while (true) { + wait(); + + // iterate over all available nodes + SharedNodePointer node; + while (try_pop(node)) { + (this->*_function)(node); + } + + bool stopping = _stop; + notify(stopping); + if (stopping) { + return; + } + } +} + +void AvatarMixerSlaveThread::wait() { + { + Lock lock(_pool._mutex); + _pool._slaveCondition.wait(lock, [&] { + assert(_pool._numStarted <= _pool._numThreads); + return _pool._numStarted != _pool._numThreads; + }); + ++_pool._numStarted; + } + if (_pool._configure) { + _pool._configure(*this); + } + _function = _pool._function; +} + +void AvatarMixerSlaveThread::notify(bool stopping) { + { + Lock lock(_pool._mutex); + assert(_pool._numFinished < _pool._numThreads); + ++_pool._numFinished; + if (stopping) { + ++_pool._numStopped; + } + } + _pool._poolCondition.notify_one(); +} + +bool AvatarMixerSlaveThread::try_pop(SharedNodePointer& node) { + return _pool._queue.try_pop(node); +} + +#ifdef AVATAR_SINGLE_THREADED +static AvatarMixerSlave slave; +#endif + +void AvatarMixerSlavePool::processIncomingPackets(ConstIter begin, ConstIter end) { + _function = &AvatarMixerSlave::processIncomingPackets; + _configure = [&](AvatarMixerSlave& slave) { + slave.configure(begin, end); + }; + run(begin, end); +} + +void AvatarMixerSlavePool::broadcastAvatarData(ConstIter begin, ConstIter end, + p_high_resolution_clock::time_point lastFrameTimestamp, + float maxKbpsPerNode, float throttlingRatio) { + _function = &AvatarMixerSlave::broadcastAvatarData; + _configure = [&](AvatarMixerSlave& slave) { + slave.configureBroadcast(begin, end, lastFrameTimestamp, maxKbpsPerNode, throttlingRatio); + }; + run(begin, end); +} + +void AvatarMixerSlavePool::run(ConstIter begin, ConstIter end) { + _begin = begin; + _end = end; + +#ifdef AUDIO_SINGLE_THREADED + _configure(slave); + std::for_each(begin, end, [&](const SharedNodePointer& node) { + _function(slave, node); +}); +#else + // fill the queue + std::for_each(_begin, _end, [&](const SharedNodePointer& node) { + _queue.emplace(node); + }); + + { + Lock lock(_mutex); + + // run + _numStarted = _numFinished = 0; + _slaveCondition.notify_all(); + + // wait + _poolCondition.wait(lock, [&] { + assert(_numFinished <= _numThreads); + return _numFinished == _numThreads; + }); + + assert(_numStarted == _numThreads); + } + + assert(_queue.empty()); +#endif +} + + +void AvatarMixerSlavePool::each(std::function functor) { +#ifdef AVATAR_SINGLE_THREADED + functor(slave); +#else + for (auto& slave : _slaves) { + functor(*slave.get()); + } +#endif +} + +void AvatarMixerSlavePool::setNumThreads(int numThreads) { + // clamp to allowed size + { + int maxThreads = QThread::idealThreadCount(); + if (maxThreads == -1) { + // idealThreadCount returns -1 if cores cannot be detected + static const int MAX_THREADS_IF_UNKNOWN = 4; + maxThreads = MAX_THREADS_IF_UNKNOWN; + } + + int clampedThreads = std::min(std::max(1, numThreads), maxThreads); + if (clampedThreads != numThreads) { + qWarning("%s: clamped to %d (was %d)", __FUNCTION__, clampedThreads, numThreads); + numThreads = clampedThreads; + } + } + + resize(numThreads); +} + +void AvatarMixerSlavePool::resize(int numThreads) { + assert(_numThreads == (int)_slaves.size()); + +#ifdef AVATAR_SINGLE_THREADED + qDebug("%s: running single threaded", __FUNCTION__, numThreads); +#else + qDebug("%s: set %d threads (was %d)", __FUNCTION__, numThreads, _numThreads); + + Lock lock(_mutex); + + if (numThreads > _numThreads) { + // start new slaves + for (int i = 0; i < numThreads - _numThreads; ++i) { + auto slave = new AvatarMixerSlaveThread(*this); + slave->start(); + _slaves.emplace_back(slave); + } + } else if (numThreads < _numThreads) { + auto extraBegin = _slaves.begin() + numThreads; + + // mark slaves to stop... + auto slave = extraBegin; + while (slave != _slaves.end()) { + (*slave)->_stop = true; + ++slave; + } + + // ...cycle them until they do stop... + _numStopped = 0; + while (_numStopped != (_numThreads - numThreads)) { + _numStarted = _numFinished = _numStopped; + _slaveCondition.notify_all(); + _poolCondition.wait(lock, [&] { + assert(_numFinished <= _numThreads); + return _numFinished == _numThreads; + }); + } + + // ...wait for threads to finish... + slave = extraBegin; + while (slave != _slaves.end()) { + QThread* thread = reinterpret_cast(slave->get()); + static const int MAX_THREAD_WAIT_TIME = 10; + thread->wait(MAX_THREAD_WAIT_TIME); + ++slave; + } + + // ...and erase them + _slaves.erase(extraBegin, _slaves.end()); + } + + _numThreads = _numStarted = _numFinished = numThreads; + assert(_numThreads == (int)_slaves.size()); +#endif +} diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.h b/assignment-client/src/avatars/AvatarMixerSlavePool.h new file mode 100644 index 0000000000..6bef0515bb --- /dev/null +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.h @@ -0,0 +1,104 @@ +// +// AvatarMixerSlavePool.h +// assignment-client/src/avatar +// +// Created by Brad Hefta-Gaub on 2/14/2017. +// Copyright 2017 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_AvatarMixerSlavePool_h +#define hifi_AvatarMixerSlavePool_h + +#include +#include +#include + +#include + +#include + +#include + +#include "AvatarMixerSlave.h" + +class AvatarMixerSlavePool; + +class AvatarMixerSlaveThread : public QThread, public AvatarMixerSlave { + Q_OBJECT + using ConstIter = NodeList::const_iterator; + using Mutex = std::mutex; + using Lock = std::unique_lock; + +public: + AvatarMixerSlaveThread(AvatarMixerSlavePool& pool) : _pool(pool) {} + + void run() override final; + +private: + friend class AvatarMixerSlavePool; + + void wait(); + void notify(bool stopping); + bool try_pop(SharedNodePointer& node); + + AvatarMixerSlavePool& _pool; + void (AvatarMixerSlave::*_function)(const SharedNodePointer& node) { nullptr }; + bool _stop { false }; +}; + +// Slave pool for audio mixers +// AvatarMixerSlavePool is not thread-safe! It should be instantiated and used from a single thread. +class AvatarMixerSlavePool { + using Queue = tbb::concurrent_queue; + using Mutex = std::mutex; + using Lock = std::unique_lock; + using ConditionVariable = std::condition_variable; + +public: + using ConstIter = NodeList::const_iterator; + + AvatarMixerSlavePool(int numThreads = QThread::idealThreadCount()) { setNumThreads(numThreads); } + ~AvatarMixerSlavePool() { resize(0); } + + // Jobs the slave pool can do... + void processIncomingPackets(ConstIter begin, ConstIter end); + void broadcastAvatarData(ConstIter begin, ConstIter end, + p_high_resolution_clock::time_point lastFrameTimestamp, float maxKbpsPerNode, float throttlingRatio); + + // iterate over all slaves + void each(std::function functor); + + void setNumThreads(int numThreads); + int numThreads() { return _numThreads; } + +private: + void run(ConstIter begin, ConstIter end); + void resize(int numThreads); + + std::vector> _slaves; + + friend void AvatarMixerSlaveThread::wait(); + friend void AvatarMixerSlaveThread::notify(bool stopping); + friend bool AvatarMixerSlaveThread::try_pop(SharedNodePointer& node); + + // synchronization state + Mutex _mutex; + ConditionVariable _slaveCondition; + ConditionVariable _poolCondition; + void (AvatarMixerSlave::*_function)(const SharedNodePointer& node); + std::function _configure; + int _numThreads { 0 }; + int _numStarted { 0 }; // guarded by _mutex + int _numFinished { 0 }; // guarded by _mutex + int _numStopped { 0 }; // guarded by _mutex + + // frame state + Queue _queue; + ConstIter _begin; + ConstIter _end; +}; + +#endif // hifi_AvatarMixerSlavePool_h diff --git a/assignment-client/src/avatars/ScriptableAvatar.cpp b/assignment-client/src/avatars/ScriptableAvatar.cpp index 95bcbb587e..516bf7a1e3 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.cpp +++ b/assignment-client/src/avatars/ScriptableAvatar.cpp @@ -14,10 +14,9 @@ #include #include "ScriptableAvatar.h" -QByteArray ScriptableAvatar::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, - bool distanceAdjust, glm::vec3 viewerPosition, QVector* sentJointDataOut) { +QByteArray ScriptableAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) { _globalPosition = getPosition(); - return AvatarData::toByteArray(dataDetail, lastSentTime, lastSentJointData, distanceAdjust, viewerPosition, sentJointDataOut); + return AvatarData::toByteArrayStateful(dataDetail); } diff --git a/assignment-client/src/avatars/ScriptableAvatar.h b/assignment-client/src/avatars/ScriptableAvatar.h index be7a90adf9..1028912e55 100644 --- a/assignment-client/src/avatars/ScriptableAvatar.h +++ b/assignment-client/src/avatars/ScriptableAvatar.h @@ -28,8 +28,7 @@ public: Q_INVOKABLE AnimationDetails getAnimationDetails(); virtual void setSkeletonModelURL(const QUrl& skeletonModelURL) override; - virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, - bool distanceAdjust = false, glm::vec3 viewerPosition = glm::vec3(0), QVector* sentJointDataOut = nullptr) override; + virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail) override; private slots: diff --git a/assignment-client/src/scripts/EntityScriptServer.cpp b/assignment-client/src/scripts/EntityScriptServer.cpp index f8f728f834..5f99dd68bc 100644 --- a/assignment-client/src/scripts/EntityScriptServer.cpp +++ b/assignment-client/src/scripts/EntityScriptServer.cpp @@ -324,16 +324,8 @@ void EntityScriptServer::nodeActivated(SharedNodePointer activatedNode) { void EntityScriptServer::nodeKilled(SharedNodePointer killedNode) { switch (killedNode->getType()) { case NodeType::EntityServer: { - if (!_shuttingDown) { - if (_entitiesScriptEngine) { - _entitiesScriptEngine->unloadAllEntityScripts(); - _entitiesScriptEngine->stop(); - } - - resetEntitiesScriptEngine(); - - _entityViewer.clear(); - } + clear(); + break; } case NodeType::Agent: { @@ -440,12 +432,12 @@ void EntityScriptServer::clear() { _entitiesScriptEngine->stop(); } + _entityViewer.clear(); + // reset the engine if (!_shuttingDown) { resetEntitiesScriptEngine(); } - - _entityViewer.clear(); } void EntityScriptServer::shutdownScriptEngine() { diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 045af9dc09..12dcb90f47 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1299,6 +1299,22 @@ "placeholder": 5.0, "default": 5.0, "advanced": true + }, + { + "name": "auto_threads", + "label": "Automatically determine thread count", + "type": "checkbox", + "help": "Allow system to determine number of threads (recommended)", + "default": false, + "advanced": true + }, + { + "name": "num_threads", + "label": "Number of Threads", + "help": "Threads to spin up for avatar mixing (if not automatically set)", + "placeholder": "1", + "default": "1", + "advanced": true } ] } diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index c5f055bed0..7642a66867 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -996,6 +996,10 @@ function saveSettings() { if (password && password.length > 0) { formJSON["security"]["http_password"] = sha256_digest(password); } + var verify_password = formJSON["security"]["verify_http_password"]; + if (verify_password && verify_password.length > 0) { + formJSON["security"]["verify_http_password"] = sha256_digest(verify_password); + } } // verify that the password and confirmation match before saving @@ -1010,7 +1014,6 @@ function saveSettings() { bootbox.alert({"message": "Passwords must match!", "title":"Password Error"}); canPost = false; } else { - formJSON["security"]["http_password"] = sha256_digest(password); delete formJSON["security"]["verify_http_password"]; } } diff --git a/interface/resources/avatar/animations/touch_point_closed_left.fbx b/interface/resources/avatar/animations/touch_point_closed_left.fbx new file mode 100644 index 0000000000..8835de4f7f Binary files /dev/null and b/interface/resources/avatar/animations/touch_point_closed_left.fbx differ diff --git a/interface/resources/avatar/animations/touch_point_closed_right.fbx b/interface/resources/avatar/animations/touch_point_closed_right.fbx new file mode 100644 index 0000000000..c4efe4ff90 Binary files /dev/null and b/interface/resources/avatar/animations/touch_point_closed_right.fbx differ diff --git a/interface/resources/avatar/animations/touch_point_open_left.fbx b/interface/resources/avatar/animations/touch_point_open_left.fbx new file mode 100644 index 0000000000..cbab6cd55d Binary files /dev/null and b/interface/resources/avatar/animations/touch_point_open_left.fbx differ diff --git a/interface/resources/avatar/animations/touch_point_open_right.fbx b/interface/resources/avatar/animations/touch_point_open_right.fbx new file mode 100644 index 0000000000..b54abe2b2e Binary files /dev/null and b/interface/resources/avatar/animations/touch_point_open_right.fbx differ diff --git a/interface/resources/avatar/animations/touch_thumb_closed_left.fbx b/interface/resources/avatar/animations/touch_thumb_closed_left.fbx new file mode 100644 index 0000000000..5a731144ba Binary files /dev/null and b/interface/resources/avatar/animations/touch_thumb_closed_left.fbx differ diff --git a/interface/resources/avatar/animations/touch_thumb_closed_right.fbx b/interface/resources/avatar/animations/touch_thumb_closed_right.fbx new file mode 100644 index 0000000000..d947cdb1fd Binary files /dev/null and b/interface/resources/avatar/animations/touch_thumb_closed_right.fbx differ diff --git a/interface/resources/avatar/animations/touch_thumb_open_left.fbx b/interface/resources/avatar/animations/touch_thumb_open_left.fbx new file mode 100644 index 0000000000..b50cbd8541 Binary files /dev/null and b/interface/resources/avatar/animations/touch_thumb_open_left.fbx differ diff --git a/interface/resources/avatar/animations/touch_thumb_open_right.fbx b/interface/resources/avatar/animations/touch_thumb_open_right.fbx new file mode 100644 index 0000000000..ca861d0250 Binary files /dev/null and b/interface/resources/avatar/animations/touch_thumb_open_right.fbx differ diff --git a/interface/resources/avatar/animations/touch_thumb_point_closed_left.fbx b/interface/resources/avatar/animations/touch_thumb_point_closed_left.fbx new file mode 100644 index 0000000000..bf8023b54a Binary files /dev/null and b/interface/resources/avatar/animations/touch_thumb_point_closed_left.fbx differ diff --git a/interface/resources/avatar/animations/touch_thumb_point_closed_right.fbx b/interface/resources/avatar/animations/touch_thumb_point_closed_right.fbx new file mode 100644 index 0000000000..46c0cd48c4 Binary files /dev/null and b/interface/resources/avatar/animations/touch_thumb_point_closed_right.fbx differ diff --git a/interface/resources/avatar/animations/touch_thumb_point_open_left.fbx b/interface/resources/avatar/animations/touch_thumb_point_open_left.fbx new file mode 100644 index 0000000000..23c36f2172 Binary files /dev/null and b/interface/resources/avatar/animations/touch_thumb_point_open_left.fbx differ diff --git a/interface/resources/avatar/animations/touch_thumb_point_open_right.fbx b/interface/resources/avatar/animations/touch_thumb_point_open_right.fbx new file mode 100644 index 0000000000..5016832aab Binary files /dev/null and b/interface/resources/avatar/animations/touch_thumb_point_open_right.fbx differ diff --git a/interface/resources/avatar/avatar-animation.json b/interface/resources/avatar/avatar-animation.json index 834a3fc277..975f01855d 100644 --- a/interface/resources/avatar/avatar-animation.json +++ b/interface/resources/avatar/avatar-animation.json @@ -128,7 +128,41 @@ "id": "rightHandGrasp", "interpTarget": 3, "interpDuration": 3, - "transitions": [] + "transitions": [ + { "var": "isRightIndexPoint", "state": "rightIndexPoint" }, + { "var": "isRightThumbRaise", "state": "rightThumbRaise" }, + { "var": "isRightIndexPointAndThumbRaise", "state": "rightIndexPointAndThumbRaise" } + ] + }, + { + "id": "rightIndexPoint", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandGrasp", "state": "rightHandGrasp" }, + { "var": "isRightThumbRaise", "state": "rightThumbRaise" }, + { "var": "isRightIndexPointAndThumbRaise", "state": "rightIndexPointAndThumbRaise" } + ] + }, + { + "id": "rightThumbRaise", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandGrasp", "state": "rightHandGrasp" }, + { "var": "isRightIndexPoint", "state": "rightIndexPoint" }, + { "var": "isRightIndexPointAndThumbRaise", "state": "rightIndexPointAndThumbRaise" } + ] + }, + { + "id": "rightIndexPointAndThumbRaise", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { "var": "isRightHandGrasp", "state": "rightHandGrasp" }, + { "var": "isRightIndexPoint", "state": "rightIndexPoint" }, + { "var": "isRightThumbRaise", "state": "rightThumbRaise" } + ] } ] }, @@ -166,6 +200,108 @@ "children": [] } ] + }, + { + "id": "rightIndexPoint", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "rightHandGraspAlpha" + }, + "children": [ + { + "id": "rightIndexPointOpen", + "type": "clip", + "data": { + "url": "animations/touch_point_open_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightIndexPointClosed", + "type": "clip", + "data": { + "url": "animations/touch_point_closed_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "rightThumbRaise", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "rightHandGraspAlpha" + }, + "children": [ + { + "id": "rightThumbRaiseOpen", + "type": "clip", + "data": { + "url": "animations/touch_thumb_open_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightThumbRaiseClosed", + "type": "clip", + "data": { + "url": "animations/touch_thumb_closed_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "rightIndexPointAndThumbRaise", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "rightHandGraspAlpha" + }, + "children": [ + { + "id": "rightIndexPointAndThumbRaiseOpen", + "type": "clip", + "data": { + "url": "animations/touch_thumb_point_open_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "rightIndexPointAndThumbRaiseClosed", + "type": "clip", + "data": { + "url": "animations/touch_thumb_point_closed_right.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] } ] }, @@ -175,7 +311,7 @@ "data": { "alpha": 0.0, "boneSet": "leftHand", - "alphaVar" : "leftHandOverlayAlpha" + "alphaVar": "leftHandOverlayAlpha" }, "children": [ { @@ -188,7 +324,41 @@ "id": "leftHandGrasp", "interpTarget": 3, "interpDuration": 3, - "transitions": [] + "transitions": [ + { "var": "isLeftIndexPoint", "state": "leftIndexPoint" }, + { "var": "isLeftThumbRaise", "state": "leftThumbRaise" }, + { "var": "isLeftIndexPointAndThumbRaise", "state": "leftIndexPointAndThumbRaise" } + ] + }, + { + "id": "leftIndexPoint", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { "var": "isLeftHandGrasp", "state": "leftHandGrasp" }, + { "var": "isLeftThumbRaise", "state": "leftThumbRaise" }, + { "var": "isLeftIndexPointAndThumbRaise", "state": "leftIndexPointAndThumbRaise" } + ] + }, + { + "id": "leftThumbRaise", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { "var": "isLeftHandGrasp", "state": "leftHandGrasp" }, + { "var": "isLeftIndexPoint", "state": "leftIndexPoint" }, + { "var": "isLeftIndexPointAndThumbRaise", "state": "leftIndexPointAndThumbRaise" } + ] + }, + { + "id": "leftIndexPointAndThumbRaise", + "interpTarget": 15, + "interpDuration": 3, + "transitions": [ + { "var": "isLeftHandGrasp", "state": "leftHandGrasp" }, + { "var": "isLeftIndexPoint", "state": "leftIndexPoint" }, + { "var": "isLeftThumbRaise", "state": "leftThumbRaise" } + ] } ] }, @@ -226,6 +396,108 @@ "children": [] } ] + }, + { + "id": "leftIndexPoint", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGraspAlpha" + }, + "children": [ + { + "id": "leftIndexPointOpen", + "type": "clip", + "data": { + "url": "animations/touch_point_open_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftIndexPointClosed", + "type": "clip", + "data": { + "url": "animations/touch_point_closed_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "leftThumbRaise", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGraspAlpha" + }, + "children": [ + { + "id": "leftThumbRaiseOpen", + "type": "clip", + "data": { + "url": "animations/touch_thumb_open_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftThumbRaiseClosed", + "type": "clip", + "data": { + "url": "animations/touch_thumb_closed_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + }, + { + "id": "leftIndexPointAndThumbRaise", + "type": "blendLinear", + "data": { + "alpha": 0.0, + "alphaVar": "leftHandGraspAlpha" + }, + "children": [ + { + "id": "leftIndexPointAndThumbRaiseOpen", + "type": "clip", + "data": { + "url": "animations/touch_thumb_point_open_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "leftIndexPointAndThumbRaiseClosed", + "type": "clip", + "data": { + "url": "animations/touch_thumb_point_closed_left.fbx", + "startFrame": 15.0, + "endFrame": 15.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] } ] }, diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index faf37d5366..58d589b667 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -107,11 +107,11 @@ Item { } StatText { visible: root.expanded - text: "Fully Simulated Avatars: " + root.fullySimulatedAvatarCount + text: "Avatars Updated: " + root.updatedAvatarCount } StatText { visible: root.expanded - text: "Partially Simulated Avatars: " + root.partiallySimulatedAvatarCount + text: "Avatars NOT Updated: " + root.notUpdatedAvatarCount } } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index c1fea7c09b..cf5ea98b81 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -13,6 +13,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import Qt.labs.settings 1.0 import "../styles-uit" import "../controls-uit" as HifiControls @@ -29,7 +30,9 @@ Rectangle { property int myCardHeight: 90 property int rowHeight: 70 property int actionButtonWidth: 55 - property int nameCardWidth: palContainer.width - actionButtonWidth*(iAmAdmin ? 4 : 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth + property int actionButtonAllowance: actionButtonWidth * 2 + property int minNameCardWidth: palContainer.width - (actionButtonAllowance * 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth + property int nameCardWidth: minNameCardWidth + (iAmAdmin ? 0 : actionButtonAllowance) property var myData: ({displayName: "", userName: "", audioLevel: 0.0, admin: true}) // valid dummy until set property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring. property var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities. @@ -52,6 +55,16 @@ Rectangle { letterboxMessage.visible = true letterboxMessage.popupRadius = 0 } + Settings { + id: settings + category: "pal" + property bool filtered: false + property int nearDistance: 30 + } + function refreshWithFilter() { + // We should just be able to set settings.filtered to filter.checked, but see #3249, so send to .js for saving. + pal.sendToScript({method: 'refresh', params: {filter: filter.checked && {distance: settings.nearDistance}}}); + } // This is the container for the PAL Rectangle { @@ -88,11 +101,32 @@ Rectangle { audioLevel: myData.audioLevel isMyCard: true // Size - width: nameCardWidth + width: minNameCardWidth height: parent.height // Anchors anchors.left: parent.left } + Row { + HifiControls.CheckBox { + id: filter + checked: settings.filtered + text: "in view" + boxSize: reload.height * 0.70 + onCheckedChanged: refreshWithFilter() + } + HifiControls.GlyphButton { + id: reload + glyph: hifi.glyphs.reload + width: reload.height + onClicked: refreshWithFilter() + } + spacing: 50 + anchors { + right: parent.right + top: parent.top + topMargin: 10 + } + } } // Rectangles used to cover up rounded edges on bottom of MyInfo Rectangle Rectangle { @@ -306,45 +340,7 @@ Rectangle { } } } - // Refresh button - Rectangle { - // Size - width: hifi.dimensions.tableHeaderHeight-1 - height: hifi.dimensions.tableHeaderHeight-1 - // Anchors - anchors.left: table.left - anchors.leftMargin: 4 - anchors.top: table.top - // Style - color: hifi.colors.tableBackgroundLight - // Actual refresh icon - HiFiGlyphs { - id: reloadButton - text: hifi.glyphs.reloadSmall - // Size - size: parent.width*1.5 - // Anchors - anchors.fill: parent - // Style - horizontalAlignment: Text.AlignHCenter - color: hifi.colors.darkGray - } - MouseArea { - id: reloadButtonArea - // Anchors - anchors.fill: parent - hoverEnabled: true - // Everyone likes a responsive refresh button! - // So use onPressed instead of onClicked - onPressed: { - reloadButton.color = hifi.colors.lightGrayText - pal.sendToScript({method: 'refresh'}) - } - onReleased: reloadButton.color = (containsMouse ? hifi.colors.baseGrayHighlight : hifi.colors.darkGray) - onEntered: reloadButton.color = hifi.colors.baseGrayHighlight - onExited: reloadButton.color = (pressed ? hifi.colors.lightGrayText: hifi.colors.darkGray) - } - } + // Separator between user and admin functions Rectangle { // Size @@ -501,7 +497,7 @@ Rectangle { if (alreadyRefreshed === true) { letterbox('', '', 'The last editor of this object is either you or not among this list of users.'); } else { - pal.sendToScript({method: 'refresh', params: message.params}); + pal.sendToScript({method: 'refresh', params: {selected: message.params}}); } } else { // If we've already refreshed the PAL and found the avatar in the model diff --git a/interface/resources/qml/hifi/toolbars/Toolbar.qml b/interface/resources/qml/hifi/toolbars/Toolbar.qml index 0080e49815..c0d984e822 100644 --- a/interface/resources/qml/hifi/toolbars/Toolbar.qml +++ b/interface/resources/qml/hifi/toolbars/Toolbar.qml @@ -120,6 +120,8 @@ Window { function addButton(properties) { properties = properties || {} + unpinnedAlpha = 1; + // If a name is specified, then check if there's an existing button with that name // and return it if so. This will allow multiple clients to listen to a single button, // and allow scripts to be idempotent so they don't duplicate buttons if they're reloaded @@ -154,7 +156,7 @@ Window { updatePinned(); if (buttons.length === 0) { - visible = false; + unpinnedAlpha = 0; } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 3de7906f56..d48fe19a99 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -548,6 +548,7 @@ const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 100.0f; const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; +const bool DEFAULT_TABLET_VISIBLE_TO_OTHERS = false; Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runServer, QString runServerPathOption) : QApplication(argc, argv), @@ -570,6 +571,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _desktopTabletScale("desktopTabletScale", DEFAULT_DESKTOP_TABLET_SCALE_PERCENT), _desktopTabletBecomesToolbarSetting("desktopTabletBecomesToolbar", DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR), _hmdTabletBecomesToolbarSetting("hmdTabletBecomesToolbar", DEFAULT_HMD_TABLET_BECOMES_TOOLBAR), + _tabletVisibleToOthersSetting("tabletVisibleToOthers", DEFAULT_TABLET_VISIBLE_TO_OTHERS), _constrainToolbarPosition("toolbar/constrainToolbarToCenterX", true), _scaleMirror(1.0f), _rotateMirror(0.0f), @@ -781,6 +783,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle())); connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails())); + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this]() { + getOverlays().deleteOverlay(getTabletScreenID()); + getOverlays().deleteOverlay(getTabletHomeButtonID()); + getOverlays().deleteOverlay(getTabletFrameID()); + }); connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused); // We could clear ATP assets only when changing domains, but it's possible that the domain you are connected @@ -1216,6 +1223,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo if (entity && entity->wantsKeyboardFocus()) { setKeyboardFocusOverlay(UNKNOWN_OVERLAY_ID); setKeyboardFocusEntity(entityItemID); + } else { + setKeyboardFocusEntity(UNKNOWN_ENTITY_ID); } }); @@ -2348,6 +2357,11 @@ void Application::setHmdTabletBecomesToolbarSetting(bool value) { updateSystemTabletMode(); } +void Application::setTabletVisibleToOthersSetting(bool value) { + _tabletVisibleToOthersSetting.set(value); + updateSystemTabletMode(); +} + void Application::setSettingConstrainToolbarPosition(bool setting) { _constrainToolbarPosition.set(setting); DependencyManager::get()->setConstrainToolbarToCenterX(setting); @@ -3093,6 +3107,7 @@ void Application::mouseMoveEvent(QMouseEvent* event) { getOverlays().mouseMoveEvent(&mappedEvent); getEntities()->mouseMoveEvent(&mappedEvent); } + _controllerScriptingInterface->emitMouseMoveEvent(&mappedEvent); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it @@ -3125,7 +3140,6 @@ void Application::mousePressEvent(QMouseEvent* event) { if (!_aboutToQuit) { getOverlays().mousePressEvent(&mappedEvent); - if (!_controllerScriptingInterface->areEntityClicksCaptured()) { getEntities()->mousePressEvent(&mappedEvent); } @@ -3429,7 +3443,7 @@ void Application::idle(float nsecsElapsed) { #ifdef Q_OS_WIN static std::once_flag once; std::call_once(once, [] { - initCpuUsage(); + initCpuUsage(); }); vec3 kernelUserAndSystem; @@ -6902,5 +6916,10 @@ OverlayID Application::getTabletScreenID() const { OverlayID Application::getTabletHomeButtonID() const { auto HMD = DependencyManager::get(); - return HMD->getCurrentHomeButtonUUID(); + return HMD->getCurrentHomeButtonID(); +} + +QUuid Application::getTabletFrameID() const { + auto HMD = DependencyManager::get(); + return HMD->getCurrentTabletFrameID(); } diff --git a/interface/src/Application.h b/interface/src/Application.h index 662523ce1d..13c1458aee 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -218,6 +218,8 @@ public: void setDesktopTabletBecomesToolbarSetting(bool value); bool getHmdTabletBecomesToolbarSetting() { return _hmdTabletBecomesToolbarSetting.get(); } void setHmdTabletBecomesToolbarSetting(bool value); + bool getTabletVisibleToOthersSetting() { return _tabletVisibleToOthersSetting.get(); } + void setTabletVisibleToOthersSetting(bool value); float getSettingConstrainToolbarPosition() { return _constrainToolbarPosition.get(); } void setSettingConstrainToolbarPosition(bool setting); @@ -300,6 +302,7 @@ public: OverlayID getTabletScreenID() const; OverlayID getTabletHomeButtonID() const; + QUuid getTabletFrameID() const; // may be an entity or an overlay signals: void svoImportRequested(const QString& url); @@ -561,6 +564,7 @@ private: Setting::Handle _desktopTabletScale; Setting::Handle _desktopTabletBecomesToolbarSetting; Setting::Handle _hmdTabletBecomesToolbarSetting; + Setting::Handle _tabletVisibleToOthersSetting; Setting::Handle _constrainToolbarPosition; float _scaleMirror; diff --git a/interface/src/Camera.cpp b/interface/src/Camera.cpp index f930424569..cf3261ee88 100644 --- a/interface/src/Camera.cpp +++ b/interface/src/Camera.cpp @@ -192,6 +192,8 @@ QVariantMap Camera::getViewFrustum() { result["orientation"].setValue(frustum.getOrientation()); result["projection"].setValue(frustum.getProjection()); result["centerRadius"].setValue(frustum.getCenterRadius()); + result["fieldOfView"].setValue(frustum.getFieldOfView()); + result["aspectRatio"].setValue(frustum.getAspectRatio()); return result; } diff --git a/interface/src/avatar/Avatar.cpp b/interface/src/avatar/Avatar.cpp index ed8f083a41..f5fe82ef4b 100644 --- a/interface/src/avatar/Avatar.cpp +++ b/interface/src/avatar/Avatar.cpp @@ -334,11 +334,6 @@ void Avatar::updateAvatarEntities() { setAvatarEntityDataChanged(false); } -bool Avatar::shouldDie() const { - const qint64 AVATAR_SILENCE_THRESHOLD_USECS = 5 * USECS_PER_SECOND; - return _owningAvatarMixer.isNull() || getUsecsSinceLastUpdate() > AVATAR_SILENCE_THRESHOLD_USECS; -} - void Avatar::simulate(float deltaTime, bool inView) { PROFILE_RANGE(simulation, "simulate"); @@ -938,6 +933,10 @@ glm::vec3 Avatar::getDefaultJointTranslation(int index) const { } glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const { + if (index < 0) { + index += numeric_limits::max() + 1; // 65536 + } + switch(index) { case SENSOR_TO_WORLD_MATRIX_INDEX: { glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix(); @@ -974,6 +973,10 @@ glm::quat Avatar::getAbsoluteJointRotationInObjectFrame(int index) const { } glm::vec3 Avatar::getAbsoluteJointTranslationInObjectFrame(int index) const { + if (index < 0) { + index += numeric_limits::max() + 1; // 65536 + } + switch(index) { case SENSOR_TO_WORLD_MATRIX_INDEX: { glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix(); diff --git a/interface/src/avatar/Avatar.h b/interface/src/avatar/Avatar.h index 80d387fd33..ca4dbd2af8 100644 --- a/interface/src/avatar/Avatar.h +++ b/interface/src/avatar/Avatar.h @@ -178,12 +178,13 @@ public: uint64_t getLastRenderUpdateTime() const { return _lastRenderUpdateTime; } void setLastRenderUpdateTime(uint64_t time) { _lastRenderUpdateTime = time; } - bool shouldDie() const; void animateScaleChanges(float deltaTime); void setTargetScale(float targetScale) override; Q_INVOKABLE float getSimulationRate(const QString& rateName = QString("")) const; + bool hasNewJointData() const { return _hasNewJointData; } + public slots: // FIXME - these should be migrated to use Pose data instead diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index c3fc974365..d806c042b9 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -85,7 +85,7 @@ AvatarManager::AvatarManager(QObject* parent) : // immediately remove that avatar instead of waiting for the absence of packets from avatar mixer connect(nodeList.data(), &NodeList::ignoredNode, this, [=](const QUuid& nodeID, bool enabled) { if (enabled) { - removeAvatar(nodeID); + removeAvatar(nodeID, KillAvatarReason::AvatarIgnored); } }); } @@ -148,16 +148,6 @@ float AvatarManager::getAvatarSimulationRate(const QUuid& sessionID, const QStri } - -class AvatarPriority { -public: - AvatarPriority(AvatarSharedPointer a, float p) : avatar(a), priority(p) {} - AvatarSharedPointer avatar; - float priority; - // NOTE: we invert the less-than operator to sort high priorities to front - bool operator<(const AvatarPriority& other) const { return priority > other.priority; } -}; - void AvatarManager::updateOtherAvatars(float deltaTime) { // lock the hash for read to check the size QReadLocker lock(&_hashLock); @@ -173,57 +163,35 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { QList avatarList = avatarMap.values(); ViewFrustum cameraView; qApp->copyDisplayViewFrustum(cameraView); - glm::vec3 frustumCenter = cameraView.getPosition(); - const float OUT_OF_VIEW_PENALTY = -10.0; + std::priority_queue sortedAvatars = AvatarData::sortAvatars( + avatarList, cameraView, - std::priority_queue sortedAvatars; - { - PROFILE_RANGE(simulation, "sort"); - for (int32_t i = 0; i < avatarList.size(); ++i) { - const auto& avatar = std::static_pointer_cast(avatarList.at(i)); - if (avatar == _myAvatar || !avatar->isInitialized()) { + [](AvatarSharedPointer avatar)->uint64_t{ + return std::static_pointer_cast(avatar)->getLastRenderUpdateTime(); + }, + + [](AvatarSharedPointer avatar)->float{ + return std::static_pointer_cast(avatar)->getBoundingRadius(); + }, + + [this](AvatarSharedPointer avatar)->bool{ + const auto& castedAvatar = std::static_pointer_cast(avatar); + if (castedAvatar == _myAvatar || !castedAvatar->isInitialized()) { // DO NOT update _myAvatar! Its update has already been done earlier in the main loop. // DO NOT update or fade out uninitialized Avatars - continue; + return true; // ignore it } if (avatar->shouldDie()) { removeAvatar(avatar->getID()); - continue; + return true; // ignore it } if (avatar->isDead()) { - continue; + return true; // ignore it } - // priority = weighted linear combination of: - // (a) apparentSize - // (b) proximity to center of view - // (c) time since last update - // (d) TIME_PENALTY to help recently updated entries sort toward back - glm::vec3 avatarPosition = avatar->getPosition(); - glm::vec3 offset = avatarPosition - frustumCenter; - float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero - float radius = avatar->getBoundingRadius(); - const glm::vec3& forward = cameraView.getDirection(); - float apparentSize = radius / distance; - float cosineAngle = glm::length(offset - glm::dot(offset, forward) * forward) / distance; - const float TIME_PENALTY = 0.080f; // seconds - float age = (float)(startTime - avatar->getLastRenderUpdateTime()) / (float)(USECS_PER_SECOND) - TIME_PENALTY; - // NOTE: we are adding values of different units to get a single measure of "priority". - // Thus we multiply each component by a conversion "weight" that scales its units - // relative to the others. These weights are pure magic tuning and are hard coded in the - // relation below: (hint: unitary weights are not explicityly shown) - float priority = apparentSize + 0.25f * cosineAngle + age; - - // decrement priority of avatars outside keyhole - if (distance > cameraView.getCenterRadius()) { - if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) { - priority += OUT_OF_VIEW_PENALTY; - } - } - sortedAvatars.push(AvatarPriority(avatar, priority)); - } - } + return false; + }); render::PendingChanges pendingChanges; const uint64_t RENDER_UPDATE_BUDGET = 1500; // usec @@ -231,8 +199,8 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { uint64_t renderExpiry = startTime + RENDER_UPDATE_BUDGET; uint64_t maxExpiry = startTime + MAX_UPDATE_BUDGET; - int fullySimulatedAvatars = 0; - int partiallySimulatedAvatars = 0; + int numAvatarsUpdated = 0; + int numAVatarsNotUpdated = 0; while (!sortedAvatars.empty()) { const AvatarPriority& sortData = sortedAvatars.top(); const auto& avatar = std::static_pointer_cast(sortData.avatar); @@ -253,33 +221,57 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { } avatar->animateScaleChanges(deltaTime); + const float OUT_OF_VIEW_THRESHOLD = 0.5f * AvatarData::OUT_OF_VIEW_PENALTY; uint64_t now = usecTimestampNow(); if (now < renderExpiry) { // we're within budget - const float OUT_OF_VIEW_THRESHOLD = 0.5f * OUT_OF_VIEW_PENALTY; bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD; + if (inView && avatar->hasNewJointData()) { + numAvatarsUpdated++; + } avatar->simulate(deltaTime, inView); avatar->updateRenderItem(pendingChanges); avatar->setLastRenderUpdateTime(startTime); - fullySimulatedAvatars++; } else if (now < maxExpiry) { // we've spent most of our time budget, but we still simulate() the avatar as it if were out of view // --> some avatars may freeze until their priority trickles up - const bool inView = false; - avatar->simulate(deltaTime, inView); - partiallySimulatedAvatars++; + bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD; + if (inView && avatar->hasNewJointData()) { + numAVatarsNotUpdated++; + } + avatar->simulate(deltaTime, false); } else { // we've spent ALL of our time budget --> bail on the rest of the avatar updates + // --> more avatars may freeze until their priority trickles up // --> some scale or fade animations may glitch // --> some avatar velocity measurements may be a little off + + // HACK: no time simulate, but we will take the time to count how many were tragically missed + bool inView = sortData.priority > OUT_OF_VIEW_THRESHOLD; + if (!inView) { + break; + } + if (inView && avatar->hasNewJointData()) { + numAVatarsNotUpdated++; + } + sortedAvatars.pop(); + while (inView && !sortedAvatars.empty()) { + const AvatarPriority& newSortData = sortedAvatars.top(); + const auto& newAvatar = std::static_pointer_cast(newSortData.avatar); + inView = newSortData.priority > OUT_OF_VIEW_THRESHOLD; + if (inView && newAvatar->hasNewJointData()) { + numAVatarsNotUpdated++; + } + sortedAvatars.pop(); + } break; } sortedAvatars.pop(); } _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; - _fullySimulatedAvatars = fullySimulatedAvatars; - _partiallySimulatedAvatars = partiallySimulatedAvatars; + _numAvatarsUpdated = numAvatarsUpdated; + _numAvatarsNotUpdated = numAVatarsNotUpdated; qApp->getMain3DScene()->enqueuePendingChanges(pendingChanges); simulateAvatarFades(deltaTime); @@ -593,3 +585,44 @@ RayToAvatarIntersectionResult AvatarManager::findRayIntersection(const PickRay& return result; } + +// HACK +float AvatarManager::getAvatarSortCoefficient(const QString& name) { + if (name == "size") { + return AvatarData::_avatarSortCoefficientSize; + } else if (name == "center") { + return AvatarData::_avatarSortCoefficientCenter; + } else if (name == "age") { + return AvatarData::_avatarSortCoefficientAge; + } + return 0.0f; +} + +// HACK +void AvatarManager::setAvatarSortCoefficient(const QString& name, const QScriptValue& value) { + bool somethingChanged = false; + if (value.isNumber()) { + float numericalValue = (float)value.toNumber(); + if (name == "size") { + AvatarData::_avatarSortCoefficientSize = numericalValue; + somethingChanged = true; + } else if (name == "center") { + AvatarData::_avatarSortCoefficientCenter = numericalValue; + somethingChanged = true; + } else if (name == "age") { + AvatarData::_avatarSortCoefficientAge = numericalValue; + somethingChanged = true; + } + } + if (somethingChanged) { + size_t packetSize = sizeof(AvatarData::_avatarSortCoefficientSize) + + sizeof(AvatarData::_avatarSortCoefficientCenter) + + sizeof(AvatarData::_avatarSortCoefficientAge); + + auto packet = NLPacket::create(PacketType::AdjustAvatarSorting, packetSize); + packet->writePrimitive(AvatarData::_avatarSortCoefficientSize); + packet->writePrimitive(AvatarData::_avatarSortCoefficientCenter); + packet->writePrimitive(AvatarData::_avatarSortCoefficientAge); + DependencyManager::get()->broadcastToNodes(std::move(packet), NodeSet() << NodeType::AvatarMixer); + } +} diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index 4d503842b9..e1f5a3b411 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -43,8 +43,8 @@ public: std::shared_ptr getMyAvatar() { return _myAvatar; } AvatarSharedPointer getAvatarBySessionID(const QUuid& sessionID) const override; - int getFullySimulatedAvatars() const { return _fullySimulatedAvatars; } - int getPartiallySimulatedAvatars() const { return _partiallySimulatedAvatars; } + int getNumAvatarsUpdated() const { return _numAvatarsUpdated; } + int getNumAvatarsNotUpdated() const { return _numAvatarsNotUpdated; } float getAvatarSimulationTime() const { return _avatarSimulationTime; } void updateMyAvatar(float deltaTime); @@ -81,6 +81,10 @@ public: const QScriptValue& avatarIdsToInclude = QScriptValue(), const QScriptValue& avatarIdsToDiscard = QScriptValue()); + // TODO: remove this HACK once we settle on optimal default sort coefficients + Q_INVOKABLE float getAvatarSortCoefficient(const QString& name); + Q_INVOKABLE void setAvatarSortCoefficient(const QString& name, const QScriptValue& value); + float getMyAvatarSendRate() const { return _myAvatarSendRate.rate(); } public slots: @@ -116,10 +120,9 @@ private: VectorOfMotionStates _motionStatesToRemoveFromPhysics; RateCounter<> _myAvatarSendRate; - int _fullySimulatedAvatars { 0 }; - int _partiallySimulatedAvatars { 0 }; + int _numAvatarsUpdated { 0 }; + int _numAvatarsNotUpdated { 0 }; float _avatarSimulationTime { 0.0f }; - }; Q_DECLARE_METATYPE(AvatarManager::LocalLight) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index e39b7e1e50..842939d938 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -148,13 +148,22 @@ MyAvatar::MyAvatar(RigPointer rig) : auto player = DependencyManager::get(); auto recorder = DependencyManager::get(); connect(player.data(), &Deck::playbackStateChanged, [=] { - if (player->isPlaying()) { + bool isPlaying = player->isPlaying(); + if (isPlaying) { auto recordingInterface = DependencyManager::get(); if (recordingInterface->getPlayFromCurrentLocation()) { setRecordingBasis(); } } else { clearRecordingBasis(); + useFullAvatarURL(_fullAvatarURLFromPreferences, _fullAvatarModelName); + } + + auto audioIO = DependencyManager::get(); + audioIO->setIsPlayingBackRecording(isPlaying); + + if (_rig) { + _rig->setEnableAnimations(!isPlaying); } }); @@ -180,8 +189,8 @@ MyAvatar::MyAvatar(RigPointer rig) : if (recordingInterface->getPlayerUseSkeletonModel() && dummyAvatar.getSkeletonModelURL().isValid() && (dummyAvatar.getSkeletonModelURL() != getSkeletonModelURL())) { - // FIXME - //myAvatar->useFullAvatarURL() + + setSkeletonModelURL(dummyAvatar.getSkeletonModelURL()); } if (recordingInterface->getPlayerUseDisplayName() && dummyAvatar.getDisplayName() != getDisplayName()) { @@ -204,6 +213,11 @@ MyAvatar::MyAvatar(RigPointer rig) : // head orientation _headData->setLookAtPosition(headData->getLookAtPosition()); } + + auto jointData = dummyAvatar.getRawJointData(); + if (jointData.length() > 0 && _rig) { + _rig->copyJointsFromJointData(jointData); + } }); connect(rig.get(), SIGNAL(onLoadComplete()), this, SIGNAL(onLoadComplete())); @@ -227,8 +241,7 @@ void MyAvatar::simulateAttachments(float deltaTime) { // don't update attachments here, do it in harvestResultsFromPhysicsSimulation() } -QByteArray MyAvatar::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, - bool distanceAdjust, glm::vec3 viewerPosition, QVector* sentJointDataOut) { +QByteArray MyAvatar::toByteArrayStateful(AvatarDataDetail dataDetail) { CameraMode mode = qApp->getCamera()->getMode(); _globalPosition = getPosition(); _globalBoundingBoxDimensions.x = _characterController.getCapsuleRadius(); @@ -239,12 +252,12 @@ QByteArray MyAvatar::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTi // fake the avatar position that is sent up to the AvatarMixer glm::vec3 oldPosition = getPosition(); setPosition(getSkeletonPosition()); - QByteArray array = AvatarData::toByteArray(dataDetail, lastSentTime, lastSentJointData, distanceAdjust, viewerPosition, sentJointDataOut); + QByteArray array = AvatarData::toByteArrayStateful(dataDetail); // copy the correct position back setPosition(oldPosition); return array; } - return AvatarData::toByteArray(dataDetail, lastSentTime, lastSentJointData, distanceAdjust, viewerPosition, sentJointDataOut); + return AvatarData::toByteArrayStateful(dataDetail); } void MyAvatar::centerBody() { @@ -472,7 +485,9 @@ void MyAvatar::simulate(float deltaTime) { { PerformanceTimer perfTimer("joints"); // copy out the skeleton joints from the model - _rig->copyJointsIntoJointData(_jointData); + if (_rigEnabled) { + _rig->copyJointsIntoJointData(_jointData); + } } { @@ -808,7 +823,7 @@ void MyAvatar::saveData() { auto hmdInterface = DependencyManager::get(); _avatarEntitiesLock.withReadLock([&] { for (auto entityID : _avatarEntityData.keys()) { - if (hmdInterface->getCurrentTabletUIID() == entityID) { + if (hmdInterface->getCurrentTabletFrameID() == entityID) { // don't persist the tablet between domains / sessions continue; } @@ -2395,6 +2410,10 @@ glm::mat4 MyAvatar::computeCameraRelativeHandControllerMatrix(const glm::mat4& c } glm::quat MyAvatar::getAbsoluteJointRotationInObjectFrame(int index) const { + if (index < 0) { + index += numeric_limits::max() + 1; // 65536 + } + switch (index) { case CONTROLLER_LEFTHAND_INDEX: { return getLeftHandControllerPoseInAvatarFrame().getRotation(); @@ -2428,6 +2447,10 @@ glm::quat MyAvatar::getAbsoluteJointRotationInObjectFrame(int index) const { } glm::vec3 MyAvatar::getAbsoluteJointTranslationInObjectFrame(int index) const { + if (index < 0) { + index += numeric_limits::max() + 1; // 65536 + } + switch (index) { case CONTROLLER_LEFTHAND_INDEX: { return getLeftHandControllerPoseInAvatarFrame().getTranslation(); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index c4fe86356d..4f86256a2f 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -338,8 +338,7 @@ private: glm::quat getWorldBodyOrientation() const; - virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, - bool distanceAdjust = false, glm::vec3 viewerPosition = glm::vec3(0), QVector* sentJointDataOut = nullptr) override; + virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail) override; void simulate(float deltaTime); void updateFromTrackers(float deltaTime); @@ -486,6 +485,7 @@ private: std::unordered_set _headBoneSet; RigPointer _rig; bool _prevShouldDrawHead; + bool _rigEnabled { true }; bool _enableDebugDrawDefaultPose { false }; bool _enableDebugDrawAnimPose { false }; diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 463a21ded8..d895d5da4c 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -29,8 +29,8 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen Q_PROPERTY(glm::quat orientation READ getOrientation) Q_PROPERTY(bool mounted READ isMounted) Q_PROPERTY(bool showTablet READ getShouldShowTablet) - Q_PROPERTY(QUuid tabletID READ getCurrentTabletUIID WRITE setCurrentTabletUIID) - Q_PROPERTY(QUuid homeButtonID READ getCurrentHomeButtonUUID WRITE setCurrentHomeButtonUUID) + Q_PROPERTY(QUuid tabletID READ getCurrentTabletFrameID WRITE setCurrentTabletFrameID) + Q_PROPERTY(QUuid homeButtonID READ getCurrentHomeButtonID WRITE setCurrentHomeButtonID) Q_PROPERTY(QUuid tabletScreenID READ getCurrentTabletScreenID WRITE setCurrentTabletScreenID) public: @@ -90,11 +90,11 @@ public: void setShouldShowTablet(bool value) { _showTablet = value; } bool getShouldShowTablet() const { return _showTablet; } - void setCurrentTabletUIID(QUuid tabletID) { _tabletUIID = tabletID; } - QUuid getCurrentTabletUIID() const { return _tabletUIID; } + void setCurrentTabletFrameID(QUuid tabletID) { _tabletUIID = tabletID; } + QUuid getCurrentTabletFrameID() const { return _tabletUIID; } - void setCurrentHomeButtonUUID(QUuid homeButtonID) { _homeButtonID = homeButtonID; } - QUuid getCurrentHomeButtonUUID() const { return _homeButtonID; } + void setCurrentHomeButtonID(QUuid homeButtonID) { _homeButtonID = homeButtonID; } + QUuid getCurrentHomeButtonID() const { return _homeButtonID; } void setCurrentTabletScreenID(QUuid tabletID) { _tabletScreenID = tabletID; } QUuid getCurrentTabletScreenID() const { return _tabletScreenID; } diff --git a/interface/src/ui/PreferencesDialog.cpp b/interface/src/ui/PreferencesDialog.cpp index dd05d5c0e1..d291510556 100644 --- a/interface/src/ui/PreferencesDialog.cpp +++ b/interface/src/ui/PreferencesDialog.cpp @@ -102,7 +102,11 @@ void setupPreferences() { auto setter = [](bool value) { qApp->setHmdTabletBecomesToolbarSetting(value); }; preferences->addPreference(new CheckPreference(UI_CATEGORY, "HMD Tablet Becomes Toolbar", getter, setter)); } - + { + auto getter = []()->bool { return qApp->getTabletVisibleToOthersSetting(); }; + auto setter = [](bool value) { qApp->setTabletVisibleToOthersSetting(value); }; + preferences->addPreference(new CheckPreference(UI_CATEGORY, "Tablet Is Visible To Others", getter, setter)); + } // Snapshots static const QString SNAPSHOTS { "Snapshots" }; { diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index e82f99bed2..1075bbdaa4 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -121,8 +121,8 @@ void Stats::updateStats(bool force) { auto avatarManager = DependencyManager::get(); // we need to take one avatar out so we don't include ourselves STAT_UPDATE(avatarCount, avatarManager->size() - 1); - STAT_UPDATE(fullySimulatedAvatarCount, avatarManager->getFullySimulatedAvatars()); - STAT_UPDATE(partiallySimulatedAvatarCount, avatarManager->getPartiallySimulatedAvatars()); + STAT_UPDATE(updatedAvatarCount, avatarManager->getNumAvatarsUpdated()); + STAT_UPDATE(notUpdatedAvatarCount, avatarManager->getNumAvatarsNotUpdated()); STAT_UPDATE(serverCount, (int)nodeList->size()); STAT_UPDATE(framerate, qApp->getFps()); if (qApp->getActiveDisplayPlugin()) { diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index f501f4b09a..6be084100c 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -49,8 +49,8 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, simrate, 0) STATS_PROPERTY(int, avatarSimrate, 0) STATS_PROPERTY(int, avatarCount, 0) - STATS_PROPERTY(int, fullySimulatedAvatarCount, 0) - STATS_PROPERTY(int, partiallySimulatedAvatarCount, 0) + STATS_PROPERTY(int, updatedAvatarCount, 0) + STATS_PROPERTY(int, notUpdatedAvatarCount, 0) STATS_PROPERTY(int, packetInCount, 0) STATS_PROPERTY(int, packetOutCount, 0) STATS_PROPERTY(float, mbpsIn, 0) @@ -159,8 +159,8 @@ signals: void simrateChanged(); void avatarSimrateChanged(); void avatarCountChanged(); - void fullySimulatedAvatarCountChanged(); - void partiallySimulatedAvatarCountChanged(); + void updatedAvatarCountChanged(); + void notUpdatedAvatarCountChanged(); void packetInCountChanged(); void packetOutCountChanged(); void mbpsInChanged(); diff --git a/interface/src/ui/overlays/Base3DOverlay.cpp b/interface/src/ui/overlays/Base3DOverlay.cpp index ff5177ed3a..70b1fa4b71 100644 --- a/interface/src/ui/overlays/Base3DOverlay.cpp +++ b/interface/src/ui/overlays/Base3DOverlay.cpp @@ -39,7 +39,8 @@ Base3DOverlay::Base3DOverlay(const Base3DOverlay* base3DOverlay) : _isDashedLine(base3DOverlay->_isDashedLine), _ignoreRayIntersection(base3DOverlay->_ignoreRayIntersection), _drawInFront(base3DOverlay->_drawInFront), - _isAA(base3DOverlay->_isAA) + _isAA(base3DOverlay->_isAA), + _isGrabbable(base3DOverlay->_isGrabbable) { setTransform(base3DOverlay->getTransform()); } @@ -59,15 +60,19 @@ QVariantMap convertOverlayLocationFromScriptSemantics(const QVariantMap& propert } else if (result["position"].isValid()) { glm::vec3 localPosition = SpatiallyNestable::worldToLocal(vec3FromVariant(result["position"]), parentID, parentJointIndex, success); - result["position"] = vec3toVariant(localPosition); + if (success) { + result["position"] = vec3toVariant(localPosition); + } } if (result["localOrientation"].isValid()) { result["orientation"] = result["localOrientation"]; } else if (result["orientation"].isValid()) { glm::quat localOrientation = SpatiallyNestable::worldToLocal(quatFromVariant(result["orientation"]), - parentID, parentJointIndex, success); - result["orientation"] = quatToVariant(localOrientation); + parentID, parentJointIndex, success); + if (success) { + result["orientation"] = quatToVariant(localOrientation); + } } return result; @@ -125,6 +130,11 @@ void Base3DOverlay::setProperties(const QVariantMap& originalProperties) { needRenderItemUpdate = true; } + auto isGrabbable = properties["grabbable"]; + if (isGrabbable.isValid()) { + setIsGrabbable(isGrabbable.toBool()); + } + if (properties["position"].isValid()) { setLocalPosition(vec3FromVariant(properties["position"])); needRenderItemUpdate = true; @@ -227,6 +237,9 @@ QVariant Base3DOverlay::getProperty(const QString& property) { if (property == "drawInFront") { return _drawInFront; } + if (property == "grabbable") { + return _isGrabbable; + } if (property == "parentID") { return getParentID(); } @@ -246,6 +259,8 @@ bool Base3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3 } void Base3DOverlay::locationChanged(bool tellPhysics) { + SpatiallyNestable::locationChanged(tellPhysics); + auto itemID = getRenderItemID(); if (render::Item::isValidID(itemID)) { render::ScenePointer scene = qApp->getMain3DScene(); @@ -253,8 +268,6 @@ void Base3DOverlay::locationChanged(bool tellPhysics) { pendingChanges.updateItem(itemID); scene->enqueuePendingChanges(pendingChanges); } - // Overlays can't currently have children. - // SpatiallyNestable::locationChanged(tellPhysics); // tell all the children, also } void Base3DOverlay::parentDeleted() { diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 7906b9d6c0..a1c23e5cd8 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -38,6 +38,7 @@ public: bool getIsSolidLine() const { return !_isDashedLine; } bool getIgnoreRayIntersection() const { return _ignoreRayIntersection; } bool getDrawInFront() const { return _drawInFront; } + bool getIsGrabbable() const { return _isGrabbable; } virtual bool isAA() const { return _isAA; } @@ -47,6 +48,7 @@ public: void setIgnoreRayIntersection(bool value) { _ignoreRayIntersection = value; } void setDrawInFront(bool value) { _drawInFront = value; } void setIsAA(bool value) { _isAA = value; } + void setIsGrabbable(bool value) { _isGrabbable = value; } virtual AABox getBounds() const override = 0; @@ -71,6 +73,7 @@ protected: bool _ignoreRayIntersection; bool _drawInFront; bool _isAA; + bool _isGrabbable { false }; }; #endif // hifi_Base3DOverlay_h diff --git a/interface/src/ui/overlays/Overlays.cpp b/interface/src/ui/overlays/Overlays.cpp index c18d9ddaef..ad7fbd6cc2 100644 --- a/interface/src/ui/overlays/Overlays.cpp +++ b/interface/src/ui/overlays/Overlays.cpp @@ -341,28 +341,18 @@ OverlayID Overlays::getOverlayAtPoint(const glm::vec2& point) { return UNKNOWN_OVERLAY_ID; } QMapIterator i(_overlaysHUD); - i.toBack(); const float LARGE_NEGATIVE_FLOAT = -9999999; glm::vec3 origin(pointCopy.x, pointCopy.y, LARGE_NEGATIVE_FLOAT); glm::vec3 direction(0, 0, 1); - BoxFace thisFace; glm::vec3 thisSurfaceNormal; - float distance; unsigned int bestStackOrder = 0; OverlayID bestOverlayID = UNKNOWN_OVERLAY_ID; - while (i.hasPrevious()) { - i.previous(); + while (i.hasNext()) { + i.next(); OverlayID thisID = i.key(); - if (i.value()->is3D()) { - auto thisOverlay = std::dynamic_pointer_cast(i.value()); - if (thisOverlay && !thisOverlay->getIgnoreRayIntersection()) { - if (thisOverlay->findRayIntersection(origin, direction, distance, thisFace, thisSurfaceNormal)) { - return thisID; - } - } - } else { + if (!i.value()->is3D()) { auto thisOverlay = std::dynamic_pointer_cast(i.value()); if (thisOverlay && thisOverlay->getVisible() && thisOverlay->isLoaded() && thisOverlay->getBoundingRect().contains(pointCopy.x, pointCopy.y, false)) { @@ -406,16 +396,25 @@ RayToOverlayIntersectionResult Overlays::findRayIntersection(const PickRay& ray, const QScriptValue& overlayIDsToInclude, const QScriptValue& overlayIDsToDiscard, bool visibleOnly, bool collidableOnly) { - float bestDistance = std::numeric_limits::max(); - bool bestIsFront = false; const QVector overlaysToInclude = qVectorOverlayIDFromScriptValue(overlayIDsToInclude); const QVector overlaysToDiscard = qVectorOverlayIDFromScriptValue(overlayIDsToDiscard); + return findRayIntersectionInternal(ray, precisionPicking, + overlaysToInclude, overlaysToDiscard, visibleOnly, collidableOnly); +} + + +RayToOverlayIntersectionResult Overlays::findRayIntersectionInternal(const PickRay& ray, bool precisionPicking, + const QVector& overlaysToInclude, + const QVector& overlaysToDiscard, + bool visibleOnly, bool collidableOnly) { + float bestDistance = std::numeric_limits::max(); + bool bestIsFront = false; + RayToOverlayIntersectionResult result; QMapIterator i(_overlaysWorld); - i.toBack(); - while (i.hasPrevious()) { - i.previous(); + while (i.hasNext()) { + i.next(); OverlayID thisID = i.key(); auto thisOverlay = std::dynamic_pointer_cast(i.value()); @@ -700,8 +699,9 @@ static PointerEvent::Button toPointerButton(const QMouseEvent& event) { } } -PointerEvent Overlays::calculatePointerEvent(Overlay::Pointer overlay, PickRay ray, - RayToOverlayIntersectionResult rayPickResult, QMouseEvent* event, PointerEvent::EventType eventType) { +PointerEvent Overlays::calculatePointerEvent(Overlay::Pointer overlay, PickRay ray, + RayToOverlayIntersectionResult rayPickResult, QMouseEvent* event, + PointerEvent::EventType eventType) { auto thisOverlay = std::dynamic_pointer_cast(overlay); @@ -719,11 +719,41 @@ PointerEvent Overlays::calculatePointerEvent(Overlay::Pointer overlay, PickRay r return pointerEvent; } -void Overlays::mousePressEvent(QMouseEvent* event) { + +RayToOverlayIntersectionResult Overlays::findRayIntersectionForMouseEvent(PickRay ray) { + QVector overlaysToInclude; + QVector overlaysToDiscard; + RayToOverlayIntersectionResult rayPickResult; + + // first priority is tablet screen + overlaysToInclude << qApp->getTabletScreenID(); + rayPickResult = findRayIntersectionInternal(ray, true, overlaysToInclude, overlaysToDiscard); + if (rayPickResult.intersects) { + return rayPickResult; + } + // then tablet home button + overlaysToInclude.clear(); + overlaysToInclude << qApp->getTabletHomeButtonID(); + rayPickResult = findRayIntersectionInternal(ray, true, overlaysToInclude, overlaysToDiscard); + if (rayPickResult.intersects) { + return rayPickResult; + } + // then tablet frame + overlaysToInclude.clear(); + overlaysToInclude << OverlayID(qApp->getTabletFrameID()); + rayPickResult = findRayIntersectionInternal(ray, true, overlaysToInclude, overlaysToDiscard); + if (rayPickResult.intersects) { + return rayPickResult; + } + // then whatever + return findRayIntersection(ray); +} + +bool Overlays::mousePressEvent(QMouseEvent* event) { PerformanceTimer perfTimer("Overlays::mousePressEvent"); PickRay ray = qApp->computePickRay(event->x(), event->y()); - RayToOverlayIntersectionResult rayPickResult = findRayIntersection(ray); + RayToOverlayIntersectionResult rayPickResult = findRayIntersectionForMouseEvent(ray); if (rayPickResult.intersects) { _currentClickingOnOverlayID = rayPickResult.overlayID; @@ -732,19 +762,18 @@ void Overlays::mousePressEvent(QMouseEvent* event) { if (thisOverlay) { auto pointerEvent = calculatePointerEvent(thisOverlay, ray, rayPickResult, event, PointerEvent::Press); emit mousePressOnOverlay(_currentClickingOnOverlayID, pointerEvent); - } else { - emit mousePressOffOverlay(); + return true; } - } else { - emit mousePressOffOverlay(); } + emit mousePressOffOverlay(); + return false; } -void Overlays::mouseReleaseEvent(QMouseEvent* event) { +bool Overlays::mouseReleaseEvent(QMouseEvent* event) { PerformanceTimer perfTimer("Overlays::mouseReleaseEvent"); PickRay ray = qApp->computePickRay(event->x(), event->y()); - RayToOverlayIntersectionResult rayPickResult = findRayIntersection(ray); + RayToOverlayIntersectionResult rayPickResult = findRayIntersectionForMouseEvent(ray); if (rayPickResult.intersects) { // Only Web overlays can have focus. @@ -756,13 +785,14 @@ void Overlays::mouseReleaseEvent(QMouseEvent* event) { } _currentClickingOnOverlayID = UNKNOWN_OVERLAY_ID; + return false; } -void Overlays::mouseMoveEvent(QMouseEvent* event) { +bool Overlays::mouseMoveEvent(QMouseEvent* event) { PerformanceTimer perfTimer("Overlays::mouseMoveEvent"); PickRay ray = qApp->computePickRay(event->x(), event->y()); - RayToOverlayIntersectionResult rayPickResult = findRayIntersection(ray); + RayToOverlayIntersectionResult rayPickResult = findRayIntersectionForMouseEvent(ray); if (rayPickResult.intersects) { // Only Web overlays can have focus. @@ -802,4 +832,34 @@ void Overlays::mouseMoveEvent(QMouseEvent* event) { _currentHoverOverOverlayID = UNKNOWN_OVERLAY_ID; } } + return false; +} + +QVector Overlays::findOverlays(const glm::vec3& center, float radius) const { + QVector result; + + QMapIterator i(_overlaysWorld); + int checked = 0; + while (i.hasNext()) { + checked++; + i.next(); + OverlayID thisID = i.key(); + auto overlay = std::dynamic_pointer_cast(i.value()); + if (overlay && overlay->getVisible() && !overlay->getIgnoreRayIntersection() && overlay->isLoaded()) { + // get AABox in frame of overlay + glm::vec3 dimensions = overlay->getDimensions(); + glm::vec3 low = dimensions * -0.5f; + AABox overlayFrameBox(low, dimensions); + + Transform overlayToWorldMatrix = overlay->getTransform(); + glm::mat4 worldToOverlayMatrix = glm::inverse(overlayToWorldMatrix.getMatrix()); + glm::vec3 overlayFrameSearchPosition = glm::vec3(worldToOverlayMatrix * glm::vec4(center, 1.0f)); + glm::vec3 penetration; + if (overlayFrameBox.findSpherePenetration(overlayFrameSearchPosition, radius, penetration)) { + result.append(thisID); + } + } + } + + return result; } diff --git a/interface/src/ui/overlays/Overlays.h b/interface/src/ui/overlays/Overlays.h index 7c6ba34f58..5c22e46880 100644 --- a/interface/src/ui/overlays/Overlays.h +++ b/interface/src/ui/overlays/Overlays.h @@ -100,9 +100,9 @@ public: OverlayID addOverlay(Overlay* overlay) { return addOverlay(Overlay::Pointer(overlay)); } OverlayID addOverlay(Overlay::Pointer overlay); - void mousePressEvent(QMouseEvent* event); - void mouseReleaseEvent(QMouseEvent* event); - void mouseMoveEvent(QMouseEvent* event); + bool mousePressEvent(QMouseEvent* event); + bool mouseReleaseEvent(QMouseEvent* event); + bool mouseMoveEvent(QMouseEvent* event); void cleanupAllOverlays(); @@ -206,6 +206,16 @@ public slots: bool visibleOnly = false, bool collidableOnly = false); + /**jsdoc + * Return a list of 3d overlays with bounding boxes that touch the given sphere + * + * @function Overlays.findOverlays + * @param {Vec3} center the point to search from. + * @param {float} radius search radius + * @return {List of Overlays.OverlayID} list of overlays withing the radius + */ + QVector findOverlays(const glm::vec3& center, float radius) const; + /**jsdoc * Check whether an overlay's assets have been loaded. For example, if the * overlay is an "image" overlay, this will indicate whether the its image @@ -317,6 +327,12 @@ private: OverlayID _currentClickingOnOverlayID { UNKNOWN_OVERLAY_ID }; OverlayID _currentHoverOverOverlayID { UNKNOWN_OVERLAY_ID }; + + RayToOverlayIntersectionResult findRayIntersectionInternal(const PickRay& ray, bool precisionPicking, + const QVector& overlaysToInclude, + const QVector& overlaysToDiscard, + bool visibleOnly = false, bool collidableOnly = false); + RayToOverlayIntersectionResult findRayIntersectionForMouseEvent(PickRay ray); }; #endif // hifi_Overlays_h diff --git a/interface/src/ui/overlays/OverlaysPayload.cpp b/interface/src/ui/overlays/OverlaysPayload.cpp index 277a86e93f..aa06741638 100644 --- a/interface/src/ui/overlays/OverlaysPayload.cpp +++ b/interface/src/ui/overlays/OverlaysPayload.cpp @@ -53,7 +53,7 @@ namespace render { return overlay->getBounds(); } template <> int payloadGetLayer(const Overlay::Pointer& overlay) { - // MAgic number while we are defining the layering mechanism: + // Magic number while we are defining the layering mechanism: const int LAYER_NO_AA = 3; const int LAYER_2D = 2; const int LAYER_3D_FRONT = 1; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index ac16b16c1d..07462e9878 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -483,6 +483,10 @@ void Rig::setEnableInverseKinematics(bool enable) { _enableInverseKinematics = enable; } +void Rig::setEnableAnimations(bool enable) { + _enabledAnimations = enable; +} + AnimPose Rig::getAbsoluteDefaultPose(int index) const { if (_animSkeleton && index >= 0 && index < _animSkeleton->getNumJoints()) { return _absoluteDefaultPoses[index]; @@ -907,7 +911,7 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 rootTransform) { setModelOffset(rootTransform); - if (_animNode) { + if (_animNode && _enabledAnimations) { PerformanceTimer perfTimer("handleTriggers"); updateAnimationStateHandlers(); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index aa091fe10c..78a669b249 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -210,6 +210,7 @@ public: void computeAvatarBoundingCapsule(const FBXGeometry& geometry, float& radiusOut, float& heightOut, glm::vec3& offsetOut) const; void setEnableInverseKinematics(bool enable); + void setEnableAnimations(bool enable); const glm::mat4& getGeometryToRigTransform() const { return _geometryToRigTransform; } @@ -314,6 +315,7 @@ protected: int32_t _numOverrides { 0 }; bool _lastEnableInverseKinematics { true }; bool _enableInverseKinematics { true }; + bool _enabledAnimations { true }; mutable uint32_t _jointNameWarningCount { 0 }; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 2e532d67bf..bd141cfb12 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -39,13 +39,10 @@ #include #include #include -#include #include #include -#include #include -#include "PositionalAudioStream.h" #include "AudioClientLogging.h" #include "AudioLogging.h" @@ -294,12 +291,12 @@ QString friendlyNameForAudioDevice(IMMDevice* pEndpoint) { IPropertyStore* pPropertyStore; pEndpoint->OpenPropertyStore(STGM_READ, &pPropertyStore); pEndpoint->Release(); - pEndpoint = NULL; + pEndpoint = nullptr; PROPVARIANT pv; PropVariantInit(&pv); HRESULT hr = pPropertyStore->GetValue(PKEY_Device_FriendlyName, &pv); pPropertyStore->Release(); - pPropertyStore = NULL; + pPropertyStore = nullptr; deviceName = QString::fromWCharArray((wchar_t*)pv.pwszVal); if (!IsWindows8OrGreater()) { // Windows 7 provides only the 31 first characters of the device name. @@ -313,9 +310,9 @@ QString friendlyNameForAudioDevice(IMMDevice* pEndpoint) { QString AudioClient::friendlyNameForAudioDevice(wchar_t* guid) { QString deviceName; HRESULT hr = S_OK; - CoInitialize(NULL); - IMMDeviceEnumerator* pMMDeviceEnumerator = NULL; - CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pMMDeviceEnumerator); + CoInitialize(nullptr); + IMMDeviceEnumerator* pMMDeviceEnumerator = nullptr; + CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pMMDeviceEnumerator); IMMDevice* pEndpoint; hr = pMMDeviceEnumerator->GetDevice(guid, &pEndpoint); if (hr == E_NOTFOUND) { @@ -325,7 +322,7 @@ QString AudioClient::friendlyNameForAudioDevice(wchar_t* guid) { deviceName = ::friendlyNameForAudioDevice(pEndpoint); } pMMDeviceEnumerator->Release(); - pMMDeviceEnumerator = NULL; + pMMDeviceEnumerator = nullptr; CoUninitialize(); return deviceName; } @@ -968,8 +965,7 @@ void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { } void AudioClient::handleAudioInput() { - - if (!_inputDevice) { + if (!_inputDevice || _isPlayingBackRecording) { return; } @@ -1120,7 +1116,7 @@ void AudioClient::prepareLocalAudioInjectors() { while (samplesNeeded > 0) { // lock for every write to avoid locking out the device callback // this lock is intentional - the buffer is only lock-free in its use in the device callback - Lock lock(_localAudioMutex); + RecursiveLock lock(_localAudioMutex); samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed); if (samplesNeeded <= 0) { @@ -1457,7 +1453,7 @@ void AudioClient::outputNotify() { bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) { bool supportedFormat = false; - Lock lock(_localAudioMutex); + RecursiveLock lock(_localAudioMutex); _localSamplesAvailable.exchange(0, std::memory_order_release); // cleanup any previously initialized device @@ -1671,7 +1667,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { int injectorSamplesPopped = 0; { - Lock lock(_audio->_localAudioMutex); + RecursiveLock lock(_audio->_localAudioMutex); bool append = networkSamplesPopped > 0; samplesRequested = std::min(samplesRequested, _audio->_localSamplesAvailable.load(std::memory_order_acquire)); if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 699ba71ef7..5619051eaf 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -94,6 +94,8 @@ public: using AudioPositionGetter = std::function; using AudioOrientationGetter = std::function; + using RecursiveMutex = std::recursive_mutex; + using RecursiveLock = std::unique_lock; using Mutex = std::mutex; using Lock = std::unique_lock; @@ -145,6 +147,8 @@ public: void setPositionGetter(AudioPositionGetter positionGetter) { _positionGetter = positionGetter; } void setOrientationGetter(AudioOrientationGetter orientationGetter) { _orientationGetter = orientationGetter; } + void setIsPlayingBackRecording(bool isPlayingBackRecording) { _isPlayingBackRecording = isPlayingBackRecording; } + Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); void checkDevices(); @@ -328,7 +332,7 @@ private: int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; AudioInjectorsThread _localAudioThread; - Mutex _localAudioMutex; + RecursiveMutex _localAudioMutex; // for output audio (used by this thread) int _outputPeriod { 0 }; @@ -367,10 +371,12 @@ private: QVector _inputDevices; QVector _outputDevices; - bool _hasReceivedFirstPacket = false; + bool _hasReceivedFirstPacket { false }; QVector _activeLocalAudioInjectors; + bool _isPlayingBackRecording { false }; + CodecPluginPointer _codec; QString _selectedCodecName; Encoder* _encoder { nullptr }; // for outbound mic stream diff --git a/libraries/audio/src/AudioInjectorOptions.cpp b/libraries/audio/src/AudioInjectorOptions.cpp index 1a92697828..0af74a796c 100644 --- a/libraries/audio/src/AudioInjectorOptions.cpp +++ b/libraries/audio/src/AudioInjectorOptions.cpp @@ -91,4 +91,4 @@ void injectorOptionsFromScriptValue(const QScriptValue& object, AudioInjectorOpt qCWarning(audio) << "Unknown audio injector option:" << it.name(); } } - } \ No newline at end of file +} diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index 47a8cc6e6e..c1dd60a3b0 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -36,6 +36,8 @@ #include #include #include +#include +#include #include "AvatarLogging.h" @@ -68,8 +70,7 @@ AvatarData::AvatarData() : _displayNameAlpha(1.0f), _errorLogExpiry(0), _owningAvatarMixer(), - _targetVelocity(0.0f), - _localAABox(DEFAULT_LOCAL_AABOX_CORNER, DEFAULT_LOCAL_AABOX_SCALE) + _targetVelocity(0.0f) { setBodyPitch(0.0f); setBodyYaw(-90.0f); @@ -120,10 +121,6 @@ void AvatarData::nextAttitude(glm::vec3 position, glm::quat orientation) { updateAttitude(); } -float AvatarData::getTargetScale() const { - return _targetScale; -} - void AvatarData::setTargetScale(float targetScale) { auto newValue = glm::clamp(targetScale, MIN_AVATAR_SCALE, MAX_AVATAR_SCALE); if (_targetScale != newValue) { @@ -141,10 +138,10 @@ void AvatarData::setHandPosition(const glm::vec3& handPosition) { _handPosition = glm::inverse(getOrientation()) * (handPosition - getPosition()); } -void AvatarData::lazyInitHeadData() { +void AvatarData::lazyInitHeadData() const { // lazily allocate memory for HeadData in case we're not an Avatar instance if (!_headData) { - _headData = new HeadData(this); + _headData = new HeadData(const_cast(this)); } if (_forceFaceTrackerConnected) { _headData->_isFaceTrackerConnected = true; @@ -152,39 +149,7 @@ void AvatarData::lazyInitHeadData() { } -bool AvatarData::avatarBoundingBoxChangedSince(quint64 time) { - return _avatarBoundingBoxChanged >= time; -} - -bool AvatarData::avatarScaleChangedSince(quint64 time) { - return _avatarScaleChanged >= time; -} - -bool AvatarData::lookAtPositionChangedSince(quint64 time) { - return _headData->lookAtPositionChangedSince(time); -} - -bool AvatarData::audioLoudnessChangedSince(quint64 time) { - return _headData->audioLoudnessChangedSince(time); -} - -bool AvatarData::sensorToWorldMatrixChangedSince(quint64 time) { - return _sensorToWorldMatrixChanged >= time; -} - -bool AvatarData::additionalFlagsChangedSince(quint64 time) { - return _additionalFlagsChanged >= time; -} - -bool AvatarData::parentInfoChangedSince(quint64 time) { - return _parentChanged >= time; -} - -bool AvatarData::faceTrackerInfoChangedSince(quint64 time) { - return true; // FIXME! -} - -float AvatarData::getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition) { +float AvatarData::getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition) const { auto distance = glm::distance(_globalPosition, viewerPosition); float result = ROTATION_CHANGE_179D; // assume worst if (distance < AVATAR_DISTANCE_LEVEL_1) { @@ -199,20 +164,24 @@ float AvatarData::getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition) { return result; } -float AvatarData::getDistanceBasedMinTranslationDistance(glm::vec3 viewerPosition) { +float AvatarData::getDistanceBasedMinTranslationDistance(glm::vec3 viewerPosition) const { return AVATAR_MIN_TRANSLATION; // Eventually make this distance sensitive as well } -QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, - bool distanceAdjust, glm::vec3 viewerPosition, QVector* sentJointDataOut) { +// we want to track outbound data in this case... +QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail) { + AvatarDataPacket::HasFlags hasFlagsOut; + auto lastSentTime = _lastToByteArray; + _lastToByteArray = usecTimestampNow(); + return AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(), + hasFlagsOut, false, false, glm::vec3(0), nullptr, + &_outboundDataRate); +} - // if no timestamp was included, then assume the avatarData is single instance - // and is tracking its own last encoding time. - if (lastSentTime == 0) { - lastSentTime = _lastToByteArray; - _lastToByteArray = usecTimestampNow(); - } +QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, + AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, + glm::vec3 viewerPosition, QVector* sentJointDataOut, AvatarDataRate* outboundDataRateOut) const { bool cullSmallChanges = (dataDetail == CullSmallData); bool sendAll = (dataDetail == SendAllData); @@ -224,6 +193,13 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); unsigned char* startPosition = destinationBuffer; + // special case, if we were asked for no data, then just include the flags all set to nothing + if (dataDetail == NoData) { + AvatarDataPacket::HasFlags packetStateFlags = 0; + memcpy(destinationBuffer, &packetStateFlags, sizeof(packetStateFlags)); + return avatarDataByteArray.left(sizeof(packetStateFlags)); + } + // FIXME - // // BUG -- if you enter a space bubble, and then back away, the avatar has wrong orientation until "send all" happens... @@ -259,26 +235,26 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent // separately bool hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime); bool hasAvatarLocalPosition = hasParent() && (sendAll || - tranlationChangedSince(lastSentTime) || - parentInfoChangedSince(lastSentTime)); + tranlationChangedSince(lastSentTime) || + parentInfoChangedSince(lastSentTime)); - bool hasFaceTrackerInfo = hasFaceTracker() && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); + bool hasFaceTrackerInfo = !dropFaceTracking && hasFaceTracker() && (sendAll || faceTrackerInfoChangedSince(lastSentTime)); bool hasJointData = sendAll || !sendMinimum; // Leading flags, to indicate how much data is actually included in the packet... AvatarDataPacket::HasFlags packetStateFlags = - (hasAvatarGlobalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_GLOBAL_POSITION : 0) - | (hasAvatarBoundingBox ? AvatarDataPacket::PACKET_HAS_AVATAR_BOUNDING_BOX : 0) - | (hasAvatarOrientation ? AvatarDataPacket::PACKET_HAS_AVATAR_ORIENTATION : 0) - | (hasAvatarScale ? AvatarDataPacket::PACKET_HAS_AVATAR_SCALE : 0) - | (hasLookAtPosition ? AvatarDataPacket::PACKET_HAS_LOOK_AT_POSITION : 0) - | (hasAudioLoudness ? AvatarDataPacket::PACKET_HAS_AUDIO_LOUDNESS : 0) - | (hasSensorToWorldMatrix ? AvatarDataPacket::PACKET_HAS_SENSOR_TO_WORLD_MATRIX : 0) - | (hasAdditionalFlags ? AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS : 0) - | (hasParentInfo ? AvatarDataPacket::PACKET_HAS_PARENT_INFO : 0) - | (hasAvatarLocalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION : 0) - | (hasFaceTrackerInfo ? AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO : 0) - | (hasJointData ? AvatarDataPacket::PACKET_HAS_JOINT_DATA : 0); + (hasAvatarGlobalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_GLOBAL_POSITION : 0) + | (hasAvatarBoundingBox ? AvatarDataPacket::PACKET_HAS_AVATAR_BOUNDING_BOX : 0) + | (hasAvatarOrientation ? AvatarDataPacket::PACKET_HAS_AVATAR_ORIENTATION : 0) + | (hasAvatarScale ? AvatarDataPacket::PACKET_HAS_AVATAR_SCALE : 0) + | (hasLookAtPosition ? AvatarDataPacket::PACKET_HAS_LOOK_AT_POSITION : 0) + | (hasAudioLoudness ? AvatarDataPacket::PACKET_HAS_AUDIO_LOUDNESS : 0) + | (hasSensorToWorldMatrix ? AvatarDataPacket::PACKET_HAS_SENSOR_TO_WORLD_MATRIX : 0) + | (hasAdditionalFlags ? AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS : 0) + | (hasParentInfo ? AvatarDataPacket::PACKET_HAS_PARENT_INFO : 0) + | (hasAvatarLocalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION : 0) + | (hasFaceTrackerInfo ? AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO : 0) + | (hasJointData ? AvatarDataPacket::PACKET_HAS_JOINT_DATA : 0); memcpy(destinationBuffer, &packetStateFlags, sizeof(packetStateFlags)); destinationBuffer += sizeof(packetStateFlags); @@ -293,7 +269,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent int numBytes = destinationBuffer - startSection; - _globalPositionRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->globalPositionRate.increment(numBytes); + } } if (hasAvatarBoundingBox) { @@ -311,7 +289,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += sizeof(AvatarDataPacket::AvatarBoundingBox); int numBytes = destinationBuffer - startSection; - _avatarBoundingBoxRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->avatarBoundingBoxRate.increment(numBytes); + } } if (hasAvatarOrientation) { @@ -320,7 +300,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, localOrientation); int numBytes = destinationBuffer - startSection; - _avatarOrientationRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->avatarOrientationRate.increment(numBytes); + } } if (hasAvatarScale) { @@ -331,7 +313,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += sizeof(AvatarDataPacket::AvatarScale); int numBytes = destinationBuffer - startSection; - _avatarScaleRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->avatarScaleRate.increment(numBytes); + } } if (hasLookAtPosition) { @@ -344,7 +328,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += sizeof(AvatarDataPacket::LookAtPosition); int numBytes = destinationBuffer - startSection; - _lookAtPositionRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->lookAtPositionRate.increment(numBytes); + } } if (hasAudioLoudness) { @@ -354,7 +340,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += sizeof(AvatarDataPacket::AudioLoudness); int numBytes = destinationBuffer - startSection; - _audioLoudnessRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->audioLoudnessRate.increment(numBytes); + } } if (hasSensorToWorldMatrix) { @@ -370,7 +358,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += sizeof(AvatarDataPacket::SensorToWorldMatrix); int numBytes = destinationBuffer - startSection; - _sensorToWorldRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->sensorToWorldRate.increment(numBytes); + } } if (hasAdditionalFlags) { @@ -403,7 +393,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += sizeof(AvatarDataPacket::AdditionalFlags); int numBytes = destinationBuffer - startSection; - _additionalFlagsRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->additionalFlagsRate.increment(numBytes); + } } if (hasParentInfo) { @@ -415,7 +407,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += sizeof(AvatarDataPacket::ParentInfo); int numBytes = destinationBuffer - startSection; - _parentInfoRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->parentInfoRate.increment(numBytes); + } } if (hasAvatarLocalPosition) { @@ -428,7 +422,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += sizeof(AvatarDataPacket::AvatarLocalPosition); int numBytes = destinationBuffer - startSection; - _localPositionRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->localPositionRate.increment(numBytes); + } } // If it is connected, pack up the data @@ -448,7 +444,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent destinationBuffer += _headData->_blendshapeCoefficients.size() * sizeof(float); int numBytes = destinationBuffer - startSection; - _faceTrackerRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->faceTrackerRate.increment(numBytes); + } } // If it is connected, pack up the data @@ -540,9 +538,9 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent glm::distance(data.translation, lastSentJointData[i].translation) > minTranslation) { if (data.translationSet) { validity |= (1 << validityBit); - #ifdef WANT_DEBUG +#ifdef WANT_DEBUG translationSentCount++; - #endif +#endif maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension); maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension); maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension); @@ -592,24 +590,25 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent #ifdef WANT_DEBUG if (sendAll) { qCDebug(avatars) << "AvatarData::toByteArray" << cullSmallChanges << sendAll - << "rotations:" << rotationSentCount << "translations:" << translationSentCount - << "largest:" << maxTranslationDimension - << "size:" - << (beforeRotations - startPosition) << "+" - << (beforeTranslations - beforeRotations) << "+" - << (destinationBuffer - beforeTranslations) << "=" - << (destinationBuffer - startPosition); + << "rotations:" << rotationSentCount << "translations:" << translationSentCount + << "largest:" << maxTranslationDimension + << "size:" + << (beforeRotations - startPosition) << "+" + << (beforeTranslations - beforeRotations) << "+" + << (destinationBuffer - beforeTranslations) << "=" + << (destinationBuffer - startPosition); } #endif int numBytes = destinationBuffer - startSection; - _jointDataRateOutbound.increment(numBytes); + if (outboundDataRateOut) { + outboundDataRateOut->jointDataRate.increment(numBytes); + } } int avatarDataSize = destinationBuffer - startPosition; return avatarDataByteArray.left(avatarDataSize); } - // NOTE: This is never used in a "distanceAdjust" mode, so it's ok that it doesn't use a variable minimum rotation/translation void AvatarData::doneEncoding(bool cullSmallChanges) { // The server has finished sending this version of the joint-data to other nodes. Update _lastSentJointData. @@ -1089,29 +1088,29 @@ float AvatarData::getDataRate(const QString& rateName) const { } else if (rateName == "jointData") { return _jointDataRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "globalPositionOutbound") { - return _globalPositionRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.globalPositionRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "localPositionOutbound") { - return _localPositionRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.localPositionRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "avatarBoundingBoxOutbound") { - return _avatarBoundingBoxRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.avatarBoundingBoxRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "avatarOrientationOutbound") { - return _avatarOrientationRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.avatarOrientationRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "avatarScaleOutbound") { - return _avatarScaleRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.avatarScaleRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "lookAtPositionOutbound") { - return _lookAtPositionRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.lookAtPositionRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "audioLoudnessOutbound") { - return _audioLoudnessRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.audioLoudnessRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "sensorToWorkMatrixOutbound") { - return _sensorToWorldRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.sensorToWorldRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "additionalFlagsOutbound") { - return _additionalFlagsRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.additionalFlagsRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "parentInfoOutbound") { - return _parentInfoRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.parentInfoRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "faceTrackerOutbound") { - return _faceTrackerRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.faceTrackerRate.rate() / BYTES_PER_KILOBIT; } else if (rateName == "jointDataOutbound") { - return _jointDataRateOutbound.rate() / BYTES_PER_KILOBIT; + return _outboundDataRate.jointDataRate.rate() / BYTES_PER_KILOBIT; } return 0.0f; } @@ -1445,7 +1444,7 @@ void AvatarData::parseAvatarIdentityPacket(const QByteArray& data, Identity& ide } static const QUrl emptyURL(""); -const QUrl& AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) { +QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const { // We don't put file urls on the wire, but instead convert to empty. return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL; } @@ -1483,7 +1482,7 @@ void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityC } } -QByteArray AvatarData::identityByteArray() { +QByteArray AvatarData::identityByteArray() const { QByteArray identityData; QDataStream identityStream(&identityData, QIODevice::Append); const QUrl& urlToSend = cannonicalSkeletonModelURL(emptyURL); @@ -1646,13 +1645,7 @@ void AvatarData::sendAvatarDataPacket() { bool cullSmallData = (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); auto dataDetail = cullSmallData ? SendAllData : CullSmallData; - QVector lastSentJointData; - { - QReadLocker readLock(&_jointDataLock); - _lastSentJointData.resize(_jointData.size()); - lastSentJointData = _lastSentJointData; - } - QByteArray avatarByteArray = toByteArray(dataDetail, 0, lastSentJointData); + QByteArray avatarByteArray = toByteArrayStateful(dataDetail); doneEncoding(cullSmallData); static AvatarDataSequenceNumber sequenceNumber = 0; @@ -2324,3 +2317,100 @@ void RayToAvatarIntersectionResultFromScriptValue(const QScriptValue& object, Ra vec3FromScriptValue(intersection, value.intersection); } } + +const float AvatarData::OUT_OF_VIEW_PENALTY = -10.0f; + +float AvatarData::_avatarSortCoefficientSize { 0.5f }; +float AvatarData::_avatarSortCoefficientCenter { 0.25 }; +float AvatarData::_avatarSortCoefficientAge { 1.0f }; + +std::priority_queue AvatarData::sortAvatars( + QList avatarList, + const ViewFrustum& cameraView, + std::function getLastUpdated, + std::function getBoundingRadius, + std::function shouldIgnore) { + + uint64_t startTime = usecTimestampNow(); + + glm::vec3 frustumCenter = cameraView.getPosition(); + + std::priority_queue sortedAvatars; + { + PROFILE_RANGE(simulation, "sort"); + for (int32_t i = 0; i < avatarList.size(); ++i) { + const auto& avatar = avatarList.at(i); + + if (shouldIgnore(avatar)) { + continue; + } + + // priority = weighted linear combination of: + // (a) apparentSize + // (b) proximity to center of view + // (c) time since last update + glm::vec3 avatarPosition = avatar->getPosition(); + glm::vec3 offset = avatarPosition - frustumCenter; + float distance = glm::length(offset) + 0.001f; // add 1mm to avoid divide by zero + + // FIXME - AvatarData has something equivolent to this + float radius = getBoundingRadius(avatar); + + const glm::vec3& forward = cameraView.getDirection(); + float apparentSize = 2.0f * radius / distance; + float cosineAngle = glm::length(glm::dot(offset, forward) * forward) / distance; + float age = (float)(startTime - getLastUpdated(avatar)) / (float)(USECS_PER_SECOND); + + // NOTE: we are adding values of different units to get a single measure of "priority". + // Thus we multiply each component by a conversion "weight" that scales its units relative to the others. + // These weights are pure magic tuning and should be hard coded in the relation below, + // but are currently exposed for anyone who would like to explore fine tuning: + float priority = _avatarSortCoefficientSize * apparentSize + + _avatarSortCoefficientCenter * cosineAngle + + _avatarSortCoefficientAge * age; + + // decrement priority of avatars outside keyhole + if (distance > cameraView.getCenterRadius()) { + if (!cameraView.sphereIntersectsFrustum(avatarPosition, radius)) { + priority += OUT_OF_VIEW_PENALTY; + } + } + sortedAvatars.push(AvatarPriority(avatar, priority)); + } + } + return sortedAvatars; +} + +QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEntityMap& value) { + QScriptValue obj = engine->newObject(); + for (auto entityID : value.keys()) { + QByteArray entityProperties = value.value(entityID); + QJsonDocument jsonEntityProperties = QJsonDocument::fromBinaryData(entityProperties); + if (!jsonEntityProperties.isObject()) { + qCDebug(avatars) << "bad AvatarEntityData in AvatarEntityMap" << QString(entityProperties.toHex()); + } + + QVariant variantEntityProperties = jsonEntityProperties.toVariant(); + QVariantMap entityPropertiesMap = variantEntityProperties.toMap(); + QScriptValue scriptEntityProperties = variantMapToScriptValue(entityPropertiesMap, *engine); + + QString key = entityID.toString(); + obj.setProperty(key, scriptEntityProperties); + } + return obj; +} + +void AvatarEntityMapFromScriptValue(const QScriptValue& object, AvatarEntityMap& value) { + QScriptValueIterator itr(object); + while (itr.hasNext()) { + itr.next(); + QUuid EntityID = QUuid(itr.name()); + + QScriptValue scriptEntityProperties = itr.value(); + QVariant variantEntityProperties = scriptEntityProperties.toVariant(); + QJsonDocument jsonEntityProperties = QJsonDocument::fromVariant(variantEntityProperties); + QByteArray binaryEntityProperties = jsonEntityProperties.toBinaryData(); + + value[EntityID] = binaryEntityProperties; + } +} diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index b28501eead..12209d9c31 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -14,6 +14,8 @@ #include #include +#include + /* VS2010 defines stdint.h, but not inttypes.h */ #if defined(_MSC_VER) typedef signed char int8_t; @@ -44,6 +46,7 @@ typedef unsigned long long quint64; #include #include #include +#include #include #include @@ -57,6 +60,7 @@ typedef unsigned long long quint64; #include #include #include +#include #include "AABox.h" #include "HeadData.h" @@ -133,6 +137,7 @@ namespace AvatarDataPacket { const HasFlags PACKET_HAS_AVATAR_LOCAL_POSITION = 1U << 9; const HasFlags PACKET_HAS_FACE_TRACKER_INFO = 1U << 10; const HasFlags PACKET_HAS_JOINT_DATA = 1U << 11; + const size_t AVATAR_HAS_FLAGS_SIZE = 2; // NOTE: AvatarDataPackets start with a uint16_t sequence number that is not reflected in the Header structure. @@ -288,6 +293,31 @@ class AttachmentData; class Transform; using TransformPointer = std::shared_ptr; +class AvatarDataRate { +public: + RateCounter<> globalPositionRate; + RateCounter<> localPositionRate; + RateCounter<> avatarBoundingBoxRate; + RateCounter<> avatarOrientationRate; + RateCounter<> avatarScaleRate; + RateCounter<> lookAtPositionRate; + RateCounter<> audioLoudnessRate; + RateCounter<> sensorToWorldRate; + RateCounter<> additionalFlagsRate; + RateCounter<> parentInfoRate; + RateCounter<> faceTrackerRate; + RateCounter<> jointDataRate; +}; + +class AvatarPriority { +public: + AvatarPriority(AvatarSharedPointer a, float p) : avatar(a), priority(p) {} + AvatarSharedPointer avatar; + float priority; + // NOTE: we invert the less-than operator to sort high priorities to front + bool operator<(const AvatarPriority& other) const { return priority < other.priority; } +}; + class AvatarData : public QObject, public SpatiallyNestable { Q_OBJECT @@ -345,14 +375,18 @@ public: void setHandPosition(const glm::vec3& handPosition); typedef enum { + NoData, MinimumData, CullSmallData, IncludeSmallData, SendAllData } AvatarDataDetail; + virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail); + virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, - bool distanceAdjust = false, glm::vec3 viewerPosition = glm::vec3(0), QVector* sentJointDataOut = nullptr); + AvatarDataPacket::HasFlags& hasFlagsOut, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, + QVector* sentJointDataOut, AvatarDataRate* outboundDataRateOut = nullptr) const; virtual void doneEncoding(bool cullSmallChanges); @@ -380,7 +414,7 @@ public: void nextAttitude(glm::vec3 position, glm::quat orientation); // Can be safely called at any time. virtual void updateAttitude() {} // Tell skeleton mesh about changes - glm::quat getHeadOrientation() { + glm::quat getHeadOrientation() const { lazyInitHeadData(); return _headData->getOrientation(); } @@ -419,7 +453,6 @@ public: void setAudioAverageLoudness(float value) { _headData->setAudioAverageLoudness(value); } // Scale - float getTargetScale() const; virtual void setTargetScale(float targetScale); float getDomainLimitedScale() const { return glm::clamp(_targetScale, _domainMinimumScale, _domainMaximumScale); } @@ -494,7 +527,7 @@ public: // displayNameChanged returns true if displayName has changed, false otherwise. void processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged); - QByteArray identityByteArray(); + QByteArray identityByteArray() const; const QUrl& getSkeletonModelURL() const { return _skeletonModelURL; } const QString& getDisplayName() const { return _displayName; } @@ -520,8 +553,6 @@ public: void setOwningAvatarMixer(const QWeakPointer& owningAvatarMixer) { _owningAvatarMixer = owningAvatarMixer; } - const AABox& getLocalAABox() const { return _localAABox; } - int getUsecsSinceLastUpdate() const { return _averageBytesReceived.getUsecsSinceLastEvent(); } int getAverageBytesReceivedPerSecond() const; int getReceiveRate() const; @@ -534,8 +565,8 @@ public: QJsonObject toJson() const; void fromJson(const QJsonObject& json, bool useFrameSkeleton = true); - glm::vec3 getClientGlobalPosition() { return _globalPosition; } - glm::vec3 getGlobalBoundingBoxCorner() { return _globalPosition + _globalBoundingBoxOffset - _globalBoundingBoxDimensions; } + glm::vec3 getClientGlobalPosition() const { return _globalPosition; } + glm::vec3 getGlobalBoundingBoxCorner() const { return _globalPosition + _globalBoundingBoxOffset - _globalBoundingBoxDimensions; } Q_INVOKABLE AvatarEntityMap getAvatarEntityData() const; Q_INVOKABLE void setAvatarEntityData(const AvatarEntityMap& avatarEntityData); @@ -550,7 +581,7 @@ public: Q_INVOKABLE float getDataRate(const QString& rateName = QString("")) const; Q_INVOKABLE float getUpdateRate(const QString& rateName = QString("")) const; - int getJointCount() { return _jointData.size(); } + int getJointCount() const { return _jointData.size(); } QVector getLastSentJointData() { QReadLocker readLock(&_jointDataLock); @@ -559,6 +590,28 @@ public: } + bool shouldDie() const { + const qint64 AVATAR_SILENCE_THRESHOLD_USECS = 5 * USECS_PER_SECOND; + return _owningAvatarMixer.isNull() || getUsecsSinceLastUpdate() > AVATAR_SILENCE_THRESHOLD_USECS; + } + + static const float OUT_OF_VIEW_PENALTY; + + static std::priority_queue sortAvatars( + QList avatarList, + const ViewFrustum& cameraView, + std::function getLastUpdated, + std::function getBoundingRadius, + std::function shouldIgnore); + + // TODO: remove this HACK once we settle on optimal sort coefficients + // These coefficients exposed for fine tuning the sort priority for transfering new _jointData to the render pipeline. + static float _avatarSortCoefficientSize; + static float _avatarSortCoefficientCenter; + static float _avatarSortCoefficientAge; + + + public slots: void sendAvatarDataPacket(); void sendIdentityPacket(); @@ -571,28 +624,27 @@ public slots: virtual bool setAbsoluteJointRotationInObjectFrame(int index, const glm::quat& rotation) override { return false; } virtual bool setAbsoluteJointTranslationInObjectFrame(int index, const glm::vec3& translation) override { return false; } - float getTargetScale() { return _targetScale; } + float getTargetScale() const { return _targetScale; } // why is this a slot? void resetLastSent() { _lastToByteArray = 0; } protected: - void lazyInitHeadData(); + void lazyInitHeadData() const; - float getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition); - float getDistanceBasedMinTranslationDistance(glm::vec3 viewerPosition); + float getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition) const; + float getDistanceBasedMinTranslationDistance(glm::vec3 viewerPosition) const; - bool avatarBoundingBoxChangedSince(quint64 time); - bool avatarScaleChangedSince(quint64 time); - bool lookAtPositionChangedSince(quint64 time); - bool audioLoudnessChangedSince(quint64 time); - bool sensorToWorldMatrixChangedSince(quint64 time); - bool additionalFlagsChangedSince(quint64 time); + bool avatarBoundingBoxChangedSince(quint64 time) const { return _avatarBoundingBoxChanged >= time; } + bool avatarScaleChangedSince(quint64 time) const { return _avatarScaleChanged >= time; } + bool lookAtPositionChangedSince(quint64 time) const { return _headData->lookAtPositionChangedSince(time); } + bool audioLoudnessChangedSince(quint64 time) const { return _headData->audioLoudnessChangedSince(time); } + bool sensorToWorldMatrixChangedSince(quint64 time) const { return _sensorToWorldMatrixChanged >= time; } + bool additionalFlagsChangedSince(quint64 time) const { return _additionalFlagsChanged >= time; } + bool parentInfoChangedSince(quint64 time) const { return _parentChanged >= time; } + bool faceTrackerInfoChangedSince(quint64 time) const { return true; } // FIXME - bool hasParent() { return !getParentID().isNull(); } - bool parentInfoChangedSince(quint64 time); - - bool hasFaceTracker() { return _headData ? _headData->_isFaceTrackerConnected : false; } - bool faceTrackerInfoChangedSince(quint64 time); + bool hasParent() const { return !getParentID().isNull(); } + bool hasFaceTracker() const { return _headData ? _headData->_isFaceTrackerConnected : false; } glm::vec3 _handPosition; virtual const QString& getSessionDisplayNameForTransport() const { return _sessionDisplayName; } @@ -616,7 +668,7 @@ protected: bool _forceFaceTrackerConnected; bool _hasNewJointData { true }; // set in AvatarData, cleared in Avatar - HeadData* _headData { nullptr }; + mutable HeadData* _headData { nullptr }; QUrl _skeletonModelURL; bool _firstSkeletonCheck { true }; @@ -624,7 +676,7 @@ protected: QVector _attachmentData; QString _displayName; QString _sessionDisplayName { }; - const QUrl& cannonicalSkeletonModelURL(const QUrl& empty); + QUrl cannonicalSkeletonModelURL(const QUrl& empty) const; float _displayNameTargetAlpha; float _displayNameAlpha; @@ -641,8 +693,6 @@ protected: glm::vec3 _targetVelocity; - AABox _localAABox; - SimpleMovingAverage _averageBytesReceived; // During recording, this holds the starting position, orientation & scale of the recorded avatar @@ -695,18 +745,7 @@ protected: RateCounter<> _jointDataUpdateRate; // Some rate data for outgoing data - RateCounter<> _globalPositionRateOutbound; - RateCounter<> _localPositionRateOutbound; - RateCounter<> _avatarBoundingBoxRateOutbound; - RateCounter<> _avatarOrientationRateOutbound; - RateCounter<> _avatarScaleRateOutbound; - RateCounter<> _lookAtPositionRateOutbound; - RateCounter<> _audioLoudnessRateOutbound; - RateCounter<> _sensorToWorldRateOutbound; - RateCounter<> _additionalFlagsRateOutbound; - RateCounter<> _parentInfoRateOutbound; - RateCounter<> _faceTrackerRateOutbound; - RateCounter<> _jointDataRateOutbound; + AvatarDataRate _outboundDataRate; glm::vec3 _globalBoundingBoxDimensions; glm::vec3 _globalBoundingBoxOffset; @@ -809,6 +848,11 @@ Q_DECLARE_METATYPE(RayToAvatarIntersectionResult) QScriptValue RayToAvatarIntersectionResultToScriptValue(QScriptEngine* engine, const RayToAvatarIntersectionResult& results); void RayToAvatarIntersectionResultFromScriptValue(const QScriptValue& object, RayToAvatarIntersectionResult& results); +Q_DECLARE_METATYPE(AvatarEntityMap) + +QScriptValue AvatarEntityMapToScriptValue(QScriptEngine* engine, const AvatarEntityMap& value); +void AvatarEntityMapFromScriptValue(const QScriptValue& object, AvatarEntityMap& value); + // faux joint indexes (-1 means invalid) const int SENSOR_TO_WORLD_MATRIX_INDEX = 65534; // -2 const int CONTROLLER_RIGHTHAND_INDEX = 65533; // -3 diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index 00c515a635..48e5d673c9 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -182,7 +182,7 @@ void AvatarHashMap::removeAvatar(const QUuid& sessionUUID, KillAvatarReason remo void AvatarHashMap::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) { qCDebug(avatars) << "Removed avatar with UUID" << uuidStringWithoutCurlyBraces(removedAvatar->getSessionUUID()) - << "from AvatarHashMap"; + << "from AvatarHashMap" << removalReason; emit avatarRemovedEvent(removedAvatar->getSessionUUID()); } @@ -190,3 +190,4 @@ void AvatarHashMap::sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& ol _lastOwnerSessionUUID = oldUUID; emit avatarSessionChangedEvent(sessionUUID, oldUUID); } + diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index dd097aef22..104ac83261 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -27,7 +27,6 @@ #include "AvatarData.h" - class AvatarHashMap : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index cd7f1235bb..a5bd0135e4 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1419,8 +1419,7 @@ QVector EntityScriptingInterface::getChildrenIDsOfJoint(const QUuid& pare return; } parent->forEachChild([&](SpatiallyNestablePointer child) { - if (child->getParentJointIndex() == jointIndex && - child->getNestableType() != NestableType::Overlay) { + if (child->getParentJointIndex() == jointIndex) { result.push_back(child->getID()); } }); diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index adef3ce2b5..2cd96a84c7 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -44,6 +44,8 @@ public: // Mutable, but must retain structure of vector using NetworkMaterials = std::vector>; + bool isGeometryLoaded() const { return (bool)_fbxGeometry; } + const FBXGeometry& getFBXGeometry() const { return *_fbxGeometry; } const GeometryMeshes& getMeshes() const { return *_meshes; } const std::shared_ptr getShapeMaterial(int shapeID) const; diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index d1fbaf767a..7ac8083d9c 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -336,8 +336,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm const double pStrength = 2.0; int width = image.width(); int height = image.height(); - // THe end result image for normal map is RGBA32 even though the A is not used - QImage result(width, height, QImage::Format_RGBA8888); + QImage result(width, height, QImage::Format_RGB888); for (int i = 0; i < width; i++) { const int iNextClamped = clampPixelCoordinate(i + 1, width - 1); @@ -377,21 +376,19 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm glm::normalize(v); // convert to rgb from the value obtained computing the filter - QRgb qRgbValue = qRgb(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z)); + QRgb qRgbValue = qRgba(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z), 1.0); result.setPixel(i, j, qRgbValue); } } gpu::Texture* theTexture = nullptr; if ((image.width() > 0) && (image.height() > 0)) { - - gpu::Element formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); - gpu::Element formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); + gpu::Element formatGPU = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); + gpu::Element formatMip = gpu::Element(gpu::VEC3, gpu::NUINT8, gpu::RGB); theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); theTexture->assignStoredMip(0, formatMip, image.byteCount(), image.constBits()); - generateMips(theTexture, image, formatMip, true); } return theTexture; diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index 5256e55397..3eb898463a 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -34,6 +34,7 @@ #include #include +#include #include "DomainHandler.h" #include "Node.h" @@ -182,15 +183,33 @@ public: // This allows multiple threads (i.e. a thread pool) to share a lock // without deadlocking when a dying node attempts to acquire a write lock template - void nestedEach(NestedNodeLambda functor) { - QReadLocker readLock(&_nodeMutex); + void nestedEach(NestedNodeLambda functor, + int* lockWaitOut = nullptr, + int* nodeTransformOut = nullptr, + int* functorOut = nullptr) { + auto start = usecTimestampNow(); + { + QReadLocker readLock(&_nodeMutex); + auto endLock = usecTimestampNow(); + if (lockWaitOut) { + *lockWaitOut = (endLock - start); + } - std::vector nodes(_nodeHash.size()); - std::transform(_nodeHash.cbegin(), _nodeHash.cend(), nodes.begin(), [](const NodeHash::value_type& it) { - return it.second; - }); + std::vector nodes(_nodeHash.size()); + std::transform(_nodeHash.cbegin(), _nodeHash.cend(), nodes.begin(), [](const NodeHash::value_type& it) { + return it.second; + }); + auto endTransform = usecTimestampNow(); + if (nodeTransformOut) { + *nodeTransformOut = (endTransform - endLock); + } - functor(nodes.cbegin(), nodes.cend()); + functor(nodes.cbegin(), nodes.cend()); + auto endFunctor = usecTimestampNow(); + if (functorOut) { + *functorOut = (endFunctor - endTransform); + } + } } template diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp index f38d24c31f..c8a7b61aa7 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.cpp @@ -16,8 +16,8 @@ void UserActivityLoggerScriptingInterface::enabledEdit() { logAction("enabled_edit"); } -void UserActivityLoggerScriptingInterface::openedTablet() { - logAction("opened_tablet"); +void UserActivityLoggerScriptingInterface::openedTablet(bool visibleToOthers) { + logAction("opened_tablet", { { "visible_to_others", visibleToOthers } }); } void UserActivityLoggerScriptingInterface::closedTablet() { diff --git a/libraries/networking/src/UserActivityLoggerScriptingInterface.h b/libraries/networking/src/UserActivityLoggerScriptingInterface.h index b827b2262a..cf38450891 100644 --- a/libraries/networking/src/UserActivityLoggerScriptingInterface.h +++ b/libraries/networking/src/UserActivityLoggerScriptingInterface.h @@ -21,7 +21,7 @@ class UserActivityLoggerScriptingInterface : public QObject, public Dependency { Q_OBJECT public: Q_INVOKABLE void enabledEdit(); - Q_INVOKABLE void openedTablet(); + Q_INVOKABLE void openedTablet(bool visibleToOthers); Q_INVOKABLE void closedTablet(); Q_INVOKABLE void openedMarketplace(); Q_INVOKABLE void toggledAway(bool isAway); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 050e3088f8..01973a6786 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -112,7 +112,8 @@ public: ReloadEntityServerScript, EntityPhysics, EntityServerScriptLog, - LAST_PACKET_TYPE = EntityServerScriptLog + AdjustAvatarSorting, + LAST_PACKET_TYPE = AdjustAvatarSorting }; }; diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 2042a16801..7c373274e4 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -114,7 +114,7 @@ public: void setBlendedVertices(int blendNumber, const Geometry::WeakPointer& geometry, const QVector& vertices, const QVector& normals); - bool isLoaded() const { return (bool)_renderGeometry; } + bool isLoaded() const { return (bool)_renderGeometry && _renderGeometry->isGeometryLoaded(); } void setIsWireframe(bool isWireframe) { _isWireframe = isWireframe; } bool isWireframe() const { return _isWireframe; } diff --git a/libraries/script-engine/src/RecordingScriptingInterface.cpp b/libraries/script-engine/src/RecordingScriptingInterface.cpp index 710f342322..41bb780b47 100644 --- a/libraries/script-engine/src/RecordingScriptingInterface.cpp +++ b/libraries/script-engine/src/RecordingScriptingInterface.cpp @@ -56,6 +56,11 @@ bool RecordingScriptingInterface::loadRecording(const QString& url) { using namespace recording; auto loader = ClipCache::instance().getClipLoader(url); + if (!loader) { + qWarning() << "Clip failed to load from " << url; + return false; + } + if (!loader->isLoaded()) { QEventLoop loop; QObject::connect(loader.data(), &Resource::loaded, &loop, &QEventLoop::quit); diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index f1ff4c4686..83f2f5ccc0 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -576,6 +576,7 @@ void ScriptEngine::init() { qScriptRegisterMetaType(this, EntityItemIDtoScriptValue, EntityItemIDfromScriptValue); qScriptRegisterMetaType(this, RayToEntityIntersectionResultToScriptValue, RayToEntityIntersectionResultFromScriptValue); qScriptRegisterMetaType(this, RayToAvatarIntersectionResultToScriptValue, RayToAvatarIntersectionResultFromScriptValue); + qScriptRegisterMetaType(this, AvatarEntityMapToScriptValue, AvatarEntityMapFromScriptValue); qScriptRegisterSequenceMetaType>(this); qScriptRegisterSequenceMetaType>(this); diff --git a/libraries/script-engine/src/TabletScriptingInterface.cpp b/libraries/script-engine/src/TabletScriptingInterface.cpp index d66bb4d2f6..32bd7f422e 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.cpp +++ b/libraries/script-engine/src/TabletScriptingInterface.cpp @@ -366,6 +366,7 @@ void TabletProxy::gotoWebScreen(const QString& url, const QString& injectedJavaS } if (root) { + removeButtonsFromHomeScreen(); QMetaObject::invokeMethod(root, "loadSource", Q_ARG(const QVariant&, QVariant(WEB_VIEW_SOURCE_URL))); QMetaObject::invokeMethod(root, "setShown", Q_ARG(const QVariant&, QVariant(true))); QMetaObject::invokeMethod(root, "loadWebUrl", Q_ARG(const QVariant&, QVariant(url)), Q_ARG(const QVariant&, QVariant(injectedJavaScriptUrl))); diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 76b98c6217..608fa937c8 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -150,7 +150,6 @@ signals: private: bool getRequestsDomainListData(); void setRequestsDomainListData(bool requests); - bool _requestsDomainListData; }; diff --git a/libraries/shared/src/SpatiallyNestable.h b/libraries/shared/src/SpatiallyNestable.h index be285eff53..820c8685d7 100644 --- a/libraries/shared/src/SpatiallyNestable.h +++ b/libraries/shared/src/SpatiallyNestable.h @@ -179,9 +179,9 @@ public: const glm::vec3& localVelocity, const glm::vec3& localAngularVelocity); - bool scaleChangedSince(quint64 time) { return _scaleChanged > time; } - bool tranlationChangedSince(quint64 time) { return _translationChanged > time; } - bool rotationChangedSince(quint64 time) { return _rotationChanged > time; } + bool scaleChangedSince(quint64 time) const { return _scaleChanged > time; } + bool tranlationChangedSince(quint64 time) const { return _translationChanged > time; } + bool rotationChangedSince(quint64 time) const { return _rotationChanged > time; } protected: const NestableType _nestableType; // EntityItem or an AvatarData diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 40a77eda55..5d8813e988 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -20,7 +20,7 @@ var DEFAULT_SCRIPTS = [ "system/bubble.js", "system/snapshot.js", "system/help.js", - "system/pal.js", //"system/mod.js", // older UX, if you prefer + "system/pal.js", // "system/mod.js", // older UX, if you prefer "system/goto.js", "system/marketplaces/marketplaces.js", "system/edit.js", @@ -54,9 +54,6 @@ if (previousSetting === true || previousSetting === 'true') { previousSetting = true; } - - - if (Menu.menuExists(MENU_CATEGORY) && !Menu.menuItemExists(MENU_CATEGORY, MENU_ITEM)) { Menu.addMenuItem({ menuName: MENU_CATEGORY, @@ -78,11 +75,11 @@ function runDefaultsSeparately() { Script.load(DEFAULT_SCRIPTS[i]); } } + // start all scripts if (Menu.isOptionChecked(MENU_ITEM)) { // we're debugging individual default scripts // so we load each into its own ScriptEngine instance - debuggingDefaultScripts = true; runDefaultsSeparately(); } else { // include all default scripts into this ScriptEngine @@ -90,32 +87,14 @@ if (Menu.isOptionChecked(MENU_ITEM)) { } function menuItemEvent(menuItem) { - if (menuItem == MENU_ITEM) { - - isChecked = Menu.isOptionChecked(MENU_ITEM); + if (menuItem === MENU_ITEM) { + var isChecked = Menu.isOptionChecked(MENU_ITEM); if (isChecked === true) { Settings.setValue(SETTINGS_KEY, true); } else if (isChecked === false) { Settings.setValue(SETTINGS_KEY, false); } - Window.alert('You must reload all scripts for this to take effect.') - } - - -} - - - -function stopLoadedScripts() { - // remove debug script loads - var runningScripts = ScriptDiscoveryService.getRunning(); - for (var i in runningScripts) { - var scriptName = runningScripts[i].name; - for (var j in DEFAULT_SCRIPTS) { - if (DEFAULT_SCRIPTS[j].slice(-scriptName.length) === scriptName) { - ScriptDiscoveryService.stopScript(runningScripts[i].url); - } - } + Menu.triggerOption("Reload All Scripts"); } } @@ -126,7 +105,6 @@ function removeMenuItem() { } Script.scriptEnding.connect(function() { - stopLoadedScripts(); removeMenuItem(); }); diff --git a/scripts/developer/debugging/debugAvatarMixer.js b/scripts/developer/debugging/debugAvatarMixer.js index ebd43fc2f0..90f2de13a9 100644 --- a/scripts/developer/debugging/debugAvatarMixer.js +++ b/scripts/developer/debugging/debugAvatarMixer.js @@ -58,11 +58,14 @@ function updateOverlays() { // setup a position for the overlay that is just above this avatar's head var overlayPosition = avatar.getJointPosition("Head"); - overlayPosition.y += 1.05; + overlayPosition.y += 1.15; + + var rows = 8; var text = avatarID + "\n" +"--- Data from Mixer ---\n" +"All: " + AvatarManager.getAvatarDataRate(avatarID).toFixed(2) + "kbps (" + AvatarManager.getAvatarUpdateRate(avatarID).toFixed(2) + "hz)" + "\n" + /* +" GP: " + AvatarManager.getAvatarDataRate(avatarID,"globalPosition").toFixed(2) + "kbps (" + AvatarManager.getAvatarUpdateRate(avatarID,"globalPosition").toFixed(2) + "hz)" + "\n" +" LP: " + AvatarManager.getAvatarDataRate(avatarID,"localPosition").toFixed(2) + "kbps (" + AvatarManager.getAvatarUpdateRate(avatarID,"localPosition").toFixed(2) + "hz)" + "\n" +" BB: " + AvatarManager.getAvatarDataRate(avatarID,"avatarBoundingBox").toFixed(2) + "kbps (" + AvatarManager.getAvatarUpdateRate(avatarID,"avatarBoundingBox").toFixed(2) + "hz)" + "\n" @@ -74,11 +77,12 @@ function updateOverlays() { +" AF: " + AvatarManager.getAvatarDataRate(avatarID,"additionalFlags").toFixed(2) + "kbps (" + AvatarManager.getAvatarUpdateRate(avatarID,"additionalFlags").toFixed(2) + "hz)" + "\n" +" PI: " + AvatarManager.getAvatarDataRate(avatarID,"parentInfo").toFixed(2) + "kbps (" + AvatarManager.getAvatarUpdateRate(avatarID,"parentInfo").toFixed(2) + "hz)" + "\n" +" FT: " + AvatarManager.getAvatarDataRate(avatarID,"faceTracker").toFixed(2) + "kbps (" + AvatarManager.getAvatarUpdateRate(avatarID,"faceTracker").toFixed(2) + "hz)" + "\n" + */ +" JD: " + AvatarManager.getAvatarDataRate(avatarID,"jointData").toFixed(2) + "kbps (" + AvatarManager.getAvatarUpdateRate(avatarID,"jointData").toFixed(2) + "hz)" + "\n" +"--- Simulation ---\n" +"All: " + AvatarManager.getAvatarSimulationRate(avatarID,"avatar").toFixed(2) + "hz \n" +" inView: " + AvatarManager.getAvatarSimulationRate(avatarID,"avatarInView").toFixed(2) + "hz \n" - +" SM: " + AvatarManager.getAvatarSimulationRate(avatarID,"skeletonModel").toFixed(2) + "hz \n" + //+" SM: " + AvatarManager.getAvatarSimulationRate(avatarID,"skeletonModel").toFixed(2) + "hz \n" +" JD: " + AvatarManager.getAvatarSimulationRate(avatarID,"jointData").toFixed(2) + "hz \n" if (avatarID in debugOverlays) { @@ -93,7 +97,7 @@ function updateOverlays() { position: overlayPosition, dimensions: { x: 1.25, - y: 19 * 0.13 + y: rows * 0.13 }, lineHeight: 0.1, font:{size:0.1}, diff --git a/scripts/developer/utilities/record/recorder.js b/scripts/developer/utilities/record/recorder.js index 083037461d..0e335116d5 100644 --- a/scripts/developer/utilities/record/recorder.js +++ b/scripts/developer/utilities/record/recorder.js @@ -12,14 +12,14 @@ HIFI_PUBLIC_BUCKET = "http://s3.amazonaws.com/hifi-public/"; Script.include("/~/system/libraries/toolBars.js"); -var recordingFile = "recording.rec"; +var recordingFile = "recording.hfr"; function setPlayerOptions() { Recording.setPlayFromCurrentLocation(true); Recording.setPlayerUseDisplayName(false); Recording.setPlayerUseAttachments(false); Recording.setPlayerUseHeadModel(false); - Recording.setPlayerUseSkeletonModel(false); + Recording.setPlayerUseSkeletonModel(true); } var windowDimensions = Controller.getViewportDimensions(); @@ -142,7 +142,6 @@ function setupTimer() { backgroundAlpha: 1.0, visible: true }); - } function updateTimer() { @@ -272,7 +271,7 @@ function mousePressEvent(event) { } } else if (loadIcon === toolBar.clicked(clickedOverlay)) { if (!Recording.isRecording() && !Recording.isPlaying()) { - recordingFile = Window.browse("Load recorcding from file", ".", "Recordings (*.hfr *.rec *.HFR *.REC)"); + recordingFile = Window.browse("Load recording from file", ".", "Recordings (*.hfr *.rec *.HFR *.REC)"); if (!(recordingFile === "null" || recordingFile === null || recordingFile === "")) { Recording.loadRecording(recordingFile); } @@ -345,5 +344,3 @@ Script.scriptEnding.connect(scriptEnding); // Should be called last to put everything into position moveUI(); - - diff --git a/scripts/system/audio.js b/scripts/system/audio.js index c0fdb43b40..6e7e95d659 100644 --- a/scripts/system/audio.js +++ b/scripts/system/audio.js @@ -16,19 +16,51 @@ var TABLET_BUTTON_NAME = "AUDIO"; var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; +var MUTE_ICONS = { + icon: "icons/tablet-icons/mic-mute-i.svg", + activeIcon: "icons/tablet-icons/mic-mute-a.svg" +}; + +var UNMUTE_ICONS = { + icon: "icons/tablet-icons/mic-unmute-i.svg", + activeIcon: "icons/tablet-icons/mic-unmute-a.svg" +}; + function onMuteToggled() { - button.editProperties({ isActive: AudioDevice.getMuted() }); + if (AudioDevice.getMuted()) { + button.editProperties(MUTE_ICONS); + } else { + button.editProperties(UNMUTE_ICONS); + } } -function onClicked(){ - var entity = HMD.tabletID; - Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); - tablet.gotoMenuScreen("Audio"); + +var shouldActivateButton = false; +var onAudioScreen = false; + +function onClicked() { + if (onAudioScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + var entity = HMD.tabletID; + Entities.editEntity(entity, { textures: JSON.stringify({ "tex.close": HOME_BUTTON_TEXTURE }) }); + shouldActivateButton = true; + tablet.gotoMenuScreen("Audio"); + onAudioScreen = true; + } +} + +function onScreenChanged(type, url) { + // for toolbar mode: change button to active when window is first openend, false otherwise. + button.editProperties({isActive: shouldActivateButton}); + shouldActivateButton = false; + onAudioScreen = false; } var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ - icon: "icons/tablet-icons/mic-unmute-i.svg", - activeIcon: "icons/tablet-icons/mic-mute-a.svg", + icon: AudioDevice.getMuted() ? MUTE_ICONS.icon : UNMUTE_ICONS.icon, + activeIcon: AudioDevice.getMuted() ? MUTE_ICONS.activeIcon : UNMUTE_ICONS.activeIcon, text: TABLET_BUTTON_NAME, sortOrder: 1 }); @@ -36,10 +68,12 @@ var button = tablet.addButton({ onMuteToggled(); button.clicked.connect(onClicked); +tablet.screenChanged.connect(onScreenChanged); AudioDevice.muteToggled.connect(onMuteToggled); Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); + tablet.screenChanged.disconnect(onScreenChanged); AudioDevice.muteToggled.disconnect(onMuteToggled); tablet.removeButton(button); }); diff --git a/scripts/system/controllers/grab.js b/scripts/system/controllers/grab.js index 74a3c3d25b..f0b6663bec 100644 --- a/scripts/system/controllers/grab.js +++ b/scripts/system/controllers/grab.js @@ -331,6 +331,12 @@ Grabber.prototype.pressEvent = function(event) { } var pickRay = Camera.computePickRay(event.x, event.y); + + var overlayResult = Overlays.findRayIntersection(pickRay, true, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]); + if (overlayResult.intersects) { + return; + } + var pickResults = Entities.findRayIntersection(pickRay, true); // accurate picking if (!pickResults.intersects) { // didn't click on anything diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 95c05c2717..d313d1cfa1 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -14,7 +14,7 @@ /* global getEntityCustomData, flatten, Xform, Script, Quat, Vec3, MyAvatar, Entities, Overlays, Settings, Reticle, Controller, Camera, Messages, Mat4, getControllerWorldLocation, getGrabPointSphereOffset, setGrabCommunications, - Menu, HMD */ + Menu, HMD, isInEditMode */ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ (function() { // BEGIN LOCAL_SCOPE @@ -399,7 +399,7 @@ function entityHasActions(entityID) { function findRayIntersection(pickRay, precise, include, exclude) { var entities = Entities.findRayIntersection(pickRay, precise, include, exclude, true); - var overlays = Overlays.findRayIntersection(pickRay); + var overlays = Overlays.findRayIntersection(pickRay, precise, [], [HMD.tabletID]); if (!overlays.intersects || (entities.intersects && (entities.distance <= overlays.distance))) { return entities; } @@ -644,6 +644,7 @@ EquipHotspotBuddy.prototype.updateHotspot = function(hotspot, timestamp) { // override default sphere with a user specified model, if it exists. overlayInfoSet.overlays.push(Overlays.addOverlay("model", { + name: "hotspot overlay", url: hotspot.modelURL ? hotspot.modelURL : DEFAULT_SPHERE_MODEL_URL, position: hotspot.worldPosition, rotation: { @@ -776,7 +777,7 @@ function MyController(hand) { }; this.actionID = null; // action this script created... - this.grabbedEntity = null; // on this entity. + this.grabbedThingID = null; // on this entity. this.grabbedOverlay = null; this.state = STATE_OFF; this.pointer = null; // entity-id of line object @@ -853,14 +854,19 @@ function MyController(hand) { }; this.callEntityMethodOnGrabbed = function(entityMethodName) { + if (this.grabbedIsOverlay) { + return; + } var args = [this.hand === RIGHT_HAND ? "right" : "left", MyAvatar.sessionUUID]; - Entities.callEntityMethod(this.grabbedEntity, entityMethodName, args); + Entities.callEntityMethod(this.grabbedThingID, entityMethodName, args); }; this.setState = function(newState, reason) { - if ((isInEditMode() && this.grabbedEntity !== HMD.tabletID )&& (newState !== STATE_OFF && - newState !== STATE_SEARCHING && - newState !== STATE_OVERLAY_STYLUS_TOUCHING)) { + if ((isInEditMode() && this.grabbedThingID !== HMD.tabletID) && + (newState !== STATE_OFF && + newState !== STATE_SEARCHING && + newState !== STATE_OVERLAY_STYLUS_TOUCHING && + newState !== STATE_OVERLAY_LASER_TOUCHING)) { return; } setGrabCommunications((newState === STATE_DISTANCE_HOLDING) || (newState === STATE_NEAR_GRABBING)); @@ -903,6 +909,7 @@ function MyController(hand) { if (!this.grabPointSphere) { this.grabPointSphere = Overlays.addOverlay("sphere", { + name: "grabPointSphere", localPosition: getGrabPointSphereOffset(this.handToController()), localRotation: { x: 0, y: 0, z: 0, w: 1 }, dimensions: GRAB_POINT_SPHERE_RADIUS * 2, @@ -933,6 +940,7 @@ function MyController(hand) { var brightColor = colorPow(color, 0.06); if (this.searchSphere === null) { var sphereProperties = { + name: "searchSphere", position: location, rotation: rotation, outerRadius: size * 1.2, @@ -955,7 +963,8 @@ function MyController(hand) { innerAlpha: 1.0, outerAlpha: 0.0, outerRadius: size * 1.2, - visible: true + visible: true, + ignoreRayIntersection: true }); } }; @@ -966,6 +975,7 @@ function MyController(hand) { } var stylusProperties = { + name: "stylus", url: Script.resourcesPath() + "meshes/tablet-stylus-fat.fbx", localPosition: Vec3.sum({ x: 0.0, y: WEB_TOUCH_Y_OFFSET, @@ -1000,6 +1010,7 @@ function MyController(hand) { this.overlayLineOn = function(closePoint, farPoint, color) { if (this.overlayLine === null) { var lineProperties = { + name: "line", glow: 1.0, start: closePoint, end: farPoint, @@ -1175,6 +1186,13 @@ function MyController(hand) { } } + var candidateOverlays = Overlays.findOverlays(worldHandPosition, WEB_DISPLAY_STYLUS_DISTANCE); + for (var j = 0; j < candidateOverlays.length; j++) { + if (this.isTablet(candidateOverlays[j])) { + nearWeb = true; + } + } + if (nearWeb) { this.showStylus(); var rayPickInfo = this.calcRayPickInfo(this.hand); @@ -1233,7 +1251,13 @@ function MyController(hand) { }); if (grabbableEntities.length > 0) { if (!this.grabPointIntersectsEntity) { - Controller.triggerHapticPulse(1, 20, this.hand); + // don't do haptic pulse for tablet + var nonTabletEntities = grabbableEntities.filter(function(entityID) { + return entityID != HMD.tabletID && entityID != HMD.homeButtonID; + }); + if (nonTabletEntities.length > 0) { + Controller.triggerHapticPulse(1, 20, this.hand); + } this.grabPointIntersectsEntity = true; this.grabPointSphereOn(); } @@ -1417,7 +1441,7 @@ function MyController(hand) { var okToEquipFromOtherHand = ((this.getOtherHandController().state == STATE_NEAR_GRABBING || this.getOtherHandController().state == STATE_DISTANCE_HOLDING) && - this.getOtherHandController().grabbedEntity == hotspot.entityID); + this.getOtherHandController().grabbedThingID == hotspot.entityID); var hasParent = true; if (props.parentID === NULL_UUID) { hasParent = false; @@ -1575,7 +1599,7 @@ function MyController(hand) { var farSearching = this.triggerSmoothedSqueezed() && (Date.now() - this.searchStartTime > FAR_SEARCH_DELAY); - this.grabbedEntity = null; + this.grabbedThingID = null; this.grabbedOverlay = null; this.isInitialGrab = false; this.preparingHoldRelease = false; @@ -1583,7 +1607,7 @@ function MyController(hand) { this.checkForUnexpectedChildren(); if ((this.triggerSmoothedReleased() && this.secondaryReleased())) { - this.grabbedEntity = null; + this.grabbedThingID = null; this.setState(STATE_OFF, "trigger released"); return; } @@ -1604,8 +1628,9 @@ function MyController(hand) { if (potentialEquipHotspot) { if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && holdEnabled) { this.grabbedHotspot = potentialEquipHotspot; - this.grabbedEntity = potentialEquipHotspot.entityID; - this.setState(STATE_HOLD, "equipping '" + entityPropertiesCache.getProps(this.grabbedEntity).name + "'"); + this.grabbedThingID = potentialEquipHotspot.entityID; + this.grabbedIsOverlay = false; + this.setState(STATE_HOLD, "equipping '" + entityPropertiesCache.getProps(this.grabbedThingID).name + "'"); return; } @@ -1616,6 +1641,11 @@ function MyController(hand) { return _this.entityIsNearGrabbable(entity, handPosition, NEAR_GRAB_MAX_DISTANCE); }); + var candidateOverlays = Overlays.findOverlays(handPosition, NEAR_GRAB_RADIUS); + var grabbableOverlays = candidateOverlays.filter(function(overlayID) { + return Overlays.getProperty(overlayID, "grabbable"); + }); + if (rayPickInfo.entityID) { this.intersectionDistance = rayPickInfo.distance; if (this.entityIsGrabbable(rayPickInfo.entityID) && rayPickInfo.distance < NEAR_GRAB_PICK_RADIUS) { @@ -1627,6 +1657,23 @@ function MyController(hand) { this.intersectionDistance = 0; } + if (grabbableOverlays.length > 0) { + grabbableOverlays.sort(function(a, b) { + var aPosition = Overlays.getProperty(a, "position"); + var aDistance = Vec3.distance(aPosition, handPosition); + var bPosition = Overlays.getProperty(b, "position"); + var bDistance = Vec3.distance(bPosition, handPosition); + return aDistance - bDistance; + }); + this.grabbedThingID = grabbableOverlays[0]; + this.grabbedIsOverlay = true; + if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && nearGrabEnabled) { + this.setState(STATE_NEAR_GRABBING, "near grab overlay '" + + Overlays.getProperty(this.grabbedThingID, "name") + "'"); + return; + } + } + var entity; if (grabbableEntities.length > 0) { // sort by distance @@ -1637,7 +1684,8 @@ function MyController(hand) { }); entity = grabbableEntities[0]; name = entityPropertiesCache.getProps(entity).name; - this.grabbedEntity = entity; + this.grabbedThingID = entity; + this.grabbedIsOverlay = false; if (this.entityWantsTrigger(entity)) { if (this.triggerSmoothedGrab()) { this.setState(STATE_NEAR_TRIGGER, "near trigger '" + name + "'"); @@ -1648,7 +1696,7 @@ function MyController(hand) { } else { // If near something grabbable, grab it! if ((this.triggerSmoothedGrab() || this.secondarySqueezed()) && nearGrabEnabled) { - this.setState(STATE_NEAR_GRABBING, "near grab '" + name + "'"); + this.setState(STATE_NEAR_GRABBING, "near grab entity '" + name + "'"); return; } else { // potentialNearGrabEntity = entity; @@ -1671,7 +1719,8 @@ function MyController(hand) { name = entityPropertiesCache.getProps(entity).name; if (this.entityWantsTrigger(entity)) { if (this.triggerSmoothedGrab()) { - this.grabbedEntity = entity; + this.grabbedThingID = entity; + this.grabbedIsOverlay = false; this.setState(STATE_FAR_TRIGGER, "far trigger '" + name + "'"); return; } else { @@ -1679,7 +1728,8 @@ function MyController(hand) { } } else if (this.entityIsDistanceGrabbable(rayPickInfo.entityID, handPosition)) { if (this.triggerSmoothedGrab() && !isEditing() && farGrabEnabled && farSearching) { - this.grabbedEntity = entity; + this.grabbedThingID = entity; + this.grabbedIsOverlay = false; this.grabbedDistance = rayPickInfo.distance; this.setState(STATE_DISTANCE_HOLDING, "distance hold '" + name + "'"); return; @@ -1759,7 +1809,8 @@ function MyController(hand) { Entities.sendHoverOverEntity(entity, pointerEvent); } - this.grabbedEntity = entity; + this.grabbedThingID = entity; + this.grabbedIsOverlay = false; this.setState(STATE_ENTITY_STYLUS_TOUCHING, "begin touching entity '" + name + "'"); return true; @@ -1887,7 +1938,8 @@ function MyController(hand) { } if (this.triggerSmoothedGrab() && (!isEditing() || this.isTablet(entity))) { - this.grabbedEntity = entity; + this.grabbedThingID = entity; + this.grabbedIsOverlay = false; this.setState(STATE_ENTITY_LASER_TOUCHING, "begin touching entity '" + name + "'"); return true; } @@ -1998,7 +2050,7 @@ function MyController(hand) { var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); - var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); + var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); var now = Date.now(); // add the action and initialize some variables @@ -2028,7 +2080,7 @@ function MyController(hand) { var timeScale = this.distanceGrabTimescale(this.mass, distanceToObject); this.actionID = NULL_UUID; - this.actionID = Entities.addAction("spring", this.grabbedEntity, { + this.actionID = Entities.addAction("spring", this.grabbedThingID, { targetPosition: this.currentObjectPosition, linearTimeScale: timeScale, targetRotation: this.currentObjectRotation, @@ -2053,12 +2105,12 @@ function MyController(hand) { this.ensureDynamic = function() { // if we distance hold something and keep it very still before releasing it, it ends up // non-dynamic in bullet. If it's too still, give it a little bounce so it will fall. - var props = Entities.getEntityProperties(this.grabbedEntity, ["velocity", "dynamic", "parentID"]); + var props = Entities.getEntityProperties(this.grabbedThingID, ["velocity", "dynamic", "parentID"]); if (props.dynamic && props.parentID == NULL_UUID) { var velocity = props.velocity; if (Vec3.length(velocity) < 0.05) { // see EntityMotionState.cpp DYNAMIC_LINEAR_VELOCITY_THRESHOLD velocity = { x: 0.0, y: 0.2, z:0.0 }; - Entities.editEntity(this.grabbedEntity, { velocity: velocity }); + Entities.editEntity(this.grabbedThingID, { velocity: velocity }); } } }; @@ -2080,7 +2132,7 @@ function MyController(hand) { var worldToSensorMat = Mat4.inverse(MyAvatar.getSensorToWorldMatrix()); var roomControllerPosition = Mat4.transformPoint(worldToSensorMat, worldControllerPosition); - var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); + var grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); var now = Date.now(); var deltaObjectTime = (now - this.currentObjectTime) / MSECS_PER_SEC; // convert to seconds @@ -2135,7 +2187,7 @@ function MyController(hand) { newTargetPosition = Vec3.sum(newTargetPosition, this.offsetPosition); var objectToAvatar = Vec3.subtract(this.currentObjectPosition, MyAvatar.position); - var handControllerData = getEntityCustomData('handControllerKey', this.grabbedEntity, defaultMoveWithHeadData); + var handControllerData = getEntityCustomData('handControllerKey', this.grabbedThingID, defaultMoveWithHeadData); if (handControllerData.disableMoveWithHead !== true) { // mix in head motion if (MOVE_WITH_HEAD) { @@ -2164,7 +2216,7 @@ function MyController(hand) { this.overlayLineOn(rayPickInfo.searchRay.origin, Vec3.subtract(grabbedProperties.position, this.offsetPosition), COLORS_GRAB_DISTANCE_HOLD); var distanceToObject = Vec3.length(Vec3.subtract(MyAvatar.position, this.currentObjectPosition)); - var success = Entities.updateAction(this.grabbedEntity, this.actionID, { + var success = Entities.updateAction(this.grabbedThingID, this.actionID, { targetPosition: newTargetPosition, linearTimeScale: this.distanceGrabTimescale(this.mass, distanceToObject), targetRotation: this.currentObjectRotation, @@ -2181,7 +2233,7 @@ function MyController(hand) { }; this.setupHoldAction = function() { - this.actionID = Entities.addAction("hold", this.grabbedEntity, { + this.actionID = Entities.addAction("hold", this.grabbedThingID, { hand: this.hand === RIGHT_HAND ? "right" : "left", timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, relativePosition: this.offsetPosition, @@ -2271,17 +2323,30 @@ function MyController(hand) { Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); if (this.entityActivated) { - var saveGrabbedID = this.grabbedEntity; + var saveGrabbedID = this.grabbedThingID; this.release(); - this.grabbedEntity = saveGrabbedID; + this.grabbedThingID = saveGrabbedID; } - var grabbedProperties = Entities.getEntityProperties(this.grabbedEntity, GRABBABLE_PROPERTIES); - if (FORCE_IGNORE_IK) { + var grabbedProperties; + if (this.grabbedIsOverlay) { + grabbedProperties = { + position: Overlays.getProperty(this.grabbedThingID, "position"), + rotation: Overlays.getProperty(this.grabbedThingID, "rotation"), + parentID: Overlays.getProperty(this.grabbedThingID, "parentID"), + parentJointIndex: Overlays.getProperty(this.grabbedThingID, "parentJointIndex"), + dynamic: false, + shapeType: "none" + }; this.ignoreIK = true; } else { - var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedEntity, DEFAULT_GRABBABLE_DATA); - this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true; + grabbedProperties = Entities.getEntityProperties(this.grabbedThingID, GRABBABLE_PROPERTIES); + if (FORCE_IGNORE_IK) { + this.ignoreIK = true; + } else { + var grabbableData = getEntityCustomData(GRABBABLE_DATA_KEY, this.grabbedThingID, DEFAULT_GRABBABLE_DATA); + this.ignoreIK = (grabbableData.ignoreIK !== undefined) ? grabbableData.ignoreIK : true; + } } var handRotation; @@ -2320,7 +2385,8 @@ function MyController(hand) { this.offsetPosition = Vec3.multiplyQbyV(Quat.inverse(Quat.multiply(handRotation, this.offsetRotation)), offset); } - var isPhysical = propsArePhysical(grabbedProperties) || entityHasActions(this.grabbedEntity); + var isPhysical = propsArePhysical(grabbedProperties) || + (!this.grabbedIsOverlay && entityHasActions(this.grabbedThingID)); if (isPhysical && this.state == STATE_NEAR_GRABBING && grabbedProperties.parentID === NULL_UUID) { // grab entity via action if (!this.setupHoldAction()) { @@ -2328,7 +2394,7 @@ function MyController(hand) { } Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'grab', - grabbedEntity: this.grabbedEntity, + grabbedEntity: this.grabbedThingID, joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); } else { @@ -2353,29 +2419,36 @@ function MyController(hand) { reparentProps.localPosition = this.offsetPosition; reparentProps.localRotation = this.offsetRotation; } - Entities.editEntity(this.grabbedEntity, reparentProps); + + if (this.grabbedIsOverlay) { + Overlays.editOverlay(this.grabbedThingID, reparentProps); + } else { + Entities.editEntity(this.grabbedThingID, reparentProps); + } if (this.thisHandIsParent(grabbedProperties)) { // this should never happen, but if it does, don't set previous parent to be this hand. - // this.previousParentID[this.grabbedEntity] = NULL; - // this.previousParentJointIndex[this.grabbedEntity] = -1; + // this.previousParentID[this.grabbedThingID] = NULL; + // this.previousParentJointIndex[this.grabbedThingID] = -1; } else { - this.previousParentID[this.grabbedEntity] = grabbedProperties.parentID; - this.previousParentJointIndex[this.grabbedEntity] = grabbedProperties.parentJointIndex; + this.previousParentID[this.grabbedThingID] = grabbedProperties.parentID; + this.previousParentJointIndex[this.grabbedThingID] = grabbedProperties.parentJointIndex; } Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'equip', - grabbedEntity: this.grabbedEntity, + grabbedEntity: this.grabbedThingID, joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); } - Entities.editEntity(this.grabbedEntity, { - velocity: { x: 0, y: 0, z: 0 }, - angularVelocity: { x: 0, y: 0, z: 0 }, - // dynamic: false - }); + if (!this.grabbedIsOverlay) { + Entities.editEntity(this.grabbedThingID, { + velocity: { x: 0, y: 0, z: 0 }, + angularVelocity: { x: 0, y: 0, z: 0 }, + // dynamic: false + }); + } if (this.state == STATE_NEAR_GRABBING) { this.callEntityMethodOnGrabbed("startNearGrab"); @@ -2441,26 +2514,39 @@ function MyController(hand) { if (dropDetected && !this.waitForTriggerRelease && this.triggerSmoothedGrab()) { // store the offset attach points into preferences. - if (USE_ATTACH_POINT_SETTINGS && this.grabbedHotspot && this.grabbedEntity) { - var prefprops = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "localRotation"]); + if (USE_ATTACH_POINT_SETTINGS && this.grabbedHotspot && this.grabbedThingID) { + var prefprops = Entities.getEntityProperties(this.grabbedThingID, ["localPosition", "localRotation"]); if (prefprops && prefprops.localPosition && prefprops.localRotation) { storeAttachPointForHotspotInSettings(this.grabbedHotspot, this.hand, prefprops.localPosition, prefprops.localRotation); } } - var grabbedEntity = this.grabbedEntity; + var grabbedEntity = this.grabbedThingID; this.release(); - this.grabbedEntity = grabbedEntity; + this.grabbedThingID = grabbedEntity; this.setState(STATE_NEAR_GRABBING, "drop gesture detected"); return; } this.prevDropDetected = dropDetected; } - var props = Entities.getEntityProperties(this.grabbedEntity, ["localPosition", "parentID", "parentJointIndex", + var props; + if (this.grabbedIsOverlay) { + props = { + localPosition: Overlays.getProperty(this.grabbedThingID, "localPosition"), + parentID: Overlays.getProperty(this.grabbedThingID, "parentID"), + parentJointIndex: Overlays.getProperty(this.grabbedThingID, "parentJointIndex"), + position: Overlays.getProperty(this.grabbedThingID, "position"), + rotation: Overlays.getProperty(this.grabbedThingID, "rotation"), + dimensions: Overlays.getProperty(this.grabbedThingID, "dimensions"), + registrationPoint: { x: 0.5, y: 0.5, z: 0.5 } + }; + } else { + props = Entities.getEntityProperties(this.grabbedThingID, ["localPosition", "parentID", "parentJointIndex", "position", "rotation", "dimensions", "registrationPoint"]); + } if (!props.position) { // server may have reset, taking our equipped entity with it. move back to "off" state this.callEntityMethodOnGrabbed("releaseGrab"); @@ -2472,7 +2558,7 @@ function MyController(hand) { // someone took it from us or otherwise edited the parentID. end the grab. We don't do this // for equipped things so that they can be adjusted while equipped. this.callEntityMethodOnGrabbed("releaseGrab"); - this.grabbedEntity = null; + this.grabbedThingID = null; this.setState(STATE_OFF, "someone took it"); return; } @@ -2554,7 +2640,7 @@ function MyController(hand) { if (this.actionID && this.actionTimeout - now < ACTION_TTL_REFRESH * MSECS_PER_SEC) { // if less than a 5 seconds left, refresh the actions ttl - var success = Entities.updateAction(this.grabbedEntity, this.actionID, { + var success = Entities.updateAction(this.grabbedThingID, this.actionID, { hand: this.hand === RIGHT_HAND ? "right" : "left", timeScale: NEAR_GRABBING_ACTION_TIMEFRAME, relativePosition: this.offsetPosition, @@ -2568,14 +2654,14 @@ function MyController(hand) { this.actionTimeout = now + (ACTION_TTL * MSECS_PER_SEC); } else { print("continueNearGrabbing -- updateAction failed"); - Entities.deleteAction(this.grabbedEntity, this.actionID); + Entities.deleteAction(this.grabbedThingID, this.actionID); this.setupHoldAction(); } } }; this.maybeScale = function(props) { - if (!objectScalingEnabled || this.isTablet(this.grabbedEntity)) { + if (!objectScalingEnabled || this.isTablet(this.grabbedThingID) || this.grabbedIsOverlay) { return; } @@ -2597,7 +2683,7 @@ function MyController(hand) { this.getOtherHandController().getHandPosition())); var currentRescale = scalingCurrentDistance / this.scalingStartDistance; var newDimensions = Vec3.multiply(currentRescale, this.scalingStartDimensions); - Entities.editEntity(this.grabbedEntity, { dimensions: newDimensions }); + Entities.editEntity(this.grabbedThingID, { dimensions: newDimensions }); } }; @@ -2648,7 +2734,7 @@ function MyController(hand) { this.nearTrigger = function(deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopNearTrigger"); - this.grabbedEntity = null; + this.grabbedThingID = null; this.setState(STATE_OFF, "trigger released"); return; } @@ -2658,7 +2744,7 @@ function MyController(hand) { this.farTrigger = function(deltaTime, timestamp) { if (this.triggerSmoothedReleased()) { this.callEntityMethodOnGrabbed("stopFarTrigger"); - this.grabbedEntity = null; + this.grabbedThingID = null; this.setState(STATE_OFF, "trigger released"); return; } @@ -2673,9 +2759,9 @@ function MyController(hand) { var intersection = findRayIntersection(pickRay, true, [], [], true); if (intersection.accurate || intersection.overlayID) { this.lastPickTime = now; - if (intersection.entityID != this.grabbedEntity) { + if (intersection.entityID != this.grabbedThingID) { this.callEntityMethodOnGrabbed("stopFarTrigger"); - this.grabbedEntity = null; + this.grabbedThingID = null; this.setState(STATE_OFF, "laser moved off of entity"); return; } @@ -2697,13 +2783,13 @@ function MyController(hand) { this.entityTouchingEnter = function() { // test for intersection between controller laser and web entity plane. - var intersectInfo = handLaserIntersectEntity(this.grabbedEntity, + var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, getControllerWorldLocation(this.handToController(), true)); if (intersectInfo) { var pointerEvent = { type: "Press", id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(this.grabbedEntity, intersectInfo.point), + pos2D: projectOntoEntityXYPlane(this.grabbedThingID, intersectInfo.point), pos3D: intersectInfo.point, normal: intersectInfo.normal, direction: intersectInfo.searchRay.direction, @@ -2711,8 +2797,8 @@ function MyController(hand) { isPrimaryHeld: true }; - Entities.sendMousePressOnEntity(this.grabbedEntity, pointerEvent); - Entities.sendClickDownOnEntity(this.grabbedEntity, pointerEvent); + Entities.sendMousePressOnEntity(this.grabbedThingID, pointerEvent); + Entities.sendClickDownOnEntity(this.grabbedThingID, pointerEvent); this.touchingEnterTimer = 0; this.touchingEnterPointerEvent = pointerEvent; @@ -2734,7 +2820,7 @@ function MyController(hand) { this.entityTouchingExit = function() { // test for intersection between controller laser and web entity plane. - var intersectInfo = handLaserIntersectEntity(this.grabbedEntity, + var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, getControllerWorldLocation(this.handToController(), true)); if (intersectInfo) { var pointerEvent; @@ -2742,7 +2828,7 @@ function MyController(hand) { pointerEvent = { type: "Release", id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(this.grabbedEntity, intersectInfo.point), + pos2D: projectOntoEntityXYPlane(this.grabbedThingID, intersectInfo.point), pos3D: intersectInfo.point, normal: intersectInfo.normal, direction: intersectInfo.searchRay.direction, @@ -2755,11 +2841,11 @@ function MyController(hand) { pointerEvent.isPrimaryHeld = false; } - Entities.sendMouseReleaseOnEntity(this.grabbedEntity, pointerEvent); - Entities.sendClickReleaseOnEntity(this.grabbedEntity, pointerEvent); - Entities.sendHoverLeaveEntity(this.grabbedEntity, pointerEvent); + Entities.sendMouseReleaseOnEntity(this.grabbedThingID, pointerEvent); + Entities.sendClickReleaseOnEntity(this.grabbedThingID, pointerEvent); + Entities.sendHoverLeaveEntity(this.grabbedThingID, pointerEvent); } - this.grabbedEntity = null; + this.grabbedThingID = null; this.grabbedOverlay = null; }; @@ -2767,7 +2853,7 @@ function MyController(hand) { this.touchingEnterTimer += dt; - entityPropertiesCache.addEntity(this.grabbedEntity); + entityPropertiesCache.addEntity(this.grabbedThingID); if (this.state == STATE_ENTITY_LASER_TOUCHING && !this.triggerSmoothedGrab()) { this.setState(STATE_OFF, "released trigger"); @@ -2775,7 +2861,7 @@ function MyController(hand) { } // test for intersection between controller laser and web entity plane. - var intersectInfo = handLaserIntersectEntity(this.grabbedEntity, + var intersectInfo = handLaserIntersectEntity(this.grabbedThingID, getControllerWorldLocation(this.handToController(), true)); if (intersectInfo) { @@ -2785,15 +2871,15 @@ function MyController(hand) { return; } - if (Entities.keyboardFocusEntity != this.grabbedEntity) { + if (Entities.keyboardFocusEntity != this.grabbedThingID) { Overlays.keyboardFocusOverlay = 0; - Entities.keyboardFocusEntity = this.grabbedEntity; + Entities.keyboardFocusEntity = this.grabbedThingID; } var pointerEvent = { type: "Move", id: this.hand + 1, // 0 is reserved for hardware mouse - pos2D: projectOntoEntityXYPlane(this.grabbedEntity, intersectInfo.point), + pos2D: projectOntoEntityXYPlane(this.grabbedThingID, intersectInfo.point), pos3D: intersectInfo.point, normal: intersectInfo.normal, direction: intersectInfo.searchRay.direction, @@ -2804,8 +2890,8 @@ function MyController(hand) { var POINTER_PRESS_TO_MOVE_DELAY = 0.25; // seconds if (this.deadspotExpired || this.touchingEnterTimer > POINTER_PRESS_TO_MOVE_DELAY || Vec3.distance(intersectInfo.point, this.touchingEnterPointerEvent.pos3D) > this.deadspotRadius) { - Entities.sendMouseMoveOnEntity(this.grabbedEntity, pointerEvent); - Entities.sendHoldingClickOnEntity(this.grabbedEntity, pointerEvent); + Entities.sendMouseMoveOnEntity(this.grabbedThingID, pointerEvent); + Entities.sendHoldingClickOnEntity(this.grabbedThingID, pointerEvent); this.deadspotExpired = true; } @@ -2815,7 +2901,7 @@ function MyController(hand) { } Reticle.setVisible(false); } else { - this.grabbedEntity = null; + this.grabbedThingID = null; this.setState(STATE_OFF, "grabbed entity was destroyed"); return; } @@ -2900,7 +2986,7 @@ function MyController(hand) { Overlays.sendMouseReleaseOnOverlay(this.grabbedOverlay, pointerEvent); Overlays.sendHoverLeaveOverlay(this.grabbedOverlay, pointerEvent); } - this.grabbedEntity = null; + this.grabbedThingID = null; this.grabbedOverlay = null; }; @@ -2923,7 +3009,7 @@ function MyController(hand) { if (this.state == STATE_OVERLAY_STYLUS_TOUCHING && intersectInfo.distance > WEB_STYLUS_LENGTH / 2.0 + WEB_TOUCH_Y_OFFSET + WEB_TOUCH_Y_TOUCH_DEADZONE_SIZE) { - this.grabbedEntity = null; + this.grabbedThingID = null; this.setState(STATE_OFF, "pulled away from overlay"); return; } @@ -2980,7 +3066,7 @@ function MyController(hand) { } Reticle.setVisible(false); } else { - this.grabbedEntity = null; + this.grabbedThingID = null; this.setState(STATE_OFF, "grabbed overlay was destroyed"); return; } @@ -2989,7 +3075,7 @@ function MyController(hand) { this.release = function() { this.turnOffVisualizations(); - if (this.grabbedEntity !== null) { + if (this.grabbedThingID !== null) { if (this.state === STATE_HOLD) { this.callEntityMethodOnGrabbed("releaseEquip"); } @@ -2997,35 +3083,49 @@ function MyController(hand) { // Make a small release haptic pulse if we really were holding something Controller.triggerHapticPulse(HAPTIC_PULSE_STRENGTH, HAPTIC_PULSE_DURATION, this.hand); if (this.actionID !== null) { - Entities.deleteAction(this.grabbedEntity, this.actionID); + Entities.deleteAction(this.grabbedThingID, this.actionID); } else { // no action, so it's a parenting grab - if (this.previousParentID[this.grabbedEntity] === NULL_UUID) { - Entities.editEntity(this.grabbedEntity, { - parentID: this.previousParentID[this.grabbedEntity], - parentJointIndex: this.previousParentJointIndex[this.grabbedEntity] - }); - this.ensureDynamic(); + if (this.previousParentID[this.grabbedThingID] === NULL_UUID) { + if (this.grabbedIsOverlay) { + Overlays.editOverlay(this.grabbedThingID, { + parentID: NULL_UUID, + parentJointIndex: -1 + }); + } else { + Entities.editEntity(this.grabbedThingID, { + parentID: this.previousParentID[this.grabbedThingID], + parentJointIndex: this.previousParentJointIndex[this.grabbedThingID] + }); + this.ensureDynamic(); + } } else { - // we're putting this back as a child of some other parent, so zero its velocity - Entities.editEntity(this.grabbedEntity, { - parentID: this.previousParentID[this.grabbedEntity], - parentJointIndex: this.previousParentJointIndex[this.grabbedEntity], - velocity: {x: 0, y: 0, z: 0}, - angularVelocity: {x: 0, y: 0, z: 0} - }); + if (this.grabbedIsOverlay) { + Overlays.editOverlay(this.grabbedThingID, { + parentID: this.previousParentID[this.grabbedThingID], + parentJointIndex: this.previousParentJointIndex[this.grabbedThingID], + }); + } else { + // we're putting this back as a child of some other parent, so zero its velocity + Entities.editEntity(this.grabbedThingID, { + parentID: this.previousParentID[this.grabbedThingID], + parentJointIndex: this.previousParentJointIndex[this.grabbedThingID], + velocity: {x: 0, y: 0, z: 0}, + angularVelocity: {x: 0, y: 0, z: 0} + }); + } } } Messages.sendMessage('Hifi-Object-Manipulation', JSON.stringify({ action: 'release', - grabbedEntity: this.grabbedEntity, + grabbedEntity: this.grabbedThingID, joint: this.hand === RIGHT_HAND ? "RightHand" : "LeftHand" })); } this.actionID = null; - this.grabbedEntity = null; + this.grabbedThingID = null; this.grabbedOverlay = null; this.grabbedHotspot = null; @@ -3113,9 +3213,13 @@ function MyController(hand) { } _this.previouslyUnhooked[childID] = now; + // we don't know if it's an entity or an overlay Entities.editEntity(childID, { parentID: previousParentID, parentJointIndex: previousParentJointIndex }); + Overlays.editOverlay(childID, { parentID: previousParentID, parentJointIndex: previousParentJointIndex }); + } else { Entities.editEntity(childID, { parentID: NULL_UUID }); + Overlays.editOverlay(childID, { parentID: NULL_UUID }); } } }); @@ -3234,8 +3338,8 @@ var handleHandMessages = function(channel, message, sender) { selectedController.release(); var wearableEntity = data.entityID; entityPropertiesCache.addEntity(wearableEntity); - selectedController.grabbedEntity = wearableEntity; - var hotspots = selectedController.collectEquipHotspots(selectedController.grabbedEntity); + selectedController.grabbedThingID = wearableEntity; + var hotspots = selectedController.collectEquipHotspots(selectedController.grabbedThingID); if (hotspots.length > 0) { if (hotspotIndex >= hotspots.length) { hotspotIndex = 0; diff --git a/scripts/system/controllers/squeezeHands.js b/scripts/system/controllers/squeezeHands.js index 3f1d21b46c..75e6249dd6 100644 --- a/scripts/system/controllers/squeezeHands.js +++ b/scripts/system/controllers/squeezeHands.js @@ -25,10 +25,13 @@ var OVERLAY_RAMP_RATE = 8.0; var animStateHandlerID; -var isPointingIndex = false; +var isBothIndexesPointing = false; var HIFI_POINT_INDEX_MESSAGE_CHANNEL = "Hifi-Point-Index"; -var indexfingerJointNames = ["LeftHandIndex1", "LeftHandIndex2", "LeftHandIndex3", "RightHandIndex1", "RightHandIndex2", "RightHandIndex3"]; +var isLeftIndexPointing = false; +var isRightIndexPointing = false; +var isLeftThumbRaised = false; +var isRightThumbRaised = false; function clamp(val, min, max) { return Math.min(Math.max(val, min), max); @@ -46,17 +49,32 @@ function init() { Script.update.connect(update); animStateHandlerID = MyAvatar.addAnimationStateHandler( animStateHandler, - ["leftHandOverlayAlpha", "rightHandOverlayAlpha", "leftHandGraspAlpha", "rightHandGraspAlpha"] + [ + "leftHandOverlayAlpha", "leftHandGraspAlpha", + "rightHandOverlayAlpha", "rightHandGraspAlpha", + "isLeftHandGrasp", "isLeftIndexPoint", "isLeftThumbRaise", "isLeftIndexPointAndThumbRaise", + "isRightHandGrasp", "isRightIndexPoint", "isRightThumbRaise", "isRightIndexPointAndThumbRaise", + ] ); Messages.subscribe(HIFI_POINT_INDEX_MESSAGE_CHANNEL); Messages.messageReceived.connect(handleMessages); } function animStateHandler(props) { - return { leftHandOverlayAlpha: leftHandOverlayAlpha, - leftHandGraspAlpha: lastLeftTrigger, - rightHandOverlayAlpha: rightHandOverlayAlpha, - rightHandGraspAlpha: lastRightTrigger }; + return { + leftHandOverlayAlpha: leftHandOverlayAlpha, + leftHandGraspAlpha: lastLeftTrigger, + rightHandOverlayAlpha: rightHandOverlayAlpha, + rightHandGraspAlpha: lastRightTrigger, + isLeftHandGrasp: !isBothIndexesPointing && !isLeftIndexPointing && !isLeftThumbRaised, + isLeftIndexPoint: (isBothIndexesPointing || isLeftIndexPointing) && !isLeftThumbRaised, + isLeftThumbRaise: !isBothIndexesPointing && !isLeftIndexPointing && isLeftThumbRaised, + isLeftIndexPointAndThumbRaise: (isBothIndexesPointing || isLeftIndexPointing) && isLeftThumbRaised, + isRightHandGrasp: !isBothIndexesPointing && !isRightIndexPointing && !isRightThumbRaised, + isRightIndexPoint: (isBothIndexesPointing || isRightIndexPointing) && !isRightThumbRaised, + isRightThumbRaise: !isBothIndexesPointing && !isRightIndexPointing && isRightThumbRaised, + isRightIndexPointAndThumbRaise: (isBothIndexesPointing || isRightIndexPointing) && isRightThumbRaised + }; } function update(dt) { @@ -84,13 +102,11 @@ function update(dt) { rightHandOverlayAlpha = clamp(rightHandOverlayAlpha - OVERLAY_RAMP_RATE * dt, 0, 1); } - // Point index finger. - if (isPointingIndex) { - var zeroRotation = { x: 0, y: 0, z: 0, w: 1 }; - for (var i = 0; i < indexfingerJointNames.length; i++) { - MyAvatar.setJointRotation(indexfingerJointNames[i], zeroRotation); - } - } + // Pointing index fingers and raising thumbs + isLeftIndexPointing = leftHandPose.valid && Controller.getValue(Controller.Standard.LeftIndexPoint) === 1; + isRightIndexPointing = rightHandPose.valid && Controller.getValue(Controller.Standard.RightIndexPoint) === 1; + isLeftThumbRaised = leftHandPose.valid && Controller.getValue(Controller.Standard.LeftThumbUp) === 1; + isRightThumbRaised = rightHandPose.valid && Controller.getValue(Controller.Standard.RightThumbUp) === 1; } function handleMessages(channel, message, sender) { @@ -98,13 +114,7 @@ function handleMessages(channel, message, sender) { var data = JSON.parse(message); if (data.pointIndex !== undefined) { print("pointIndex: " + data.pointIndex); - isPointingIndex = data.pointIndex; - - if (!isPointingIndex) { - for (var i = 0; i < indexfingerJointNames.length; i++) { - MyAvatar.clearJointData(indexfingerJointNames[i]); - } - } + isBothIndexesPointing = data.pointIndex; } } } diff --git a/scripts/system/edit.js b/scripts/system/edit.js index da39edf8ba..ad3af3a659 100644 --- a/scripts/system/edit.js +++ b/scripts/system/edit.js @@ -564,6 +564,11 @@ function findClickedEntity(event) { var pickRay = Camera.computePickRay(event.x, event.y); + var overlayResult = Overlays.findRayIntersection(pickRay, true, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]); + if (overlayResult.intersects) { + return null; + } + var entityResult = Entities.findRayIntersection(pickRay, true); // want precision picking var lightResult = lightOverlayManager.findRayIntersection(pickRay); lightResult.accurate = true; diff --git a/scripts/system/libraries/WebTablet.js b/scripts/system/libraries/WebTablet.js index 0990440801..dd2aaf346b 100644 --- a/scripts/system/libraries/WebTablet.js +++ b/scripts/system/libraries/WebTablet.js @@ -7,8 +7,8 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -/* global getControllerWorldLocation, setEntityCustomData, Tablet, WebTablet:true, HMD, Settings, Script, - Vec3, Quat, MyAvatar, Entities, Overlays, Camera, Messages, Xform, clamp */ +/* global getControllerWorldLocation, Tablet, WebTablet:true, HMD, Settings, Script, + Vec3, Quat, MyAvatar, Entities, Overlays, Camera, Messages, Xform, clamp, Controller, Mat4 */ Script.include(Script.resolvePath("../libraries/utils.js")); Script.include(Script.resolvePath("../libraries/controllers.js")); @@ -34,7 +34,7 @@ var TABLET_NATURAL_DIMENSIONS = {x: 33.797, y: 50.129, z: 2.269}; var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png"; // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-close.png"; var TABLET_MODEL_PATH = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx"; -// var TABLET_MODEL_PATH = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx"; +var LOCAL_TABLET_MODEL_PATH = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx"; // returns object with two fields: // * position - position in front of the user @@ -49,24 +49,30 @@ function calcSpawnInfo(hand, height) { var handController = getControllerWorldLocation(hand, true); var controllerPosition = handController.position; - // compute the angle of the chord with length (height / 2) - var theta = Math.asin(height / (2 * Vec3.distance(headPos, controllerPosition))); + // base of the tablet is slightly above controller position + var TABLET_BASE_DISPLACEMENT = {x: 0, y: 0.1, z: 0}; + var tabletBase = Vec3.sum(controllerPosition, TABLET_BASE_DISPLACEMENT); - // then we can use this angle to rotate the vector between the HMD position and the center of the tablet. - // this vector, u, will become our new look at direction. - var d = Vec3.normalize(Vec3.subtract(headPos, controllerPosition)); + var d = Vec3.subtract(headPos, tabletBase); + var theta = Math.acos(d.y / Vec3.length(d)); + d.y = 0; + if (Vec3.length(d) < 0.0001) { + d = {x: 1, y: 0, z: 0}; + } else { + d = Vec3.normalize(d); + } var w = Vec3.normalize(Vec3.cross(Y_AXIS, d)); - var q = Quat.angleAxis(theta * (180 / Math.PI), w); + var ANGLE_OFFSET = 25; + var q = Quat.angleAxis(theta * (180 / Math.PI) - (90 - ANGLE_OFFSET), w); var u = Vec3.multiplyQbyV(q, d); // use u to compute a full lookAt quaternion. - var lookAtRot = Quat.lookAt(controllerPosition, Vec3.sum(controllerPosition, u), Y_AXIS); - - // adjust the tablet position by a small amount. - var yDisplacement = (height / 2) + 0.1; + var lookAtRot = Quat.lookAt(tabletBase, Vec3.sum(tabletBase, u), Y_AXIS); + var yDisplacement = (height / 2); var zDisplacement = 0.05; var tabletOffset = Vec3.multiplyQbyV(lookAtRot, {x: 0, y: yDisplacement, z: zDisplacement}); - finalPosition = Vec3.sum(controllerPosition, tabletOffset); + finalPosition = Vec3.sum(tabletBase, tabletOffset); + return { position: finalPosition, rotation: lookAtRot @@ -106,10 +112,19 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { this.dpi = DEFAULT_DPI * (DEFAULT_WIDTH / this.width); } + var modelURL; + if (Settings.getValue("tabletVisibleToOthers")) { + modelURL = TABLET_MODEL_PATH; + } else { + modelURL = LOCAL_TABLET_MODEL_PATH; + } + var tabletProperties = { name: "WebTablet Tablet", type: "Model", - modelURL: TABLET_MODEL_PATH, + modelURL: modelURL, + url: modelURL, // for overlay + grabbable: true, // for overlay userData: JSON.stringify({ "grabbableKey": {"grabbable": true} }), @@ -121,7 +136,14 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { this.calculateTabletAttachmentProperties(hand, true, tabletProperties); this.cleanUpOldTablets(); - this.tabletEntityID = Entities.addEntity(tabletProperties, clientOnly); + + if (Settings.getValue("tabletVisibleToOthers")) { + this.tabletEntityID = Entities.addEntity(tabletProperties, clientOnly); + this.tabletIsOverlay = false; + } else { + this.tabletEntityID = Overlays.addOverlay("model", tabletProperties); + this.tabletIsOverlay = true; + } if (this.webOverlayID) { Overlays.deleteOverlay(this.webOverlayID); @@ -145,12 +167,12 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { isAA: HMD.active }); - var HOME_BUTTON_Y_OFFSET = (this.height / 2) - 0.035; - this.homeButtonEntity = Overlays.addOverlay("sphere", { + var HOME_BUTTON_Y_OFFSET = (this.height / 2) - 0.009; + this.homeButtonID = Overlays.addOverlay("sphere", { name: "homeButton", - localPosition: {x: 0.0, y: -HOME_BUTTON_Y_OFFSET, z: -0.01}, + localPosition: {x: -0.001, y: -HOME_BUTTON_Y_OFFSET, z: 0.0}, localRotation: Quat.angleAxis(0, Y_AXIS), - dimensions: { x: 0.04, y: 0.04, z: 0.02}, + dimensions: { x: 4 * tabletScaleFactor, y: 4 * tabletScaleFactor, z: 4 * tabletScaleFactor}, alpha: 0.0, visible: true, drawInFront: false, @@ -159,7 +181,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { }); this.receive = function (channel, senderID, senderUUID, localOnly) { - if (_this.homeButtonEntity == senderID) { + if (_this.homeButtonID == senderID) { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var onHomeScreen = tablet.onHomeScreen(); if (onHomeScreen) { @@ -178,7 +200,16 @@ WebTablet = function (url, width, dpi, hand, clientOnly) { }; this.getLocation = function() { - return Entities.getEntityProperties(_this.tabletEntityID, ["localPosition", "localRotation"]); + if (this.tabletIsOverlay) { + var location = Overlays.getProperty(this.tabletEntityID, "localPosition"); + var orientation = Overlays.getProperty(this.tabletEntityID, "localOrientation"); + return { + localPosition: location, + localRotation: orientation + }; + } else { + return Entities.getEntityProperties(_this.tabletEntityID, ["localPosition", "localRotation"]); + } }; this.clicked = false; @@ -236,8 +267,12 @@ WebTablet.prototype.getOverlayObject = function () { WebTablet.prototype.destroy = function () { Overlays.deleteOverlay(this.webOverlayID); - Entities.deleteEntity(this.tabletEntityID); - Overlays.deleteOverlay(this.homeButtonEntity); + if (this.tabletIsOverlay) { + Overlays.deleteOverlay(this.tabletEntityID); + } else { + Entities.deleteEntity(this.tabletEntityID); + } + Overlays.deleteOverlay(this.homeButtonID); HMD.displayModeChanged.disconnect(this.myOnHmdChanged); Controller.mousePressEvent.disconnect(this.myMousePressEvent); @@ -420,10 +455,16 @@ WebTablet.prototype.getPosition = function () { WebTablet.prototype.mousePressEvent = function (event) { var pickRay = Camera.computePickRay(event.x, event.y); - var entityPickResults = Entities.findRayIntersection(pickRay, true, [this.tabletEntityID]); // non-accurate picking - if (entityPickResults.intersects && entityPickResults.entityID === this.tabletEntityID) { - var overlayPickResults = Overlays.findRayIntersection(pickRay); - if (overlayPickResults.intersects && overlayPickResults.overlayID === HMD.homeButtonID) { + var entityPickResults; + if (this.tabletIsOverlay) { + entityPickResults = Overlays.findRayIntersection(pickRay, true, [this.tabletEntityID]); + } else { + entityPickResults = Entities.findRayIntersection(pickRay, true, [this.tabletEntityID]); + } + if (entityPickResults.intersects && (entityPickResults.entityID === this.tabletEntityID || + entityPickResults.overlayID === this.tabletEntityID)) { + var overlayPickResults = Overlays.findRayIntersection(pickRay, true, [this.webOverlayID, this.homeButtonID], []); + if (overlayPickResults.intersects && overlayPickResults.overlayID === this.homeButtonID) { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var onHomeScreen = tablet.onHomeScreen(); if (onHomeScreen) { @@ -432,11 +473,15 @@ WebTablet.prototype.mousePressEvent = function (event) { tablet.gotoHomeScreen(); this.setHomeButtonTexture(); } - } else if (!HMD.active && (!overlayPickResults.intersects || !overlayPickResults.overlayID === this.webOverlayID)) { + } else if (!HMD.active && (!overlayPickResults.intersects || overlayPickResults.overlayID !== this.webOverlayID)) { this.dragging = true; var invCameraXform = new Xform(Camera.orientation, Camera.position).inv(); this.initialLocalIntersectionPoint = invCameraXform.xformPoint(entityPickResults.intersection); - this.initialLocalPosition = Entities.getEntityProperties(this.tabletEntityID, ["localPosition"]).localPosition; + if (this.tabletIsOverlay) { + this.initialLocalPosition = Overlays.getProperty(this.tabletEntityID, "localPosition"); + } else { + this.initialLocalPosition = Entities.getEntityProperties(this.tabletEntityID, ["localPosition"]).localPosition; + } } } }; @@ -482,9 +527,15 @@ WebTablet.prototype.mouseMoveEvent = function (event) { var localIntersectionPoint = Vec3.sum(localPickRay.origin, Vec3.multiply(localPickRay.direction, result.distance)); var localOffset = Vec3.subtract(localIntersectionPoint, this.initialLocalIntersectionPoint); var localPosition = Vec3.sum(this.initialLocalPosition, localOffset); - Entities.editEntity(this.tabletEntityID, { - localPosition: localPosition - }); + if (this.tabletIsOverlay) { + Overlays.editOverlay(this.tabletEntityID, { + localPosition: localPosition + }); + } else { + Entities.editEntity(this.tabletEntityID, { + localPosition: localPosition + }); + } } } }; diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index b9bae72d14..9c1626caf4 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -3866,6 +3866,12 @@ SelectionDisplay = (function() { var somethingClicked = false; var pickRay = generalComputePickRay(event.x, event.y); + var result = Overlays.findRayIntersection(pickRay, true, [HMD.tabletID, HMD.tabletScreenID, HMD.homeButtonID]); + if (result.intersects) { + // mouse clicks on the tablet should override the edit affordances + return false; + } + // before we do a ray test for grabbers, disable the ray intersection for our selection box Overlays.editOverlay(selectionBox, { ignoreRayIntersection: true diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index c5ce5a634b..68da7696be 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -52,10 +52,16 @@ function onMessageBoxClosed(id, button) { Window.messageBoxClosed.connect(onMessageBoxClosed); +var shouldActivateButton = false; +var onMarketplaceScreen = false; + function showMarketplace() { UserActivityLogger.openedMarketplace(); + shouldActivateButton = true; tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); + onMarketplaceScreen = true; + tablet.webEventReceived.connect(function (message) { if (message === GOTO_DIRECTORY) { @@ -98,15 +104,10 @@ function showMarketplace() { }); } -function toggleMarketplace() { - var entity = HMD.tabletID; - Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); - showMarketplace(); -} - var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var marketplaceButton = tablet.addButton({ icon: "icons/tablet-icons/market-i.svg", + activeIcon: "icons/tablet-icons/market-a.svg", text: "MARKET", sortOrder: 9 }); @@ -117,16 +118,30 @@ function onCanWriteAssetsChanged() { } function onClick() { - toggleMarketplace(); + if (onMarketplaceScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + var entity = HMD.tabletID; + Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); + showMarketplace(); + } +} + +function onScreenChanged(type, url) { + // for toolbar mode: change button to active when window is first openend, false otherwise. + marketplaceButton.editProperties({isActive: shouldActivateButton}); + shouldActivateButton = false; + onMarketplaceScreen = false; } marketplaceButton.clicked.connect(onClick); +tablet.screenChanged.connect(onScreenChanged); Entities.canWriteAssetsChanged.connect(onCanWriteAssetsChanged); Script.scriptEnding.connect(function () { - if (tablet) { - tablet.removeButton(marketplaceButton); - } + tablet.removeButton(marketplaceButton); + tablet.screenChanged.disconnect(onScreenChanged); Entities.canWriteAssetsChanged.disconnect(onCanWriteAssetsChanged); }); diff --git a/scripts/system/menu.js b/scripts/system/menu.js index 13c6ce1e0d..1d5f8bccd6 100644 --- a/scripts/system/menu.js +++ b/scripts/system/menu.js @@ -10,26 +10,46 @@ // var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; -//var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; +// var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; (function() { var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ icon: "icons/tablet-icons/menu-i.svg", + activeIcon: "icons/tablet-icons/menu-a.svg", text: "MENU", sortOrder: 3 }); + var shouldActivateButton = false; + var onMenuScreen = false; + function onClicked() { - var entity = HMD.tabletID; - Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); - tablet.gotoMenuScreen(); + if (onMenuScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + var entity = HMD.tabletID; + Entities.editEntity(entity, {textures: JSON.stringify({"tex.close": HOME_BUTTON_TEXTURE})}); + shouldActivateButton = true; + tablet.gotoMenuScreen(); + onMenuScreen = true; + } + } + + function onScreenChanged(type, url) { + // for toolbar mode: change button to active when window is first openend, false otherwise. + button.editProperties({isActive: shouldActivateButton}); + shouldActivateButton = false; + onMenuScreen = false; } button.clicked.connect(onClicked); + tablet.screenChanged.connect(onScreenChanged); Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); tablet.removeButton(button); + tablet.screenChanged.disconnect(onScreenChanged); }); }()); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index d47544e0f0..9df4b2df92 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -37,6 +37,15 @@ var conserveResources = true; Script.include("/~/system/libraries/controllers.js"); +function projectVectorOntoPlane(normalizedVector, planeNormal) { + return Vec3.cross(planeNormal, Vec3.cross(normalizedVector, planeNormal)); +} +function angleBetweenVectorsInPlane(from, to, normal) { + var projectedFrom = projectVectorOntoPlane(from, normal); + var projectedTo = projectVectorOntoPlane(to, normal); + return Vec3.orientedAngle(projectedFrom, projectedTo, normal); +} + // // Overlays. // @@ -229,7 +238,11 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See break; case 'refresh': removeOverlays(); - populateUserList(message.params); + // If filter is specified from .qml instead of through settings, update the settings. + if (message.params.filter !== undefined) { + Settings.setValue('pal/filtered', !!message.params.filter); + } + populateUserList(message.params.selected); UserActivityLogger.palAction("refresh", ""); break; case 'updateGain': @@ -271,13 +284,42 @@ function addAvatarNode(id) { color: color(selected, false, 0.0), ignoreRayIntersection: false}, selected, !conserveResources); } +// Each open/refresh will capture a stable set of avatarsOfInterest, within the specified filter. +var avatarsOfInterest = {}; function populateUserList(selectData) { + var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')}; var data = [], avatars = AvatarList.getAvatarIdentifiers(); - conserveResources = avatars.length > 20; + avatarsOfInterest = {}; + var myPosition = filter && Camera.position, + frustum = filter && Camera.frustum, + verticalHalfAngle = filter && (frustum.fieldOfView / 2), + horizontalHalfAngle = filter && (verticalHalfAngle * frustum.aspectRatio), + orientation = filter && Camera.orientation, + front = filter && Quat.getFront(orientation), + verticalAngleNormal = filter && Quat.getRight(orientation), + horizontalAngleNormal = filter && Quat.getUp(orientation); avatars.forEach(function (id) { // sorting the identifiers is just an aid for debugging var avatar = AvatarList.getAvatar(id); + var name = avatar.sessionDisplayName; + if (!name) { + // Either we got a data packet but no identity yet, or something is really messed up. In any case, + // we won't be able to do anything with this user, so don't include them. + // In normal circumstances, a refresh will bring in the new user, but if we're very heavily loaded, + // we could be losing and gaining people randomly. + print('No avatar identity data for', id); + return; + } + if (id && myPosition && (Vec3.distance(avatar.position, myPosition) > filter.distance)) { + return; + } + var normal = id && filter && Vec3.normalize(Vec3.subtract(avatar.position, myPosition)); + var horizontal = normal && angleBetweenVectorsInPlane(normal, front, horizontalAngleNormal); + var vertical = normal && angleBetweenVectorsInPlane(normal, front, verticalAngleNormal); + if (id && filter && ((Math.abs(horizontal) > horizontalHalfAngle) || (Math.abs(vertical) > verticalHalfAngle))) { + return; + } var avatarPalDatum = { - displayName: avatar.sessionDisplayName, + displayName: name, userName: '', sessionId: id || '', audioLevel: 0.0, @@ -289,10 +331,12 @@ function populateUserList(selectData) { addAvatarNode(id); // No overlay for ourselves // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. Users.requestUsernameFromID(id); + avatarsOfInterest[id] = true; } data.push(avatarPalDatum); print('PAL data:', JSON.stringify(avatarPalDatum)); }); + conserveResources = Object.keys(avatarsOfInterest).length > 20; sendToQml({ method: 'users', params: data }); if (selectData) { selectData[2] = true; @@ -317,8 +361,8 @@ var pingPong = true; function updateOverlays() { var eye = Camera.position; AvatarList.getAvatarIdentifiers().forEach(function (id) { - if (!id) { - return; // don't update ourself + if (!id || !avatarsOfInterest[id]) { + return; // don't update ourself, or avatars we're not interested in } var avatar = AvatarList.getAvatar(id); if (!avatar) { @@ -477,23 +521,17 @@ var button; var buttonName = "PEOPLE"; var tablet = null; -function onTabletScreenChanged(type, url) { - if (type !== "QML" || url !== "../Pal.qml") { - off(); - } -} - function startup() { tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); button = tablet.addButton({ text: buttonName, icon: "icons/tablet-icons/people-i.svg", + activeIcon: "icons/tablet-icons/people-a.svg", sortOrder: 7 }); tablet.fromQml.connect(fromQml); button.clicked.connect(onTabletButtonClicked); tablet.screenChanged.connect(onTabletScreenChanged); - Users.usernameFromIDReply.connect(usernameFromIDReply); Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); @@ -524,17 +562,39 @@ function off() { Users.requestsDomainListData = false; } +var onPalScreen = false; +var shouldActivateButton = false; + function onTabletButtonClicked() { - tablet.loadQMLSource("../Pal.qml"); - Users.requestsDomainListData = true; - populateUserList(); - isWired = true; - Script.update.connect(updateOverlays); - Controller.mousePressEvent.connect(handleMouseEvent); - Controller.mouseMoveEvent.connect(handleMouseMoveEvent); - triggerMapping.enable(); - triggerPressMapping.enable(); - audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS); + if (onPalScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + shouldActivateButton = true; + tablet.loadQMLSource("../Pal.qml"); + onPalScreen = true; + Users.requestsDomainListData = true; + populateUserList(); + isWired = true; + Script.update.connect(updateOverlays); + Controller.mousePressEvent.connect(handleMouseEvent); + Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + triggerMapping.enable(); + triggerPressMapping.enable(); + audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS); + } +} + +function onTabletScreenChanged(type, url) { + // for toolbar mode: change button to active when window is first openend, false otherwise. + button.editProperties({isActive: shouldActivateButton}); + shouldActivateButton = false; + onPalScreen = false; + + // disable sphere overlays when not on pal screen. + if (type !== "QML" || url !== "../Pal.qml") { + off(); + } } // @@ -621,14 +681,12 @@ function shutdown() { button.clicked.disconnect(onTabletButtonClicked); tablet.removeButton(button); tablet.screenChanged.disconnect(onTabletScreenChanged); - Users.usernameFromIDReply.disconnect(usernameFromIDReply); Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); Messages.subscribe(CHANNEL); Messages.messageReceived.disconnect(receiveMessage); Users.avatarDisconnected.disconnect(avatarDisconnected); - off(); } diff --git a/scripts/system/tablet-goto.js b/scripts/system/tablet-goto.js index 6c3e12cd9b..eb95d9d8a3 100644 --- a/scripts/system/tablet-goto.js +++ b/scripts/system/tablet-goto.js @@ -14,10 +14,27 @@ (function() { // BEGIN LOCAL_SCOPE var gotoQmlSource = "TabletAddressDialog.qml"; var buttonName = "GOTO"; + var onGotoScreen = false; + var shouldActivateButton = false; - function onClicked(){ - tablet.loadQMLSource(gotoQmlSource); + function onClicked() { + if (onGotoScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + shouldActivateButton = true; + tablet.loadQMLSource(gotoQmlSource); + onGotoScreen = true; + } } + + function onScreenChanged(type, url) { + // for toolbar mode: change button to active when window is first openend, false otherwise. + button.editProperties({isActive: shouldActivateButton}); + shouldActivateButton = false; + onGotoScreen = false; + } + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ icon: "icons/tablet-icons/goto-i.svg", @@ -27,12 +44,12 @@ }); button.clicked.connect(onClicked); + tablet.screenChanged.connect(onScreenChanged); Script.scriptEnding.connect(function () { button.clicked.disconnect(onClicked); - if (tablet) { - tablet.removeButton(button); - } + tablet.removeButton(button); + tablet.screenChanged.disconnect(onScreenChanged); }); }()); // END LOCAL_SCOPE diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index 632cb40bb5..d5065cb826 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -26,12 +26,19 @@ print("show tablet-ui"); var DEFAULT_WIDTH = 0.4375; - var DEFAULT_HMD_TABLET_SCALE = 100; - var HMD_TABLET_SCALE = Settings.getValue("hmdTabletScale") || DEFAULT_HMD_TABLET_SCALE; - UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", DEFAULT_WIDTH * (HMD_TABLET_SCALE / 100), null, activeHand, true); + var DEFAULT_TABLET_SCALE = 100; + var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode; + var TABLET_SCALE = DEFAULT_TABLET_SCALE; + if (toolbarMode) { + TABLET_SCALE = Settings.getValue("desktopTabletScale") || DEFAULT_TABLET_SCALE; + } else { + TABLET_SCALE = Settings.getValue("hmdTabletScale") || DEFAULT_TABLET_SCALE; + } + UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml", DEFAULT_WIDTH * (TABLET_SCALE / 100), null, activeHand, true); UIWebTablet.register(); HMD.tabletID = UIWebTablet.tabletEntityID; - HMD.homeButtonID = UIWebTablet.homeButtonEntity; + HMD.homeButtonID = UIWebTablet.homeButtonID; + HMD.tabletScreenID = UIWebTablet.webOverlayID; } function hideTabletUI() { @@ -48,6 +55,7 @@ UIWebTablet = null; HMD.tabletID = null; HMD.homeButtonID = null; + HMD.tabletScreenID = null; } } @@ -77,7 +85,7 @@ hideTabletUI(); HMD.closeTablet(); } else if (HMD.showTablet && !tabletShown && !toolbarMode) { - UserActivityLogger.openedTablet(); + UserActivityLogger.openedTablet(Settings.getValue("tabletVisibleToOthers")); showTabletUI(); } else if (!HMD.showTablet && tabletShown) { UserActivityLogger.closedTablet(); @@ -126,5 +134,6 @@ Entities.deleteEntity(HMD.tabletID); HMD.tabletID = null; HMD.homeButtonID = null; + HMD.tabletScreenID = null; }); }()); // END LOCAL_SCOPE diff --git a/scripts/system/tablet-users.js b/scripts/system/tablet-users.js index ce50c4686d..8e89ac74b7 100644 --- a/scripts/system/tablet-users.js +++ b/scripts/system/tablet-users.js @@ -1,7 +1,7 @@ "use strict"; // -// users.js +// tablet-users.js // // Created by Faye Li on 18 Jan 2017. // Copyright 2017 High Fidelity, Inc. @@ -36,16 +36,34 @@ var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); var button = tablet.addButton({ icon: "icons/tablet-icons/users-i.svg", + activeIcon: "icons/tablet-icons/users-a.svg", text: "USERS", sortOrder: 11 }); + var onUsersScreen = false; + var shouldActivateButton = false; + function onClicked() { - var tabletEntity = HMD.tabletID; - if (tabletEntity) { - Entities.editEntity(tabletEntity, {textures: JSON.stringify({"tex.close" : HOME_BUTTON_TEXTURE})}); + if (onUsersScreen) { + // for toolbar-mode: go back to home screen, this will close the window. + tablet.gotoHomeScreen(); + } else { + var tabletEntity = HMD.tabletID; + if (tabletEntity) { + Entities.editEntity(tabletEntity, {textures: JSON.stringify({"tex.close" : HOME_BUTTON_TEXTURE})}); + } + shouldActivateButton = true; + tablet.gotoWebScreen(USERS_URL); + onUsersScreen = true; } - tablet.gotoWebScreen(USERS_URL); + } + + function onScreenChanged() { + // for toolbar mode: change button to active when window is first openend, false otherwise. + button.editProperties({isActive: shouldActivateButton}); + shouldActivateButton = false; + onUsersScreen = false; } function onWebEventReceived(event) { @@ -88,12 +106,13 @@ // update your visibility (all, friends, or none) myVisibility = event.data.visibility; GlobalServices.findableBy = myVisibility; - } + } } } button.clicked.connect(onClicked); tablet.webEventReceived.connect(onWebEventReceived); + tablet.screenChanged.connect(onScreenChanged); function cleanup() { button.clicked.disconnect(onClicked); diff --git a/unpublishedScripts/marketplace/shortbow/README.md b/unpublishedScripts/marketplace/shortbow/README.md new file mode 100644 index 0000000000..2e056fecfb --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/README.md @@ -0,0 +1,111 @@ +# Shortbow + +Shortbow is a wave-based archery game. + +## Notes + +There are several design decisions that are based on certain characteristics of the High Fidelity platform, +and in particular, [server entity scripts](https://wiki.highfidelity.com/wiki/Creating_Entity_Server_Scripts), +which are touched on below. +It is recommended that you understand the basics of client entity scripts and server entity scripts (and their +differences) if you plan on digging into the shortbow code. + + * Client entity scripts end in `ClientEntity.js` and server entity scripts end in `ServerEntity.js`. + * Server entity scripts are not guaranteed to have access to *other* entities, and should not rely on it. + * You should not rely on using `Entities.getEntityProperties` to access the properties of entities + other than the entity that the server entity script is running on. This also applies to other + functions like `Entities.findEntities`. This means that something like the `ShortGameManager` (described below) + will not know the entity IDs of entities like the start button or scoreboard text entities, so it + has to find ways to work around that limitation. + * You can, however, use `Entities.editEntity` to edit other entities. + * *NOTE*: It is likely that this will change in the future, and server entity scripts will be able to + query the existence of other entities in some way. This will obviate the need for some of the workarounds + used in shortbow. + * The Entity Script Server (where server entity scripts) does not run a physics simulation + * Server entity scripts do not generate collision events like would be used with + `Script.addEventHandler(entityID, "collisionWithEntity", collideHandler)`. + * High Fidelity distributes its physics simulation to clients, so collisions need to be handled + there. In the case of enemies in shortbow, for instance, the collisions are handled in the + client entity script `enemeyClientEntity.js`. + * If no client is present to run the physics simulation, entities will not physically interact with other + entities. + * But, entities will still apply their basic physical properties. + +## Shortbow Game Manager + +This section describes both `shortbowServerEntity.js` and `shortbowGameManager.js`. The `shortbowServerEntity.js` script +exists on the main arena model entity, and is the parent of all of the other parts of the game, other than the +enemies and bows that are spawned during gameplay. + +The `shortbowServerEntity.js` script is a server entity script that runs the shortbow game. The actual logic for +the game is stored inside `shortbowGameManager.js`, in the `ShortbowGameManager` prototype. + +## Enemy Scripts + +These scripts exist on each enemy that is spawned. + +### enemyClientEntity.js + +This script handles collision events on the client. There are two collisions events that it is interested in: + + 1. Collisions with arrows + 1. Arrow entities have "projectile" in their name + 1. Any other entity with "projectile" in its name could be used to destroy the enemy + 1. Collisions with the gate that the enemies roll towards + 1. The gate entity has "GateCollider" in its name + +### enemyServerEntity.js + +This script acts as a fail-safe to work around some of the limitations that are mentioned under [Notes](#notes). +In particular, if no client is running the physics simulation of an enemy entity, it may fall through the floor +of the arena or never have a collision event generated against the gate. A server entity script also +cannot access the properties of another entity, so it can't know if the entity has moved far away from +the arena. + +To handle this, this script is used to periodically send heartbeats to the [ShortbowGameManager](#shortbow-game-manager) +that runs the game. The heartbeats include the position of the enemy. If the script that received the +heartbeats hasn't heard from `enemyServerEntity.js` in too long, or the entity has moved too far away, it +will remove it from it's local list of enemies that still need to be destroyed. This ensure that the game +doesn't get into a "hung" state where it's waiting for an enemy that will never escape through the gate or be +destroyed. + +## Start Button + +These scripts exist on the start button. + +### startGameButtonClientEntity.js + +This script sends a message to the [ShortbowGameManager](#shortbow-game-manager) to request that the game be started. + +### startGameButtonServerEntity.js + +When the shortbow game starts the start button is hidden, and when the shortbow game ends it is shown again. +As described in the [Notes](#notes), server entity scripts cannot access other entities, including their IDs. +Because of this, the [ShortbowGameManager](#shortbow-game-manager) has no way of knowing the id of the start button, +and thus being able to use `Entities.editEntity` to set its `visible` property to `true` or `false`. One way around +this, and is what is used here, is to use `Messages` on a common channel to send messages to a server entity script +on the start button to request that it be shown or hidden. + +## Display (Scoreboard) + +This script exists on each text entity that scoreboard is composed of. The scoreboard area is composed of a text entity for each of the following: Score, High Score, Wave, Lives. + +### displayServerEntity.js + +The same problem that exists for [startGameButtonServerEntity.js](#startgamebuttonserverentityjs) exists for +the text entities on the scoreboard. This script works around the problem in the same way, but instead of +receiving a message that tells it whether to hide or show itself, it receives a message that contains the +text to update the text entity with. For intance, the "lives" display entity will receive a message each +time a life is lost with the current number of lives. + +## How is the "common channel" determined that is used in some of the client and server entity scripts? + +Imagine that there are two instances of shortbow next to each. If the channel being used is always the same, +and not differentiated in some way between the two instances, a "start game" message sent from [startGameButtonClientEntity.js](#startgamebuttoncliententityjs) +on one game will be received and handled by both games, causing both of them to start. We need a way to create +a channel that is unique to the scripts that are running for a particular instance of shortbow. + +All of the entities in shortbow, besides the enemy entities, are children of the main arena entity that +runs the game. All of the scripts on these entities can access their parentID, so they can use +a prefix plus the parentID to generate a unique channel for that instance of shortbow. + diff --git a/unpublishedScripts/marketplace/shortbow/bow/Arrow_impact1.L.wav b/unpublishedScripts/marketplace/shortbow/bow/Arrow_impact1.L.wav new file mode 100644 index 0000000000..30aede7a5a Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/bow/Arrow_impact1.L.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/bow/Bow_draw.1.L.wav b/unpublishedScripts/marketplace/shortbow/bow/Bow_draw.1.L.wav new file mode 100644 index 0000000000..254e20d937 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/bow/Bow_draw.1.L.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/bow/String_release2.L.wav b/unpublishedScripts/marketplace/shortbow/bow/String_release2.L.wav new file mode 100644 index 0000000000..4f3ad767a2 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/bow/String_release2.L.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/bow/arrow-sparkle.png b/unpublishedScripts/marketplace/shortbow/bow/arrow-sparkle.png new file mode 100644 index 0000000000..6663b2ff1e Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/bow/arrow-sparkle.png differ diff --git a/unpublishedScripts/marketplace/shortbow/bow/arrow.fbx b/unpublishedScripts/marketplace/shortbow/bow/arrow.fbx new file mode 100644 index 0000000000..533e8eb1f7 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/bow/arrow.fbx differ diff --git a/unpublishedScripts/marketplace/shortbow/bow/bow-deadly.fbx b/unpublishedScripts/marketplace/shortbow/bow/bow-deadly.fbx new file mode 100644 index 0000000000..51f0a25c73 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/bow/bow-deadly.fbx differ diff --git a/unpublishedScripts/marketplace/shortbow/bow/bow.js b/unpublishedScripts/marketplace/shortbow/bow/bow.js new file mode 100644 index 0000000000..f8ef025728 --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/bow/bow.js @@ -0,0 +1,671 @@ +// +// Created by Seth Alves on 2016-9-7 +// Copyright 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 +/* global MyAvatar, Vec3, Controller, Quat */ + +var GRAB_COMMUNICATIONS_SETTING = "io.highfidelity.isFarGrabbing"; +setGrabCommunications = function setFarGrabCommunications(on) { + Settings.setValue(GRAB_COMMUNICATIONS_SETTING, on ? "on" : ""); +} +getGrabCommunications = function getFarGrabCommunications() { + return !!Settings.getValue(GRAB_COMMUNICATIONS_SETTING, ""); +} + +// this offset needs to match the one in libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp:378 +var GRAB_POINT_SPHERE_OFFSET = { x: 0.04, y: 0.13, z: 0.039 }; // x = upward, y = forward, z = lateral + +getGrabPointSphereOffset = function(handController) { + if (handController === Controller.Standard.RightHand) { + return GRAB_POINT_SPHERE_OFFSET; + } + return { + x: GRAB_POINT_SPHERE_OFFSET.x * -1, + y: GRAB_POINT_SPHERE_OFFSET.y, + z: GRAB_POINT_SPHERE_OFFSET.z + }; +}; + +// controllerWorldLocation is where the controller would be, in-world, with an added offset +getControllerWorldLocation = function (handController, doOffset) { + var orientation; + var position; + var pose = Controller.getPoseValue(handController); + var valid = pose.valid; + var controllerJointIndex; + if (pose.valid) { + if (handController === Controller.Standard.RightHand) { + controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND"); + } else { + controllerJointIndex = MyAvatar.getJointIndex("_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"); + } + orientation = Quat.multiply(MyAvatar.orientation, MyAvatar.getAbsoluteJointRotationInObjectFrame(controllerJointIndex)); + position = Vec3.sum(MyAvatar.position, Vec3.multiplyQbyV(MyAvatar.orientation, MyAvatar.getAbsoluteJointTranslationInObjectFrame(controllerJointIndex))); + + // add to the real position so the grab-point is out in front of the hand, a bit + if (doOffset) { + var offset = getGrabPointSphereOffset(handController); + position = Vec3.sum(position, Vec3.multiplyQbyV(orientation, offset)); + } + + } else if (!HMD.isHandControllerAvailable()) { + // NOTE: keep this offset in sync with scripts/system/controllers/handControllerPointer.js:493 + var VERTICAL_HEAD_LASER_OFFSET = 0.1; + position = Vec3.sum(Camera.position, Vec3.multiplyQbyV(Camera.orientation, {x: 0, y: VERTICAL_HEAD_LASER_OFFSET, z: 0})); + orientation = Quat.multiply(Camera.orientation, Quat.angleAxis(-90, { x: 1, y: 0, z: 0 })); + valid = true; + } + + return {position: position, + translation: position, + orientation: orientation, + rotation: orientation, + valid: valid}; +}; + + + +// +// +// +// +// +// +// bow.js +// +// This script attaches to a bow that you can pick up with a hand controller. +// Created by James B. Pollack @imgntn on 10/19/2015 +// Copyright 2015 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 +// + +/*global Script, Controller, SoundCache, Entities, getEntityCustomData, setEntityCustomData, MyAvatar, Vec3, Quat, Messages */ + + +function getControllerLocation(controllerHand) { + var standardControllerValue = + (controllerHand === "right") ? Controller.Standard.RightHand : Controller.Standard.LeftHand; + return getControllerWorldLocation(standardControllerValue, true); +}; + +(function() { + + Script.include("/~/system/libraries/utils.js"); + + const NULL_UUID = "{00000000-0000-0000-0000-000000000000}"; + + const NOTCH_ARROW_SOUND_URL = Script.resolvePath('notch.wav'); + const SHOOT_ARROW_SOUND_URL = Script.resolvePath('String_release2.L.wav'); + const STRING_PULL_SOUND_URL = Script.resolvePath('Bow_draw.1.L.wav'); + const ARROW_HIT_SOUND_URL = Script.resolvePath('Arrow_impact1.L.wav'); + + const ARROW_MODEL_URL = Script.resolvePath('arrow.fbx'); + const ARROW_DIMENSIONS = { + x: 0.20, + y: 0.19, + z: 0.93 + }; + + const MIN_ARROW_SPEED = 3.0; + const MAX_ARROW_SPEED = 30.0; + + const ARROW_TIP_OFFSET = 0.47; + const ARROW_GRAVITY = { + x: 0, + y: -9.8, + z: 0 + }; + + + const ARROW_LIFETIME = 15; // seconds + const ARROW_PARTICLE_LIFESPAN = 2; // seconds + + const TOP_NOTCH_OFFSET = 0.6; + const BOTTOM_NOTCH_OFFSET = 0.6; + + const LINE_DIMENSIONS = { + x: 5.0, + y: 5.0, + z: 5.0 + }; + + const DRAW_STRING_THRESHOLD = 0.80; + const DRAW_STRING_PULL_DELTA_HAPTIC_PULSE = 0.09; + const DRAW_STRING_MAX_DRAW = 0.7; + + + const MIN_ARROW_DISTANCE_FROM_BOW_REST = 0.2; + const MAX_ARROW_DISTANCE_FROM_BOW_REST = ARROW_DIMENSIONS.z - 0.2; + const MIN_HAND_DISTANCE_FROM_BOW_TO_KNOCK_ARROW = 0.25; + const MIN_ARROW_DISTANCE_FROM_BOW_REST_TO_SHOOT = 0.30; + + const NOTCH_OFFSET_FORWARD = 0.08; + const NOTCH_OFFSET_UP = 0.035; + + const SHOT_SCALE = { + min1: 0.0, + max1: 0.6, + min2: 1.0, + max2: 15.0 + }; + + const USE_DEBOUNCE = false; + + const TRIGGER_CONTROLS = [ + Controller.Standard.LT, + Controller.Standard.RT, + ]; + + function interval() { + var lastTime = new Date().getTime(); + + return function getInterval() { + var newTime = new Date().getTime(); + var delta = newTime - lastTime; + lastTime = newTime; + return delta; + }; + } + + var checkInterval = interval(); + + var _this; + + function Bow() { + _this = this; + return; + } + + const STRING_NAME = 'Hifi-Bow-String'; + const ARROW_NAME = 'Hifi-Arrow-projectile'; + + const STATE_IDLE = 0; + const STATE_ARROW_GRABBED = 1; + + Bow.prototype = { + topString: null, + aiming: false, + arrowTipPosition: null, + preNotchString: null, + stringID: null, + arrow: null, + stringData: { + currentColor: { + red: 255, + green: 255, + blue: 255 + } + }, + + state: STATE_IDLE, + sinceLastUpdate: 0, + preload: function(entityID) { + this.entityID = entityID; + this.stringPullSound = SoundCache.getSound(STRING_PULL_SOUND_URL); + this.shootArrowSound = SoundCache.getSound(SHOOT_ARROW_SOUND_URL); + this.arrowHitSound = SoundCache.getSound(ARROW_HIT_SOUND_URL); + this.arrowNotchSound = SoundCache.getSound(NOTCH_ARROW_SOUND_URL); + var userData = Entities.getEntityProperties(this.entityID, ["userData"]).userData; + print(userData); + this.userData = JSON.parse(userData); + this.stringID = null; + }, + + unload: function() { + Messages.sendLocalMessage('Hifi-Hand-Disabler', "none"); + Entities.deleteEntity(this.arrow); + }, + + startEquip: function(entityID, args) { + this.hand = args[0]; + this.bowHand = args[0]; + this.stringHand = this.bowHand === "right" ? "left" : "right"; + + Entities.editEntity(_this.entityID, { + collidesWith: "", + }); + + var data = getEntityCustomData('grabbableKey', this.entityID, {}); + data.grabbable = false; + setEntityCustomData('grabbableKey', this.entityID, data); + + this.initString(); + + var self = this; + this.updateIntervalID = Script.setInterval(function() { self.update(); }, 11); + }, + + getStringHandPosition: function() { + return getControllerLocation(this.stringHand).position; + }, + + releaseEquip: function(entityID, args) { + Script.clearInterval(this.updateIntervalID); + this.updateIntervalID = null; + + Messages.sendLocalMessage('Hifi-Hand-Disabler', "none"); + + var data = getEntityCustomData('grabbableKey', this.entityID, {}); + data.grabbable = true; + setEntityCustomData('grabbableKey', this.entityID, data); + Entities.deleteEntity(this.arrow); + this.resetStringToIdlePosition(); + this.destroyArrow(); + Entities.editEntity(_this.entityID, { + collidesWith: "static,dynamic,kinematic,otherAvatar,myAvatar" + }); + }, + + update: function(entityID) { + var self = this; + self.deltaTime = checkInterval(); + //debounce during debugging -- maybe we're updating too fast? + if (USE_DEBOUNCE === true) { + self.sinceLastUpdate = self.sinceLastUpdate + self.deltaTime; + + if (self.sinceLastUpdate > 60) { + self.sinceLastUpdate = 0; + } else { + return; + } + } + + //invert the hands because our string will be held with the opposite hand of the first one we pick up the bow with + this.triggerValue = Controller.getValue(TRIGGER_CONTROLS[(this.hand === 'left') ? 1 : 0]); + + this.bowProperties = Entities.getEntityProperties(this.entityID, ['position', 'rotation']); + var notchPosition = this.getNotchPosition(this.bowProperties); + var stringHandPosition = this.getStringHandPosition(); + var handToNotch = Vec3.subtract(notchPosition, stringHandPosition); + var pullBackDistance = Vec3.length(handToNotch); + + if (this.state === STATE_IDLE) { + this.pullBackDistance = 0; + + this.resetStringToIdlePosition(); + if (this.triggerValue >= DRAW_STRING_THRESHOLD && pullBackDistance < MIN_HAND_DISTANCE_FROM_BOW_TO_KNOCK_ARROW && !this.backHandBusy) { + //the first time aiming the arrow + var handToDisable = (this.hand === 'right' ? 'left' : 'right'); + this.state = STATE_ARROW_GRABBED; + } + } + + if (this.state === STATE_ARROW_GRABBED) { + if (!this.arrow) { + var handToDisable = (this.hand === 'right' ? 'left' : 'right'); + Messages.sendLocalMessage('Hifi-Hand-Disabler', handToDisable); + this.playArrowNotchSound(); + this.arrow = this.createArrow(); + this.playStringPullSound(); + } + + if (this.triggerValue < DRAW_STRING_THRESHOLD) { + if (pullBackDistance >= (MIN_ARROW_DISTANCE_FROM_BOW_REST_TO_SHOOT)) { + // The arrow has been pulled far enough back that we can release it + Messages.sendLocalMessage('Hifi-Hand-Disabler', "none"); + this.updateArrowPositionInNotch(true, true); + this.arrow = null; + this.state = STATE_IDLE; + this.resetStringToIdlePosition(); + } else { + // The arrow has not been pulled far enough back so we just remove the arrow + Messages.sendLocalMessage('Hifi-Hand-Disabler', "none"); + Entities.deleteEntity(this.arrow); + this.arrow = null; + this.state = STATE_IDLE; + this.resetStringToIdlePosition(); + } + } else { + this.updateArrowPositionInNotch(false, true); + this.updateString(); + } + } + }, + + destroyArrow: function() { + var children = Entities.getChildrenIDs(this.entityID); + children.forEach(function(childID) { + var childName = Entities.getEntityProperties(childID, ["name"]).name; + if (childName == ARROW_NAME) { + Entities.deleteEntity(childID); + // Continue iterating through children in case we've ended up in + // a bad state where there are multiple arrows. + } + }); + }, + + createArrow: function() { + this.playArrowNotchSound(); + + var arrow = Entities.addEntity({ + name: ARROW_NAME, + type: 'Model', + modelURL: ARROW_MODEL_URL, + shapeType: 'simple-compound', + dimensions: ARROW_DIMENSIONS, + position: this.bowProperties.position, + parentID: this.entityID, + dynamic: false, + collisionless: true, + collisionSoundURL: ARROW_HIT_SOUND_URL, + damping: 0.01, + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + }, + creatorSessionUUID: MyAvatar.sessionUUID + }) + }); + + var makeArrowStick = function(entityA, entityB, collision) { + Entities.editEntity(entityA, { + localAngularVelocity: { + x: 0, + y: 0, + z: 0 + }, + localVelocity: { + x: 0, + y: 0, + z: 0 + }, + gravity: { + x: 0, + y: 0, + z: 0 + }, + parentID: entityB, + dynamic: false, + collisionless: true, + collidesWith: "" + }); + Script.removeEventHandler(arrow, "collisionWithEntity", makeArrowStick); + }; + + Script.addEventHandler(arrow, "collisionWithEntity", makeArrowStick); + + return arrow; + }, + + initString: function() { + // Check for existence of string + var children = Entities.getChildrenIDs(this.entityID); + children.forEach(function(childID) { + var childName = Entities.getEntityProperties(childID, ["name"]).name; + if (childName == STRING_NAME) { + this.stringID = childID; + } + }); + + // If thie string wasn't found, create it + if (this.stringID === null) { + this.stringID = Entities.addEntity({ + collisionless: true, + dimensions: { "x": 5, "y": 5, "z": 5 }, + ignoreForCollisions: 1, + linePoints: [ { "x": 0, "y": 0, "z": 0 }, { "x": 0, "y": -1.2, "z": 0 } ], + lineWidth: 5, + color: { red: 153, green: 102, blue: 51 }, + name: STRING_NAME, + parentID: this.entityID, + localPosition: { "x": 0, "y": 0.6, "z": 0.1 }, + localRotation: { "w": 1, "x": 0, "y": 0, "z": 0 }, + type: 'Line', + userData: JSON.stringify({ + grabbableKey: { + grabbable: false + } + }) + }); + } + + this.resetStringToIdlePosition(); + }, + + // This resets the string to a straight line + resetStringToIdlePosition: function() { + Entities.editEntity(this.stringID, { + linePoints: [ { "x": 0, "y": 0, "z": 0 }, { "x": 0, "y": -1.2, "z": 0 } ], + lineWidth: 5, + localPosition: { "x": 0, "y": 0.6, "z": 0.1 }, + localRotation: { "w": 1, "x": 0, "y": 0, "z": 0 }, + }); + }, + + updateString: function() { + var upVector = Quat.getUp(this.bowProperties.rotation); + var upOffset = Vec3.multiply(upVector, TOP_NOTCH_OFFSET); + var downVector = Vec3.multiply(-1, Quat.getUp(this.bowProperties.rotation)); + var downOffset = Vec3.multiply(downVector, BOTTOM_NOTCH_OFFSET); + var backOffset = Vec3.multiply(-0.1, Quat.getFront(this.bowProperties.rotation)); + + var topStringPosition = Vec3.sum(this.bowProperties.position, upOffset); + this.topStringPosition = Vec3.sum(topStringPosition, backOffset); + var bottomStringPosition = Vec3.sum(this.bowProperties.position, downOffset); + this.bottomStringPosition = Vec3.sum(bottomStringPosition, backOffset); + + var stringProps = Entities.getEntityProperties(this.stringID, ['position', 'rotation']); + var handPositionLocal = Vec3.subtract(this.arrowRearPosition, stringProps.position); + handPositionLocal = Vec3.multiplyQbyV(Quat.inverse(stringProps.rotation), handPositionLocal); + + var linePoints = [ + { x: 0, y: 0, z: 0 }, + handPositionLocal, + { x: 0, y: -1.2, z: 0 }, + ]; + + Entities.editEntity(this.stringID, { + linePoints: linePoints, + }); + }, + + getNotchPosition: function(bowProperties) { + var frontVector = Quat.getFront(bowProperties.rotation); + var notchVectorForward = Vec3.multiply(frontVector, NOTCH_OFFSET_FORWARD); + var upVector = Quat.getUp(bowProperties.rotation); + var notchVectorUp = Vec3.multiply(upVector, NOTCH_OFFSET_UP); + var notchPosition = Vec3.sum(bowProperties.position, notchVectorForward); + notchPosition = Vec3.sum(notchPosition, notchVectorUp); + return notchPosition; + }, + + updateArrowPositionInNotch: function(shouldReleaseArrow, doHapticPulses) { + //set the notch that the arrow should go through + var notchPosition = this.getNotchPosition(this.bowProperties); + //set the arrow rotation to be between the notch and other hand + var stringHandPosition = this.getStringHandPosition(); + var handToNotch = Vec3.subtract(notchPosition, stringHandPosition); + var arrowRotation = Quat.rotationBetween(Vec3.FRONT, handToNotch); + + var backHand = this.hand === 'left' ? 1 : 0; + var pullBackDistance = Vec3.length(handToNotch); + // pulse as arrow is drawn + if (doHapticPulses && + Math.abs(pullBackDistance - this.pullBackDistance) > DRAW_STRING_PULL_DELTA_HAPTIC_PULSE) { + Controller.triggerHapticPulse(1, 20, backHand); + this.pullBackDistance = pullBackDistance; + } + + if (pullBackDistance > DRAW_STRING_MAX_DRAW) { + pullBackDistance = DRAW_STRING_MAX_DRAW; + } + + var handToNotchDistance = Vec3.length(handToNotch); + var stringToNotchDistance = Math.max(MIN_ARROW_DISTANCE_FROM_BOW_REST, Math.min(MAX_ARROW_DISTANCE_FROM_BOW_REST, handToNotchDistance)); + var halfArrowVec = Vec3.multiply(Vec3.normalize(handToNotch), ARROW_DIMENSIONS.z / 2.0); + var offset = Vec3.subtract(notchPosition, Vec3.multiply(Vec3.normalize(handToNotch), stringToNotchDistance - ARROW_DIMENSIONS.z / 2.0)); + + var arrowPosition = offset; + + // Set arrow rear position + var frontVector = Quat.getFront(arrowRotation); + var frontOffset = Vec3.multiply(frontVector, -ARROW_TIP_OFFSET); + var arrorRearPosition = Vec3.sum(arrowPosition, frontOffset); + this.arrowRearPosition = arrorRearPosition; + + //if we're not shooting, we're updating the arrow's orientation + if (shouldReleaseArrow !== true) { + Entities.editEntity(this.arrow, { + position: arrowPosition, + rotation: arrowRotation + }); + } else { + //shoot the arrow + var arrowAge = Entities.getEntityProperties(this.arrow, ["age"]).age; + + //scale the shot strength by the distance you've pulled the arrow back and set its release velocity to be + // in the direction of the v + var arrowForce = this.scaleArrowShotStrength(stringToNotchDistance); + var handToNotchNorm = Vec3.normalize(handToNotch); + + var releaseVelocity = Vec3.multiply(handToNotchNorm, arrowForce); + + //make the arrow physical, give it gravity, a lifetime, and set our velocity + var arrowProperties = { + dynamic: true, + collisionless: false, + collidesWith: "static,dynamic,otherAvatar", // workaround: not with kinematic --> no collision with bow + velocity: releaseVelocity, + parentID: NULL_UUID, + gravity: ARROW_GRAVITY, + lifetime: arrowAge + ARROW_LIFETIME, + }; + + // add a particle effect to make the arrow easier to see as it flies + var arrowParticleProperties = { + accelerationSpread: { x: 0, y: 0, z: 0 }, + alpha: 1, + alphaFinish: 0, + alphaSpread: 0, + alphaStart: 0.3, + azimuthFinish: 3.1, + azimuthStart: -3.14159, + color: { red: 255, green: 255, blue: 255 }, + colorFinish: { red: 255, green: 255, blue: 255 }, + colorSpread: { red: 0, green: 0, blue: 0 }, + colorStart: { red: 255, green: 255, blue: 255 }, + emitAcceleration: { x: 0, y: 0, z: 0 }, + emitDimensions: { x: 0, y: 0, z: 0 }, + emitOrientation: { x: -0.7, y: 0.0, z: 0.0, w: 0.7 }, + emitRate: 0.01, + emitSpeed: 0, + emitterShouldTrail: 0, + isEmitting: 1, + lifespan: ARROW_PARTICLE_LIFESPAN, + lifetime: ARROW_PARTICLE_LIFESPAN + 1, + maxParticles: 1000, + name: 'arrow-particles', + parentID: this.arrow, + particleRadius: 0.132, + polarFinish: 0, + polarStart: 0, + radiusFinish: 0.35, + radiusSpread: 0, + radiusStart: 0.132, + speedSpread: 0, + textures: Script.resolvePath('arrow-sparkle.png'), + type: 'ParticleEffect' + }; + + Entities.addEntity(arrowParticleProperties); + + // actually shoot the arrow + Entities.editEntity(this.arrow, arrowProperties); + + // play the sound of a shooting arrow + this.playShootArrowSound(); + + Entities.addAction("travel-oriented", this.arrow, { + forward: { x: 0, y: 0, z: -1 }, + angularTimeScale: 0.1, + tag: "arrow from hifi-bow", + ttl: ARROW_LIFETIME + }); + + + } + }, + + scaleArrowShotStrength: function(value) { + var percentage = (value - MIN_ARROW_DISTANCE_FROM_BOW_REST) + / (MAX_ARROW_DISTANCE_FROM_BOW_REST - MIN_ARROW_DISTANCE_FROM_BOW_REST); + return MIN_ARROW_SPEED + (percentage * (MAX_ARROW_SPEED - MIN_ARROW_SPEED)) ; + }, + + playStringPullSound: function() { + var audioProperties = { + volume: 0.10, + position: this.bowProperties.position + }; + this.stringPullInjector = Audio.playSound(this.stringPullSound, audioProperties); + }, + + playShootArrowSound: function(sound) { + var audioProperties = { + volume: 0.15, + position: this.bowProperties.position + }; + Audio.playSound(this.shootArrowSound, audioProperties); + }, + + playArrowNotchSound: function() { + var audioProperties = { + volume: 0.15, + position: this.bowProperties.position + }; + Audio.playSound(this.arrowNotchSound, audioProperties); + }, + + changeStringPullSoundVolume: function(pullBackDistance) { + var audioProperties = { + volume: this.scaleSoundVolume(pullBackDistance), + position: this.bowProperties.position + }; + + this.stringPullInjector.options = audioProperties; + }, + + scaleSoundVolume: function(value) { + var min1 = SHOT_SCALE.min1; + var max1 = SHOT_SCALE.max1; + var min2 = 0; + var max2 = 0.2; + return min2 + (max2 - min2) * ((value - min1) / (max1 - min1)); + }, + + handleMessages: function(channel, message, sender) { + if (sender !== MyAvatar.sessionUUID) { + return; + } + if (channel !== 'Hifi-Object-Manipulation') { + return; + } + try { + var data = JSON.parse(message); + var action = data.action; + var hand = data.joint; + var isBackHand = ((_this.hand == "left" && hand == "RightHand") || + (_this.hand == "right" && hand == "LeftHand")); + if ((action == "equip" || action == "grab") && isBackHand) { + _this.backHandBusy = true; + } + if (action == "release" && isBackHand) { + _this.backHandBusy = false; + } + } catch (e) { + print("WARNING: bow.js -- error parsing Hifi-Object-Manipulation message: " + message); + } + } + }; + + var bow = new Bow(); + + Messages.subscribe('Hifi-Object-Manipulation'); + Messages.messageReceived.connect(bow.handleMessages); + + return bow; +}); diff --git a/unpublishedScripts/marketplace/shortbow/bow/bow.json b/unpublishedScripts/marketplace/shortbow/bow/bow.json new file mode 100644 index 0000000000..a510df729f --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/bow/bow.json @@ -0,0 +1,44 @@ +{ + "Entities": [ + { + "clientOnly": 0, + "collisionsWillMove": 1, + "compoundShapeURL": "http://mpassets.highfidelity.com/a9221d01-95eb-4b2e-85e5-d9e0970a7f51-v1/bow_collision_hull.obj", + "created": "2017-02-14T18:54:38Z", + "dimensions": { + "x": 0.039999999105930328, + "y": 1.2999999523162842, + "z": 0.20000000298023224 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -9.8000001907348633, + "z": 0 + }, + "id": "{73954924-2e18-4787-91a7-092c2afb6242}", + "lastEdited": 1487098438422164, + "lastEditedBy": "{d2da5e17-9125-414d-ac4e-cd7fba6c22f8}", + "modelURL": "http://mpassets.highfidelity.com/a9221d01-95eb-4b2e-85e5-d9e0970a7f51-v1/bow-deadly.fbx", + "name": "WG.Hifi-Bow", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "queryAACube": { + "scale": 1.3159027099609375, + "x": -0.65795135498046875, + "y": -0.65795135498046875, + "z": -0.65795135498046875 + }, + "rotation": { + "w": 0.9717707633972168, + "x": 0.15437555313110352, + "y": -0.10472267866134644, + "z": -0.14421302080154419 + }, + "script": "http://mpassets.highfidelity.com/a9221d01-95eb-4b2e-85e5-d9e0970a7f51-v1/bow.js", + "shapeType": "compound", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.0813,\"y\":0.0452,\"z\":0.0095},{\"x\":-0.3946,\"y\":-0.6604,\"z\":0.4748,\"w\":-0.4275}],\"LeftHand\":[{\"x\":-0.0881,\"y\":0.0259,\"z\":0.0159},{\"x\":0.4427,\"y\":-0.6519,\"z\":0.4592,\"w\":0.4099}]}}}" + } + ], + "Version": 67 +} diff --git a/unpublishedScripts/marketplace/shortbow/bow/bow.svo.json b/unpublishedScripts/marketplace/shortbow/bow/bow.svo.json new file mode 100644 index 0000000000..1ef66860a6 --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/bow/bow.svo.json @@ -0,0 +1,32 @@ +{ + "Entities": [ { + "collisionsWillMove": 1, + "compoundShapeURL": "http://hifi-content.s3.amazonaws.com/caitlyn/production/bow/bow_collision_hull.obj", + "created": "2016-09-01T23:57:55Z", + "dimensions": { + "x": 0.039999999105930328, + "y": 1.2999999523162842, + "z": 0.20000000298023224 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -1, + "z": 0 + }, + "modelURL": "http://hifi-content.s3.amazonaws.com/caitlyn/production/bow/bow-deadly.fbx", + "name": "Hifi-Bow", + "rotation": { + "w": 0.9718012809753418, + "x": 0.15440607070922852, + "y": -0.10469216108322144, + "z": -0.14418250322341919 + }, + "script": "http://hifi-content.s3.amazonaws.com/caitlyn/production/bow/bow.js", + "shapeType": "compound", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.0813,\"y\":0.0452,\"z\":0.0095},{\"x\":-0.3946,\"y\":-0.6604,\"z\":0.4748,\"w\":-0.4275}],\"LeftHand\":[{\"x\":-0.0881,\"y\":0.0259,\"z\":0.0159},{\"x\":0.4427,\"y\":-0.6519,\"z\":0.4592,\"w\":0.4099}]}}}" + } + ], + "Version": 57 +} \ No newline at end of file diff --git a/unpublishedScripts/marketplace/shortbow/bow/bow_collision_hull.obj b/unpublishedScripts/marketplace/shortbow/bow/bow_collision_hull.obj new file mode 100644 index 0000000000..d25786e74f --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/bow/bow_collision_hull.obj @@ -0,0 +1,21 @@ +v -0.016461 -0.431491 -0.033447 +v -0.007624 0.437384 -0.046243 +v 0.011984 -0.424659 -0.03691 +v 0.015514 0.425913 -0.028648 +v -0.010788 -0.421429 0.093711 +v 0.007135 -0.423115 0.098735 +v -0.010208 0.425558 0.096005 +v 0.006734 0.43913 0.088902 + +f 1 2 3 +f 3 2 4 +f 5 6 7 +f 7 6 8 +f 1 5 2 +f 2 5 7 +f 3 4 6 +f 6 4 8 +f 1 3 5 +f 5 3 6 +f 2 7 4 +f 4 7 8 diff --git a/unpublishedScripts/marketplace/shortbow/bow/notch.wav b/unpublishedScripts/marketplace/shortbow/bow/notch.wav new file mode 100644 index 0000000000..2aa67bac33 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/bow/notch.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/bow/spawnBow.js b/unpublishedScripts/marketplace/shortbow/bow/spawnBow.js new file mode 100644 index 0000000000..cb94b05556 --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/bow/spawnBow.js @@ -0,0 +1,67 @@ +// +// Created by Ryan Huffman on 1/10/2017 +// Copyright 2017 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 +// + + +var leftHandPosition = { + "x": 0, + "y": 0.0559, + "z": 0.0159 +}; +var leftHandRotation = Quat.fromPitchYawRollDegrees(90, -90, 0); +var rightHandPosition = Vec3.multiplyVbyV(leftHandPosition, { x: -1, y: 0, z: 0 }); +var rightHandRotation = Quat.fromPitchYawRollDegrees(90, 90, 0); + +var userData = { + "grabbableKey": { + "grabbable": true + }, + "wearable": { + "joints": { + "LeftHand": [ + leftHandPosition, + leftHandRotation + ], + "RightHand": [ + rightHandPosition, + rightHandRotation + ] + } + } +}; + +var id = Entities.addEntity({ + "position": MyAvatar.position, + "collisionsWillMove": 1, + "compoundShapeURL": Script.resolvePath("bow_collision_hull.obj"), + "created": "2016-09-01T23:57:55Z", + "dimensions": { + "x": 0.039999999105930328, + "y": 1.2999999523162842, + "z": 0.20000000298023224 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -9.8, + "z": 0 + }, + "modelURL": Script.resolvePath("bow-deadly.fbx"), + "name": "Hifi-Bow", + "rotation": { + "w": 0.9718012809753418, + "x": 0.15440607070922852, + "y": -0.10469216108322144, + "z": -0.14418250322341919 + }, + "script": Script.resolvePath("bow.js"), + "shapeType": "compound", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.0813,\"y\":0.0452,\"z\":0.0095},{\"x\":-0.3946,\"y\":-0.6604,\"z\":0.4748,\"w\":-0.4275}],\"LeftHand\":[{\"x\":-0.0881,\"y\":0.0259,\"z\":0.0159},{\"x\":0.4427,\"y\":-0.6519,\"z\":0.4592,\"w\":0.4099}]}}}", + "lifetime": 600 +}); +print("Created bow:", id); diff --git a/unpublishedScripts/marketplace/shortbow/enemyClientEntity.js b/unpublishedScripts/marketplace/shortbow/enemyClientEntity.js new file mode 100644 index 0000000000..3abdaa46fb --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/enemyClientEntity.js @@ -0,0 +1,61 @@ +// +// Created by Ryan Huffman on 1/10/2017 +// Copyright 2017 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 +// + +/* globals utils */ + +(function() { + Script.include('utils.js'); + + function Enemy() { + } + Enemy.prototype = { + preload: function(entityID) { + this.entityID = entityID; + + // To avoid sending extraneous messages and checking entities that we've already + // seen, we keep track of which entities we've collided with previously. + this.entityIDsThatHaveCollidedWithMe = []; + + Script.addEventHandler(entityID, "collisionWithEntity", this.onCollide.bind(this)); + + var userData = Entities.getEntityProperties(this.entityID, 'userData').userData; + var data = utils.parseJSON(userData); + if (data !== undefined && data.gameChannel !== undefined) { + this.gameChannel = data.gameChannel; + } else { + print("enemyEntity.js | ERROR: userData does not contain a game channel and/or team number"); + } + }, + onCollide: function(entityA, entityB, collision) { + if (this.entityIDsThatHaveCollidedWithMe.indexOf(entityB) > -1) { + return; + } + this.entityIDsThatHaveCollidedWithMe.push(entityB); + + var colliderName = Entities.getEntityProperties(entityB, 'name').name; + + if (colliderName.indexOf("projectile") > -1) { + Messages.sendMessage(this.gameChannel, JSON.stringify({ + type: "enemy-killed", + entityID: this.entityID, + position: Entities.getEntityProperties(this.entityID, 'position').position + })); + Entities.deleteEntity(this.entityID); + } else if (colliderName.indexOf("GateCollider") > -1) { + Messages.sendMessage(this.gameChannel, JSON.stringify({ + type: "enemy-escaped", + entityID: this.entityID, + position: Entities.getEntityProperties(this.entityID, 'position').position + })); + Entities.deleteEntity(this.entityID); + } + } + }; + + return new Enemy(); +}); diff --git a/unpublishedScripts/marketplace/shortbow/enemyServerEntity.js b/unpublishedScripts/marketplace/shortbow/enemyServerEntity.js new file mode 100644 index 0000000000..bd3f76c94e --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/enemyServerEntity.js @@ -0,0 +1,41 @@ +// +// Created by Ryan Huffman on 1/10/2017 +// Copyright 2017 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 +// + +/* globals utils */ + +(function() { + Script.include('utils.js'); + + function Enemy() { + } + Enemy.prototype = { + preload: function(entityID) { + this.entityID = entityID; + var userData = Entities.getEntityProperties(this.entityID, 'userData').userData; + var data = utils.parseJSON(userData); + if (data !== undefined && data.gameChannel !== undefined) { + this.gameChannel = data.gameChannel; + } else { + print("enemyServerEntity.js | ERROR: userData does not contain a game channel and/or team number"); + } + var self = this; + this.heartbeatTimerID = Script.setInterval(function() { + Messages.sendMessage(self.gameChannel, JSON.stringify({ + type: "enemy-heartbeat", + entityID: self.entityID, + position: Entities.getEntityProperties(self.entityID, 'position').position + })); + }, 1000); + }, + unload: function() { + Script.clearInterval(this.heartbeatTimerID); + } + }; + + return new Enemy(); +}); diff --git a/unpublishedScripts/marketplace/shortbow/models/Amber.fbx b/unpublishedScripts/marketplace/shortbow/models/Amber.fbx new file mode 100644 index 0000000000..4da921f70a Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/models/Amber.fbx differ diff --git a/unpublishedScripts/marketplace/shortbow/models/shortbow-button.fbx b/unpublishedScripts/marketplace/shortbow/models/shortbow-button.fbx new file mode 100644 index 0000000000..817e39cc3c Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/models/shortbow-button.fbx differ diff --git a/unpublishedScripts/marketplace/shortbow/models/shortbow-platform.fbx b/unpublishedScripts/marketplace/shortbow/models/shortbow-platform.fbx new file mode 100644 index 0000000000..b82755a8ca Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/models/shortbow-platform.fbx differ diff --git a/unpublishedScripts/marketplace/shortbow/models/shortbow-scoreboard.fbx b/unpublishedScripts/marketplace/shortbow/models/shortbow-scoreboard.fbx new file mode 100644 index 0000000000..b689fe2eee Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/models/shortbow-scoreboard.fbx differ diff --git a/unpublishedScripts/marketplace/shortbow/shortbow.js b/unpublishedScripts/marketplace/shortbow/shortbow.js new file mode 100644 index 0000000000..641e9c45a6 --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/shortbow.js @@ -0,0 +1,877 @@ +// +// Created by Ryan Huffman on 1/10/2017 +// Copyright 2017 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 +// + +/* globals SHORTBOW_ENTITIES:true */ + +// This is a copy of the data in shortbow.json, which is an export of the shortbow +// scene. +// +// Because .json can't be Script.include'd directly, the contents are copied over +// to here and exposed as a global variable. +// + +SHORTBOW_ENTITIES = +{ + "Entities": [ + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{02f39515-cab4-41d5-b315-5fb41613f844}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708750446, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -5.1684012413024902, + "y": 0.54034698009490967, + "z": -11.257695198059082 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": -1.3604279756546021, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "backgroundColor": { + "blue": 65, + "green": 78, + "red": 82 + }, + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 2, + "y": 0.69999998807907104, + "z": 0.0099999997764825821 + }, + "id": "{3eae601e-3c6e-49ab-8f40-dedee32f7573}", + "lastEdited": 1487894036038423, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "lineHeight": 0.5, + "name": "SB.DisplayScore", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -8.0707607269287109, + "y": 1.5265679359436035, + "z": -9.5913219451904297 + }, + "queryAACube": { + "scale": 2.118985652923584, + "x": -5.1109838485717773, + "y": -1803.69189453125, + "z": -26.774648666381836 + }, + "rotation": { + "w": 0.70708787441253662, + "x": -1.52587890625e-05, + "y": 0.70708787441253662, + "z": -1.52587890625e-05 + }, + "text": "0", + "type": "Text", + "userData": "{\"displayType\":\"score\"}" + }, + { + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.71920669078826904, + "y": 3.3160061836242676, + "z": 2.2217941284179688 + }, + "id": "{04288f77-64df-4323-ac38-9c1960a393a5}", + "lastEdited": 1487893058314990, + "lastEditedBy": "{fce8028a-4bac-43e8-96ff-4c7286ea4ab3}", + "modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-button.fbx", + "name": "SB.StartButton", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -9.8358345031738281, + "y": 0.45674961805343628, + "z": -13.044205665588379 + }, + "queryAACube": { + "scale": 4.0558013916015625, + "x": -7.844393253326416, + "y": -1805.730224609375, + "z": -31.195960998535156 + }, + "rotation": { + "w": 1, + "x": 1.52587890625e-05, + "y": 1.52587890625e-05, + "z": 1.52587890625e-05 + }, + "script": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/startGameButtonClientEntity.js", + "shapeType": "static-mesh", + "type": "Model", + "userData": "{\"grabbableKey\":{\"wantsTrigger\":true}}" + }, + { + "backgroundColor": { + "blue": 65, + "green": 78, + "red": 82 + }, + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 2, + "y": 0.69999998807907104, + "z": 0.0099999997764825821 + }, + "id": "{1196096f-bcc9-4b19-970d-605113474c1b}", + "lastEdited": 1487894037900323, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "lineHeight": 0.5, + "name": "SB.DisplayHighScore", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -8.0707607269287109, + "y": 0.26189804077148438, + "z": -9.5913219451904297 + }, + "queryAACube": { + "scale": 2.118985652923584, + "x": -5.11102294921875, + "y": -1804.95654296875, + "z": -26.77461051940918 + }, + "rotation": { + "w": 0.70708787441253662, + "x": -1.52587890625e-05, + "y": 0.70708787441253662, + "z": -1.52587890625e-05 + }, + "text": "0", + "type": "Text", + "userData": "{\"displayType\":\"highscore\"}" + }, + { + "backgroundColor": { + "blue": 65, + "green": 78, + "red": 82 + }, + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 1.4120937585830688, + "y": 0.71569448709487915, + "z": 0.0099999997764825821 + }, + "id": "{293c294d-1df5-461e-82a3-66abee852d44}", + "lastEdited": 1487894033695485, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "lineHeight": 0.5, + "name": "SB.DisplayWave", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -8.0707607269287109, + "y": 1.5265679359436035, + "z": -7.2889409065246582 + }, + "queryAACube": { + "scale": 1.5831384658813477, + "x": -4.8431310653686523, + "y": -1803.4239501953125, + "z": -24.204343795776367 + }, + "rotation": { + "w": 0.70708787441253662, + "x": -1.52587890625e-05, + "y": 0.70708787441253662, + "z": -1.52587890625e-05 + }, + "text": "0", + "type": "Text", + "userData": "{\"displayType\":\"wave\"}" + }, + { + "backgroundColor": { + "blue": 65, + "green": 78, + "red": 82 + }, + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 1.4120937585830688, + "y": 0.71569448709487915, + "z": 0.0099999997764825821 + }, + "id": "{379afa7b-c668-4c4e-b290-e5c37fb3440c}", + "lastEdited": 1487893055310428, + "lastEditedBy": "{fce8028a-4bac-43e8-96ff-4c7286ea4ab3}", + "lineHeight": 0.5, + "name": "SB.DisplayLives", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -8.0707607269287109, + "y": 0.26189804077148438, + "z": -7.2889409065246582 + }, + "queryAACube": { + "scale": 1.5831384658813477, + "x": -4.8431692123413086, + "y": -1804.6885986328125, + "z": -24.204303741455078 + }, + "rotation": { + "w": 0.70708787441253662, + "x": -1.52587890625e-05, + "y": 0.70708787441253662, + "z": -1.52587890625e-05 + }, + "text": "0", + "type": "Text", + "userData": "{\"displayType\":\"lives\"}" + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 171, + "green": 50, + "red": 62 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{760e81a1-a804-4f5e-9769-393d021fc8fe}", + "ignoreForCollisions": 1, + "lastEdited": 1487892440234633, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.EnemySpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -1.89238440990448, + "y": -5.3368110656738281, + "z": 11.512755393981934 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 1.9147146940231323, + "y": -1809.7066650390625, + "z": -4.8219971656799316 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 171, + "green": 50, + "red": 62 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{0a76d0ac-6353-467b-8edc-56417d5a987c}", + "ignoreForCollisions": 1, + "lastEdited": 1487892440235124, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.EnemySpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 3.6569130420684814, + "y": -5.3365960121154785, + "z": 10.01292610168457 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 7.4640579223632812, + "y": -1809.7066650390625, + "z": -6.3216567039489746 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 171, + "green": 50, + "red": 62 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{f8549c8a-e646-4feb-bbaf-70e7d5be755a}", + "ignoreForCollisions": 1, + "lastEdited": 1487892440235339, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.EnemySpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 8.8902750015258789, + "y": -5.3364419937133789, + "z": 10.195274353027344 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 12.697414398193359, + "y": -1809.7066650390625, + "z": -6.1391491889953613 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{f3aea4ae-4445-4a2d-8d61-e9fd72f04008}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708751269, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -2.5027251243591309, + "y": 0.54042834043502808, + "z": -11.257777214050293 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 1.3052481412887573, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{cc1ac907-124b-4372-8c4c-82d175546725}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708751135, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 2.7972855567932129, + "y": 0.54059004783630371, + "z": -11.257938385009766 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 6.6052589416503906, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{e25ce690-e267-4c51-80a0-f63a72474b8a}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708751527, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 0.17114110291004181, + "y": 0.54050993919372559, + "z": -11.257858276367188 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 3.979114294052124, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{91ee2285-38f8-4795-b6bc-7abc8dcde07c}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708750806, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 5.4656705856323242, + "y": 0.54067152738571167, + "z": -11.258020401000977 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 9.2736434936523438, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{d81e5fae-8a8d-4186-bbd2-0c3ae737b0f2}", + "ignoreForCollisions": 1, + "lastEdited": 1487892552671000, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 9.6099967956542969, + "y": 0.64012420177459717, + "z": -9.9802846908569336 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 13.417934417724609, + "y": -1803.730712890625, + "z": -26.314868927001953 + }, + "rotation": { + "w": 0.22495110332965851, + "x": -2.9734959753113799e-05, + "y": 0.97437006235122681, + "z": 2.9735869247815572e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{7056e21e-bce6-4c4b-bbca-36bea1dce303}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708750993, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 8.1799373626708984, + "y": 0.54075431823730469, + "z": -11.258102416992188 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 11.987911224365234, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 171, + "green": 50, + "red": 62 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{ed073620-e304-4b8e-b12a-5371b595bbf6}", + "ignoreForCollisions": 1, + "lastEdited": 1487892440234415, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.EnemySpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -2.3618791103363037, + "y": -2.0691573619842529, + "z": 11.254574775695801 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 1.4453276395797729, + "y": -1806.43896484375, + "z": -5.0802912712097168 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 171, + "green": 50, + "red": 62 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{32ed7820-c386-4da1-b676-7e63762861a3}", + "ignoreForCollisions": 1, + "lastEdited": 1487892440234854, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.EnemySpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 0.64757472276687622, + "y": -2.5217375755310059, + "z": 10.08248233795166 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 4.454803466796875, + "y": -1806.8917236328125, + "z": -6.2522788047790527 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 26.619264602661133, + "y": 14.24090576171875, + "z": 39.351066589355469 + }, + "id": "{d4c8f577-944d-4d50-ac85-e56387c0ef0a}", + "lastEdited": 1487892440231278, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-platform.fbx", + "name": "SB.Platform", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 0.097909502685070038, + "y": -1.0163799524307251, + "z": 2.0321114063262939 + }, + "queryAACube": { + "scale": 49.597328186035156, + "x": -20.681917190551758, + "y": -1829.9739990234375, + "z": -38.890060424804688 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shapeType": "static-mesh", + "type": "Model" + }, + { + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 23.341892242431641, + "y": 12.223045349121094, + "z": 32.012016296386719 + }, + "friction": 1, + "id": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "lastEdited": 1487892440231832, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-scoreboard.fbx", + "name": "SB.Scoreboard", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "queryAACube": { + "scale": 41.461017608642578, + "x": -20.730508804321289, + "y": -20.730508804321289, + "z": -20.730508804321289 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "serverScripts": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/shortbowServerEntity.js", + "shapeType": "static-mesh", + "type": "Model" + }, + { + "clientOnly": 0, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 15.710711479187012, + "y": 4.7783288955688477, + "z": 1.6129581928253174 + }, + "id": "{84cdff6e-a68d-4bbf-8660-2d6a8c2f1fd0}", + "lastEdited": 1487892440231522, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.GateCollider", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 0.31728419661521912, + "y": -4.3002614974975586, + "z": -12.531644821166992 + }, + "queryAACube": { + "scale": 16.50031852722168, + "x": -3.913693904876709, + "y": -1816.709716796875, + "z": -36.905204772949219 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + } + ], + "Version": 68 +}; + +// Add LocalPosition to entity data if parent properties are available +var entities = SHORTBOW_ENTITIES.Entities; +var entitiesByID = {}; +var i, entity; +for (i = 0; i < entities.length; ++i) { + entity = entities[i]; + entitiesByID[entity.id] = entity; +} +for (i = 0; i < entities.length; ++i) { + entity = entities[i]; + if (entity.parentID !== undefined) { + var parent = entitiesByID[entity.parentID]; + if (parent !== undefined) { + entity.localPosition = Vec3.subtract(entity.position, parent.position); + delete entity.position; + } + } +} diff --git a/unpublishedScripts/marketplace/shortbow/shortbow.json b/unpublishedScripts/marketplace/shortbow/shortbow.json new file mode 100644 index 0000000000..47934baea5 --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/shortbow.json @@ -0,0 +1,840 @@ +{ + "Entities": [ + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{02f39515-cab4-41d5-b315-5fb41613f844}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708750446, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -5.1684012413024902, + "y": 0.54034698009490967, + "z": -11.257695198059082 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": -1.3604279756546021, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "backgroundColor": { + "blue": 65, + "green": 78, + "red": 82 + }, + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 2, + "y": 0.69999998807907104, + "z": 0.0099999997764825821 + }, + "id": "{3eae601e-3c6e-49ab-8f40-dedee32f7573}", + "lastEdited": 1487894036038423, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "lineHeight": 0.5, + "name": "SB.DisplayScore", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -8.0707607269287109, + "y": 1.5265679359436035, + "z": -9.5913219451904297 + }, + "queryAACube": { + "scale": 2.118985652923584, + "x": -5.1109838485717773, + "y": -1803.69189453125, + "z": -26.774648666381836 + }, + "rotation": { + "w": 0.70708787441253662, + "x": -1.52587890625e-05, + "y": 0.70708787441253662, + "z": -1.52587890625e-05 + }, + "text": "0", + "type": "Text", + "userData": "{\"displayType\":\"score\"}" + }, + { + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.71920669078826904, + "y": 3.3160061836242676, + "z": 2.2217941284179688 + }, + "id": "{04288f77-64df-4323-ac38-9c1960a393a5}", + "lastEdited": 1487893058314990, + "lastEditedBy": "{fce8028a-4bac-43e8-96ff-4c7286ea4ab3}", + "modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-button.fbx", + "name": "SB.StartButton", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -9.8358345031738281, + "y": 0.45674961805343628, + "z": -13.044205665588379 + }, + "queryAACube": { + "scale": 4.0558013916015625, + "x": -7.844393253326416, + "y": -1805.730224609375, + "z": -31.195960998535156 + }, + "rotation": { + "w": 1, + "x": 1.52587890625e-05, + "y": 1.52587890625e-05, + "z": 1.52587890625e-05 + }, + "script": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/startGameButtonClientEntity.js", + "shapeType": "static-mesh", + "type": "Model", + "userData": "{\"grabbableKey\":{\"wantsTrigger\":true}}" + }, + { + "backgroundColor": { + "blue": 65, + "green": 78, + "red": 82 + }, + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 2, + "y": 0.69999998807907104, + "z": 0.0099999997764825821 + }, + "id": "{1196096f-bcc9-4b19-970d-605113474c1b}", + "lastEdited": 1487894037900323, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "lineHeight": 0.5, + "name": "SB.DisplayHighScore", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -8.0707607269287109, + "y": 0.26189804077148438, + "z": -9.5913219451904297 + }, + "queryAACube": { + "scale": 2.118985652923584, + "x": -5.11102294921875, + "y": -1804.95654296875, + "z": -26.77461051940918 + }, + "rotation": { + "w": 0.70708787441253662, + "x": -1.52587890625e-05, + "y": 0.70708787441253662, + "z": -1.52587890625e-05 + }, + "text": "0", + "type": "Text", + "userData": "{\"displayType\":\"highscore\"}" + }, + { + "backgroundColor": { + "blue": 65, + "green": 78, + "red": 82 + }, + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 1.4120937585830688, + "y": 0.71569448709487915, + "z": 0.0099999997764825821 + }, + "id": "{293c294d-1df5-461e-82a3-66abee852d44}", + "lastEdited": 1487894033695485, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "lineHeight": 0.5, + "name": "SB.DisplayWave", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -8.0707607269287109, + "y": 1.5265679359436035, + "z": -7.2889409065246582 + }, + "queryAACube": { + "scale": 1.5831384658813477, + "x": -4.8431310653686523, + "y": -1803.4239501953125, + "z": -24.204343795776367 + }, + "rotation": { + "w": 0.70708787441253662, + "x": -1.52587890625e-05, + "y": 0.70708787441253662, + "z": -1.52587890625e-05 + }, + "text": "0", + "type": "Text", + "userData": "{\"displayType\":\"wave\"}" + }, + { + "backgroundColor": { + "blue": 65, + "green": 78, + "red": 82 + }, + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 1.4120937585830688, + "y": 0.71569448709487915, + "z": 0.0099999997764825821 + }, + "id": "{379afa7b-c668-4c4e-b290-e5c37fb3440c}", + "lastEdited": 1487893055310428, + "lastEditedBy": "{fce8028a-4bac-43e8-96ff-4c7286ea4ab3}", + "lineHeight": 0.5, + "name": "SB.DisplayLives", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -8.0707607269287109, + "y": 0.26189804077148438, + "z": -7.2889409065246582 + }, + "queryAACube": { + "scale": 1.5831384658813477, + "x": -4.8431692123413086, + "y": -1804.6885986328125, + "z": -24.204303741455078 + }, + "rotation": { + "w": 0.70708787441253662, + "x": -1.52587890625e-05, + "y": 0.70708787441253662, + "z": -1.52587890625e-05 + }, + "text": "0", + "type": "Text", + "userData": "{\"displayType\":\"lives\"}" + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 171, + "green": 50, + "red": 62 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{760e81a1-a804-4f5e-9769-393d021fc8fe}", + "ignoreForCollisions": 1, + "lastEdited": 1487892440234633, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.EnemySpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -1.89238440990448, + "y": -5.3368110656738281, + "z": 11.512755393981934 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 1.9147146940231323, + "y": -1809.7066650390625, + "z": -4.8219971656799316 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 171, + "green": 50, + "red": 62 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{0a76d0ac-6353-467b-8edc-56417d5a987c}", + "ignoreForCollisions": 1, + "lastEdited": 1487892440235124, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.EnemySpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 3.6569130420684814, + "y": -5.3365960121154785, + "z": 10.01292610168457 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 7.4640579223632812, + "y": -1809.7066650390625, + "z": -6.3216567039489746 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 171, + "green": 50, + "red": 62 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{f8549c8a-e646-4feb-bbaf-70e7d5be755a}", + "ignoreForCollisions": 1, + "lastEdited": 1487892440235339, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.EnemySpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 8.8902750015258789, + "y": -5.3364419937133789, + "z": 10.195274353027344 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 12.697414398193359, + "y": -1809.7066650390625, + "z": -6.1391491889953613 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{f3aea4ae-4445-4a2d-8d61-e9fd72f04008}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708751269, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -2.5027251243591309, + "y": 0.54042834043502808, + "z": -11.257777214050293 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 1.3052481412887573, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{cc1ac907-124b-4372-8c4c-82d175546725}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708751135, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 2.7972855567932129, + "y": 0.54059004783630371, + "z": -11.257938385009766 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 6.6052589416503906, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{e25ce690-e267-4c51-80a0-f63a72474b8a}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708751527, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 0.17114110291004181, + "y": 0.54050993919372559, + "z": -11.257858276367188 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 3.979114294052124, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{91ee2285-38f8-4795-b6bc-7abc8dcde07c}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708750806, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 5.4656705856323242, + "y": 0.54067152738571167, + "z": -11.258020401000977 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 9.2736434936523438, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{d81e5fae-8a8d-4186-bbd2-0c3ae737b0f2}", + "ignoreForCollisions": 1, + "lastEdited": 1487892552671000, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 9.6099967956542969, + "y": 0.64012420177459717, + "z": -9.9802846908569336 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 13.417934417724609, + "y": -1803.730712890625, + "z": -26.314868927001953 + }, + "rotation": { + "w": 0.22495110332965851, + "x": -2.9734959753113799e-05, + "y": 0.97437006235122681, + "z": 2.9735869247815572e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{7056e21e-bce6-4c4b-bbca-36bea1dce303}", + "ignoreForCollisions": 1, + "lastEdited": 1487892708750993, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.BowSpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 8.1799373626708984, + "y": 0.54075431823730469, + "z": -11.258102416992188 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 11.987911224365234, + "y": -1803.830078125, + "z": -27.592727661132812 + }, + "rotation": { + "w": 0.17366324365139008, + "x": 4.9033405957743526e-07, + "y": -0.98480510711669922, + "z": -2.9563087082351558e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 171, + "green": 50, + "red": 62 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{ed073620-e304-4b8e-b12a-5371b595bbf6}", + "ignoreForCollisions": 1, + "lastEdited": 1487892440234415, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.EnemySpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": -2.3618791103363037, + "y": -2.0691573619842529, + "z": 11.254574775695801 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 1.4453276395797729, + "y": -1806.43896484375, + "z": -5.0802912712097168 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "collidesWith": "", + "collisionMask": 0, + "collisionless": 1, + "color": { + "blue": 171, + "green": 50, + "red": 62 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 0.24400754272937775, + "y": 0.24400754272937775, + "z": 0.24400754272937775 + }, + "id": "{32ed7820-c386-4da1-b676-7e63762861a3}", + "ignoreForCollisions": 1, + "lastEdited": 1487892440234854, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.EnemySpawn", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 0.64757472276687622, + "y": -2.5217375755310059, + "z": 10.08248233795166 + }, + "queryAACube": { + "scale": 0.42263346910476685, + "x": 4.454803466796875, + "y": -1806.8917236328125, + "z": -6.2522788047790527 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + }, + { + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 26.619264602661133, + "y": 14.24090576171875, + "z": 39.351066589355469 + }, + "id": "{d4c8f577-944d-4d50-ac85-e56387c0ef0a}", + "lastEdited": 1487892440231278, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-platform.fbx", + "name": "SB.Platform", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 0.097909502685070038, + "y": -1.0163799524307251, + "z": 2.0321114063262939 + }, + "queryAACube": { + "scale": 49.597328186035156, + "x": -20.681917190551758, + "y": -1829.9739990234375, + "z": -38.890060424804688 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shapeType": "static-mesh", + "type": "Model" + }, + { + "clientOnly": 0, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 23.341892242431641, + "y": 12.223045349121094, + "z": 32.012016296386719 + }, + "friction": 1, + "id": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "lastEdited": 1487892440231832, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "modelURL": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/models/shortbow-scoreboard.fbx", + "name": "SB.Scoreboard", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "queryAACube": { + "scale": 41.461017608642578, + "x": -20.730508804321289, + "y": -20.730508804321289, + "z": -20.730508804321289 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "serverScripts": "file:///c:/Users/ryanh/dev/hifi/unpublishedScripts/marketplace/shortbow/shortbowServerEntity.js", + "shapeType": "static-mesh", + "type": "Model" + }, + { + "clientOnly": 0, + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "created": "2017-02-23T23:28:32Z", + "dimensions": { + "x": 15.710711479187012, + "y": 4.7783288955688477, + "z": 1.6129581928253174 + }, + "id": "{84cdff6e-a68d-4bbf-8660-2d6a8c2f1fd0}", + "lastEdited": 1487892440231522, + "lastEditedBy": "{91f193dd-829a-4b33-ab27-e9a26160634a}", + "name": "SB.GateCollider", + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "parentID": "{0cd1f1f7-53b9-4c15-bf25-42c0760d16f0}", + "position": { + "x": 0.31728419661521912, + "y": -4.3002614974975586, + "z": -12.531644821166992 + }, + "queryAACube": { + "scale": 16.50031852722168, + "x": -3.913693904876709, + "y": -1816.709716796875, + "z": -36.905204772949219 + }, + "rotation": { + "w": 1, + "x": -1.52587890625e-05, + "y": -1.52587890625e-05, + "z": -1.52587890625e-05 + }, + "shape": "Cube", + "type": "Box", + "visible": 0 + } + ], + "Version": 68 +} diff --git a/unpublishedScripts/marketplace/shortbow/shortbowGameManager.js b/unpublishedScripts/marketplace/shortbow/shortbowGameManager.js new file mode 100644 index 0000000000..bd42e40427 --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/shortbowGameManager.js @@ -0,0 +1,621 @@ +// +// Created by Ryan Huffman on 1/10/2017 +// Copyright 2017 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 +// + +/* globals ShortbowGameManager:true, utils */ + +Script.include('utils.js'); + +// +--------+ +-----------+ +-----------------+ +// | | | |<-----+ | +// | IDLE +----->| PLAYING | | BETWEEN_WAVES | +// | | | +----->| | +// +--------+ +-----+-----+ +-----------------+ +// ^ | +// | v +// | +-------------+ +// | | | +// +---------+ GAME_OVER | +// | | +// +-------------+ +var GAME_STATES = { + IDLE: 0, + PLAYING: 1, + BETWEEN_WAVES: 2, + GAME_OVER: 3 +}; + +// Load the sounds that we will be using in the game so they are ready to be +// used when we need them. +var BEGIN_BUILDING_SOUND = SoundCache.getSound(Script.resolvePath("sounds/gameOn.wav")); +var GAME_OVER_SOUND = SoundCache.getSound(Script.resolvePath("sounds/gameOver.wav")); +var WAVE_COMPLETE_SOUND = SoundCache.getSound(Script.resolvePath("sounds/waveComplete.wav")); +var EXPLOSION_SOUND = SoundCache.getSound(Script.resolvePath("sounds/explosion.wav")); +var TARGET_HIT_SOUND = SoundCache.getSound(Script.resolvePath("sounds/targetHit.wav")); +var ESCAPE_SOUND = SoundCache.getSound(Script.resolvePath("sounds/escape.wav")); + +const STARTING_NUMBER_OF_LIVES = 6; +const ENEMIES_PER_WAVE_MULTIPLIER = 2; +const POINTS_PER_KILL = 100; +const ENEMY_SPEED = 3.0; + +// Encode a set of key-value pairs into a param string. Does NOT do any URL escaping. +function encodeURLParams(params) { + var paramPairs = []; + for (var key in params) { + paramPairs.push(key + "=" + params[key]); + } + return paramPairs.join("&"); +} + +function sendAndUpdateHighScore(entityID, score, wave, numPlayers, onResposeReceived) { + const URL = 'https://script.google.com/macros/s/AKfycbwbjCm9mGd1d5BzfAHmVT_XKmWyUYRkjCEqDOKm1368oM8nqWni/exec'; + print("Sending high score"); + + const paramString = encodeURLParams({ + entityID: entityID, + score: score, + wave: wave, + numPlayers: numPlayers + }); + + var request = new XMLHttpRequest(); + request.onreadystatechange = function() { + print("ready state: ", request.readyState, request.status, request.readyState === request.DONE, request.response); + if (request.readyState === request.DONE && request.status === 200) { + print("Got response for high score: ", request.response); + var response = JSON.parse(request.responseText); + if (response.highScore !== undefined) { + onResposeReceived(response.highScore); + } + } + }; + request.open('GET', URL + "?" + paramString); + request.timeout = 10000; + request.send(); +} + +function findChildrenWithName(parentID, name) { + var childrenIDs = Entities.getChildrenIDs(parentID); + var matchingIDs = []; + for (var i = 0; i < childrenIDs.length; ++i) { + var id = childrenIDs[i]; + var childName = Entities.getEntityProperties(id, 'name').name; + if (childName === name) { + matchingIDs.push(id); + } + } + return matchingIDs; +} + +function getPropertiesForEntities(entityIDs, desiredProperties) { + var properties = []; + for (var i = 0; i < entityIDs.length; ++i) { + properties.push(Entities.getEntityProperties(entityIDs[i], desiredProperties)); + } + return properties; +} + + +var baseEnemyProperties = { + "name": "SB.Enemy", + "damping": 0, + "linearDamping": 0, + "angularDamping": 0, + "acceleration": { + "x": 0, + "y": -9, + "z": 0 + }, + "angularVelocity": { + "x": -0.058330666273832321, + "y": -0.77943277359008789, + "z": -2.1163818836212158 + }, + "clientOnly": 0, + "collisionsWillMove": 1, + "dimensions": { + "x": 0.63503998517990112, + "y": 0.63503998517990112, + "z": 0.63503998517990112 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -15, + "z": 0 + }, + "lifetime": 30, + "id": "{ed8f7339-8bbd-4750-968e-c3ceb9d64721}", + "modelURL": Script.resolvePath("models/Amber.fbx"), + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "queryAACube": { + "scale": 1.0999215841293335, + "x": -0.54996079206466675, + "y": -0.54996079206466675, + "z": -0.54996079206466675 + }, + "shapeType": "sphere", + "type": "Model", + "script": Script.resolvePath('enemyClientEntity.js'), + "serverScripts": Script.resolvePath('enemyServerEntity.js') +}; + +function searchForChildren(parentID, names, callback, timeoutMs) { + // Map from name to entity ID for the children that have been found + var foundEntities = {}; + for (var i = 0; i < names.length; ++i) { + foundEntities[names[i]] = null; + } + + const CHECK_EVERY_MS = 500; + const maxChecks = Math.ceil(timeoutMs / CHECK_EVERY_MS); + + var check = 0; + var intervalID = Script.setInterval(function() { + check++; + + var childrenIDs = Entities.getChildrenIDs(parentID); + print("\tNumber of children:", childrenIDs.length); + + for (var i = 0; i < childrenIDs.length; ++i) { + print("\t\t" + i + ".", Entities.getEntityProperties(childrenIDs[i]).name); + var id = childrenIDs[i]; + var name = Entities.getEntityProperties(id, 'name').name; + var idx = names.indexOf(name); + if (idx > -1) { + foundEntities[name] = id; + print(name, id); + names.splice(idx, 1); + } + } + + if (names.length === 0 || check >= maxChecks) { + Script.clearInterval(intervalID); + callback(foundEntities); + } + }, CHECK_EVERY_MS); +} + +ShortbowGameManager = function(rootEntityID, bowPositions, spawnPositions) { + print("Starting game manager"); + var self = this; + + this.gameState = GAME_STATES.IDLE; + + this.rootEntityID = rootEntityID; + this.bowPositions = bowPositions; + this.rootPosition = null; + this.spawnPositions = spawnPositions; + + this.loadedChildren = false; + + const START_BUTTON_NAME = 'SB.StartButton'; + const WAVE_DISPLAY_NAME = 'SB.DisplayWave'; + const SCORE_DISPLAY_NAME = 'SB.DisplayScore'; + const LIVES_DISPLAY_NAME = 'SB.DisplayLives'; + const HIGH_SCORE_DISPLAY_NAME = 'SB.DisplayHighScore'; + + const SEARCH_FOR_CHILDREN_TIMEOUT = 5000; + + searchForChildren(rootEntityID, [ + START_BUTTON_NAME, + WAVE_DISPLAY_NAME, + SCORE_DISPLAY_NAME, + LIVES_DISPLAY_NAME, + HIGH_SCORE_DISPLAY_NAME + ], function(children) { + self.loadedChildren = true; + self.startButtonID = children[START_BUTTON_NAME]; + self.waveDisplayID = children[WAVE_DISPLAY_NAME]; + self.scoreDisplayID = children[SCORE_DISPLAY_NAME]; + self.livesDisplayID = children[LIVES_DISPLAY_NAME]; + self.highScoreDisplayID = children[HIGH_SCORE_DISPLAY_NAME]; + + sendAndUpdateHighScore(self.rootEntityID, self.score, self.waveNumber, 1, self.setHighScore.bind(self)); + + self.reset(); + }, SEARCH_FOR_CHILDREN_TIMEOUT); + + // Gameplay state + this.waveNumber = 0; + this.livesLeft = STARTING_NUMBER_OF_LIVES; + this.score = 0; + this.nextWaveTimer = null; + this.spawnEnemyTimers = []; + this.remainingEnemies = []; + this.bowIDs = []; + + this.startButtonChannelName = 'button-' + this.rootEntityID; + + // Entity client and server scripts will send messages to this channel + this.commChannelName = "shortbow-" + this.rootEntityID; + Messages.subscribe(this.commChannelName); + Messages.messageReceived.connect(this, this.onReceivedMessage); + print("Listening on: ", this.commChannelName); + Messages.sendMessage(this.commChannelName, 'hi'); +}; +ShortbowGameManager.prototype = { + reset: function() { + Entities.editEntity(this.startButtonID, { visible: true }); + }, + cleanup: function() { + Messages.unsubscribe(this.commChannelName); + Messages.messageReceived.disconnect(this, this.onReceivedMessage); + + if (this.checkEnemiesTimer) { + Script.clearInterval(this.checkEnemiesTimer); + this.checkEnemiesTimer = null; + } + + for (var i = this.bowIDs.length - 1; i >= 0; i--) { + Entities.deleteEntity(this.bowIDs[i]); + } + this.bowIDs = []; + for (i = 0; i < this.remainingEnemies.length; i++) { + Entities.deleteEntity(this.remainingEnemies[i].id); + } + this.remainingEnemies = []; + + this.gameState = GAME_STATES.IDLE; + }, + startGame: function() { + if (this.gameState !== GAME_STATES.IDLE) { + print("shortbowGameManagerManager.js | Error, trying to start game when not in idle state"); + return; + } + + if (this.loadedChildren === false) { + print('shortbowGameManager.js | Children have not loaded, not allowing game to start'); + return; + } + + print("Game started!!"); + + this.rootPosition = Entities.getEntityProperties(this.rootEntityID, 'position').position; + + Entities.editEntity(this.startButtonID, { visible: false }); + + // Spawn bows + var bowSpawnEntityIDs = findChildrenWithName(this.rootEntityID, 'SB.BowSpawn'); + var bowSpawnProperties = getPropertiesForEntities(bowSpawnEntityIDs, ['position', 'rotation']); + for (var i = 0; i < bowSpawnProperties.length; ++i) { + const props = bowSpawnProperties[i]; + Vec3.print("Creating bow: " + i, props.position); + this.bowIDs.push(Entities.addEntity({ + "position": props.position, + "rotation": props.rotation, + "collisionsWillMove": 1, + "compoundShapeURL": Script.resolvePath("bow/bow_collision_hull.obj"), + "created": "2016-09-01T23:57:55Z", + "dimensions": { + "x": 0.039999999105930328, + "y": 1.2999999523162842, + "z": 0.20000000298023224 + }, + "dynamic": 1, + "gravity": { + "x": 0, + "y": -9.8, + "z": 0 + }, + "modelURL": Script.resolvePath("bow/bow-deadly.fbx"), + "name": "WG.Hifi-Bow", + "script": Script.resolvePath("bow/bow.js"), + "shapeType": "compound", + "type": "Model", + "userData": "{\"grabbableKey\":{\"grabbable\":true},\"wearable\":{\"joints\":{\"RightHand\":[{\"x\":0.0813,\"y\":0.0452,\"z\":0.0095},{\"x\":-0.3946,\"y\":-0.6604,\"z\":0.4748,\"w\":-0.4275}],\"LeftHand\":[{\"x\":-0.0881,\"y\":0.0259,\"z\":0.0159},{\"x\":0.4427,\"y\":-0.6519,\"z\":0.4592,\"w\":0.4099}]}}}" + })); + } + + // Initialize game state + this.waveNumber = 0; + this.setScore(0); + this.setLivesLeft(STARTING_NUMBER_OF_LIVES); + + this.nextWaveTimer = Script.setTimeout(this.startNextWave.bind(this), 100); + this.spawnEnemyTimers = []; + this.checkEnemiesTimer = null; + this.remainingEnemies = []; + + // SpawnQueue is a list of enemies left to spawn. Each entry looks like: + // + // { spawnAt: 1000, position: { x: 0, y: 0, z: 0 } } + // + // where spawnAt is the number of millseconds after the start of the wave + // to spawn the enemy. The list is sorted by spawnAt, ascending. + this.spawnQueue = []; + + this.gameState = GAME_STATES.BETWEEN_WAVES; + + Audio.playSound(BEGIN_BUILDING_SOUND, { + volume: 1.0, + position: this.rootPosition + }); + }, + startNextWave: function() { + if (this.gameState !== GAME_STATES.BETWEEN_WAVES) { + return; + } + + print("Starting next wave"); + this.gameState = GAME_STATES.PLAYING; + this.waveNumber++; + this.remainingEnemies= []; + this.spawnQueue = []; + this.spawnStartTime = Date.now(); + + Entities.editEntity(this.waveDisplayID, { text: this.waveNumber}); + + var numberOfEnemiesLeftToSpawn = this.waveNumber * ENEMIES_PER_WAVE_MULTIPLIER; + var delayBetweenSpawns = 2000 / Math.max(1, Math.log(this.waveNumber)); + var currentDelay = 2000; + + print("Number of enemies:", numberOfEnemiesLeftToSpawn); + this.checkEnemiesTimer = Script.setInterval(this.checkEnemies.bind(this), 100); + + var enemySpawnEntityIDs = findChildrenWithName(this.rootEntityID, 'SB.EnemySpawn'); + var enemySpawnProperties = getPropertiesForEntities(enemySpawnEntityIDs, ['position', 'rotation']); + + for (var i = 0; i < numberOfEnemiesLeftToSpawn; ++i) { + print("Adding enemy"); + var idx = Math.floor(Math.random() * enemySpawnProperties.length); + var props = enemySpawnProperties[idx]; + this.spawnQueue.push({ + spawnAt: currentDelay, + position: props.position, + rotation: props.rotation, + velocity: Vec3.multiply(ENEMY_SPEED, Quat.getFront(props.rotation)) + + }); + currentDelay += delayBetweenSpawns; + } + + print("Starting wave", this.waveNumber); + + }, + checkWaveComplete: function() { + if (this.gameState !== GAME_STATES.PLAYING) { + return; + } + + if (this.spawnQueue.length === 0 && this.remainingEnemies.length === 0) { + this.gameState = GAME_STATES.BETWEEN_WAVES; + Script.setTimeout(this.startNextWave.bind(this), 5000); + + Script.clearInterval(this.checkEnemiesTimer); + this.checkEnemiesTimer = null; + + // Play after 1.5s to let other sounds finish playing + var self = this; + Script.setTimeout(function() { + Audio.playSound(WAVE_COMPLETE_SOUND, { + volume: 1.0, + position: self.rootPosition + }); + }, 1500); + } + }, + setHighScore: function(highScore) { + print("Setting high score to:", this.highScoreDisplayID, highScore); + Entities.editEntity(this.highScoreDisplayID, { text: highScore }); + }, + setLivesLeft: function(lives) { + lives = Math.max(0, lives); + this.livesLeft = lives; + Entities.editEntity(this.livesDisplayID, { text: this.livesLeft }); + }, + setScore: function(score) { + this.score = score; + Entities.editEntity(this.scoreDisplayID, { text: this.score }); + }, + checkEnemies: function() { + if (this.gameState !== GAME_STATES.PLAYING) { + return; + } + + // Check the spawn queueu to see if there are any enemies that need to + // be spawned + var waveElapsedTime = Date.now() - this.spawnStartTime; + while (this.spawnQueue.length > 0 && waveElapsedTime > this.spawnQueue[0].spawnAt) { + baseEnemyProperties.position = this.spawnQueue[0].position; + baseEnemyProperties.rotation = this.spawnQueue[0].rotation; + baseEnemyProperties.velocity= this.spawnQueue[0].velocity; + + baseEnemyProperties.userData = JSON.stringify({ + gameChannel: this.commChannelName, + grabbableKey: { + grabbable: false + } + }); + + var entityID = Entities.addEntity(baseEnemyProperties); + this.remainingEnemies.push({ + id: entityID, + lastKnownPosition: baseEnemyProperties.position, + lastHeartbeat: Date.now() + }); + this.spawnQueue.splice(0, 1); + Script.setTimeout(function() { + const JUMP_SPEED = 5.0; + var velocity = Entities.getEntityProperties(entityID, 'velocity').velocity; + velocity.y += JUMP_SPEED; + Entities.editEntity(entityID, { velocity: velocity }); + + }, 500 + Math.random() * 4000); + } + + // Check the list of remaining enemies to see if any are too far away + // or haven't been heard from in awhile - if so, delete them. + var enemiesEscaped = false; + const MAX_UNHEARD_TIME_BEFORE_DESTROYING_ENTITY_MS = 5000; + const MAX_DISTANCE_FROM_GAME_BEFORE_DESTROYING_ENTITY = 200; + for (var i = this.remainingEnemies.length - 1; i >= 0; --i) { + var enemy = this.remainingEnemies[i]; + var timeSinceLastHeartbeat = Date.now() - enemy.lastHeartbeat; + var distance = Vec3.distance(enemy.lastKnownPosition, this.rootPosition); + if (timeSinceLastHeartbeat > MAX_UNHEARD_TIME_BEFORE_DESTROYING_ENTITY_MS + || distance > MAX_DISTANCE_FROM_GAME_BEFORE_DESTROYING_ENTITY) { + + print("EXPIRING: ", enemy.id); + Entities.deleteEntity(enemy.id); + this.remainingEnemies.splice(i, 1); + Audio.playSound(TARGET_HIT_SOUND, { + volume: 1.0, + position: this.rootPosition + }); + this.setScore(this.score + POINTS_PER_KILL); + enemiesEscaped = true; + } + } + + if (enemiesEscaped) { + this.checkWaveComplete(); + } + }, + endGame: function() { + if (this.gameState !== GAME_STATES.PLAYING) { + return; + } + + var self = this; + Script.setTimeout(function() { + Audio.playSound(GAME_OVER_SOUND, { + volume: 1.0, + position: self.rootPosition + }); + }, 1500); + + this.gameState = GAME_STATES.GAME_OVER; + print("GAME OVER"); + + // Update high score + sendAndUpdateHighScore(this.rootEntityID, this.score, this.waveNumber, 1, this.setHighScore.bind(this)); + + // Cleanup + Script.clearTimeout(this.nextWaveTimer); + this.nextWaveTimer = null; + var i; + for (i = 0; i < this.spawnEnemyTimers.length; ++i) { + Script.clearTimeout(this.spawnEnemyTimers[i]); + } + this.spawnEnemyTimers = []; + + Script.clearInterval(this.checkEnemiesTimer); + this.checkEnemiesTimer = null; + + + for (i = this.bowIDs.length - 1; i >= 0; i--) { + var id = this.bowIDs[i]; + print("Checking bow: ", id); + var userData = utils.parseJSON(Entities.getEntityProperties(id, 'userData').userData); + var bowIsHeld = userData.grabKey !== undefined && userData.grabKey !== undefined && userData.grabKey.refCount > 0; + print("Held: ", bowIsHeld); + if (!bowIsHeld) { + Entities.deleteEntity(id); + this.bowIDs.splice(i, 1); + } + } + + for (i = 0; i < this.remainingEnemies.length; i++) { + Entities.deleteEntity(this.remainingEnemies[i].id); + } + this.remainingEnemies = []; + + // Wait a short time before showing the start button so that any current sounds + // can finish playing. + const WAIT_TO_REENABLE_GAME_TIMEOUT_MS = 3000; + Script.setTimeout(function() { + Entities.editEntity(this.startButtonID, { visible: true }); + this.gameState = GAME_STATES.IDLE; + }.bind(this), WAIT_TO_REENABLE_GAME_TIMEOUT_MS); + }, + onReceivedMessage: function(channel, messageJSON, senderID) { + if (channel === this.commChannelName) { + var message = utils.parseJSON(messageJSON); + if (message === undefined) { + print("shortbowGameManager.js | Received non-json message:", JSON.stringify(messageJSON)); + return; + } + switch (message.type) { + case 'start-game': + this.startGame(); + break; + case 'enemy-killed': + this.onEnemyKilled(message.entityID, message.position); + break; + case 'enemy-escaped': + this.onEnemyEscaped(message.entityID); + break; + case 'enemy-heartbeat': + this.onEnemyHeartbeat(message.entityID, message.position); + break; + default: + print("shortbowGameManager.js | Ignoring unknown message type: ", message.type); + break; + } + } + }, + onEnemyKilled: function(entityID, position) { + if (this.gameState !== GAME_STATES.PLAYING) { + return; + } + + for (var i = this.remainingEnemies.length - 1; i >= 0; --i) { + var enemy = this.remainingEnemies[i]; + if (enemy.id === entityID) { + this.remainingEnemies.splice(i, 1); + Audio.playSound(TARGET_HIT_SOUND, { + volume: 1.0, + position: this.rootPosition + }); + + // Update score + this.setScore(this.score + POINTS_PER_KILL); + print("SCORE: ", this.score); + + this.checkWaveComplete(); + break; + } + } + }, + onEnemyEscaped: function(entityID, position) { + if (this.gameState !== GAME_STATES.PLAYING) { + return; + } + + var enemiesEscaped = false; + for (var i = this.remainingEnemies.length - 1; i >= 0; --i) { + var enemy = this.remainingEnemies[i]; + if (enemy.id === entityID) { + Entities.deleteEntity(enemy.id); + this.remainingEnemies.splice(i, 1); + this.setLivesLeft(this.livesLeft - 1); + Audio.playSound(ESCAPE_SOUND, { + volume: 1.0, + position: this.rootPosition + }); + enemiesEscaped = true; + } + } + if (this.livesLeft <= 0) { + this.endGame(); + } else if (enemiesEscaped) { + this.checkWaveComplete(); + } + }, + onEnemyHeartbeat: function(entityID, position) { + for (var i = 0; i < this.remainingEnemies.length; i++) { + var enemy = this.remainingEnemies[i]; + if (enemy.id === entityID) { + enemy.lastHeartbeat = Date.now(); + enemy.lastKnownPosition = position; + break; + } + } + } +}; diff --git a/unpublishedScripts/marketplace/shortbow/shortbowServerEntity.js b/unpublishedScripts/marketplace/shortbow/shortbowServerEntity.js new file mode 100644 index 0000000000..385220717f --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/shortbowServerEntity.js @@ -0,0 +1,42 @@ +// +// Created by Ryan Huffman on 1/10/2017 +// Copyright 2017 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 +// + +/* globals TEMPLATES:true, SHORTBOW_ENTITIES, ShortbowGameManager */ + +(function() { + Script.include('utils.js'); + Script.include('shortbow.js'); + Script.include('shortbowGameManager.js'); + + TEMPLATES = SHORTBOW_ENTITIES.Entities; + + this.entityID = null; + var gameManager = null; + this.preload = function(entityID) { + this.entityID = entityID; + + var bowPositions = []; + var spawnPositions = []; + for (var i = 0; i < TEMPLATES.length; ++i) { + var template = TEMPLATES[i]; + if (template.name === "SB.BowSpawn") { + bowPositions.push(template.localPosition); + } else if (template.name === "SB.EnemySpawn") { + spawnPositions.push(template.localPosition); + } + } + + gameManager = new ShortbowGameManager(this.entityID, bowPositions, spawnPositions); + }; + this.unload = function() { + if (gameManager) { + gameManager.cleanup(); + gameManager = null; + } + }; +}); diff --git a/unpublishedScripts/marketplace/shortbow/sounds/escape.wav b/unpublishedScripts/marketplace/shortbow/sounds/escape.wav new file mode 100644 index 0000000000..b576703916 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/sounds/escape.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/sounds/explosion.wav b/unpublishedScripts/marketplace/shortbow/sounds/explosion.wav new file mode 100644 index 0000000000..13ee9993e6 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/sounds/explosion.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/sounds/fight.wav b/unpublishedScripts/marketplace/shortbow/sounds/fight.wav new file mode 100644 index 0000000000..439684fe70 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/sounds/fight.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/sounds/gameOn.wav b/unpublishedScripts/marketplace/shortbow/sounds/gameOn.wav new file mode 100644 index 0000000000..8da091b1e3 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/sounds/gameOn.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/sounds/gameOver.wav b/unpublishedScripts/marketplace/shortbow/sounds/gameOver.wav new file mode 100644 index 0000000000..04eb9fd7ee Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/sounds/gameOver.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/sounds/letTheGamesBegin.wav b/unpublishedScripts/marketplace/shortbow/sounds/letTheGamesBegin.wav new file mode 100644 index 0000000000..c8884a04ea Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/sounds/letTheGamesBegin.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/sounds/spawn.wav b/unpublishedScripts/marketplace/shortbow/sounds/spawn.wav new file mode 100644 index 0000000000..ad6579993f Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/sounds/spawn.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/sounds/targetHit.wav b/unpublishedScripts/marketplace/shortbow/sounds/targetHit.wav new file mode 100644 index 0000000000..5357fc94a6 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/sounds/targetHit.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/sounds/tenSecondsRemaining.wav b/unpublishedScripts/marketplace/shortbow/sounds/tenSecondsRemaining.wav new file mode 100644 index 0000000000..b5375b3024 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/sounds/tenSecondsRemaining.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/sounds/waveComplete.wav b/unpublishedScripts/marketplace/shortbow/sounds/waveComplete.wav new file mode 100644 index 0000000000..5fb9f27b45 Binary files /dev/null and b/unpublishedScripts/marketplace/shortbow/sounds/waveComplete.wav differ diff --git a/unpublishedScripts/marketplace/shortbow/spawnShortbow.js b/unpublishedScripts/marketplace/shortbow/spawnShortbow.js new file mode 100644 index 0000000000..6b0be7f7f5 --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/spawnShortbow.js @@ -0,0 +1,210 @@ +// +// Created by Ryan Huffman on 1/10/2017 +// Copyright 2017 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 +// +/* globals utils, SHORTBOW_ENTITIES, TEMPLATES:true */ + +Script.include('utils.js'); +Script.include('shortbow.js'); +Script.include('shortbowGameManager.js'); +TEMPLATES = SHORTBOW_ENTITIES.Entities; + +// Merge two objects into a new object. If a key name appears in both a and b, +// the value in a will be used. +// +// @param {object} a +// @param {object} b +// @returns {object} The new object +function mergeObjects(a, b) { + var obj = {}; + var key; + for (key in b) { + obj[key] = b[key]; + } + for (key in a) { + obj[key] = a[key]; + } + return obj; +} + +// Spawn an entity from a template. +// +// The overrides can be used to override or add properties in the template. For instance, +// it's common to override the `position` property so that you can set the position +// of the entity to be spawned. +// +// @param {string} templateName The name of the template to spawn +// @param {object} overrides An object containing properties that will override +// any properties set in the template. +function spawnTemplate(templateName, overrides) { + var template = getTemplate(templateName); + if (template === null) { + print("ERROR, unknown template name:", templateName); + return null; + } + print("Spawning: ", templateName); + var properties = mergeObjects(overrides, template); + return Entities.addEntity(properties); +} + +function spawnTemplates(templateName, overrides) { + var templates = getTemplates(templateName); + if (template.length === 0) { + print("ERROR, unknown template name:", templateName); + return []; + } + + var spawnedEntities = []; + for (var i = 0; i < templates.length; ++i) { + print("Spawning: ", templateName); + var properties = mergeObjects(overrides, templates[i]); + spawnedEntities.push(Entities.addEntity(properties)); + } + return spawnedEntities; +} + +// TEMPLATES contains a dictionary of different named entity templates. An entity +// template is just a list of properties. +// +// @param name Name of the template to get +// @return {object} The matching template, or null if not found +function getTemplate(name) { + for (var i = 0; i < TEMPLATES.length; ++i) { + if (TEMPLATES[i].name === name) { + return TEMPLATES[i]; + } + } + return null; +} +function getTemplates(name) { + var templates = []; + for (var i = 0; i < TEMPLATES.length; ++i) { + if (TEMPLATES[i].name === name) { + templates.push(TEMPLATES[i]); + } + } + return templates; +} + + +// Cleanup Shortbow template data +for (var i = 0; i < TEMPLATES.length; ++i) { + var template = TEMPLATES[i]; + + // Fixup model url + if (template.type === "Model") { + var urlParts = template.modelURL.split("/"); + var filename = urlParts[urlParts.length - 1]; + var newURL = Script.resolvePath("models/" + filename); + print("Updated url", template.modelURL, "to", newURL); + template.modelURL = newURL; + } +} + +var entityIDs = []; + +var scoreboardID = null; +var buttonID = null; +var waveDisplayID = null; +var scoreDisplayID = null; +var highScoreDisplayID = null; +var livesDisplayID = null; +var platformID = null; +function createLocalGame() { + var rootPosition = utils.findSurfaceBelowPosition(MyAvatar.position); + rootPosition.y += 6.11; + + scoreboardID = spawnTemplate("SB.Scoreboard", { + position: rootPosition + }); + entityIDs.push(scoreboardID); + + // Create start button + buttonID = spawnTemplate("SB.StartButton", { + parentID: scoreboardID, + script: Script.resolvePath("startGameButtonClientEntity.js"), + userData: JSON.stringify({ + grabbableKey: { + wantsTrigger: true + } + }) + }); + entityIDs.push(buttonID); + + + waveDisplayID = spawnTemplate("SB.DisplayWave", { + parentID: scoreboardID, + userData: JSON.stringify({ + displayType: "wave" + }) + }); + entityIDs.push(waveDisplayID); + + scoreDisplayID = spawnTemplate("SB.DisplayScore", { + parentID: scoreboardID, + userData: JSON.stringify({ + displayType: "score" + }) + }); + entityIDs.push(scoreDisplayID); + + livesDisplayID = spawnTemplate("SB.DisplayLives", { + parentID: scoreboardID, + userData: JSON.stringify({ + displayType: "lives" + }) + }); + entityIDs.push(livesDisplayID); + + highScoreDisplayID = spawnTemplate("SB.DisplayHighScore", { + parentID: scoreboardID, + userData: JSON.stringify({ + displayType: "highscore" + }) + }); + entityIDs.push(highScoreDisplayID); + + platformID = spawnTemplate("SB.Platform", { + parentID: scoreboardID + }); + entityIDs.push(platformID); + + spawnTemplate("SB.GateCollider", { + parentID: scoreboardID, + visible: false + }); + entityIDs.push(platformID); + + Entities.editEntity(scoreboardID, { + serverScripts: Script.resolvePath('shortbowServerEntity.js') + }); + + spawnTemplates("SB.BowSpawn", { + parentID: scoreboardID, + visible: false + }); + spawnTemplates("SB.EnemySpawn", { + parentID: scoreboardID, + visible: false + }); + + var bowPositions = []; + var spawnPositions = []; + for (var i = 0; i < TEMPLATES.length; ++i) { + var template = TEMPLATES[i]; + + if (template.name === "SB.BowSpawn") { + bowPositions.push(Vec3.sum(rootPosition, template.localPosition)); + Vec3.print("Pushing bow position", Vec3.sum(rootPosition, template.localPosition)); + } else if (template.name === "SB.EnemySpawn") { + spawnPositions.push(Vec3.sum(rootPosition, template.localPosition)); + Vec3.print("Pushing spawnposition", Vec3.sum(rootPosition, template.localPosition)); + } + } +} + +createLocalGame(); +Script.stop(); diff --git a/unpublishedScripts/marketplace/shortbow/startGameButtonClientEntity.js b/unpublishedScripts/marketplace/shortbow/startGameButtonClientEntity.js new file mode 100644 index 0000000000..c15b93c047 --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/startGameButtonClientEntity.js @@ -0,0 +1,41 @@ +// +// Created by Ryan Huffman on 1/10/2017 +// Copyright 2017 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 +// + +/* globals utils */ + +(function() { + Script.include('utils.js'); + + function StartButton() { + } + StartButton.prototype = { + preload: function(entityID) { + this.entityID = entityID; + this.commChannel = "shortbow-" + Entities.getEntityProperties(entityID, 'parentID').parentID; + Script.addEventHandler(entityID, "collisionWithEntity", this.onCollide.bind(this)); + }, + signalAC: function() { + Messages.sendMessage(this.commChannel, JSON.stringify({ + type: 'start-game' + })); + }, + onCollide: function(entityA, entityB, collision) { + var colliderName = Entities.getEntityProperties(entityB, 'name').name; + + if (colliderName.indexOf("projectile") > -1) { + this.signalAC(); + } + } + }; + + StartButton.prototype.startNearTrigger = StartButton.prototype.signalAC; + StartButton.prototype.startFarTrigger = StartButton.prototype.signalAC; + StartButton.prototype.clickDownOnEntity = StartButton.prototype.signalAC; + + return new StartButton(); +}); diff --git a/unpublishedScripts/marketplace/shortbow/utils.js b/unpublishedScripts/marketplace/shortbow/utils.js new file mode 100644 index 0000000000..3ef82dcc13 --- /dev/null +++ b/unpublishedScripts/marketplace/shortbow/utils.js @@ -0,0 +1,57 @@ +// +// Created by Ryan Huffman on 1/10/2017 +// Copyright 2017 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 +// + +/* globals utils:true */ + +if (!Function.prototype.bind) { + Function.prototype.bind = function(oThis) { + if (typeof this !== 'function') { + // closest thing possible to the ECMAScript 5 + // internal IsCallable function + throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + NOP = function() {}, + fBound = function() { + return fToBind.apply(this instanceof NOP + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + if (this.prototype) { + // Function.prototype doesn't have a prototype property + NOP.prototype = this.prototype; + } + fBound.prototype = new NOP(); + + return fBound; + }; +} + +utils = { + parseJSON: function(json) { + try { + return JSON.parse(json); + } catch (e) { + return undefined; + } + }, + findSurfaceBelowPosition: function(pos) { + var result = Entities.findRayIntersection({ + origin: pos, + direction: { x: 0.0, y: -1.0, z: 0.0 } + }, true); + if (result.intersects) { + return result.intersection; + } + return pos; + } +};