diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 9e6ec1209c..23bee2c91a 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -474,24 +474,18 @@ void Agent::processAgentAvatar() { nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); } } -void Agent::flushEncoder() { +void Agent::encodeFrameOfZeros(QByteArray& encodedZeros) { _flushEncoder = false; - static QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, 0); - static QByteArray encodedZeros; + static const QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, 0); if (_encoder) { _encoder->encode(zeros, encodedZeros); + } else { + encodedZeros = zeros; } } void Agent::processAgentAvatarAudio() { if (_isAvatar && (_isListeningToAudioStream || _avatarSound)) { - // after sound is done playing, encoder has a bit of state in it, - // and needs some 0s to forget or you get a little click next time - // you play something - if (_flushEncoder) { - flushEncoder(); - } - // if we have an avatar audio stream then send it out to our audio-mixer auto scriptedAvatar = DependencyManager::get(); bool silentFrame = true; @@ -528,7 +522,7 @@ void Agent::processAgentAvatarAudio() { } } - auto audioPacket = NLPacket::create(silentFrame + auto audioPacket = NLPacket::create(silentFrame && !_flushEncoder ? PacketType::SilentAudioFrame : PacketType::MicrophoneAudioNoEcho); @@ -564,13 +558,17 @@ void Agent::processAgentAvatarAudio() { glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); audioPacket->writePrimitive(headOrientation); - QByteArray decodedBuffer(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); QByteArray encodedBuffer; - // encode it - if(_encoder) { - _encoder->encode(decodedBuffer, encodedBuffer); + if (_flushEncoder) { + encodeFrameOfZeros(encodedBuffer); } else { - encodedBuffer = decodedBuffer; + QByteArray decodedBuffer(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); + if (_encoder) { + // encode it + _encoder->encode(decodedBuffer, encodedBuffer); + } else { + encodedBuffer = decodedBuffer; + } } audioPacket->write(encodedBuffer.constData(), encodedBuffer.size()); } diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index b882ac3125..939b51625a 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -81,7 +81,7 @@ signals: private: void negotiateAudioFormat(); void selectAudioFormat(const QString& selectedCodecName); - void flushEncoder(); + void encodeFrameOfZeros(QByteArray& encodedZeros); std::unique_ptr _scriptEngine; EntityEditPacketSender _entityEditSender; diff --git a/assignment-client/src/AvatarAudioTimer.cpp b/assignment-client/src/AvatarAudioTimer.cpp index 857209df7c..77dd61043e 100644 --- a/assignment-client/src/AvatarAudioTimer.cpp +++ b/assignment-client/src/AvatarAudioTimer.cpp @@ -15,19 +15,23 @@ // this should send a signal every 10ms, with pretty good precision. Hardcoding // to 10ms since that's what you'd want for audio. void AvatarAudioTimer::start() { - qDebug() << "AvatarAudioTimer::start called"; + qDebug() << __FUNCTION__; auto startTime = usecTimestampNow(); quint64 frameCounter = 0; const int TARGET_INTERVAL_USEC = 10000; // 10ms while (!_quit) { - frameCounter++; - // simplest possible timer + ++frameCounter; + + // tick every 10ms from startTime quint64 targetTime = startTime + frameCounter * TARGET_INTERVAL_USEC; - quint64 interval = std::max((quint64)0, targetTime - usecTimestampNow()); - usleep(interval); + quint64 now = usecTimestampNow(); + + // avoid quint64 underflow + if (now < targetTime) { + usleep(targetTime - now); + } + emit avatarTick(); } qDebug() << "AvatarAudioTimer is finished"; } - - diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 0252c037bf..bc356b8ce1 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -90,8 +90,8 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : PacketType::InjectAudio, PacketType::SilentAudioFrame, PacketType::AudioStreamStats }, this, "handleNodeAudioPacket"); - packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); packetReceiver.registerListener(PacketType::NegotiateAudioFormat, this, "handleNegotiateAudioFormat"); + packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); @@ -481,6 +481,7 @@ void AudioMixer::sendAudioEnvironmentPacket(SharedNodePointer node) { } void AudioMixer::handleNodeAudioPacket(QSharedPointer message, SharedNodePointer sendingNode) { + getOrCreateClientData(sendingNode.data()); DependencyManager::get()->updateNodeWithDataFromPacket(message, sendingNode); } @@ -579,18 +580,8 @@ void AudioMixer::handleNegotiateAudioFormat(QSharedPointer mess } } - auto clientData = dynamic_cast(sendingNode->getLinkedData()); - - // FIXME - why would we not have client data at this point?? - if (!clientData) { - qDebug() << "UNEXPECTED -- didn't have node linked data in " << __FUNCTION__; - sendingNode->setLinkedData(std::unique_ptr { new AudioMixerClientData(sendingNode->getUUID()) }); - clientData = dynamic_cast(sendingNode->getLinkedData()); - connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector); - } - + auto clientData = getOrCreateClientData(sendingNode.data()); clientData->setupCodec(selectedCodec, selectedCodecName); - qDebug() << "selectedCodecName:" << selectedCodecName; clientData->sendSelectAudioFormat(sendingNode, selectedCodecName); } @@ -646,7 +637,8 @@ void AudioMixer::sendStatsPacket() { statsObject["trailing_sleep_percentage"] = _trailingSleepRatio * 100.0f; statsObject["performance_throttling_ratio"] = _performanceThrottlingRatio; - statsObject["avg_listeners_per_frame"] = (float) _sumListeners / (float) _numStatFrames; + statsObject["avg_streams_per_frame"] = (float)_sumStreams / (float)_numStatFrames; + statsObject["avg_listeners_per_frame"] = (float)_sumListeners / (float)_numStatFrames; QJsonObject mixStats; mixStats["%_hrtf_mixes"] = percentageForMixStats(_hrtfRenders); @@ -660,6 +652,7 @@ void AudioMixer::sendStatsPacket() { statsObject["mix_stats"] = mixStats; + _sumStreams = 0; _sumListeners = 0; _hrtfRenders = 0; _hrtfSilentRenders = 0; @@ -707,17 +700,24 @@ void AudioMixer::run() { ThreadedAssignment::commonInit(AUDIO_MIXER_LOGGING_TARGET_NAME, NodeType::AudioMixer); } +AudioMixerClientData* AudioMixer::getOrCreateClientData(Node* node) { + auto clientData = dynamic_cast(node->getLinkedData()); + + if (!clientData) { + node->setLinkedData(std::unique_ptr { new AudioMixerClientData(node->getUUID()) }); + clientData = dynamic_cast(node->getLinkedData()); + connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector); + } + + return clientData; +} + void AudioMixer::domainSettingsRequestComplete() { auto nodeList = DependencyManager::get(); nodeList->addNodeTypeToInterestSet(NodeType::Agent); - nodeList->linkedDataCreateCallback = [&](Node* node) { - node->setLinkedData(std::unique_ptr { new AudioMixerClientData(node->getUUID()) }); - auto clientData = dynamic_cast(node->getLinkedData()); - - connect(clientData, &AudioMixerClientData::injectorStreamFinished, this, &AudioMixer::removeHRTFsForFinishedInjector); - }; + nodeList->linkedDataCreateCallback = [&](Node* node) { getOrCreateClientData(node); }; DomainHandler& domainHandler = nodeList->getDomainHandler(); const QJsonObject& settingsObject = domainHandler.getSettingsObject(); @@ -730,79 +730,71 @@ void AudioMixer::domainSettingsRequestComplete() { } void AudioMixer::broadcastMixes() { + const int TRAILING_AVERAGE_FRAMES = 100; + const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES; + const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; + + const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f; + const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f; + + const float RATIO_BACK_OFF = 0.02f; + auto nodeList = DependencyManager::get(); auto nextFrameTimestamp = p_high_resolution_clock::now(); auto timeToSleep = std::chrono::microseconds(0); - const int TRAILING_AVERAGE_FRAMES = 100; + int currentFrame = 1; + int numFramesPerSecond = (int) ceil(AudioConstants::NETWORK_FRAMES_PER_SEC); int framesSinceCutoffEvent = TRAILING_AVERAGE_FRAMES; - int currentFrame { 1 }; - int numFramesPerSecond { (int) ceil(AudioConstants::NETWORK_FRAMES_PER_SEC) }; - while (!_isFinished) { - const float STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.10f; - const float BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD = 0.20f; + // manage mixer load + { + _trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio) + + // ratio of frame spent sleeping / total frame time + ((CURRENT_FRAME_RATIO * timeToSleep.count()) / (float) AudioConstants::NETWORK_FRAME_USECS); - const float RATIO_BACK_OFF = 0.02f; + bool hasRatioChanged = false; - const float CURRENT_FRAME_RATIO = 1.0f / TRAILING_AVERAGE_FRAMES; - const float PREVIOUS_FRAMES_RATIO = 1.0f - CURRENT_FRAME_RATIO; - - if (timeToSleep.count() < 0) { - timeToSleep = std::chrono::microseconds(0); - } - - _trailingSleepRatio = (PREVIOUS_FRAMES_RATIO * _trailingSleepRatio) - + (timeToSleep.count() * CURRENT_FRAME_RATIO / (float) AudioConstants::NETWORK_FRAME_USECS); - - float lastCutoffRatio = _performanceThrottlingRatio; - bool hasRatioChanged = false; - - if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) { - if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) { - // we're struggling - change our min required loudness to reduce some load - _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 required loudness - _performanceThrottlingRatio = _performanceThrottlingRatio - RATIO_BACK_OFF; - - if (_performanceThrottlingRatio < 0) { - _performanceThrottlingRatio = 0; + if (framesSinceCutoffEvent >= TRAILING_AVERAGE_FRAMES) { + if (_trailingSleepRatio <= STRUGGLE_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD) { + qDebug() << "Mixer is struggling"; + // change our min required loudness to reduce some load + _performanceThrottlingRatio = _performanceThrottlingRatio + (0.5f * (1.0f - _performanceThrottlingRatio)); + hasRatioChanged = true; + } else if (_trailingSleepRatio >= BACK_OFF_TRIGGER_SLEEP_PERCENTAGE_THRESHOLD && _performanceThrottlingRatio != 0) { + qDebug() << "Mixer is recovering"; + // back off the required loudness + _performanceThrottlingRatio = std::max(0.0f, _performanceThrottlingRatio - RATIO_BACK_OFF); + hasRatioChanged = true; } - qDebug() << "Mixer is recovering, sleeping" << _trailingSleepRatio * 100 << "% of frame time. Old cutoff was" - << lastCutoffRatio << "and is now" << _performanceThrottlingRatio; - hasRatioChanged = true; + if (hasRatioChanged) { + // set out min audability threshold from the new ratio + _minAudibilityThreshold = LOUDNESS_TO_DISTANCE_RATIO / (2.0f * (1.0f - _performanceThrottlingRatio)); + framesSinceCutoffEvent = 0; + + qDebug() << "Sleeping" << _trailingSleepRatio << "of frame"; + qDebug() << "Cutoff is" << _performanceThrottlingRatio; + qDebug() << "Minimum audibility to be mixed is" << _minAudibilityThreshold; + } } - if (hasRatioChanged) { - // set out min audability threshold from the new ratio - _minAudibilityThreshold = LOUDNESS_TO_DISTANCE_RATIO / (2.0f * (1.0f - _performanceThrottlingRatio)); - qDebug() << "Minimum audability required to be mixed is now" << _minAudibilityThreshold; - - framesSinceCutoffEvent = 0; + if (!hasRatioChanged) { + ++framesSinceCutoffEvent; } } - if (!hasRatioChanged) { - ++framesSinceCutoffEvent; - } - + // mix nodeList->eachNode([&](const SharedNodePointer& node) { - if (node->getLinkedData()) { AudioMixerClientData* nodeData = (AudioMixerClientData*)node->getLinkedData(); // this function will attempt to pop a frame from each audio stream. // a pointer to the popped data is stored as a member in InboundAudioStream. // That's how the popped audio data will be read for mixing (but only if the pop was successful) - nodeData->checkBuffersBeforeFrameSend(); + _sumStreams += nodeData->checkBuffersBeforeFrameSend(); // if the stream should be muted, send mute packet if (nodeData->getAvatarAudioStream() @@ -818,7 +810,8 @@ void AudioMixer::broadcastMixes() { std::unique_ptr mixPacket; - if (mixHasAudio) { + if (mixHasAudio || nodeData->shouldFlushEncoder()) { + int mixPacketBytes = sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE + AudioConstants::NETWORK_FRAME_BYTES_STEREO; mixPacket = NLPacket::create(PacketType::MixedAudio, mixPacketBytes); @@ -831,12 +824,17 @@ void AudioMixer::broadcastMixes() { QString codecInPacket = nodeData->getCodecName(); mixPacket->writeString(codecInPacket); - QByteArray decodedBuffer(reinterpret_cast(_clampedSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO); QByteArray encodedBuffer; - nodeData->encode(decodedBuffer, encodedBuffer); - + if (mixHasAudio) { + QByteArray decodedBuffer(reinterpret_cast(_clampedSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO); + nodeData->encode(decodedBuffer, encodedBuffer); + } else { + // time to flush, which resets the shouldFlush until next time we encode something + nodeData->encodeFrameOfZeros(encodedBuffer); + } // pack mixed audio samples mixPacket->write(encodedBuffer.constData(), encodedBuffer.size()); + } else { int silentPacketBytes = sizeof(quint16) + sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE; mixPacket = NLPacket::create(PacketType::SilentAudioFrame, silentPacketBytes); @@ -876,24 +874,32 @@ void AudioMixer::broadcastMixes() { ++_numStatFrames; - // since we're a while loop we need to help Qt's event processing - QCoreApplication::processEvents(); + // play nice with qt event-looping + { + // since we're a while loop we need to help qt's event processing + QCoreApplication::processEvents(); - if (_isFinished) { - // at this point the audio-mixer is done - // check if we have a deferred delete event to process (which we should once finished) - QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete); - break; + if (_isFinished) { + // alert qt that this is finished + QCoreApplication::sendPostedEvents(this, QEvent::DeferredDelete); + break; + } } - // push the next frame timestamp to when we should send the next - nextFrameTimestamp += std::chrono::microseconds(AudioConstants::NETWORK_FRAME_USECS); + // sleep until the next frame, if necessary + { + nextFrameTimestamp += std::chrono::microseconds(AudioConstants::NETWORK_FRAME_USECS); - // sleep as long as we need until next frame, if we can - auto now = p_high_resolution_clock::now(); - timeToSleep = std::chrono::duration_cast(nextFrameTimestamp - now); + auto now = p_high_resolution_clock::now(); + timeToSleep = std::chrono::duration_cast(nextFrameTimestamp - now); - std::this_thread::sleep_for(timeToSleep); + if (timeToSleep.count() < 0) { + nextFrameTimestamp = now; + timeToSleep = std::chrono::microseconds(0); + } + + std::this_thread::sleep_for(timeToSleep); + } } } diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index bccac529c1..764c54c6cb 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -52,6 +52,7 @@ private slots: void removeHRTFsForFinishedInjector(const QUuid& streamID); private: + AudioMixerClientData* getOrCreateClientData(Node* node); void domainSettingsRequestComplete(); /// adds one stream to the mix for a listening node @@ -85,6 +86,7 @@ private: float _attenuationPerDoublingInDistance; float _noiseMutingThreshold; int _numStatFrames { 0 }; + int _sumStreams { 0 }; int _sumListeners { 0 }; int _hrtfRenders { 0 }; int _hrtfSilentRenders { 0 }; diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 1eb36cd8a7..58d89697af 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -180,7 +180,7 @@ int AudioMixerClientData::parseData(ReceivedMessage& message) { return 0; } -void AudioMixerClientData::checkBuffersBeforeFrameSend() { +int AudioMixerClientData::checkBuffersBeforeFrameSend() { QWriteLocker writeLocker { &_streamsLock }; auto it = _audioStreams.begin(); @@ -208,6 +208,8 @@ void AudioMixerClientData::checkBuffersBeforeFrameSend() { ++it; } } + + return (int)_audioStreams.size(); } bool AudioMixerClientData::shouldSendStats(int frameNumber) { @@ -355,7 +357,10 @@ QJsonObject AudioMixerClientData::getAudioStreamStats() { } void AudioMixerClientData::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { - qDebug() << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec; + qDebug() << __FUNCTION__ << + "sendingNode:" << *node << + "currentCodec:" << currentCodec << + "receivedCodec:" << recievedCodec; sendSelectAudioFormat(node, currentCodec); } @@ -366,6 +371,17 @@ void AudioMixerClientData::sendSelectAudioFormat(SharedNodePointer node, const Q nodeList->sendPacket(std::move(replyPacket), *node); } +void AudioMixerClientData::encodeFrameOfZeros(QByteArray& encodedZeros) { + static QByteArray zeros(AudioConstants::NETWORK_FRAME_BYTES_STEREO, 0); + if (_shouldFlushEncoder) { + if (_encoder) { + _encoder->encode(zeros, encodedZeros); + } else { + encodedZeros = zeros; + } + } + _shouldFlushEncoder = false; +} void AudioMixerClientData::setupCodec(CodecPluginPointer codec, const QString& codecName) { cleanupCodec(); // cleanup any previously allocated coders first diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 2f8ff4d049..34263f9cbe 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -52,7 +52,8 @@ public: int parseData(ReceivedMessage& message) override; - void checkBuffersBeforeFrameSend(); + // attempt to pop a frame from each audio stream, and return the number of streams from this client + int checkBuffersBeforeFrameSend(); void removeDeadInjectedStreams(); @@ -76,7 +77,11 @@ public: } else { encodedBuffer = decodedBuffer; } + // once you have encoded, you need to flush eventually. + _shouldFlushEncoder = true; } + void encodeFrameOfZeros(QByteArray& encodedZeros); + bool shouldFlushEncoder() { return _shouldFlushEncoder; } QString getCodecName() { return _selectedCodecName; } @@ -105,6 +110,8 @@ private: QString _selectedCodecName; Encoder* _encoder{ nullptr }; // for outbound mixed stream Decoder* _decoder{ nullptr }; // for mic stream + + bool _shouldFlushEncoder { false }; }; #endif // hifi_AudioMixerClientData_h diff --git a/interface/resources/controllers/vive.json b/interface/resources/controllers/vive.json index dce3e9660c..4fbdb37abf 100644 --- a/interface/resources/controllers/vive.json +++ b/interface/resources/controllers/vive.json @@ -34,7 +34,7 @@ { "from": "Vive.RSCenter", "to": "Standard.RightPrimaryThumb" }, { "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" }, - { "from": "Vive.LeftHand", "to": "Standard.LeftHand" }, - { "from": "Vive.RightHand", "to": "Standard.RightHand" } + { "from": "Vive.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] }, + { "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] } ] -} \ No newline at end of file +} diff --git a/interface/resources/html/img/controls-help-gamepad.png b/interface/resources/html/img/controls-help-gamepad.png index e0c8c8901d..cb77dbdabd 100644 Binary files a/interface/resources/html/img/controls-help-gamepad.png and b/interface/resources/html/img/controls-help-gamepad.png differ diff --git a/interface/resources/images/steam-min-spec-failed.png b/interface/resources/images/steam-min-spec-failed.png new file mode 100644 index 0000000000..2d3c85bda0 Binary files /dev/null and b/interface/resources/images/steam-min-spec-failed.png differ diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index abaf11a8e2..f388e5c120 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -27,6 +27,7 @@ Item { WebEngineView { id: root + objectName: "webEngineView" x: 0 y: 0 width: parent.width diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 255b543ad8..5fe15fa8e5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -534,6 +534,8 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _maxOctreePPS(maxOctreePacketsPerSecond.get()), _lastFaceTrackerUpdate(0) { + setProperty("com.highfidelity.launchedFromSteam", SteamClient::isRunning()); + _runningMarker.startRunningMarker(); PluginContainer* pluginContainer = dynamic_cast(this); // set the container for any plugins that care @@ -569,6 +571,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _deadlockWatchdogThread = new DeadlockWatchdogThread(); _deadlockWatchdogThread->start(); + qCDebug(interfaceapp) << "[VERSION] SteamVR buildID:" << SteamClient::getSteamVRBuildID(); qCDebug(interfaceapp) << "[VERSION] Build sequence:" << qPrintable(applicationVersion()); qCDebug(interfaceapp) << "[VERSION] MODIFIED_ORGANIZATION:" << BuildInfo::MODIFIED_ORGANIZATION; qCDebug(interfaceapp) << "[VERSION] VERSION:" << BuildInfo::VERSION; @@ -1191,6 +1194,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); properties["sim_rate"] = getAverageSimsPerSecond(); properties["avatar_sim_rate"] = getAvatarSimrate(); + properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); auto bandwidthRecorder = DependencyManager::get(); properties["packet_rate_in"] = bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond(); @@ -1234,6 +1238,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo auto glInfo = getGLContextData(); properties["gl_info"] = glInfo; properties["gpu_free_memory"] = (int)BYTES_TO_MB(gpu::Context::getFreeGPUMemory()); + properties["ideal_thread_count"] = QThread::idealThreadCount(); auto hmdHeadPose = getHMDSensorPose(); properties["hmd_head_pose_changed"] = isHMDMode() && (hmdHeadPose != lastHMDHeadPose); diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index cd8d39b739..d357713bff 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -620,6 +620,14 @@ Menu::Menu() { // Developer > Audio >>> MenuWrapper* audioDebugMenu = developerMenu->addMenu("Audio"); + action = addActionToQMenuAndActionHash(audioDebugMenu, "Stats..."); + connect(action, &QAction::triggered, [] { + auto scriptEngines = DependencyManager::get(); + QUrl defaultScriptsLoc = defaultScriptsLocation(); + defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/utilities/audio/stats.js"); + scriptEngines->loadScript(defaultScriptsLoc.toString()); + }); + action = addActionToQMenuAndActionHash(audioDebugMenu, "Buffers..."); connect(action, &QAction::triggered, [] { DependencyManager::get()->toggle(QString("hifi/dialogs/AudioPreferencesDialog.qml"), "AudioPreferencesDialog"); diff --git a/interface/src/main.cpp b/interface/src/main.cpp index ab4ab689f7..a88388050b 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -37,7 +37,11 @@ #include #endif - +#ifdef Q_OS_WIN +extern "C" { + typedef int(__stdcall * CHECKMINSPECPROC) (); +} +#endif int main(int argc, const char* argv[]) { #if HAS_BUGSPLAT @@ -155,15 +159,33 @@ int main(int argc, const char* argv[]) { SteamClient::init(); +#ifdef Q_OS_WIN + // If we're running in steam mode, we need to do an explicit check to ensure we're up to the required min spec + if (SteamClient::isRunning()) { + QString appPath; + { + char filename[MAX_PATH]; + GetModuleFileName(NULL, filename, MAX_PATH); + QFileInfo appInfo(filename); + appPath = appInfo.absolutePath(); + } + QString openvrDllPath = appPath + "/plugins/openvr.dll"; + HMODULE openvrDll; + CHECKMINSPECPROC checkMinSpecPtr; + if ((openvrDll = LoadLibrary(openvrDllPath.toLocal8Bit().data())) && + (checkMinSpecPtr = (CHECKMINSPECPROC)GetProcAddress(openvrDll, "CheckMinSpec"))) { + if (!checkMinSpecPtr()) { + return -1; + } + } + } +#endif int exitCode; { QSettings::setDefaultFormat(QSettings::IniFormat); Application app(argc, const_cast(argv), startupTime, runServer, serverContentPathOptionValue); - bool launchedFromSteam = SteamClient::isRunning(); - app.setProperty("com.highfidelity.launchedFromSteam", launchedFromSteam); - // If we failed the OpenGLVersion check, log it. if (override) { auto accountManager = DependencyManager::get(); diff --git a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp index 8d17602c97..ded0107314 100644 --- a/libraries/entities-renderer/src/RenderableWebEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableWebEntityItem.cpp @@ -328,7 +328,17 @@ void RenderableWebEntityItem::handlePointerEvent(const PointerEvent& event) { void RenderableWebEntityItem::destroyWebSurface() { if (_webSurface) { --_currentWebCount; + + QQuickItem* rootItem = _webSurface->getRootItem(); + if (rootItem) { + QObject* obj = rootItem->findChild("webEngineView"); + if (obj) { + QMetaObject::invokeMethod(obj, "stop"); + } + } + _webSurface->pause(); + _webSurface->disconnect(_connection); QObject::disconnect(_mousePressConnection); _mousePressConnection = QMetaObject::Connection(); diff --git a/libraries/gl/src/gl/OffscreenQmlSurface.cpp b/libraries/gl/src/gl/OffscreenQmlSurface.cpp index c82c2b4a32..06f755c1dd 100644 --- a/libraries/gl/src/gl/OffscreenQmlSurface.cpp +++ b/libraries/gl/src/gl/OffscreenQmlSurface.cpp @@ -344,7 +344,6 @@ bool OffscreenQmlSurface::allowNewFrame(uint8_t fps) { OffscreenQmlSurface::OffscreenQmlSurface() { } -static const uint64_t MAX_SHUTDOWN_WAIT_SECS = 2; OffscreenQmlSurface::~OffscreenQmlSurface() { QObject::disconnect(&_updateTimer); QObject::disconnect(qApp); diff --git a/libraries/networking/src/udt/Socket.cpp b/libraries/networking/src/udt/Socket.cpp index 43bd7dd973..fbf2a1c86a 100644 --- a/libraries/networking/src/udt/Socket.cpp +++ b/libraries/networking/src/udt/Socket.cpp @@ -306,6 +306,13 @@ void Socket::checkForReadyReadBackup() { if (_udpSocket.hasPendingDatagrams()) { qCDebug(networking) << "Socket::checkForReadyReadBackup() detected blocked readyRead signal. Flushing pending datagrams."; + // so that birarda can possibly figure out how the heck we get into this state in the first place + // output the sequence number and socket address of the last processed packet + qCDebug(networking) << "Socket::checkForReadyReadyBackup() last sequence number" + << (uint32_t) _lastReceivedSequenceNumber << "from" << _lastPacketSockAddr << "-" + << _lastPacketSizeRead << "bytes"; + + // drop all of the pending datagrams on the floor while (_udpSocket.hasPendingDatagrams()) { _udpSocket.readDatagram(nullptr, 0); @@ -334,6 +341,10 @@ void Socket::readPendingDatagrams() { auto sizeRead = _udpSocket.readDatagram(buffer.get(), packetSizeWithHeader, senderSockAddr.getAddressPointer(), senderSockAddr.getPortPointer()); + // save information for this packet, in case it is the one that sticks readyRead + _lastPacketSizeRead = sizeRead; + _lastPacketSockAddr = senderSockAddr; + if (sizeRead <= 0) { // we either didn't pull anything for this packet or there was an error reading (this seems to trigger // on windows even if there's not a packet available) @@ -373,6 +384,9 @@ void Socket::readPendingDatagrams() { auto packet = Packet::fromReceivedPacket(std::move(buffer), packetSizeWithHeader, senderSockAddr); packet->setReceiveTime(receiveTime); + // save the sequence number in case this is the packet that sticks readyRead + _lastReceivedSequenceNumber = packet->getSequenceNumber(); + // call our verification operator to see if this packet is verified if (!_packetFilterOperator || _packetFilterOperator(*packet)) { if (packet->isReliable()) { diff --git a/libraries/networking/src/udt/Socket.h b/libraries/networking/src/udt/Socket.h index a811d78958..1919e00b41 100644 --- a/libraries/networking/src/udt/Socket.h +++ b/libraries/networking/src/udt/Socket.h @@ -144,6 +144,10 @@ private: std::unique_ptr _ccFactory { new CongestionControlFactory() }; bool _shouldChangeSocketOptions { true }; + + int _lastPacketSizeRead { 0 }; + SequenceNumber _lastReceivedSequenceNumber; + HifiSockAddr _lastPacketSockAddr; friend UDTTest; }; diff --git a/libraries/plugins/src/plugins/DisplayPlugin.h b/libraries/plugins/src/plugins/DisplayPlugin.h index c025b03075..3a9107390a 100644 --- a/libraries/plugins/src/plugins/DisplayPlugin.h +++ b/libraries/plugins/src/plugins/DisplayPlugin.h @@ -139,6 +139,7 @@ public: virtual bool isStereo() const { return isHmd(); } virtual bool isThrottled() const { return false; } virtual float getTargetFrameRate() const { return 0.0f; } + virtual bool hasAsyncReprojection() const { return false; } /// Returns a boolean value indicating whether the display is currently visible /// to the user. For monitor displays, false might indicate that a screensaver, diff --git a/libraries/script-engine/src/BatchLoader.cpp b/libraries/script-engine/src/BatchLoader.cpp index 605d7e95bd..65c1bc5a2a 100644 --- a/libraries/script-engine/src/BatchLoader.cpp +++ b/libraries/script-engine/src/BatchLoader.cpp @@ -44,33 +44,40 @@ void BatchLoader::start() { return; } + for (const auto& rawURL : _urls) { QUrl url = expandScriptUrl(normalizeScriptURL(rawURL)); qCDebug(scriptengine) << "Loading script at " << url; - QPointer self = this; - DependencyManager::get()->getScriptContents(url.toString(), [this, self](const QString& url, const QString& contents, bool isURL, bool success) { - if (!self) { - return; - } + auto scriptCache = DependencyManager::get(); - // Because the ScriptCache may call this callback from differents threads, - // we need to make sure this is thread-safe. - std::lock_guard lock(_dataLock); + // Use a proxy callback to handle the call and emit the signal in a thread-safe way. + // If BatchLoader is deleted before the callback is called, the subsequent "emit" call will not do + // anything. + ScriptCacheSignalProxy* proxy = new ScriptCacheSignalProxy(scriptCache.data()); + scriptCache->getScriptContents(url.toString(), [proxy](const QString& url, const QString& contents, bool isURL, bool success) { + proxy->receivedContent(url, contents, isURL, success); + proxy->deleteLater(); + }, false); + connect(proxy, &ScriptCacheSignalProxy::contentAvailable, this, [this](const QString& url, const QString& contents, bool isURL, bool success) { if (isURL && success) { _data.insert(url, contents); qCDebug(scriptengine) << "Loaded: " << url; } else { _data.insert(url, QString()); - qCDebug(scriptengine) << "Could not load" << url; + qCDebug(scriptengine) << "Could not load: " << url; } if (!_finished && _urls.size() == _data.size()) { _finished = true; emit finished(_data); } - }, false); + }); } } + +void ScriptCacheSignalProxy::receivedContent(const QString& url, const QString& contents, bool isURL, bool success) { + emit contentAvailable(url, contents, isURL, success); +} diff --git a/libraries/script-engine/src/BatchLoader.h b/libraries/script-engine/src/BatchLoader.h index 40b43d23b6..a03a8d80c6 100644 --- a/libraries/script-engine/src/BatchLoader.h +++ b/libraries/script-engine/src/BatchLoader.h @@ -21,10 +21,20 @@ #include +class ScriptCacheSignalProxy : public QObject { + Q_OBJECT +public: + ScriptCacheSignalProxy(QObject* parent) : QObject(parent) { } + void receivedContent(const QString& url, const QString& contents, bool isURL, bool success); + +signals: + void contentAvailable(const QString& url, const QString& contents, bool isURL, bool success); +}; + class BatchLoader : public QObject { Q_OBJECT public: - BatchLoader(const QList& urls) ; + BatchLoader(const QList& urls); void start(); bool isFinished() const { return _finished; }; @@ -39,7 +49,6 @@ private: bool _finished; QSet _urls; QMap _data; - std::mutex _dataLock; }; #endif // hifi_BatchLoader_h diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp index 235d258d21..9936027302 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.cpp @@ -245,6 +245,32 @@ void SteamClient::shutdown() { steamCallbackManager.getTicketRequests().stopAll(); } +int SteamClient::getSteamVRBuildID() { + if (initialized) { + static const int MAX_PATH_SIZE = 512; + static const int STEAMVR_APPID = 250820; + char rawPath[MAX_PATH_SIZE]; + SteamApps()->GetAppInstallDir(STEAMVR_APPID, rawPath, MAX_PATH_SIZE); + + QString path(rawPath); + path += "\\bin\\version.txt"; + qDebug() << "SteamVR version file path:" << path; + + QFile file(path); + if (file.open(QIODevice::ReadOnly)) { + QString buildIDString = file.readLine(); + + bool ok = false; + int buildID = buildIDString.toInt(&ok); + if (ok) { + return buildID; + } + } + } + return 0; +} + + void SteamClient::runCallbacks() { if (!initialized) { return; diff --git a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h index 5bf0d4db56..a191adee97 100644 --- a/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h +++ b/libraries/steamworks-wrapper/src/steamworks-wrapper/SteamClient.h @@ -37,6 +37,7 @@ public: static void openInviteOverlay(); static void joinLobby(QString lobbyId); + static int getSteamVRBuildID(); }; class SteamScriptingInterface : public QObject { diff --git a/plugins/oculus/src/OculusBaseDisplayPlugin.h b/plugins/oculus/src/OculusBaseDisplayPlugin.h index 023f933acf..e5dc75095d 100644 --- a/plugins/oculus/src/OculusBaseDisplayPlugin.h +++ b/plugins/oculus/src/OculusBaseDisplayPlugin.h @@ -19,6 +19,9 @@ public: ~OculusBaseDisplayPlugin(); bool isSupported() const override; + bool hasAsyncReprojection() const override { return true; } + + // Stereo specific methods void resetSensors() override final; bool beginFrameRender(uint32_t frameIndex) override; diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 18a2ce99c3..1a4067a847 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -43,7 +43,6 @@ PoseData _nextSimPoseData; #define MIN_CORES_FOR_NORMAL_RENDER 5 bool forceInterleavedReprojection = (QThread::idealThreadCount() < MIN_CORES_FOR_NORMAL_RENDER); - static std::array VR_EYES { { vr::Eye_Left, vr::Eye_Right } }; bool _openVrDisplayActive { false }; // Flip y-axis since GL UV coords are backwards. @@ -354,6 +353,13 @@ bool OpenVrDisplayPlugin::isSupported() const { return openVrSupported(); } +float OpenVrDisplayPlugin::getTargetFrameRate() const { + if (forceInterleavedReprojection && !_asyncReprojectionActive) { + return TARGET_RATE_OpenVr / 2.0f; + } + return TARGET_RATE_OpenVr; +} + void OpenVrDisplayPlugin::init() { Plugin::init(); @@ -373,9 +379,6 @@ void OpenVrDisplayPlugin::init() { emit deviceConnected(getName()); } -// FIXME remove once OpenVR header is updated -#define VRCompositor_ReprojectionAsync 0x04 - bool OpenVrDisplayPlugin::internalActivate() { if (!_system) { _system = acquireOpenVrSystem(); @@ -398,9 +401,10 @@ bool OpenVrDisplayPlugin::internalActivate() { memset(&timing, 0, sizeof(timing)); timing.m_nSize = sizeof(vr::Compositor_FrameTiming); vr::VRCompositor()->GetFrameTiming(&timing); - bool asyncReprojectionActive = timing.m_nReprojectionFlags & VRCompositor_ReprojectionAsync; + _asyncReprojectionActive = timing.m_nReprojectionFlags & VRCompositor_ReprojectionAsync; - _threadedSubmit = !asyncReprojectionActive; + _threadedSubmit = !_asyncReprojectionActive; + qDebug() << "OpenVR Async Reprojection active: " << _asyncReprojectionActive; qDebug() << "OpenVR Threaded submit enabled: " << _threadedSubmit; _openVrDisplayActive = true; @@ -697,4 +701,4 @@ bool OpenVrDisplayPlugin::isKeyboardVisible() { int OpenVrDisplayPlugin::getRequiredThreadCount() const { return Parent::getRequiredThreadCount() + (_threadedSubmit ? 1 : 0); -} \ No newline at end of file +} diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.h b/plugins/openvr/src/OpenVrDisplayPlugin.h index d867c71cb6..3403bae27c 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.h +++ b/plugins/openvr/src/OpenVrDisplayPlugin.h @@ -40,7 +40,8 @@ public: void init() override; - float getTargetFrameRate() const override { return TARGET_RATE_OpenVr; } + float getTargetFrameRate() const override; + bool hasAsyncReprojection() const override { return _asyncReprojectionActive; } void customizeContext() override; void uncustomizeContext() override; @@ -82,4 +83,6 @@ private: std::shared_ptr _submitThread; std::shared_ptr _submitCanvas; friend class OpenVrSubmitThread; + + bool _asyncReprojectionActive { false }; }; diff --git a/plugins/openvr/src/OpenVrHelpers.cpp b/plugins/openvr/src/OpenVrHelpers.cpp index 91cad67d46..2803ca424e 100644 --- a/plugins/openvr/src/OpenVrHelpers.cpp +++ b/plugins/openvr/src/OpenVrHelpers.cpp @@ -12,11 +12,13 @@ #include #include +#include #include #include #include #include +#include #include #include #include @@ -324,3 +326,107 @@ controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat result.angularVelocity = angularVelocity; return result; } + +#define FAILED_MIN_SPEC_OVERLAY_NAME "FailedMinSpecOverlay" +#define FAILED_MIN_SPEC_OVERLAY_FRIENDLY_NAME "Minimum specifications for SteamVR not met" +#define FAILED_MIN_SPEC_UPDATE_INTERVAL_MS 10 +#define FAILED_MIN_SPEC_AUTO_QUIT_INTERVAL_MS (MSECS_PER_SECOND * 30) +#define MIN_CORES_SPEC 5 + +void showMinSpecWarning() { + auto vrSystem = acquireOpenVrSystem(); + auto vrOverlay = vr::VROverlay(); + if (!vrOverlay) { + qFatal("Unable to initialize SteamVR overlay manager"); + } + + vr::VROverlayHandle_t minSpecFailedOverlay = 0; + if (vr::VROverlayError_None != vrOverlay->CreateOverlay(FAILED_MIN_SPEC_OVERLAY_NAME, FAILED_MIN_SPEC_OVERLAY_FRIENDLY_NAME, &minSpecFailedOverlay)) { + qFatal("Unable to create overlay"); + } + + // Needed here for PathUtils + QCoreApplication miniApp(__argc, __argv); + + vrSystem->ResetSeatedZeroPose(); + QString imagePath = PathUtils::resourcesPath() + "/images/steam-min-spec-failed.png"; + vrOverlay->SetOverlayFromFile(minSpecFailedOverlay, imagePath.toLocal8Bit().toStdString().c_str()); + vrOverlay->SetHighQualityOverlay(minSpecFailedOverlay); + vrOverlay->SetOverlayWidthInMeters(minSpecFailedOverlay, 1.4f); + vrOverlay->SetOverlayInputMethod(minSpecFailedOverlay, vr::VROverlayInputMethod_Mouse); + vrOverlay->ShowOverlay(minSpecFailedOverlay); + + QTimer* timer = new QTimer(&miniApp); + timer->setInterval(FAILED_MIN_SPEC_UPDATE_INTERVAL_MS); + QObject::connect(timer, &QTimer::timeout, [&] { + vr::TrackedDevicePose_t vrPoses[vr::k_unMaxTrackedDeviceCount]; + vrSystem->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseSeated, 0, vrPoses, vr::k_unMaxTrackedDeviceCount); + auto headPose = toGlm(vrPoses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking); + auto overlayPose = toOpenVr(headPose * glm::translate(glm::mat4(), vec3(0, 0, -1))); + vrOverlay->SetOverlayTransformAbsolute(minSpecFailedOverlay, vr::TrackingUniverseSeated, &overlayPose); + + vr::VREvent_t event; + while (vrSystem->PollNextEvent(&event, sizeof(event))) { + switch (event.eventType) { + case vr::VREvent_Quit: + vrSystem->AcknowledgeQuit_Exiting(); + QCoreApplication::quit(); + break; + + case vr::VREvent_ButtonPress: + // Quit on any button press except for 'putting on the headset' + if (event.data.controller.button != vr::k_EButton_ProximitySensor) { + QCoreApplication::quit(); + } + break; + + default: + break; + } + } + + }); + timer->start(); + + QTimer::singleShot(FAILED_MIN_SPEC_AUTO_QUIT_INTERVAL_MS, &miniApp, &QCoreApplication::quit); + miniApp.exec(); +} + + +bool checkMinSpecImpl() { + // If OpenVR isn't supported, we have no min spec, so pass + if (!openVrSupported()) { + return true; + } + + // If we have at least 5 cores, pass + auto coreCount = QThread::idealThreadCount(); + if (coreCount >= MIN_CORES_SPEC) { + return true; + } + + // Even if we have too few cores... if the compositor is using async reprojection, pass + auto system = acquireOpenVrSystem(); + auto compositor = vr::VRCompositor(); + if (system && compositor) { + vr::Compositor_FrameTiming timing; + memset(&timing, 0, sizeof(timing)); + timing.m_nSize = sizeof(vr::Compositor_FrameTiming); + compositor->GetFrameTiming(&timing); + releaseOpenVrSystem(); + if (timing.m_nReprojectionFlags & VRCompositor_ReprojectionAsync) { + return true; + } + } + + // We're using OpenVR and we don't have enough cores... + showMinSpecWarning(); + + return false; +} + +extern "C" { + __declspec(dllexport) int __stdcall CheckMinSpec() { + return checkMinSpecImpl() ? 1 : 0; + } +} diff --git a/plugins/openvr/src/OpenVrHelpers.h b/plugins/openvr/src/OpenVrHelpers.h index 4279e6a6ac..be79dd1155 100644 --- a/plugins/openvr/src/OpenVrHelpers.h +++ b/plugins/openvr/src/OpenVrHelpers.h @@ -82,5 +82,7 @@ struct PoseData { } }; +// FIXME remove once OpenVR header is updated +#define VRCompositor_ReprojectionAsync 0x04 controller::Pose openVrControllerPoseToHandPose(bool isLeftHand, const mat4& mat, const vec3& linearVelocity, const vec3& angularVelocity); diff --git a/scripts/system/assets/images/progress-bar-2k.svg b/scripts/system/assets/images/progress-bar-2k.svg new file mode 100644 index 0000000000..45758c7c68 --- /dev/null +++ b/scripts/system/assets/images/progress-bar-2k.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/progress-bar-4k.svg b/scripts/system/assets/images/progress-bar-4k.svg new file mode 100644 index 0000000000..609ab9610b --- /dev/null +++ b/scripts/system/assets/images/progress-bar-4k.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/progress-bar-background.svg b/scripts/system/assets/images/progress-bar-background.svg deleted file mode 100644 index a8b4e1aab5..0000000000 --- a/scripts/system/assets/images/progress-bar-background.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/scripts/system/assets/images/progress-bar-text.svg b/scripts/system/assets/images/progress-bar-text.svg new file mode 100644 index 0000000000..05ebb3f637 --- /dev/null +++ b/scripts/system/assets/images/progress-bar-text.svg @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/progress-bar.svg b/scripts/system/assets/images/progress-bar.svg deleted file mode 100644 index e24a2cbff4..0000000000 --- a/scripts/system/assets/images/progress-bar.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - diff --git a/scripts/system/away.js b/scripts/system/away.js index 79ec22967d..96813031f1 100644 --- a/scripts/system/away.js +++ b/scripts/system/away.js @@ -304,7 +304,7 @@ function setEnabled(value) { var CHANNEL_AWAY_ENABLE = "Hifi-Away-Enable"; var handleMessage = function(channel, message, sender) { - if (channel === CHANNEL_AWAY_ENABLE) { + if (channel === CHANNEL_AWAY_ENABLE && sender === MyAvatar.sessionUUID) { print("away.js | Got message on Hifi-Away-Enable: ", message); setEnabled(message === 'enable'); } @@ -344,6 +344,7 @@ Script.scriptEnding.connect(function () { Controller.mousePressEvent.disconnect(goActive); Controller.keyPressEvent.disconnect(maybeGoActive); Messages.messageReceived.disconnect(handleMessage); + Messages.unsubscribe(CHANNEL_AWAY_ENABLE); }); if (HMD.active && !HMD.mounted) { diff --git a/scripts/system/controllers/controllerDisplay.js b/scripts/system/controllers/controllerDisplay.js index f42ac3aeda..6135f18426 100644 --- a/scripts/system/controllers/controllerDisplay.js +++ b/scripts/system/controllers/controllerDisplay.js @@ -31,14 +31,24 @@ function resolveHardware(path) { return resolveInner(Controller.Hardware, parts, 0); } +var DEBUG = true; +function debug() { + if (DEBUG) { + var args = Array.prototype.slice.call(arguments); + args.unshift("controllerDisplay.js | "); + print.apply(this, args); + } +} + createControllerDisplay = function(config) { var controllerDisplay = { overlays: [], partOverlays: {}, parts: {}, - mappingName: "mapping-display", + mappingName: "mapping-display-" + Math.random(), setVisible: function(visible) { + debug("Setting visible", this.overlays.length); for (var i = 0; i < this.overlays.length; ++i) { Overlays.editOverlay(this.overlays[i], { visible: visible @@ -166,7 +176,7 @@ createControllerDisplay = function(config) { } else if (part.type === "static") { // do nothing } else { - print("TYPE NOT SUPPORTED: ", part.type); + debug("TYPE NOT SUPPORTED: ", part.type); } controllerDisplay.overlays.push(overlayID); diff --git a/scripts/system/controllers/controllerDisplayManager.js b/scripts/system/controllers/controllerDisplayManager.js index 2c621f7e90..550357d659 100644 --- a/scripts/system/controllers/controllerDisplayManager.js +++ b/scripts/system/controllers/controllerDisplayManager.js @@ -69,7 +69,6 @@ ControllerDisplayManager = function() { } } - Messages.subscribe('Controller-Display'); var handleMessages = function(channel, message, sender) { var i, data, name, visible; if (!controllerLeft && !controllerRight) { diff --git a/scripts/system/controllers/viveControllerConfiguration.js b/scripts/system/controllers/viveControllerConfiguration.js index 341b8256f7..b49c3e1d04 100644 --- a/scripts/system/controllers/viveControllerConfiguration.js +++ b/scripts/system/controllers/viveControllerConfiguration.js @@ -62,6 +62,7 @@ var TIP_TEXTURE_BASE_URL = BASE_URL + "meshes/controller/vive_tips.fbm/"; var viveModelURL = BASE_URL + "meshes/controller/vive_body.fbx"; var viveTipsModelURL = BASE_URL + "meshes/controller/vive_tips.fbx"; +var viveTriggerModelURL = "meshes/controller/vive_trigger.fbx" VIVE_CONTROLLER_CONFIGURATION_LEFT = { name: "Vive", @@ -86,7 +87,7 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = { defaultTextureLayer: "blank", textureLayers: { blank: { - defaultTextureURL: TIP_TEXTURE_BASE_URL + "Blank.png" + defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Blank.png" }, trigger: { defaultTextureURL: TIP_TEXTURE_BASE_URL + "/Trigger.png" @@ -141,7 +142,18 @@ VIVE_CONTROLLER_CONFIGURATION_LEFT = { minValue: 0.0, maxValue: 1.0, axis: { x: -1, y: 0, z: 0 }, - maxAngle: 20 + maxAngle: 25, + + textureName: "Tex.black-trigger", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/black.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/yellow.jpg", + } + } }, l_grip: { @@ -262,7 +274,18 @@ VIVE_CONTROLLER_CONFIGURATION_RIGHT = { minValue: 0.0, maxValue: 1.0, axis: { x: -1, y: 0, z: 0 }, - maxAngle: 25 + maxAngle: 25, + + textureName: "Tex.black-trigger", + defaultTextureLayer: "normal", + textureLayers: { + normal: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/black.jpg", + }, + highlight: { + defaultTextureURL: BASE_URL + viveTriggerModelURL + "/Trigger.fbm/yellow.jpg", + } + } }, l_grip: { diff --git a/scripts/system/progress.js b/scripts/system/progress.js index d7f6713c69..c0b7143a11 100644 --- a/scripts/system/progress.js +++ b/scripts/system/progress.js @@ -13,18 +13,14 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -(function() { // BEGIN LOCAL_SCOPE +(function () { // BEGIN LOCAL_SCOPE function debug() { - return; - print.apply(null, arguments); + //print.apply(null, arguments); } var rawProgress = 100, // % raw value. displayProgress = 100, // % smoothed value to display. - DISPLAY_PROGRESS_MINOR_MAXIMUM = 8, // % displayed progress bar goes up to while 0% raw progress. - DISPLAY_PROGRESS_MINOR_INCREMENT = 0.1, // % amount to increment display value each update when 0% raw progress. - DISPLAY_PROGRESS_MAJOR_INCREMENT = 5, // % maximum amount to increment display value when >0% raw progress. alpha = 0.0, alphaDelta = 0.0, // > 0 if fading in; < 0 if fading out. ALPHA_DELTA_IN = 0.15, @@ -34,19 +30,60 @@ fadeWaitTimer = null, FADE_OUT_WAIT = 1000, // Wait before starting to fade out after progress 100%. visible = false, - BAR_WIDTH = 480, // Dimension of SVG in pixels of visible portion (half) of the bar. - BAR_HEIGHT = 10, - BAR_Y_OFFSET_2D = -10, // Offset of progress bar while in desktop mode - BAR_Y_OFFSET_HMD = -300, // Offset of progress bar while in HMD - BAR_URL = Script.resolvePath("assets/images/progress-bar.svg"), - BACKGROUND_WIDTH = 520, - BACKGROUND_HEIGHT = 50, - BACKGROUND_URL = Script.resolvePath("assets/images/progress-bar-background.svg"), + + BAR_DESKTOP_2K_WIDTH = 2240, // Width of SVG image in pixels. Sized for 1920 x 1080 display with 6 visible repeats. + BAR_DESKTOP_2K_REPEAT = 320, // Length of repeat in bar = 2240 / 7. + BAR_DESKTOP_2K_HEIGHT = 3, // Display height of SVG + BAR_DESKTOP_2K_URL = Script.resolvePath("assets/images/progress-bar-2k.svg"), + + BAR_DESKTOP_4K_WIDTH = 4480, // Width of SVG image in pixels. Sized for 1920 x 1080 display with 6 visible repeats. + BAR_DESKTOP_4K_REPEAT = 640, // Length of repeat in bar = 2240 / 7. + BAR_DESKTOP_4K_HEIGHT = 6, // Display height of SVG + BAR_DESKTOP_4K_URL = Script.resolvePath("assets/images/progress-bar-4k.svg"), + + BAR_HMD_WIDTH = 2240, // Desktop image works with HMD well. + BAR_HMD_REPEAT = 320, + BAR_HMD_HEIGHT = 3, + BAR_HMD_URL = Script.resolvePath("assets/images/progress-bar-2k.svg"), + + BAR_Y_OFFSET_DESKTOP = 0, // Offset of progress bar while in desktop mode + BAR_Y_OFFSET_HMD = -100, // Offset of progress bar while in HMD + + ANIMATION_SECONDS_PER_REPEAT = 4, // Speed of bar animation + + TEXT_HEIGHT = 32, + TEXT_WIDTH = 256, + TEXT_URL = Script.resolvePath("assets/images/progress-bar-text.svg"), windowWidth = 0, windowHeight = 0, - background2D = {}, - bar2D = {}, - SCALE_2D = 0.35; // Scale the SVGs for 2D display. + barDesktop = {}, + barHMD = {}, + textDesktop = {}, // Separate desktop and HMD overlays because can't change text size after overlay created. + textHMD = {}, + SCALE_TEXT_DESKTOP = 0.6, + SCALE_TEXT_HMD = 1.0, + isHMD = false, + + // Max seen since downloads started. This is reset when all downloads have completed. + maxSeen = 0, + + // Progress is defined as: (pending_downloads + active_downloads) / max_seen + // We keep track of both the current progress (rawProgress) and the + // best progress we've seen (bestRawProgress). As you are downloading, you may + // encounter new assets that require downloads, increasing the number of + // pending downloads and thus decreasing your overall progress. + bestRawProgress = 0, + + // True if we have known active downloads + isDownloading = false, + + // Entities are streamed to users, so you don't receive them all at once; instead, you + // receive them over a period of time. In many cases we end up in a situation where + // + // The initial delay cooldown keeps us from tracking progress before the allotted time + // has passed. + INITIAL_DELAY_COOLDOWN_TIME = 1000, + initialDelayCooldown = 0; function fade() { @@ -67,45 +104,32 @@ visible = false; } - Overlays.editOverlay(background2D.overlay, { + Overlays.editOverlay(barDesktop.overlay, { alpha: alpha, - visible: visible + visible: visible && !isHMD }); - Overlays.editOverlay(bar2D.overlay, { + Overlays.editOverlay(barHMD.overlay, { alpha: alpha, - visible: visible + visible: visible && isHMD + }); + Overlays.editOverlay(textDesktop.overlay, { + alpha: alpha, + visible: visible && !isHMD + }); + Overlays.editOverlay(textHMD.overlay, { + alpha: alpha, + visible: visible && isHMD }); } - Window.domainChanged.connect(function() { + Window.domainChanged.connect(function () { isDownloading = false; bestRawProgress = 100; rawProgress = 100; displayProgress = 100; }); - // Max seen since downloads started. This is reset when all downloads have completed. - var maxSeen = 0; - - // Progress is defined as: (pending_downloads + active_downloads) / max_seen - // We keep track of both the current progress (rawProgress) and the - // best progress we've seen (bestRawProgress). As you are downloading, you may - // encounter new assets that require downloads, increasing the number of - // pending downloads and thus decreasing your overall progress. - var bestRawProgress = 0; - - // True if we have known active downloads - var isDownloading = false; - - // Entities are streamed to users, so you don't receive them all at once; instead, you - // receive them over a period of time. In many cases we end up in a situation where - // - // The initial delay cooldown keeps us from tracking progress before the allotted time - // has passed. - var INITIAL_DELAY_COOLDOWN_TIME = 1000; - var initialDelayCooldown = 0; function onDownloadInfoChanged(info) { - var i; debug("PROGRESS: Download info changed ", info.downloading.length, info.pending, maxSeen); @@ -140,43 +164,96 @@ } function createOverlays() { - background2D.overlay = Overlays.addOverlay("image", { - imageURL: BACKGROUND_URL, - width: background2D.width, - height: background2D.height, - visible: false, - alpha: 0.0 - }); - bar2D.overlay = Overlays.addOverlay("image", { - imageURL: BAR_URL, + barDesktop.overlay = Overlays.addOverlay("image", { + imageURL: barDesktop.url, subImage: { x: 0, y: 0, - width: BAR_WIDTH, - height: BAR_HEIGHT + width: barDesktop.width - barDesktop.repeat, + height: barDesktop.height }, - width: bar2D.width, - height: bar2D.height, + width: barDesktop.width, + height: barDesktop.height, + visible: false, + alpha: 0.0 + }); + barHMD.overlay = Overlays.addOverlay("image", { + imageURL: BAR_HMD_URL, + subImage: { + x: 0, + y: 0, + width: BAR_HMD_WIDTH - BAR_HMD_REPEAT, + height: BAR_HMD_HEIGHT + }, + width: barHMD.width, + height: barHMD.height, + visible: false, + alpha: 0.0 + }); + textDesktop.overlay = Overlays.addOverlay("image", { + imageURL: TEXT_URL, + width: textDesktop.width, + height: textDesktop.height, + visible: false, + alpha: 0.0 + }); + textHMD.overlay = Overlays.addOverlay("image", { + imageURL: TEXT_URL, + width: textHMD.width, + height: textHMD.height, visible: false, alpha: 0.0 }); } function deleteOverlays() { - Overlays.deleteOverlay(background2D.overlay); - Overlays.deleteOverlay(bar2D.overlay); + Overlays.deleteOverlay(barDesktop.overlay); + Overlays.deleteOverlay(barHMD.overlay); + Overlays.deleteOverlay(textDesktop.overlay); + Overlays.deleteOverlay(textHMD.overlay); + } + + function updateProgressBarLocation() { + var viewport = Controller.getViewportDimensions(); + + windowWidth = viewport.x; + windowHeight = viewport.y; + isHMD = HMD.active; + + if (isHMD) { + + Overlays.editOverlay(barHMD.overlay, { + x: windowWidth / 2 - barHMD.width / 2, + y: windowHeight - 2 * barHMD.height + BAR_Y_OFFSET_HMD + }); + + Overlays.editOverlay(textHMD.overlay, { + x: windowWidth / 2 - textHMD.width / 2, + y: windowHeight - 2 * barHMD.height - textHMD.height + BAR_Y_OFFSET_HMD + }); + + } else { + + Overlays.editOverlay(barDesktop.overlay, { + x: windowWidth / 2 - barDesktop.width / 2, + y: windowHeight - 2 * barDesktop.height + BAR_Y_OFFSET_DESKTOP, + width: barDesktop.width + }); + + Overlays.editOverlay(textDesktop.overlay, { + x: windowWidth / 2 - textDesktop.width / 2, + y: windowHeight - 2 * barDesktop.height - textDesktop.height + BAR_Y_OFFSET_DESKTOP + }); + } } - var b = 0; - var currentOrientation = null; function update() { + var viewport, diff, x; + initialDelayCooldown -= 30; - var viewport, - eyePosition, - avatarOrientation; if (displayProgress < rawProgress) { - var diff = rawProgress - displayProgress; + diff = rawProgress - displayProgress; if (diff < 0.5) { displayProgress = rawProgress; } else { @@ -204,7 +281,7 @@ } else { // Fully visible because downloading or recently so if (fadeWaitTimer === null) { if (rawProgress === 100) { // Was downloading but have finished so fade out soon - fadeWaitTimer = Script.setTimeout(function() { + fadeWaitTimer = Script.setTimeout(function () { alphaDelta = ALPHA_DELTA_OUT; fadeTimer = Script.setInterval(fade, FADE_INTERVAL); fadeWaitTimer = null; @@ -219,59 +296,67 @@ } if (visible) { + x = ((Date.now() / 1000) % ANIMATION_SECONDS_PER_REPEAT) / ANIMATION_SECONDS_PER_REPEAT; + if (isHMD) { + x = x * barDesktop.repeat; + } else { + x = x * BAR_HMD_REPEAT; + } // Update progress bar - Overlays.editOverlay(bar2D.overlay, { - visible: true, + Overlays.editOverlay(barDesktop.overlay, { + visible: !isHMD, subImage: { - x: BAR_WIDTH * (1 - displayProgress / 100), + x: barDesktop.repeat - x, y: 0, - width: BAR_WIDTH, - height: BAR_HEIGHT - }, + width: barDesktop.width - barDesktop.repeat, + height: barDesktop.height + } }); - Overlays.editOverlay(background2D.overlay, { - visible: true, + Overlays.editOverlay(barHMD.overlay, { + visible: isHMD, + subImage: { + x: BAR_HMD_REPEAT - x, + y: 0, + width: BAR_HMD_WIDTH - BAR_HMD_REPEAT, + height: BAR_HMD_HEIGHT + } + }); + + Overlays.editOverlay(textDesktop.overlay, { + visible: !isHMD + }); + + Overlays.editOverlay(textHMD.overlay, { + visible: isHMD }); // Update 2D overlays to maintain positions at bottom middle of window viewport = Controller.getViewportDimensions(); - if (viewport.x !== windowWidth || viewport.y !== windowHeight) { + if (viewport.x !== windowWidth || viewport.y !== windowHeight || isHMD !== HMD.active) { updateProgressBarLocation(); } } } - function updateProgressBarLocation() { - var viewport = Controller.getViewportDimensions(); - windowWidth = viewport.x; - windowHeight = viewport.y; - - var yOffset = HMD.active ? BAR_Y_OFFSET_HMD : BAR_Y_OFFSET_2D; - - background2D.width = SCALE_2D * BACKGROUND_WIDTH; - background2D.height = SCALE_2D * BACKGROUND_HEIGHT; - bar2D.width = SCALE_2D * BAR_WIDTH; - bar2D.height = SCALE_2D * BAR_HEIGHT; - - Overlays.editOverlay(background2D.overlay, { - x: windowWidth / 2 - background2D.width / 2, - y: windowHeight - background2D.height - bar2D.height + yOffset - }); - - Overlays.editOverlay(bar2D.overlay, { - x: windowWidth / 2 - bar2D.width / 2, - y: windowHeight - background2D.height - bar2D.height + (background2D.height - bar2D.height) / 2 + yOffset - }); - } - function setUp() { - background2D.width = SCALE_2D * BACKGROUND_WIDTH; - background2D.height = SCALE_2D * BACKGROUND_HEIGHT; - bar2D.width = SCALE_2D * BAR_WIDTH; - bar2D.height = SCALE_2D * BAR_HEIGHT; + var is4k = Window.innerWidth > 3000; + + isHMD = HMD.active; + + barDesktop.width = is4k ? BAR_DESKTOP_4K_WIDTH - BAR_DESKTOP_4K_REPEAT : BAR_DESKTOP_2K_WIDTH - BAR_DESKTOP_2K_REPEAT; + barDesktop.height = is4k ? BAR_DESKTOP_4K_HEIGHT : BAR_DESKTOP_2K_HEIGHT; + barDesktop.repeat = is4k ? BAR_DESKTOP_4K_REPEAT : BAR_DESKTOP_2K_REPEAT; + barDesktop.url = is4k ? BAR_DESKTOP_4K_URL : BAR_DESKTOP_2K_URL; + barHMD.width = BAR_HMD_WIDTH - BAR_HMD_REPEAT; + barHMD.height = BAR_HMD_HEIGHT; + + textDesktop.width = SCALE_TEXT_DESKTOP * TEXT_WIDTH; + textDesktop.height = SCALE_TEXT_DESKTOP * TEXT_HEIGHT; + textHMD.width = SCALE_TEXT_HMD * TEXT_WIDTH; + textHMD.height = SCALE_TEXT_HMD * TEXT_HEIGHT; createOverlays(); } @@ -283,7 +368,7 @@ setUp(); GlobalServices.downloadInfoChanged.connect(onDownloadInfoChanged); GlobalServices.updateDownloadInfo(); - Script.setInterval(update, 1000/60); + Script.setInterval(update, 1000 / 60); Script.scriptEnding.connect(tearDown); }()); // END LOCAL_SCOPE diff --git a/server-console/src/main.js b/server-console/src/main.js index 7b328c0b6e..577c56d9e2 100644 --- a/server-console/src/main.js +++ b/server-console/src/main.js @@ -812,7 +812,8 @@ for (var key in trayIcons) { const notificationIcon = path.join(__dirname, '../resources/console-notification.png'); function onContentLoaded() { - maybeShowSplash(); + // Disable splash window for now. + // maybeShowSplash(); if (buildInfo.releaseType == 'PRODUCTION') { var currentVersion = null; diff --git a/tutorial/entityData.js b/tutorial/entityData.js index 76eb4d98ed..b14185e78f 100644 --- a/tutorial/entityData.js +++ b/tutorial/entityData.js @@ -1,12 +1,13 @@ -birdFirework1 = { - "clientOnly": 0, - "collisionsWillMove": 1, - "created": "2016-09-13T23:05:08Z", - "dimensions": { - "x": 0.10120716691017151, - "y": 0.12002291530370712, - "z": 0.18833979964256287 - }, +fireworkURLs = [ + "atp:/tutorial_models/bomb1.fbx", + "atp:/tutorial_models/bomb2.fbx", + "atp:/tutorial_models/bomb3.fbx", + "atp:/tutorial_models/bomb4.fbx", + "atp:/tutorial_models/bomb5.fbx", + "atp:/tutorial_models/bomb6.fbx", +]; + +fireworkBaseProps = { "collisionsWillMove": 1, velocity: { x: 0, @@ -20,7 +21,7 @@ birdFirework1 = { "z": 0 }, "id": "{1c4061bc-b2e7-4435-bc47-3fcc39ae6624}", - "modelURL": "atp:/tutorial_models/birdStatue15.fbx", + "modelURL": "atp:/tutorial_models/bomb1.fbx", "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", "position": { "x": 0.11612319946289062, @@ -44,15 +45,8 @@ birdFirework1 = { "userData": "{\n \"hifiHomeKey\": {\n \"reset\": true\n }\n}" } ; + birdFirework2 = { - "clientOnly": 0, - "collisionsWillMove": 1, - "created": "2016-09-12T22:56:48Z", - "dimensions": { - "x": 0.098819166421890259, - "y": 0.11143554747104645, - "z": 0.18833979964256287 - }, "collisionsWillMove": 1, velocity: { x: 0, @@ -66,7 +60,7 @@ birdFirework2 = { "z": 0 }, "id": "{ba067084-8d0f-4eeb-a8a1-c6814527c1bb}", - "modelURL": "atp:/tutorial_models/statuebird4.fbx", + "modelURL": "atp:/tutorial_models/bomb2.fbx", "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", "position": { "x": 0, diff --git a/tutorial/firePit/fire.js b/tutorial/firePit/fire.js index 077d79a42a..4565975351 100644 --- a/tutorial/firePit/fire.js +++ b/tutorial/firePit/fire.js @@ -4,6 +4,12 @@ (function() { + function debug() { + var args = Array.prototype.slice.call(arguments); + args.unshift("fire.js | "); + print.apply(this, args); + } + var _this = this; function Fire() { @@ -54,30 +60,45 @@ var colors = [RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET]; + var firePitSoundURL = Script.resolvePath("fire_burst.wav"); + debug("Firepit burst sound url is: ", firePitSoundURL); + + var explodeTextureURL = Script.resolvePath("explode.png"); + debug("Firepit explode texture url is: ", explodeTextureURL); + Fire.prototype = { preload: function(entityID) { + debug("Preload"); this.entityID = entityID; - this.EXPLOSION_SOUND = SoundCache.getSound("atp:/firepit/fire_burst.wav"); + this.EXPLOSION_SOUND = SoundCache.getSound(firePitSoundURL); }, collisionWithEntity: function(myID, otherID, collisionInfo) { + debug("Collided with entity: ", myID, otherID); var otherProps = Entities.getEntityProperties(otherID); var data = null; try { - data = JSON.parse(otherProps.userData) + data = JSON.parse(otherProps.userData); } catch (err) { - print('ERROR GETTING USERDATA!'); + debug('ERROR GETTING USERDATA!'); } if (data === null || "") { + debug("Data is null or empty", data); return; } else { + debug("Got data", data); if (data.hasOwnProperty('hifiHomeKey')) { + debug("Has hifiHomeKey"); if (data.hifiHomeKey.reset === true) { + debug("Reset is true"); _this.playSoundAtCurrentPosition(); _this.explodeWithColor(); Entities.deleteEntity(otherID) + debug("Sending local message"); Messages.sendLocalMessage('Entity-Exploded', JSON.stringify({ entityID: otherID, + position: Entities.getEntityProperties(this.entityID).position })); + debug("Done sending local message"); } } } @@ -137,7 +158,7 @@ "alphaStart": -0.2, "alphaFinish": 0.5, "emitterShouldTrail": 0, - "textures": "atp:/firepit/explode.png", + "textures": explodeTextureURL, "type": "ParticleEffect", lifetime: 1, position: myProps.position diff --git a/tutorial/fuse.js b/tutorial/fuse.js index 842695d85c..59306f4113 100644 --- a/tutorial/fuse.js +++ b/tutorial/fuse.js @@ -11,13 +11,17 @@ (function() { Script.include('utils.js'); - var DEBUG = false; + var DEBUG = true; function debug() { if (DEBUG) { - print.apply(self, arguments); + var args = Array.prototype.slice.call(arguments); + args.unshift("fuse.js | "); + print.apply(this, args); } } + var active = false; + var fuseSound = SoundCache.getSound("atp:/tutorial_sounds/fuse.wav"); function getChildProperties(entityID, propertyNames) { var childEntityIDs = Entities.getChildrenIDs(entityID); @@ -33,12 +37,20 @@ }; Fuse.prototype = { light: function() { - debug("LIT", this.entityID); - var anim = Entities.getEntityProperties(this.entityID, ['animation']).animation; + debug("Received light()", this.entityID); - if (anim.currentFrame < 140) { + var visible = Entities.getEntityProperties(this.entityID, ['visible']).visible; + if (!visible) { + debug("Fuse is not visible, returning"); return; } + + if (active) { + debug("Fuse is active, returning"); + return; + } + active = true; + Entities.editEntity(this.entityID, { animation: { currentFrame: 1, @@ -56,6 +68,7 @@ var childrenProps = getChildProperties(this.entityID, ['type']); for (var childEntityID in childrenProps) { + debug("Updating: ", childEntityID); var props = childrenProps[childEntityID]; if (props.type == "ParticleEffect") { Entities.editEntity(childEntityID, { @@ -70,13 +83,14 @@ var self = this; Script.setTimeout(function() { - debug("BLOW UP"); - var spinnerID = Utils.findEntity({ name: "tutorial/equip/spinner" }, 20); + debug("Setting off fireworks"); + var spinnerID = "{dd13fcd5-616f-4749-ab28-2e1e8bc512e9}"; Entities.callEntityMethod(spinnerID, "onLit"); injector.stop(); var childrenProps = getChildProperties(self.entityID, ['type']); for (var childEntityID in childrenProps) { + debug("Updating: ", childEntityID); var props = childrenProps[childEntityID]; if (props.type == "ParticleEffect") { Entities.editEntity(childEntityID, { @@ -90,8 +104,14 @@ } }, 4900); + + Script.setTimeout(function() { + debug("Setting fuse to inactive"); + active = false; + }, 14000); }, preload: function(entityID) { + debug("Preload"); this.entityID = entityID; }, }; diff --git a/tutorial/fuseCollider.js b/tutorial/fuseCollider.js index 0ad5cfb371..953fcd316d 100644 --- a/tutorial/fuseCollider.js +++ b/tutorial/fuseCollider.js @@ -5,11 +5,12 @@ }; Fuse.prototype = { onLit: function() { - print("LIT", this.entityID); - var fuseID = Utils.findEntity({ name: "tutorial/equip/fuse" }, 20); + print("fuseCollider.js | Lit", this.entityID); + var fuseID = "{c8944a13-9acb-4d77-b1ee-851845e98357}" Entities.callEntityMethod(fuseID, "light"); }, preload: function(entityID) { + print("fuseCollider.js | preload"); this.entityID = entityID; }, }; diff --git a/tutorial/lighter/createButaneLighter.js b/tutorial/lighter/createButaneLighter.js index caf3188b14..1a6b94d0f6 100644 --- a/tutorial/lighter/createButaneLighter.js +++ b/tutorial/lighter/createButaneLighter.js @@ -53,33 +53,39 @@ createButaneLighter = function(transform) { shapeType: 'simple-compound', type: 'Model', userData: JSON.stringify({ - tag: "equip-temporary", - grabbableKey: { - invertSolidWhileHeld: true + "tag": "equip-temporary", + "grabbableKey": { + "invertSolidWhileHeld": true }, - wearable: { - joints: { - RightHand: [{ - x: 0.029085848480463028, - y: 0.09807153046131134, - z: 0.03062543272972107 - }, { - x: 0.5929139256477356, - y: 0.3207578659057617, - z: 0.7151655554771423, - w: -0.18468326330184937 - }], - LeftHand: [{ - x: -0.029085848480463028, - y: 0.09807153046131134, - z: 0.03062543272972107 - }, { - x: -0.5929139256477356, - y: 0.3207578659057617, - z: 0.7151655554771423, - w: -0.18468326330184937 - }] - } + "wearable": { + "joints": { + "RightHand": [ + { + "x": 0.049671292304992676, + "y": 0.09825992584228516, + "z": 0.03760027885437012 + }, + { + "x": 0.6562752723693848, + "y": 0.27598991990089417, + "z": 0.6638742685317993, + "w": -0.22890058159828186 + } + ], + "LeftHand": [ + { + "x": -0.028073370456695557, + "y": 0.09609812498092651, + "z": 0.039550721645355225 + }, + { + "x": -0.6697965264320374, + "y": 0.22050897777080536, + "z": 0.6544681191444397, + "w": 0.27283111214637756 + } + ] + } } }), script: SCRIPT_URL diff --git a/tutorial/ownershipToken.js b/tutorial/ownershipToken.js index 745eee44e4..4a970af66d 100644 --- a/tutorial/ownershipToken.js +++ b/tutorial/ownershipToken.js @@ -81,7 +81,6 @@ var TOKEN_STATE_OWNED = 2; OwnershipToken = function(name, parentEntityID, options) { this.name = MyAvatar.sessionUUID + "-" + Math.floor(Math.random() * 10000000); - this.name = Math.floor(Math.random() * 10000000); this.parentEntityID = parentEntityID; // How often to check whether the token is available if we don't currently own it @@ -160,7 +159,7 @@ OwnershipToken.prototype = { var ownerID = getOwnershipTokenID(this.parentEntityID); if (ownerID !== null) { // Already owned, return - debug(this.name, "Token already owned by another client, return"); + debug(this.name, "Token already owned by another client, returning. Owner: " + owenerID + ", Us: " + this.name); return; } @@ -185,3 +184,5 @@ OwnershipToken.prototype = { Script.setTimeout(checkOwnershipRequest.bind(this), 2000); }, }; + +debug("Returning from ownershipToken"); diff --git a/tutorial/spinner.js b/tutorial/spinner.js index b50db2704e..2edbb43700 100644 --- a/tutorial/spinner.js +++ b/tutorial/spinner.js @@ -9,10 +9,10 @@ // (function() { - var DEBUG = false; + var DEBUG = true; function debug() { if (DEBUG) { - print.apply(self, arguments); + print.apply(this, arguments); } } @@ -31,7 +31,7 @@ } Spinner.prototype = { onLit: function() { - debug("LIT SPINNER", this.entityID); + debug("spinner.js | Spinner lit"); Entities.editEntity(this.entityID, { "angularDamping": 0.1, "angularVelocity": { @@ -50,6 +50,7 @@ for (var childEntityID in childrenProps) { var props = childrenProps[childEntityID]; if (props.type == "ParticleEffect") { + debug("spinner.js | Modifying: ", childEntityID); Entities.editEntity(childEntityID, { emitRate: 35, }); @@ -59,13 +60,14 @@ var self = this; Script.setTimeout(function() { - debug("BLOW UP"); + debug("spinner.js | Finishing spinner"); injector.stop(); var childrenProps = getChildProperties(self.entityID, ['type']); for (var childEntityID in childrenProps) { var props = childrenProps[childEntityID]; if (props.type == "ParticleEffect") { + debug("spinner.js | Modifying: ", childEntityID); Entities.editEntity(childEntityID, { emitRate: 0, }); @@ -74,6 +76,7 @@ }, 4900); }, preload: function(entityID) { + debug("spinner.js | Preload"); this.entityID = entityID; }, }; diff --git a/tutorial/tutorial.js b/tutorial/tutorial.js index 8c74bddc29..0fb86d7247 100644 --- a/tutorial/tutorial.js +++ b/tutorial/tutorial.js @@ -32,7 +32,7 @@ if (!Function.prototype.bind) { if (this.prototype) { // Function.prototype doesn't have a prototype property - fNOP.prototype = this.prototype; + fNOP.prototype = this.prototype; } fBound.prototype = new fNOP(); @@ -40,26 +40,36 @@ if (!Function.prototype.bind) { }; } -var DEBUG = false; +var DEBUG = true; function debug() { if (DEBUG) { - print.apply(this, arguments); + var args = Array.prototype.slice.call(arguments); + args.unshift("tutorial.js | "); + print.apply(this, args); } } var INFO = true; function info() { if (INFO) { - print.apply(this, arguments); + var args = Array.prototype.slice.call(arguments); + args.unshift("tutorial.js | "); + print.apply(this, args); } } +// Return a number between min (inclusive) and max (exclusive) +function randomInt(min, max) { + return min + Math.floor(Math.random() * (max - min)) +} + var NEAR_BOX_SPAWN_NAME = "tutorial/nearGrab/box_spawn"; var FAR_BOX_SPAWN_NAME = "tutorial/farGrab/box_spawn"; var GUN_SPAWN_NAME = "tutorial/gun_spawn"; var TELEPORT_PAD_NAME = "tutorial/teleport/pad" var successSound = SoundCache.getSound("atp:/tutorial_sounds/good_one.L.wav"); +var firecrackerSound = SoundCache.getSound("atp:/tutorial_sounds/Pops_Firecracker.wav"); var CHANNEL_AWAY_ENABLE = "Hifi-Away-Enable"; @@ -106,14 +116,6 @@ findEntities = function(properties, searchRadius, filterFn) { return matchedEntities; } -function setControllerVisible(name, visible) { - return; - Messages.sendLocalMessage('Controller-Display', JSON.stringify({ - name: name, - visible: visible, - })); -} - function setControllerPartsVisible(parts) { Messages.sendLocalMessage('Controller-Display-Parts', JSON.stringify(parts)); } @@ -191,12 +193,17 @@ function deleteEntitiesWithTag(tag) { } } function editEntitiesWithTag(tag, propertiesOrFn) { - var entityIDs = findEntitiesWithTag(tag); - for (var i = 0; i < entityIDs.length; ++i) { - if (isFunction(propertiesOrFn)) { - Entities.editEntity(entityIDs[i], propertiesOrFn(entityIDs[i])); - } else { - Entities.editEntity(entityIDs[i], propertiesOrFn); + var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag]; + + debug("Editing tag: ", tag); + if (entities) { + for (entityID in entities) { + debug("Editing: ", entityID, ", ", propertiesOrFn, ", Is in local tree: ", isEntityInLocalTree(entityID)); + if (isFunction(propertiesOrFn)) { + Entities.editEntity(entityID, propertiesOrFn(entityIDs[i])); + } else { + Entities.editEntity(entityID, propertiesOrFn); + } } } } @@ -205,7 +212,7 @@ function findEntitiesWithTag(tag) { return findEntities({ userData: "" }, 10000, function(properties, key, value) { data = parseJSON(value); return data.tag == tag; - }); + }); } // From http://stackoverflow.com/questions/5999998/how-can-i-check-if-a-javascript-variable-is-function-type @@ -222,21 +229,30 @@ function playSuccessSound() { }); } + +function playFirecrackerSound(position) { + Audio.playSound(firecrackerSound, { + position: position, + volume: 0.7, + loop: false + }); +} + /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// // // // STEP: DISABLE CONTROLLERS // // // /////////////////////////////////////////////////////////////////////////////// -var stepDisableControllers = function(name) { +var stepStart = function(name) { this.tag = name; - this.shouldLog = false; } -stepDisableControllers.prototype = { +stepStart.prototype = { start: function(onFinish) { - HMD.requestShowHandControllers(); disableEverything(); + HMD.requestShowHandControllers(); + onFinish(); }, cleanup: function() { @@ -258,6 +274,7 @@ function disableEverything() { setControllerPartLayer('tips', 'blank'); hideEntitiesWithTag('finish'); + setAwayEnabled(false); } @@ -275,7 +292,6 @@ function reenableEverything() { setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('tips', 'blank'); MyAvatar.shouldRenderLocally = true; - HMD.requestHideHandControllers(); setAwayEnabled(true); } @@ -293,6 +309,7 @@ var stepEnableControllers = function(name) { stepEnableControllers.prototype = { start: function(onFinish) { reenableEverything(); + HMD.requestHideHandControllers(); onFinish(); }, cleanup: function() { @@ -340,13 +357,11 @@ stepOrient.prototype = { var tag = this.tag; // Spawn content set - debug("raise hands...", this.tag); editEntitiesWithTag(this.tag, { visible: true }); - this.checkIntervalID = null; function checkForHandsAboveHead() { - debug("Orient: Checking for hands above head..."); + debug("Orient | Checking for hands above head"); if (MyAvatar.getLeftPalmPosition().y > (MyAvatar.getHeadPosition().y + 0.1)) { Script.clearInterval(this.checkIntervalID); this.checkIntervalID = null; @@ -359,6 +374,7 @@ stepOrient.prototype = { this.checkIntervalID = Script.setInterval(checkForHandsAboveHead.bind(this), 500); }, cleanup: function() { + debug("Orient | Cleanup"); if (this.active) { this.active = false; } @@ -394,13 +410,12 @@ stepRaiseAboveHead.prototype = { var STATE_HANDS_UP = 2; this.state = STATE_START; - debug("raise hands...", this.tag); editEntitiesWithTag(this.tag, { visible: true }); // Wait 2 seconds before starting to check for hands this.checkIntervalID = null; function checkForHandsAboveHead() { - debug("Raise above head: Checking hands..."); + debug("RaiseAboveHead | Checking hands"); if (this.state == STATE_START) { if (MyAvatar.getLeftPalmPosition().y < (MyAvatar.getHeadPosition().y - 0.1)) { this.state = STATE_HANDS_DOWN; @@ -418,6 +433,7 @@ stepRaiseAboveHead.prototype = { this.checkIntervalID = Script.setInterval(checkForHandsAboveHead.bind(this), 500); }, cleanup: function() { + debug("RaiseAboveHead | Cleanup"); if (this.checkIntervalID) { Script.clearInterval(this.checkIntervalID); this.checkIntervalID = null @@ -451,30 +467,26 @@ stepNearGrab.prototype = { this.finished = false; this.onFinish = onFinish; - setControllerVisible("trigger", true); setControllerPartLayer('tips', 'trigger'); + setControllerPartLayer('trigger', 'highlight'); var tag = this.tag; // Spawn content set showEntitiesWithTag(this.tag, { visible: true }); showEntitiesWithTag('bothGrab', { visible: true }); - var boxSpawnID = findEntity({ name: NEAR_BOX_SPAWN_NAME }, 10000); - if (!boxSpawnID) { - info("Error creating block, cannot find spawn"); - return null; - } - var boxSpawnPosition = Entities.getEntityProperties(boxSpawnID, 'position').position; - function createBlock() { - //Step1BlockData.position = boxSpawnPosition; - birdFirework1.position = boxSpawnPosition; - return spawnWithTag([birdFirework1], null, this.tempTag)[0]; + var boxSpawnPosition = getEntityWithName(NEAR_BOX_SPAWN_NAME).position; + function createBlock(fireworkNumber) { + fireworkBaseProps.position = boxSpawnPosition; + fireworkBaseProps.modelURL = fireworkURLs[fireworkNumber % fireworkURLs.length]; + debug("Creating firework with url: ", fireworkBaseProps.modelURL); + return spawnWithTag([fireworkBaseProps], null, this.tempTag)[0]; } this.birdIDs = []; - this.birdIDs.push(createBlock.bind(this)()); - this.birdIDs.push(createBlock.bind(this)()); - this.birdIDs.push(createBlock.bind(this)()); + this.birdIDs.push(createBlock.bind(this)(0)); + this.birdIDs.push(createBlock.bind(this)(1)); + this.birdIDs.push(createBlock.bind(this)(2)); this.positionWatcher = new PositionWatcher(this.birdIDs, boxSpawnPosition, -0.4, 4); }, onMessage: function(channel, message, seneder) { @@ -482,10 +494,12 @@ stepNearGrab.prototype = { return; } if (channel == "Entity-Exploded") { - debug("TUTORIAL: Got entity-exploded message"); + debug("NearGrab | Got entity-exploded message: ", message); var data = parseJSON(message); if (this.birdIDs.indexOf(data.entityID) >= 0) { + debug("NearGrab | It's one of the firecrackers"); + playFirecrackerSound(data.position); playSuccessSound(); this.finished = true; this.onFinish(); @@ -493,10 +507,10 @@ stepNearGrab.prototype = { } }, cleanup: function() { - debug("cleaning up near grab"); + debug("NearGrab | Cleanup"); this.finished = true; - setControllerVisible("trigger", false); setControllerPartLayer('tips', 'blank'); + setControllerPartLayer('trigger', 'normal'); hideEntitiesWithTag(this.tag, { visible: false}); deleteEntitiesWithTag(this.tempTag); if (this.positionWatcher) { @@ -530,8 +544,8 @@ stepFarGrab.prototype = { showEntitiesWithTag('bothGrab', { visible: true }); - setControllerVisible("trigger", true); setControllerPartLayer('tips', 'trigger'); + setControllerPartLayer('trigger', 'highlight'); Messages.sendLocalMessage('Hifi-Grab-Disable', JSON.stringify({ farGrabEnabled: true, })); @@ -540,21 +554,18 @@ stepFarGrab.prototype = { // Spawn content set showEntitiesWithTag(this.tag); - var boxSpawnID = findEntity({ name: FAR_BOX_SPAWN_NAME }, 10000); - if (!boxSpawnID) { - debug("Error creating block, cannot find spawn"); - return null; - } - var boxSpawnPosition = Entities.getEntityProperties(boxSpawnID, 'position').position; - function createBlock() { - birdFirework1.position = boxSpawnPosition; - return spawnWithTag([birdFirework1], null, this.tempTag)[0]; + var boxSpawnPosition = getEntityWithName(FAR_BOX_SPAWN_NAME).position; + function createBlock(fireworkNumber) { + fireworkBaseProps.position = boxSpawnPosition; + fireworkBaseProps.modelURL = fireworkURLs[fireworkNumber % fireworkURLs.length]; + debug("Creating firework with url: ", fireworkBaseProps.modelURL); + return spawnWithTag([fireworkBaseProps], null, this.tempTag)[0]; } this.birdIDs = []; - this.birdIDs.push(createBlock.bind(this)()); - this.birdIDs.push(createBlock.bind(this)()); - this.birdIDs.push(createBlock.bind(this)()); + this.birdIDs.push(createBlock.bind(this)(3)); + this.birdIDs.push(createBlock.bind(this)(4)); + this.birdIDs.push(createBlock.bind(this)(5)); this.positionWatcher = new PositionWatcher(this.birdIDs, boxSpawnPosition, -0.4, 4); }, onMessage: function(channel, message, seneder) { @@ -562,9 +573,11 @@ stepFarGrab.prototype = { return; } if (channel == "Entity-Exploded") { - debug("TUTORIAL: Got entity-exploded message"); + debug("FarGrab | Got entity-exploded message: ", message); var data = parseJSON(message); if (this.birdIDs.indexOf(data.entityID) >= 0) { + debug("FarGrab | It's one of the firecrackers"); + playFirecrackerSound(data.position); playSuccessSound(); this.finished = true; this.onFinish(); @@ -572,9 +585,10 @@ stepFarGrab.prototype = { } }, cleanup: function() { + debug("FarGrab | Cleanup"); this.finished = true; - setControllerVisible("trigger", false); setControllerPartLayer('tips', 'blank'); + setControllerPartLayer('trigger', 'normal'); hideEntitiesWithTag(this.tag, { visible: false}); hideEntitiesWithTag('bothGrab', { visible: false}); deleteEntitiesWithTag(this.tempTag); @@ -586,12 +600,13 @@ stepFarGrab.prototype = { }; function PositionWatcher(entityIDs, originalPosition, minY, maxDistance) { + debug("Creating position watcher"); this.watcherIntervalID = Script.setInterval(function() { for (var i = 0; i < entityIDs.length; ++i) { var entityID = entityIDs[i]; var props = Entities.getEntityProperties(entityID, ['position']); if (props.position.y < minY || Vec3.distance(originalPosition, props.position) > maxDistance) { - Entities.editEntity(entityID, { + Entities.editEntity(entityID, { position: originalPosition, velocity: { x: 0, y: -0.01, z: 0 }, angularVelocity: { x: 0, y: 0, z: 0 } @@ -603,6 +618,7 @@ function PositionWatcher(entityIDs, originalPosition, minY, maxDistance) { PositionWatcher.prototype = { destroy: function() { + debug("Destroying position watcher"); Script.clearInterval(this.watcherIntervalID); } }; @@ -630,8 +646,8 @@ var stepEquip = function(name) { } stepEquip.prototype = { start: function(onFinish) { - setControllerVisible("trigger", true); setControllerPartLayer('tips', 'trigger'); + setControllerPartLayer('trigger', 'highlight'); Messages.sendLocalMessage('Hifi-Grab-Disable', JSON.stringify({ holdEnabled: true, })); @@ -644,40 +660,41 @@ stepEquip.prototype = { this.currentPart = this.PART1; - function createGun() { - var boxSpawnID = findEntity({ name: GUN_SPAWN_NAME }, 10000); - if (!boxSpawnID) { - info("Error creating block, cannot find spawn"); - return null; - } - + function createLighter() { var transform = {}; - transform.position = Entities.getEntityProperties(boxSpawnID, 'position').position; - transform.rotation = Entities.getEntityProperties(boxSpawnID, 'rotation').rotation; + var boxSpawnProps = getEntityWithName(GUN_SPAWN_NAME); + transform.position = boxSpawnProps.position; + transform.rotation = boxSpawnProps.rotation; + transform.velocity = { x: 0, y: -0.01, z: 0 }; + transform.angularVelocity = { x: 0, y: 0, z: 0 }; this.spawnTransform = transform; return doCreateButaneLighter(transform).id; } - this.gunID = createGun.bind(this)(); - this.startWatchingGun(); - debug("Created", this.gunID); + this.lighterID = createLighter.bind(this)(); + this.startWatchingLighter(); + debug("Created lighter", this.lighterID); this.onFinish = onFinish; }, - startWatchingGun: function() { + startWatchingLighter: function() { if (!this.watcherIntervalID) { + debug("Starting to watch lighter position"); this.watcherIntervalID = Script.setInterval(function() { - var props = Entities.getEntityProperties(this.gunID, ['position']); - if (props.position.y < -0.4 + debug("Checking lighter position"); + var props = Entities.getEntityProperties(this.lighterID, ['position']); + if (props.position.y < -0.4 || Vec3.distance(this.spawnTransform.position, props.position) > 4) { - Entities.editEntity(this.gunID, this.spawnTransform); + debug("Moving lighter back to table"); + Entities.editEntity(this.lighterID, this.spawnTransform); } }.bind(this), 1000); } }, stopWatchingGun: function() { if (this.watcherIntervalID) { + debug("Stopping watch of lighter position"); Script.clearInterval(this.watcherIntervalID); this.watcherIntervalID = null; } @@ -687,24 +704,28 @@ stepEquip.prototype = { return; } - debug("Got message", channel, message, sender, MyAvatar.sessionUUID); + debug("Equip | Got message", channel, message, sender, MyAvatar.sessionUUID); if (channel == "Tutorial-Spinner") { if (this.currentPart == this.PART1 && message == "wasLit") { this.currentPart = this.PART2; + debug("Equip | Starting part 2"); Script.setTimeout(function() { + debug("Equip | Starting part 3"); this.currentPart = this.PART3; hideEntitiesWithTag(this.tagPart1); showEntitiesWithTag(this.tagPart2); + setControllerPartLayer('trigger', 'normal'); setControllerPartLayer('tips', 'grip'); Messages.subscribe('Hifi-Object-Manipulation'); + debug("Equip | Finished starting part 3"); }.bind(this), 9000); } } else if (channel == "Hifi-Object-Manipulation") { if (this.currentPart == this.PART3) { var data = parseJSON(message); - if (data.action == 'release' && data.grabbedEntity == this.gunID) { - info("got release"); + if (data.action == 'release' && data.grabbedEntity == this.lighterID) { + debug("Equip | Got release, finishing step"); this.stopWatchingGun(); this.currentPart = this.COMPLETE; playSuccessSound(); @@ -714,13 +735,14 @@ stepEquip.prototype = { } }, cleanup: function() { + debug("Equip | Got yaw action"); if (this.watcherIntervalID) { Script.clearInterval(this.watcherIntervalID); this.watcherIntervalID = null; } - setControllerVisible("trigger", false); setControllerPartLayer('tips', 'blank'); + setControllerPartLayer('trigger', 'normal'); this.stopWatchingGun(); this.currentPart = this.COMPLETE; @@ -752,9 +774,6 @@ var stepTurnAround = function(name) { } stepTurnAround.prototype = { start: function(onFinish) { - setControllerVisible("left", true); - setControllerVisible("right", true); - setControllerPartLayer('touchpad', 'arrows'); setControllerPartLayer('tips', 'arrows'); @@ -764,8 +783,9 @@ stepTurnAround.prototype = { Controller.actionEvent.connect(this.onActionBound); this.interval = Script.setInterval(function() { - var FORWARD_THRESHOLD = 30; - var REQ_NUM_TIMES_PRESSED = 6; + debug("TurnAround | Checking if finished", this.numTimesTurnPressed); + var FORWARD_THRESHOLD = 90; + var REQ_NUM_TIMES_PRESSED = 3; var dir = Quat.getFront(MyAvatar.orientation); var angle = Math.atan2(dir.z, dir.x); @@ -782,18 +802,17 @@ stepTurnAround.prototype = { onAction: function(action, value) { var STEP_YAW_ACTION = 6; if (action == STEP_YAW_ACTION && value != 0) { + debug("TurnAround | Got yaw action"); this.numTimesTurnPressed += 1; } }, cleanup: function() { + debug("TurnAround | Cleanup"); try { Controller.actionEvent.disconnect(this.onActionBound); } catch (e) { } - setControllerVisible("left", false); - setControllerVisible("right", false); - setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('tips', 'blank'); @@ -826,22 +845,21 @@ stepTeleport.prototype = { Messages.sendLocalMessage('Hifi-Teleport-Disabler', 'none'); // Wait until touching teleport pad... - var padID = findEntity({ name: TELEPORT_PAD_NAME }, 100); - var padProps = Entities.getEntityProperties(padID, ["position", "dimensions"]); + var padProps = getEntityWithName(TELEPORT_PAD_NAME); var xMin = padProps.position.x - padProps.dimensions.x / 2; var xMax = padProps.position.x + padProps.dimensions.x / 2; var zMin = padProps.position.z - padProps.dimensions.z / 2; var zMax = padProps.position.z + padProps.dimensions.z / 2; function checkCollides() { - debug("Checking if on pad..."); + debug("Teleport | Checking if on pad..."); var pos = MyAvatar.position; - debug('x', pos.x, xMin, xMax); - debug('z', pos.z, zMin, zMax); + debug('Teleport | x', pos.x, xMin, xMax); + debug('Teleport | z', pos.z, zMin, zMax); if (pos.x > xMin && pos.x < xMax && pos.z > zMin && pos.z < zMax) { - debug("On teleport pad"); + debug("Teleport | On teleport pad"); Script.clearInterval(this.checkCollidesTimer); this.checkCollidesTimer = null; playSuccessSound(); @@ -853,6 +871,7 @@ stepTeleport.prototype = { showEntitiesWithTag(this.tag); }, cleanup: function() { + debug("Teleport | Cleanup"); setControllerPartLayer('touchpad', 'blank'); setControllerPartLayer('tips', 'blank'); @@ -903,6 +922,10 @@ stepCleanupFinish.prototype = { +function isEntityInLocalTree(entityID) { + return Entities.getEntityProperties(entityID, 'visible').visible !== undefined; +} + function showEntitiesWithTag(tag) { var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag]; if (entities) { @@ -921,6 +944,7 @@ function showEntitiesWithTag(tag) { collisionless: collisionless, userData: JSON.stringify(data), }; + debug("Showing: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID)); Entities.editEntity(entityID, newProperties); } } @@ -945,6 +969,7 @@ function showEntitiesWithTag(tag) { Entities.editEntity(entityID, newProperties); }); } + function hideEntitiesWithTag(tag) { var entities = TUTORIAL_TAG_TO_ENTITY_IDS_MAP[tag]; if (entities) { @@ -960,6 +985,8 @@ function hideEntitiesWithTag(tag) { ignoreForCollisions: 1, userData: JSON.stringify(data), }; + + debug("Hiding: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID)); Entities.editEntity(entityID, newProperties); } } @@ -982,6 +1009,15 @@ function hideEntitiesWithTag(tag) { }); } +// Return the entity properties for an entity with a given name if it is in our +// cached list of entities. Otherwise, return undefined. +function getEntityWithName(name) { + debug("Getting entity with name:", name); + var entityID = TUTORIAL_NAME_TO_ENTITY_PROPERTIES_MAP[name]; + debug("Entity id: ", entityID, ", Is in local tree: ", isEntityInLocalTree(entityID)); + return entityID; +} + TutorialManager = function() { var STEPS; @@ -998,9 +1034,9 @@ TutorialManager = function() { currentStep = null; startedTutorialAt = Date.now(); STEPS = [ - new stepDisableControllers("step0"), + new stepStart("start"), new stepOrient("orient"), - new stepRaiseAboveHead("raiseHands"), + //new stepRaiseAboveHead("raiseHands"), new stepNearGrab("nearGrab"), new stepFarGrab("farGrab"), new stepEquip("equip"), @@ -1017,6 +1053,7 @@ TutorialManager = function() { } this.onFinish = function() { + debug("onFinish", currentStepNum); if (currentStep && currentStep.shouldLog !== false) { var timeToFinishStep = (Date.now() - startedLastStepAt) / 1000; var tutorialTimeElapsed = (Date.now() - startedTutorialAt) / 1000; @@ -1059,6 +1096,7 @@ TutorialManager = function() { this.stopTutorial = function() { if (currentStep) { currentStep.cleanup(); + HMD.requestHideHandControllers(); } reenableEverything(); currentStepNum = -1; diff --git a/tutorial/tutorialEntityIDs.js b/tutorial/tutorialEntityIDs.js index 38bd06e5ff..14b2a69892 100644 --- a/tutorial/tutorialEntityIDs.js +++ b/tutorial/tutorialEntityIDs.js @@ -83,7 +83,7 @@ TUTORIAL_TAG_TO_ENTITY_IDS_MAP = { } }, "equip-part2": { - "{8b92eec5-aeed-4368-bce0-432cc9ad4c51}": { + "{b5d17eda-90ab-40cf-b973-efcecb2e992e}": { "tag": "equip-part2" }, "{6307cd16-dd1d-4988-a339-578178436b45}": { @@ -148,4 +148,168 @@ TUTORIAL_TAG_TO_ENTITY_IDS_MAP = { "tag": "orient" } } -} +}; + +TUTORIAL_NAME_TO_ENTITY_PROPERTIES_MAP = { + "tutorial/gun_spawn": { + "userData": "{\"tag\":\"equip\",\"visible\":false}", + "dimensions": { + "y": 0.0649842768907547, + "x": 0.0649842768907547, + "z": 0.0649842768907547 + }, + "collisionless": 1, + "created": "2016-09-08T18:38:24Z", + "color": { + "blue": 0, + "green": 0, + "red": 255 + }, + "queryAACube": { + "y": 0.6283726096153259, + "x": 0.6865367293357849, + "scale": 0.11255607008934021, + "z": 0.3359576463699341 + }, + "visible": 0, + "shape": "Cube", + "clientOnly": 0, + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "position": { + "y": 0.6846506595611572, + "x": 0.7428147792816162, + "z": 0.3922356963157654 + }, + "rotation": { + "y": 0.7066605091094971, + "x": 0.7066605091094971, + "z": -0.025131583213806152, + "w": -0.025101065635681152 + }, + "ignoreForCollisions": 1, + "type": "Box", + "id": "{9df518da-9e65-4b76-8a79-eeefdb0b7310}", + "name": "tutorial/gun_spawn" + }, + "tutorial/nearGrab/box_spawn": { + "userData": "{\"tag\":\"nearGrab\",\"visible\":false}", + "dimensions": { + "y": 0.08225371688604355, + "x": 0.08225371688604355, + "z": 0.08225371688604355 + }, + "collisionless": 1, + "created": "2016-09-08T18:38:24Z", + "color": { + "blue": 255, + "green": 0, + "red": 255 + }, + "queryAACube": { + "y": 0.738319456577301, + "x": 0.8985498547554016, + "scale": 0.14246761798858643, + "z": 0.29067665338516235 + }, + "visible": 0, + "shape": "Cube", + "clientOnly": 0, + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "position": { + "y": 0.8095532655715942, + "x": 0.9697836637496948, + "z": 0.36191046237945557 + }, + "rotation": { + "y": -1.52587890625e-05, + "x": -1.52587890625e-05, + "z": -1.52587890625e-05, + "w": 1 + }, + "ignoreForCollisions": 1, + "type": "Box", + "id": "{5cf22b9c-fb22-4854-8821-554422980b24}", + "name": "tutorial/nearGrab/box_spawn" + }, + "tutorial/farGrab/box_spawn": { + "userData": "{\"tag\":\"farGrab\",\"visible\":false}", + "dimensions": { + "y": 0.37358683347702026, + "x": 0.37358683347702026, + "z": 0.37358683347702026 + }, + "collisionless": 1, + "created": "2016-09-08T18:38:24Z", + "color": { + "blue": 255, + "green": 0, + "red": 255 + }, + "queryAACube": { + "y": 0.3304251432418823, + "x": 3.0951309204101562, + "scale": 0.647071361541748, + "z": 0.18027013540267944 + }, + "visible": 0, + "shape": "Cube", + "clientOnly": 0, + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "position": { + x: 3.4866, + y: 0.6716, + z: 0.4789 + }, + "rotation": { + "y": -1.52587890625e-05, + "x": -1.52587890625e-05, + "z": -1.52587890625e-05, + "w": 1 + }, + "ignoreForCollisions": 1, + "type": "Box", + "id": "{70fcd96c-cd59-4f23-9ca5-a167f2f85680}", + "name": "tutorial/farGrab/box_spawn" + }, + "tutorial/teleport/pad": { + "userData": "{\"tag\":\"teleport\"}", + "rotation": { + "y": -0.9702650308609009, + "x": -2.1246911273919977e-05, + "z": -4.222852112434339e-06, + "w": 0.2420452982187271 + }, + "dimensions": { + "y": 0.4365682601928711, + "x": 2.1751723289489746, + "z": 2.175173044204712 + }, + "collisionless": 1, + "created": "2016-09-08T18:38:24Z", + "queryAACube": { + "y": -1.7979401350021362, + "x": 7.5136213302612305, + "scale": 3.106983184814453, + "z": -1.4602710008621216 + }, + "visible": 0, + "angularVelocity": { + "y": -0.5235987901687622, + "x": 0, + "z": 0 + }, + "clientOnly": 0, + "owningAvatarID": "{00000000-0000-0000-0000-000000000000}", + "angularDamping": 0, + "position": { + "y": -0.2444484978914261, + "x": 9.067112922668457, + "z": 0.09322060644626617 + }, + "modelURL": "atp:/alan/dev/Teleport-Pad.fbx", + "ignoreForCollisions": 1, + "type": "Model", + "id": "{4478f7b5-d3ac-4213-9a7b-ad8cd69575b8}", + "name": "tutorial/teleport/pad" + } +}; diff --git a/tutorial/tutorialStartZone.js b/tutorial/tutorialStartZone.js index 5cff1a4e99..cb0d223200 100644 --- a/tutorial/tutorialStartZone.js +++ b/tutorial/tutorialStartZone.js @@ -20,7 +20,7 @@ print("TutorialStartZone | Parent ID is: ", parentID); if (parentID) { print("TutorialStartZone | Sending start"); - Entities.callEntityMethod(parentID, 'start'); + Entities.callEntityMethod(parentID, 'onEnteredStartZone'); } else { print("TutorialStartZone | ERROR: No parent id found on tutorial start zone"); } @@ -29,7 +29,7 @@ sendStart(); } else { print("TutorialStartZone | User tried to go to tutorial with HMD and hand controllers, sending back to /"); - Window.alert("To proceed with this tutorial, please connect your VR headset and hand controllers."); + Window.alert("To proceed with this tutorial, please connect your Vive headset and hand controllers."); location = "/"; } }, @@ -38,6 +38,12 @@ if (this.sendStartIntervalID) { Script.clearInterval(this.sendStartIntervalID); } + var parentID = Entities.getEntityProperties(this.entityID, 'parentID').parentID; + print("TutorialStartZone | Parent ID is: ", parentID); + if (parentID) { + print("TutorialStartZone | Sending onLeftStartZone"); + Entities.callEntityMethod(parentID, 'on'); + } } }; diff --git a/tutorial/tutorialZone.js b/tutorial/tutorialZone.js index db7306a529..40028c9cd7 100644 --- a/tutorial/tutorialZone.js +++ b/tutorial/tutorialZone.js @@ -27,10 +27,14 @@ if (!Function.prototype.bind) { } (function() { - var ownershipTokenPath = Script.resolvePath("ownershipToken.js"); - var tutorialPath = Script.resolvePath("tutorial.js"); - Script.include(ownershipTokenPath); - Script.include(tutorialPath); + Script.include("ownershipToken.js"); + Script.include("tutorial.js"); + + var CHANNEL_AWAY_ENABLE = "Hifi-Away-Enable"; + function setAwayEnabled(value) { + var message = value ? 'enable' : 'disable'; + Messages.sendLocalMessage(CHANNEL_AWAY_ENABLE, message); + } var TutorialZone = function() { print("TutorialZone | Creating"); @@ -59,11 +63,16 @@ if (!Function.prototype.bind) { print("TutorialZone | Preload"); this.entityID = entityID; }, - start: function() { - print("TutorialZone | Got start"); + onEnteredStartZone: function() { + print("TutorialZone | Got onEnteredStartZone"); var self = this; if (!this.token) { print("TutorialZone | Creating token"); + // The start zone has been entered, hide the overlays immediately + setAwayEnabled(false); + Menu.setIsOptionChecked("Overlays", false); + MyAvatar.shouldRenderLocally = false; + Toolbars.getToolbar("com.highfidelity.interface.toolbar.system").writeProperty("visible", false); this.token = new OwnershipToken(Math.random() * 100000, this.entityID, { onGainedOwnership: function(token) { print("TutorialZone | GOT OWNERSHIP"); @@ -91,6 +100,18 @@ if (!Function.prototype.bind) { }); } }, + onLeftStartZone: function() { + print("TutorialZone | Got onLeftStartZone"); + + // If the start zone was exited, and the tutorial hasn't started, go ahead and + // re-enable the HUD/Overlays + if (!this.tutorialManager) { + Menu.setIsOptionChecked("Overlays", true); + MyAvatar.shouldRenderLocally = true; + setAwayEnabled(true); + Toolbars.getToolbar("com.highfidelity.interface.toolbar.system").writeProperty("visible", true); + } + }, enterEntity: function() { print("TutorialZone | ENTERED THE TUTORIAL AREA"); @@ -102,6 +123,10 @@ if (!Function.prototype.bind) { this.token.destroy(); this.token = null; } + if (this.tutorialManager) { + this.tutorialManager.stopTutorial(); + this.tutorialManager = null; + } } }; diff --git a/unpublishedScripts/DomainContent/Home/portal.js b/unpublishedScripts/DomainContent/Home/portal.js index 0ea090a6f7..ea6241265c 100644 --- a/unpublishedScripts/DomainContent/Home/portal.js +++ b/unpublishedScripts/DomainContent/Home/portal.js @@ -1,40 +1,47 @@ (function(){ var teleport; var portalDestination; + var thisEntityID; function playSound() { - Audio.playSound(teleport, { volume: 0.40, localOnly: true }); + var properties = Entities.getEntityProperties(thisEntityID, 'position'); + if (properties) { + Audio.playSound(teleport, { position: properties.position, volume: 0.40, localOnly: true }); + } }; this.preload = function(entityID) { + thisEntityID = entityID; teleport = SoundCache.getSound("atp:/sounds/teleport.raw"); - var properties = Entities.getEntityProperties(entityID); - portalDestination = properties.userData; - - print("portal.js | The portal destination is " + portalDestination); + var properties = Entities.getEntityProperties(entityID, 'userData'); + if (properties) { + portalDestination = properties.userData; + print("portal.js | The portal destination is " + portalDestination); + } } this.enterEntity = function(entityID) { print("portal.js | enterEntity"); - var properties = Entities.getEntityProperties(entityID); // in case the userData/portalURL has changed - portalDestination = properties.userData; + var properties = Entities.getEntityProperties(entityID, 'userData'); // in case the userData/portalURL has changed + if (properties) { + portalDestination = properties.userData; - print("portal.js | enterEntity() .... The portal destination is " + portalDestination); + print("portal.js | enterEntity() .... The portal destination is " + portalDestination); - if (portalDestination.length > 0) { - if (portalDestination[0] == '/') { - print("Teleporting to " + portalDestination); - Window.location = portalDestination; + if (portalDestination.length > 0) { + if (portalDestination[0] == '/') { + print("Teleporting to " + portalDestination); + Window.location = portalDestination; + } else { + print("Teleporting to hifi://" + portalDestination); + Window.location = "hifi://" + portalDestination; + } } else { - print("Teleporting to hifi://" + portalDestination); - Window.location = "hifi://" + portalDestination; + location.goToEntry(); // going forward: no data means go to appropriate entry point } - } else { - location.goToEntry(); // going forward: no data means go to appropriate entry point } - }; this.leaveEntity = function(entityID) {